diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..de8921796 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +target +bin +build +bin +.gradle +.springBeans +.ant-targets-build.xml +pom.xml +src/ant/.ant-targets-upload-dist.xml +*.iml +*.ipr +*.iws diff --git a/.settings/org.maven.ide.eclipse.prefs b/.settings/org.maven.ide.eclipse.prefs new file mode 100644 index 000000000..fc929aaef --- /dev/null +++ b/.settings/org.maven.ide.eclipse.prefs @@ -0,0 +1,9 @@ +#Mon Oct 11 17:02:20 EDT 2010 +activeProfiles= +eclipse.preferences.version=1 +fullBuildGoals=process-test-resources +includeModules=false +resolveWorkspaceProjects=true +resourceFilterGoals=process-resources resources\:testResources +skipCompilerPlugin=true +version=1 diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..2546282d0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,140 @@ +// used for artifact names, building doc upload urls, etc. +description = 'Spring Data Key Value' +abbreviation = 'DATAKV' + +apply plugin: 'base' +apply plugin: 'idea' + +buildscript { + repositories { +// add(new org.apache.ivy.plugins.resolver.FileSystemResolver()) { +// name = "local" +// addIvyPattern "e:/work/i21/gradle-plugins/build/repo/[organisation].[module]-ivy-[revision].xml" +// addArtifactPattern "e:/work/i21/gradle-plugins/build/repo/[organisation].[module]-[revision](-[classifier]).[ext]" +// } + + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "GitHub" + addIvyPattern 'http://cloud.github.com/downloads/costin/gradle-stuff/[organization].[module]-[artifact]-[revision].[ext]' + addArtifactPattern 'http://cloud.github.com/downloads/costin/gradle-stuff/[organization].[module]-[revision].[ext]' + } + mavenCentral() + mavenRepo name: "springsource-org-release", urls: "http://repository.springsource.com/maven/bundles/release" + mavenRepo name: "springsource-org-external", urls: "http://repository.springsource.com/maven/bundles/external" + } + + dependencies { + classpath 'org.springframework:gradle-stuff:0.1-20110421' + classpath 'net.sf.docbook:docbook-xsl:1.75.2:ns-resources@zip' + } +} + +allprojects { + group = 'org.springframework.data' + version = '1.0.0.BUILD-SNAPSHOT' + + releaseBuild = version.endsWith('RELEASE') + snapshotBuild = version.endsWith('SNAPSHOT') + + + repositories { + mavenLocal() + mavenCentral() + // Public Spring artefacts + mavenRepo name: "springsource-org-release", urls: "http://repository.springsource.com/maven/bundles/release" + mavenRepo name: "spring-release", urls: "http://maven.springframework.org/release" + mavenRepo name: "spring-milestone", urls: "http://maven.springframework.org/milestone" + mavenRepo name: "spring-snapshot", urls: "http://maven.springframework.org/snapshot" + mavenRepo name: "sonatype-snapshot", urls: "http://oss.sonatype.org/content/repositories/snapshots" + mavenRepo name: "jboss", urls: "http://repository.jboss.org/maven2/" + mavenRepo name: "java.net", urls: "http://download.java.net/maven/2/" + } +} + +javaprojects = subprojects.findAll { + project -> project.path.startsWith(':spring-data-') +} + +configure(javaprojects) { + apply plugin: "java" + apply plugin: "maven" + apply plugin: 'eclipse' // `gradle eclipse` to generate .classpath/.project + apply plugin: 'idea' // `gradle idea` to generate .ipr/.iml + apply plugin: 'docbook' + apply plugin: 'bundlor' // all core projects should be OSGi-compliant + + bundlor.useProjectProps = true + [compileJava, compileTestJava]*.options*.compilerArgs = ["-Xlint:-serial"] + + // Common dependencies + dependencies { + // Logging + compile "org.slf4j:slf4j-api:$slf4jVersion" + compile "org.slf4j:jcl-over-slf4j:$slf4jVersion" + runtime "log4j:log4j:$log4jVersion" + runtime "org.slf4j:slf4j-log4j12:$slf4jVersion" + // Spring Framework + compile("org.springframework:spring-core:$springVersion") { + exclude module: "commons-logging" + } + compile "org.springframework:spring-beans:$springVersion" + compile "org.springframework:spring-context:$springVersion" + compile "org.springframework:spring-context-support:$springVersion" + compile "org.springframework:spring-tx:$springVersion" + // Jackson JSON Mapper + compile "org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion" + // Testing + testCompile "junit:junit:$junitVersion" + testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.mockito:mockito-all:$mockitoVersion" + } + + apply from: "$rootDir/maven.gradle" +} + +ideaProject { + withXml { provider -> + provider.node.component.find { it.@name == 'VcsDirectoryMappings' }.mapping.@vcs = 'Git' + } +} + +task wrapper(type: Wrapper) { + gradleVersion = '0.9.2' + description = "Generate the Gradle wrapper" + group = "Distribution" +} + +// Distribution tasks +task dist(type: Zip) { + description = "Generate the ZIP Distribution" + group = "Distribution" + dependsOn subprojects*.tasks*.matching { task -> task.name == 'assemble' } + + evaluationDependsOn(':docs') + + def zipRootDir = "${project.name}-$version" + into(zipRootDir) { + from('/docs/src/info') { + include '*.txt' + } + from('/docs/build/') { + into 'docs' + include 'reference/**/*' + include 'api/**/*' + } + into('dist') { + from javaprojects.collect {project -> project.libsDir } + } + } + doLast { + ant.checksum(file: archivePath, algorithm: 'SHA1', fileext: '.sha1') + } +} + +task uploadDist(type: org.springframework.gradle.tasks.S3DistroUpload, dependsOn: dist) { + description = "Upload the ZIP Distribution" + group = "Distribution" + archiveFile = dist.archivePath + projectKey = 'DATAKV' + projectName = 'Spring Data Key Value' +} \ No newline at end of file diff --git a/docs/build.gradle b/docs/build.gradle new file mode 100644 index 000000000..ebdf04155 --- /dev/null +++ b/docs/build.gradle @@ -0,0 +1,136 @@ +import org.apache.tools.ant.filters.FixCrLfFilter +import org.apache.tools.ant.filters.ReplaceTokens + +// ----------------------------------------------------------------------------- +// Configuration for the docs subproject +// ----------------------------------------------------------------------------- + +apply plugin: 'base' +apply plugin: 'docbook' + +assemble.dependsOn = ['api', 'docbook'] + + +[docbookHtml, docbookFoPdf, docbookHtmlSingle]*.group = 'Documentation' +[docbookHtml, docbookFoPdf, docbookHtmlSingle]*.sourceFileName = 'index.xml' +[docbookHtml, docbookFoPdf, docbookHtmlSingle]*.sourceDirectory = new File(projectDir, 'src/reference/docbook') + +docbookHtml.stylesheet = new File(projectDir, 'src/reference/resources/xsl/html-custom.xsl') +docbookHtmlSingle.stylesheet = new File(projectDir, 'src/reference/resources/xsl/html-single-custom.xsl') +docbookFoPdf.stylesheet = new File(projectDir, 'src/reference/resources/xsl/pdf-custom.xsl') + +def imagesDir = new File(projectDir, 'src/reference/resources/images'); +docbookFoPdf.admonGraphicsPath = "${imagesDir}/admon" +docbookFoPdf.imgSrcPath = "${imagesDir}" + +refSpec = copySpec { + into ('reference') { + from("$buildDir/docs") + from("$projectDir/src/reference/resources") + } + into ('reference/images') { + from (imagesDir) + } + + p = new Properties() + + for (e in project.properties) { + if (e.key != null && e.value != null) + p.setProperty(e.key, e.value.toString()) + } + + filter(ReplaceTokens, tokens: p) +} + +task reference (type: Copy) { + dependsOn 'docbook' + description = "Builds aggregated DocBook" + group = "Documentation" + logger.info("Version is " + version) + destinationDir = buildDir + with(refSpec) +} + +task api(type: Javadoc) { + group = "Documentation" + description = "Builds aggregated JavaDoc HTML for all core project classes." + + // this task is a bit ugly to configure. it was a user contribution, and + // Hans tells me it's on the roadmap to redesign it. + + srcDir = file("${projectDir}/src/api") + destinationDir = file("${buildDir}/api") + tmpDir = file("${buildDir}/api-work") + + configure(options) { + stylesheetFile = file("${srcDir}/spring-javadoc.css") + links = ["http://static.springframework.org/spring/docs/3.0.x/javadoc-api"] + overview = "${srcDir}/overview.html" + docFilesSubDirs = true + outputLevel = org.gradle.external.javadoc.JavadocOutputLevel.QUIET + breakIterator = true + showFromProtected() + groups = [ + 'Spring Data Key Value Core': ['org.springframework.data.keyvalue*'], + 'Spring Data Redis Support' : ['org.springframework.data.keyvalue.redis*'], + 'Spring Data Riak Support' : ['org.springframework.data.keyvalue.riak*'] + ] + + links = [ + "http://static.springframework.org/spring/docs/3.0.x/javadoc-api", + "http://download.oracle.com/javase/6/docs/api/" + ] + + exclude "org/springframework/data/keyvalue/redis/config/**" + } + + title = "${rootProject.description} ${version} API" + + // collect all the sources that will be included in the javadoc output + source javaprojects.collect {project -> + project.sourceSets.main.allJava + } + + // collect all main classpaths to be able to resolve @see refs, etc. + // this collection also determines the set of projects that this + // task dependsOn, thus the runtimeClasspath is used to ensure all + // projects are included, not just *dependencies* of all classes. + // this is awkward and took me a while to figure out. + classpath = files(javaprojects.collect {project -> + project.sourceSets.main.runtimeClasspath + }) + + // copy the images from the doc-files dir over to the target + doLast { task -> + copy { + from file("${task.srcDir}/doc-files") + into file("${task.destinationDir}/doc-files") + } + } +} + +apiSpec = copySpec { + into('api') { + from(api.destinationDir) + } +} + +task docSiteLogin(type: org.springframework.gradle.tasks.Login) { + if (project.hasProperty('sshHost')) { + host = project.property('sshHost') + username = project.property('sshUsername') + key = project.property('sshPrivateKey') + } +} + +// upload task +task uploadApi(type: org.springframework.gradle.tasks.ScpUpload) { + dependsOn api, docbook + description = "Upload API Distribution" + group = "Distribution" + remoteDir = "./static.spring/spring-data/data-keyvalue/docs/${project.version}" + login = docSiteLogin + + with(apiSpec) + with(refSpec) +} \ No newline at end of file diff --git a/docs/src/api/doc-files/th-background.png b/docs/src/api/doc-files/th-background.png new file mode 100644 index 000000000..72d65e771 Binary files /dev/null and b/docs/src/api/doc-files/th-background.png differ diff --git a/docs/src/api/javadoc.options b/docs/src/api/javadoc.options new file mode 100644 index 000000000..04964ca74 --- /dev/null +++ b/docs/src/api/javadoc.options @@ -0,0 +1,13 @@ +-breakiterator +-header "Spring Data Key-Value" +-source 1.6 +-protected +-quiet +-docfilessubdirs +-group "Spring Data Key Value Core" "org.springframework.data.keyvalue*" +-group "Spring Data Redis Support" "org.springframework.data.keyvalue.redis*" +-group "Spring Data Riak Support" "org.springframework.data.keyvalue.riak*" +-link http://static.springframework.org/spring/docs/3.0.x/javadoc-api +-link http://download.oracle.com/javase/6/docs/api/ +-exclude org.springframework.data.keyvalue.redis.config + diff --git a/docs/src/api/overview.html b/docs/src/api/overview.html new file mode 100644 index 000000000..d2bdf9b41 --- /dev/null +++ b/docs/src/api/overview.html @@ -0,0 +1,24 @@ + + +This document is the API specification for the Spring Data project. +
+ +
+ +

+ If you are interested in commercial training, consultancy and + support for the Spring Data Framework, + SpringSource provides + such commercial support. +

+
+ + \ No newline at end of file diff --git a/docs/src/api/spring-javadoc.css b/docs/src/api/spring-javadoc.css new file mode 100644 index 000000000..1f009c4bc --- /dev/null +++ b/docs/src/api/spring-javadoc.css @@ -0,0 +1,48 @@ +/* Spring-specific Javadoc style sheet rules */ + +#overviewBody { + +} + +.code { + border: 1px solid black; + background-color: #F4F4F4; + padding: 5px; +} + +/* Vanilla Javadoc style sheet rules */ + +body { + font-family: Helvetica, Arial, sans-serif; + background-color: white; + font-size: 10pt; +} + +td { font-size: 10pt; font-family: Helvetica, Arial, sans-serif }/* Javadoc style sheet */ + +/* Define colors, fonts and other style attributes here to override the defaults */ + +/* Page background color */ +body { background-color: #FFFFFF } + +/* Headings */ +h1 { font-size: 145% } + +/* Table colors */ +.TableHeadingColor { background: #CCCCFF } /* Dark mauve */ +.TableSubHeadingColor { background: #EEEEFF } /* Light mauve */ +.TableRowColor { background: #FFFFFF } /* White */ + +/* Font used in left-hand frame lists */ +.FrameTitleFont { font-size: 100%; font-family: Helvetica, Arial, sans-serif } +.FrameHeadingFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } +.FrameItemFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } + +/* Navigation bar fonts and colors */ +.NavBarCell1 { background-color:#EEEEFF;} /* Light mauve */ +.NavBarCell1Rev { background-color:#00008B;} /* Dark Blue */ +.NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;} +.NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;} + +.NavBarCell2 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} +.NavBarCell3 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} diff --git a/docs/src/info/apache-license.txt b/docs/src/info/apache-license.txt new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/docs/src/info/apache-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/src/info/changelog.txt b/docs/src/info/changelog.txt new file mode 100644 index 000000000..d2d5c4626 --- /dev/null +++ b/docs/src/info/changelog.txt @@ -0,0 +1,92 @@ +SPRING DATA KEY/VALUE INTEGRATION CHANGELOG +=========================================== +http://www.springsource.org/spring-data + +Changes in version 1.0.0.M3 (2011-04-06) +---------------------------------------- + +Redis +----- + +General +* Added support for RJC (new Redis client) +* Added dedicated SORT and SORT/GET support +* Introduced HashMapper feature for mapping objects to and from maps +* Improved exception hierarchy to be more consistent with Spring DAO +* Made several Redis dependencies optional to eliminate unnecessary jars from the classpath + +Package o.s.d.k.redis.connection +* Added support for indexes to RedisConnectionFactories +* Added new key operations to KeyOperations (formerly KeyBound) +* Improved handling of Jedis exceptions + +Package o.s.d.k.redis.core +* Serializers are exposed to RedisCallback +* Added missing operations (move, select) to RedisTemplate +* Fixed the signature of various method + +Package o.s.d.k.redis.support.atomic +* Fixed incorrect serialization leading to error for RedisAtomicInteger & RedisAtomicLong + + +Changes in version 1.0.0.M2 (2011-02-10) +---------------------------------------- + +Redis +----- + +General +* Added PubSub support (message listener container and namespace) +* Added JSON and Object/XML Mapping serializers +* Completed support for Redis (2.2) commands +* Improved documentation +* Upgraded to Redis 2.2 +* Updraded to Jedis 1.5.2 + +Package o.s.d.k.redis.connection +* Added sort support +* Added pipelining support +* Added StringRedisConnection for String-focused operations +* Renamed JedisConnectionFactory pooling to usePool +* Renamed JredisConnectionFactory pooling to usePool + +Package o.s.d.k.redis.connection.jedis +* Added support for Jedis rich exceptions +* Added support for broken pooled connection + +Package o.s.d.k.redis.core +* Fix serializationg bug for hash value inside RedisTemplate +* Added injection for Redis operations ("views") + +Package o.s.d.k.redis.support +* Refined AtomicInteger and AtomicLong constructors to use the backing store value as initial counter + + +Riak +---- + +General +* Important bug fixes +* Fully asynchronous AsyncRiakTemplate object +* Groovy DSL for Riak access using async template underneath + + +Changes in version Riak 1.0.0.M1 (2010-12-15) +--------------------------------------------- +General +* Generified RiakTemplate for exception translation, serialization, and data access +* Built-in HTTP REST client based on Spring 3.0 RestTemplate +* java.io and Spring IO resource abstractions for reading/writing streams +* java.io.File subclass that represents a Riak resource + + +Changes in version Redis 1.0.0.M1 (2010-12-13) +---------------------------------------------- +General +* Configuration support for Redis Jedis and JRedis drivers/connectors +* Connection package as low-level abstraction across multiple drivers +* Exception translation +* Generified RedisTemplate for exception translation and serialization support +* Various serialization strategies +* Atomic counter support classes +* JDK Collection implementations on top of Redis diff --git a/docs/src/info/notice.txt b/docs/src/info/notice.txt new file mode 100644 index 000000000..e6900be90 --- /dev/null +++ b/docs/src/info/notice.txt @@ -0,0 +1,21 @@ + ======================================================================== + == NOTICE file corresponding to section 4 d of the Apache License, == + == Version 2.0, in this case for the Spring Integration distribution. == + ======================================================================== + + This product includes software developed by + the Apache Software Foundation (http://www.apache.org). + + The end-user documentation included with a redistribution, if any, + must include the following acknowledgement: + + "This product includes software developed by the Spring Framework + Project (http://www.springframework.org)." + + Alternatively, this acknowledgement may appear in the software itself, + if and wherever such third-party acknowledgements normally appear. + + The names "Spring", "Spring Framework", and "Spring Data" must + not be used to endorse or promote products derived from this software + without prior written permission. For written permission, please contact + enquiries@springsource.com. diff --git a/docs/src/info/readme.txt b/docs/src/info/readme.txt new file mode 100644 index 000000000..eb45a4e73 --- /dev/null +++ b/docs/src/info/readme.txt @@ -0,0 +1,17 @@ +SPRING DATASTORE KEY-VALUE 1.0.0 M2 (2010 02 10) +------------------------------------------------ + +Spring Datastore Key-Value is released under the terms of the Apache Software License Version 2.0 (see license.txt). + + +DISTRIBUTION CONTENTS: + +The JARs are available in the 'dist' directory, and the source JARs are in the 'src' directory. + +The reference manual and javadoc are located in the 'docs' directory. + + +ADDITIONAL RESOURCES: + +Spring Data Homepage: http://www.springsource.org/spring-data +Spring Data Forum : http://forum.springsource.org/forumdisplay.php?f=80 diff --git a/docs/src/reference/docbook/appendix/appendix-schema.xml b/docs/src/reference/docbook/appendix/appendix-schema.xml new file mode 100644 index 000000000..2e23ffcd8 --- /dev/null +++ b/docs/src/reference/docbook/appendix/appendix-schema.xml @@ -0,0 +1,13 @@ + + + Spring Data Key Value Schema(s) + + Spring Data - Redis support + + + FIXME: REDIS SCHEMA LOCATION/NAME CHANGED + + + + + diff --git a/docs/src/reference/docbook/appendix/introduction.xml b/docs/src/reference/docbook/appendix/introduction.xml new file mode 100644 index 000000000..6d9d95050 --- /dev/null +++ b/docs/src/reference/docbook/appendix/introduction.xml @@ -0,0 +1,10 @@ + + Document structure + + + Various appendixes outside the reference documentation. + + + defines the schemas provided by Spring Data + Key Value. + \ No newline at end of file diff --git a/docs/src/reference/docbook/index.xml b/docs/src/reference/docbook/index.xml new file mode 100644 index 000000000..5e2782b6d --- /dev/null +++ b/docs/src/reference/docbook/index.xml @@ -0,0 +1,80 @@ + + + + + Spring Data Key-Value - Reference Documentation + Spring Data Key-Value ${version} + @version@ + Spring Data Key-Value + + + + Costin + Leau + SpringSource + + + Jon + Brisbin + SpringSource + + + + + + Copies of this document may be made for your own use and for distribution + to others, provided that you do not charge any fee for such copies and + further provided that each copy contains this Copyright Notice, whether + distributed in print or electronically. + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/reference/docbook/introduction/getting-started.xml b/docs/src/reference/docbook/introduction/getting-started.xml new file mode 100644 index 000000000..755444df9 --- /dev/null +++ b/docs/src/reference/docbook/introduction/getting-started.xml @@ -0,0 +1,110 @@ + + + Getting Started + + Learning a new framework is not always straight forward. In this section, we (the Spring Data team) + tried to provide, what we think is, an easy to follow guide for starting with Spring Data Key Value module. + Of course, feel free to create your own learning 'path' as you see fit and, if possible, please report back + any improvements to the documentation that can help others. + +
+ First Steps + + As explained in , Spring Data Key Value (SDKV) provides integration + between Spring framework and key value (KV) stores. Thus, it is important to become acquainted with both of these + frameworks (storages or environments depending on how you want to name them). Throughout the SDKV documentation, + each section provides links to resources relevant however, it is best to become familiar with these topics beforehand. + +
+ Knowing Spring + Spring Data uses heavily Spring framework's core functionality, + such as the IoC container, + resource abstract or + AOP infrastructure. While it is not important + to know the Spring APIs, understanding the concepts behind them is. At a minimum, the idea behind IoC should be familiar. + These being said, the more knowledge one has about the Spring, the faster she will pick Spring Data Key Value. + Besides the very comprehensive (and sometimes disarming) documentation that explains in detail the Spring Framework, + there are a lot of articles, blog entries and books on the matter - take a look at the Spring framework + home page for more information. In general, this should be the starting point for + developers wanting to try Spring DKV. +
+
+ Knowing NoSQL and Key Value stores + NoSQL stores have taken the storage world by storm. It is a vast domain with a plethora of solutions, terms and patterns (to make things worth even the + term itself has multiple meanings). + While some of the principles are common, it is crucial that the user is familiar to some degree with the stores supported by SDKV. + The best way to get acquainted to this solutions is to read their documentation and follow their examples - it usually doesn't take more then 5-10 minutes + to go through them and if you are coming from an RDMBS-only background many times these exercises can be an eye opener. + +
+
+ Trying Out The Samples + Unfortunately the SDKV project is very young and there are no samples available yet. However we are working on them and plan to make them available + as soon as possible. In the meantime however, one can use our test suite as a code example (assuming the documentation is not enough) - we provide extensive + integration tests for our code base. + + +
+
+ +
+ Need Help? + + If you encounter issues or you are just looking for an advice, feel free to use one of the links below: + +
+ Community Support + The Spring Data forum is a message board for all Spring Data (not just Key Value) users to + share information and help each other. Note that registration is needed only for posting. + +
+
+ Professional Support + Professional, from-the-source support, with guaranteed response time, is available from SpringSource, + the company behind Spring Data and Spring. + +
+
+ +
+ Following Development + + For information on the Spring Data source code repository, nightly builds and snapshot artifacts please see the Spring Data home + page. + + You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the Spring Community + forums. + If you encounter a bug or want to suggest an improvement, + please create a ticket on the Spring Data issue tracker. + To stay up to date with the latest news and announcements in the Spring eco system, subscribe to the + Spring Community Portal. + Lastly, you can follow the SpringSource Data blog or the project team on Twitter + (Costin) +
+ +
\ No newline at end of file diff --git a/docs/src/reference/docbook/introduction/introduction.xml b/docs/src/reference/docbook/introduction/introduction.xml new file mode 100644 index 000000000..3e11048f5 --- /dev/null +++ b/docs/src/reference/docbook/introduction/introduction.xml @@ -0,0 +1,15 @@ + + + + + This document is the reference guide for Spring Data - Key Value Support. + It explains Key Value module concepts and semantics and the syntax for various + stores namespaces. + + For an introduction to key value stores or Spring, or Spring Data examples, please refer to + - this documentation refers only to Spring Data Key Value Support and + assumes the user is familiar with the key value storages and Spring concepts. + + + + \ No newline at end of file diff --git a/docs/src/reference/docbook/introduction/requirements.xml b/docs/src/reference/docbook/introduction/requirements.xml new file mode 100644 index 000000000..6f3c0198e --- /dev/null +++ b/docs/src/reference/docbook/introduction/requirements.xml @@ -0,0 +1,11 @@ + + Requirements + + Spring Data Key Value 1.x binaries requires JDK level 6.0 and above, + and Spring Framework + 3.0.x and above. + + In terms of key value stores, Redis 2.0.x + and Riak 0.13 are required. + + \ No newline at end of file diff --git a/docs/src/reference/docbook/introduction/why-sd-kv.xml b/docs/src/reference/docbook/introduction/why-sd-kv.xml new file mode 100644 index 000000000..4983b5023 --- /dev/null +++ b/docs/src/reference/docbook/introduction/why-sd-kv.xml @@ -0,0 +1,19 @@ + + + Why Spring Data - Key Value? + + The Spring Framework is the leading full-stack Java/JEE + application framework. It provides a lightweight container and a + non-invasive programming model enabled by the use of dependency + injection, AOP, and portable service abstractions. + + NoSQL + storages provide an alternative to classical RDBMS for horizontal scalability + and speed. In terms of implementation, Key Value stores represent one of the + largest (and oldest) member in the NoSQL space. + + The Spring Data Key Value (or SDKV) framework makes it easy to + write Spring applications that use a Key Value store by eliminating the redundant + tasks and boiler place code required for interacting with the store through + Spring's excellent infrastructure support. + \ No newline at end of file diff --git a/docs/src/reference/docbook/preface.xml b/docs/src/reference/docbook/preface.xml new file mode 100644 index 000000000..dd182d079 --- /dev/null +++ b/docs/src/reference/docbook/preface.xml @@ -0,0 +1,9 @@ + + + Preface + + The Spring Data Key-Value project applies core Spring concepts to the development of solutions using a key-value style data store. + We provide a "template" as a high-level abstraction for sending and receiving messages. + You will notice similarities to the JDBC support in the Spring Framework. + + diff --git a/docs/src/reference/docbook/reference/introduction.xml b/docs/src/reference/docbook/reference/introduction.xml new file mode 100644 index 000000000..1bba3527a --- /dev/null +++ b/docs/src/reference/docbook/reference/introduction.xml @@ -0,0 +1,10 @@ + + Document structure + + This part of the reference documentation explains the core functionality + offered by Spring Data Key Value. + + introduces the Redis module feature set. + introduces the Riak module feature set. + + \ No newline at end of file diff --git a/docs/src/reference/docbook/reference/redis-messaging.xml b/docs/src/reference/docbook/reference/redis-messaging.xml new file mode 100644 index 000000000..7bfbc8508 --- /dev/null +++ b/docs/src/reference/docbook/reference/redis-messaging.xml @@ -0,0 +1,200 @@ + +
+ Redis Messaging/PubSub + Spring Data provides dedicated messaging integration for Redis, + very similar in functionality and naming to the JMS integration in + Spring Framework; in fact, users familiar with the JMS support in Spring, should + feel right at home. + + Redis messaging can be roughly divided into two areas of functionality, namely + the production or publication and consumption or subscription of messages, hence the shortcut + pubsub (Publish/Subscribe). The + RedisTemplate class is used for message production. + For asynchronous reception similar to + Java EE's message-driven bean style, Spring Data provides a dedicated message + listener containers that is used to create Message-Driven POJOs + (MDPs) and for synchronous reception, the RedisConnection contract. + + The package org.springframework.data.keyvalue.redis.connection and + org.springframework.data.keyvalue.redis.listener provide + the core functionality for using Redis messaging. + +
+ Sending/Publishing messages + + To publish a message, one can use, as with the other operations, either the low-level + RedisConnection or the high-level RedisTemplate. + Both entities offer the publish method that accepts as argument the message + that needs to be sent as well as the destination channel. While RedisConnection + requires raw-data (array of bytes), the RedisTemplate allow arbitrary objects to be passed + in as messages: + + +
+ +
+ Receiving/Subscribing for messages + + On the receiving side, one can subscribe to one or multiple channels either by naming them directly or by using + pattern matching. The latter approach is quite useful as it not only allows multiple subscriptions to be created with + one command but to also listen on channels not yet created at subscription time (as long as match the pattern). + + + At the low-level, RedisConnection offers subscribe and + pSubscribe methods that map the Redis commands for subscribing by channel respectively by pattern. + Note that multiple channels or patterns can be used as arguments. To change the subscription of a connection or simply query + whether it is listening or not, RedisConnection + provides getSubscription and isSubscribed method. + + Subscribing commands are synchronized and thus blocking. That is, calling subscribe on a connection will cause + the current thread to block as it will start waiting for messages - the thread will be released only if the subscription + is canceled, that is an additional thread invokes unsubscribe respectively pUnsubscribe + on the same connection. See message listener container below + for a solution to these problem. + + As mentioned above, one subscribed a connection starts waiting for messages - no other commands can be invoked on it except + for adding new subscriptions or modifying/canceling the existing ones, that is invoking anything else then subscribe, + pSubscribe, unsubscribe, pUnsubscribe or is illegal and will + through an exception. + + In order to subscribe for messages, one needs to implement the MessageListener callback: each time + a new message arrives, the callback gets invoked and the user code executed through onMessage method. + The interface gives access not only to the actual message but to the channel it has been received through and the pattern (if any) used + by the subscription to match the channel. This information allows the callee to differentiate between various messages not just by content but + also through data. + + +
+ Message Listener Containers + + Due to its blocking nature, low-level subscription is not attractive as it requires connection and thread management for every single + listener. To alleviate this problem, Spring Data offers RedisMessageListenerContainer which does all the heavy lifting + on behalf of the user - users familiar with EJB and JMS should find the concepts familiar as it is designed as close as possible to the + support in Spring Framework and its message-driven POJOs (MDPs) + + RedisMessageListenerContainer acts as a message listener container; it is used to receive messages from a + Redis channel and drive the MessageListener that are injected into + it. The listener container is responsible for all threading of message + reception and dispatches into the listener for processing. A message + listener container is the intermediary between an MDP and a messaging + provider, and takes care of registering to receive messages, resource acquisition and release, + exception conversion and suchlike. This allows you as an application + developer to write the (possibly complex) business logic associated with + receiving a message (and reacting to it), and delegates + boilerplate Redis infrastructure concerns to the framework. + + + Further more, to minimize the application footprint, RedisMessageListenerContainer performs allows one connection and one thread + to be shared by multiple listeners even though they do not share a subscription. Thus no matter how many listeners or channels an application tracks, + the runtime cost will remain the same through out its lifetime. Moreover, the container allows runtime configuration changes so one can add or remove + listeners while an application is running without the need for restart. Additionally, the container uses a lazy subscription approach, using a + RedisConnection only when needed - if all the listeners are unsubscribed, cleanup is automatically performed and the used + thread released. + + To help with the asynch manner of messages, the container requires a java.util.concurrent.Executor ( + or Spring's TaskExecutor) for dispatching the messages. Depending on the load, the number of listeners or the runtime + environment, one should change or tweak the executor to better serve her needs - in particular in managed environments (such as app servers), it is + highly recommended to pick a a proper TaskExecutor to take advantage of its runtime. +
+ +
+ The <classname>MessageListenerAdapter</classname> + + The MessageListenerAdapter class is the + final component in Spring's asynchronous messaging support: in a + nutshell, it allows you to expose almost any class + as a MDP (there are of course some constraints). + + Consider the following interface definition. Notice that although + the interface extends the + MessageListener interface, + it can still be used as a MDP via the use of the + MessageListenerAdapter class. Notice also how the + various message handling methods are strongly typed according to the + contents of the various + Message types that they can receive and + handle. + + public interface MessageDelegate { + + void handleMessage(String message); + + void handleMessage(Map message); + + void handleMessage(byte[] message); + + void handleMessage(Serializable message); +} + + public class DefaultMessageDelegate implements MessageDelegate { + // implementation elided for clarity... +} + + In particular, note how the above implementation of the + MessageDelegate interface (the above + DefaultMessageDelegate class) has + no Redis dependencies at all. It truly is a POJO that + we will make into an MDP via the following configuration. + + <?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:redis="http://www.springframework.org/schema/redis" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/redis http://www.springframework.org/schema/redis/spring-redis.xsd"> + + <!-- the default ConnectionFactory --> + <redis:listener-container> + <!-- the method attribute can be skipped as the default method name is "handleMessage" --> + <redis:listener ref="listener" method="handleMessage" topic="chatroom" /> + </redis:listener-container> + + <bean class="redisexample.DefaultMessageDelegate"/> + ... +<beans> + + The listener topic can be either a channel (e.g. topic="chatroom") or a pattern (e.g. topic="*room") + + The example above uses the Redis namespace to declare the message listener container and automatically register the POJOs as listeners. The full blown, beans definition + is displayed below: + + <!-- this is the Message Driven POJO (MDP) --> +<bean id="messageListener" class="org.springframework.data.keyvalue.redis.listener.adapter.MessageListenerAdapter"> + <constructor-arg> + <bean class="redisexample.DefaultMessageDelegate"/> + </constructor-arg> +</bean> + +<!-- and this is the message listener container... --> +<bean id="redisContainer" class="org.springframework.data.keyvalue.redis.listener.RedisMessageListenerContainer"> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="messageListeners"> + <!-- map of listeners and their associated topics (channels or/and patterns) --> + <map> + <entry key-ref="messageListener"> + <bean class="org.springframework.data.keyvalue.redis.listener.ChannelTopic"> + <constructor-arg value="chatroom"> + </bean> + </entry> + </map> + </property> +</bean> + + Each time a message is received, the adapter automatically performs + translation (using the configured RedisSerializer) + between the low-level format and the required object type transparently. Any exception caused by the method invocation + is caught and handled by the container (by default, being logged). + + +
+
+
\ No newline at end of file diff --git a/docs/src/reference/docbook/reference/redis.xml b/docs/src/reference/docbook/reference/redis.xml new file mode 100644 index 000000000..df2ece116 --- /dev/null +++ b/docs/src/reference/docbook/reference/redis.xml @@ -0,0 +1,415 @@ + + + Redis support + + One of the key value stores supported by SDKV is Redis. + To quote the project home page: + + Redis is an advanced key-value store. It is similar to memcached but the dataset is not volatile, and values can be strings, + exactly like in memcached, but also lists, sets, and ordered sets. All this data types can be manipulated with atomic operations + to push/pop elements, add/remove elements, perform server side union, intersection, difference between sets, and so forth. + Redis supports different kind of sorting abilities. + + Spring Data Key Value provides easy configuration and access to Redis from Spring application. Offers both low-level and + high-level abstraction for interacting with the store, freeing the user from infrastructural concerns. + + +
+ Redis Requirements + SDKV requires Redis 2.0 or above (Redis 2.2 is recommended) and Java SE 6.0 or above. + In terms of language bindings (or connectors), SDKV integrates with Jedis, + JRedis and RJC, three popular open source Java libraries for Redis. + If you are aware of any other connector that we should be integrating is, please send us feedback. + +
+ +
+ Redis Support High Level View + + The Redis support provides several components (in order of dependencies): + + Low-Level Abstractions - for configuring and handling communication with Redis through the various connector libraries supported as + described in . + High-Level Abstractions - providing a generified, user friendly template classes for interacting with Redis. + explains the abstraction builds on top of the low-level Connection API to handle the + infrastructural concerns and object conversion. + Support Classes - that offer reusable components (built on the aforementioned abstractions) such as + java.util.Collection backed by Redis as documented in + + + For most tasks, the high-level abstractions and support services are the best choice. Note that at any point, one can move between layers - for example, it's very + easy to get a hold of the low level connection (or even the native libray) to communicate directly with Redis. +
+ +
+ Connecting to Redis + + One of the first tasks when using Redis and Spring is to connect to the store through the IoC container. To do that, a Java connector (or binding) is required; + currently SDKV has support for Jedis and JRedis. No matter the library one chooses, there only one set of SDKV API that one needs to use that behaves consistently + across all connectors, namely the org.springframework.data.keyvalue.redis.connection package and its + RedisConnection and RedisConnectionFactory interfaces for working respectively for retrieving active + connection to Redis. + +
+ <interfacename>RedisConnection</interfacename> and <interfacename>RedisConnectionFactory</interfacename> + + RedisConnection provides the building block for Redis communication as it handles the communication with the Redis back-end. + It also automatically translates the underlying connecting library exceptions to Spring's consistent DAO exception + hierarchy so one can switch the connectors + without any code changes as the operation semantics remain the same. + + For the corner cases where the native library API is required, RedisConnection provides a dedicated method + getNativeConnection which returns the raw, underlying object used for communication. + + Active RedisConnection are created through RedisConnectionFactory. In addition, the factories act as + PersistenceExceptionTranslator meaning once declared, allow one to do transparent exception translation for example through the use of the + @Repository annotation and AOP. For more information see the dedicated + section in Spring Framework documentation. + + Depending on the underlying configuration, the factory can return a new connection or an existing connection (in case a pool is used). +
+ + The easiest way to work with a RedisConnectionFactory is to configure the appropriate connector through the IoC container and + inject it into the using class. + + + Connector features + + Unfortunately, currently, not connectors support all of Redis features - in particular JRedis does not have support for hashes yet though this is currently being worked on. + When invoking a method on the Connection API that is unsupported by the underlying library, a UnsupportedOperationException + is thrown. + This situation is likely to be fixed in the future, as the various connectors mature. + + + +
+ Configuring Jedis connector + + Jedis is one of the connectors supported by the Key Value module through the + org.springframework.data.keyvalue.redis.connection.jedis package. In its simples form, the Jedis configuration looks as follow: + + + + + + +]]> + + For production use however, one might want to tweak the settings such as the host or password: + + + + + +]]> + +
+ +
+ Configuring JRedis connector + + JRedis is another popular, open-source connector supported by SDKV through the + org.springframework.data.keyvalue.redis.connection.jredis package. + Since JRedis itself does not support (yet) Redis 2.x commands, SDKV uses an updated fork available + here. + + A typical JRedis configuration can looks like this: + + + + + +]]> + + As one can note, the configuration is quite similar to the Jedis one. + + Currently, JRedis does not have support for binary keys. This forces the JredisConnection to perform encoding internally + (through base64 schema). In practice, this means it's safe to read/write arbitrary data however + the Redis key stored values will differ from the decoded ones, even in the simplest cases, since everything (no matter the format) is encoded. This will not be + the case for Redis values. + This issue is currently being addressed in the JRedis project and once fixed, will be incorporated by Spring Data Redis. + + +
+ +
+ Configuring RJC connector + + RJC is the third, open-source connector supported by SDKV through the + org.springframework.data.keyvalue.redis.connection.rjc package. + + Similar to the other connectors, a typical RJC configuration can looks like this: + + + + + +]]> + + As one can note, the configuration is quite similar to the Jredis or Jedis one. + + Currently, RJC does not have support for binary keys. This forces the RjcConnection to perform encoding internally + (through base64 schema). In practice, this means it's safe to read/write arbitrary data however + the Redis key stored values will differ from the decoded ones, even in the simplest cases, since everything (no matter the format) is encoded. This will not be + the case for Redis values. + This issue is currently being addressed in the RJC project and once fixed, will be incorporated by Spring Data Redis. + + +
+ +
+ +
+ Working with Objects through <classname>RedisTemplate</classname> + + Most users are likely to use RedisTemplate and its coresponding package org.springframework.data.keyvalue.redis.core - the + template is in fact the central class of the Redis module due to its rich feature set. + The template offers a high-level abstraction for Redis interaction - while RedisConnection offer low level methods that accept and return + binary values (byte arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details. + + Moreover, the template provides operations views (following the grouping from Redis command reference) + that offer rich, generified interfaces for working against a certain type or certain key (through the KeyBound interfaces) as described below: + + + Operational views + + + + + + + + Interface + Description + + + + + + + + ValueOperations + Redis string (or value) operations + + + ListOperations + Redis list operations + + + SetOperations + Redis set operations + + + ZSetOperations + Redis zset (or sorted set) operations + + + HashOperations + Redis hash operations + + + + + + BoundValueOperations + Redis string (or value) key bound operations + + + BoundListOperations + Redis list key bound operations + + + BoundSetOperations + Redis set key bound operations + + + BoundZSetOperations + Redis zset (or sorted set) key bound operations + + + BoundHashOperations + Redis hash key bound operations + + + +
+ + Once configured, the template is thread-safe and can be reused across multiple instances. + + Out of the box, RedisTemplate uses a Java-based serializer for most of its operations. This means that any object written or read by the template will be + serializer/deserialized through Java. The serialization mechanism can be easily changed on the template and the Redis module offers several implementations available in the + org.springframework.data.keyvalue.redis.serializer package - see for more information. + Note that the template requires all keys to be non-null - values can be null as long as the underlying + serializer accepts them; read the javadoc of each serializer for more information. + + For cases where a certain template view is needed, one the view as a dependency and inject the template: the container will automatically perform the conversion + eliminating the opsFor[X] calls: + + + + + + + + + + ... +]]> + + template; + + // inject the template as ListOperations + @Autowired + private ListOperations listOps; + + public void addLink(String userId, URL url) { + listOps.leftPush(userId, url.toExternalForm()); + } +}]]> +
+ +
+ String-focused convenience classes + + Since it's quite the keys and values stored in Redis can be java.lang.String, the Redis modules provides two extensions to RedisConnection + and RedisTemplate respectively the StringRedisConnection (and its DefaultStringRedisConnection implementation) + and StringRedisTemplate as a convenient one-stop solution + for intensive String operations. In addition to be bound to String keys, the template and the connection use the + StringRedisSerializer underneath which means the stored keys and values are human readable (assuming the same encoding is used both in Redis and your code). + For example: + + + + + + + + + + ... +]]> + + + + + As with the other Spring templates, RedisTemplate and StringRedisTemplate allow the developer to talk directly to Redis through + the RedisCallback interface: this gives complete control to the developer as it talks directly to the RedisConnection. + + + () { + + public Object doInRedis(RedisConnection connection) throws DataAccessException { + Long size = connection.dbSize(); + ... + } + }); +}]]> +
+ +
+ Serializers + + From the framework perspective, the data stored in Redis are just bytes. While Redis itself supports various types, for the most part these refer to the way the data is stored + rather then what it represents. It is up to the user to decide whether the information gets translated into Strings or any other objects. The conversion between the user (custom) + types and raw data (and vice-versa) is handled in SDKV Redis through the RedisSerializer interface + (package org.springframework.data.keyvalue.redis.serializer) which as the name implies, takes care of the serialization process. Multiple implementations are + available out of the box, two of which have been already mentioned before in this documentation: the StringRedisSerializer and + the JdkSerializationRedisSerializer. However one can use OxmSerializer for Object/XML mapping through Spring 3 + OXM support or JacksonJsonRedisSerializer for storing + data in JSON format. Do note that the storage format is not limited only to values - it can be used for keys, values or hashes + without any restrictions. +
+ + + +
+ Support Classes + + Package org.springframework.data.keyvalue.redis.support offers various reusable components that rely on Redis as a backing store. Curently the package contains + various JDK-based interface implementations on top of Redis such as atomic + counters and JDK Collections. + + The atomic counters make it easy to wrap Redis key incrementation while the collections allow easy management of Redis keys with minimal storage exposure or API leakage: in particular + the RedisSet and RedisZSet interfaces offer easy access to the set operations supported by Redis such as + intersection and union while RedisList implements the List, + Queue and Deque contracts (and their equivalent blocking siblings) on top of Redis, exposing the storage as a + FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration: + + + + + + + + + +]]> + + queue; + + public void addTag(String tag) { + queue.push(tag); + } +}]]> + + As shown in the example above, the consuming code is decoupled from the actual storage implementation - in fact there is no indication that Redis is used underneath. This makes moving from + development to production environments transparent and highly increases testability (the Redis implementation can just as well be replaced with an in-memory one). +
+ +
+ Roadmap ahead + + Spring Data Redis project is in its early stages. We are interested in feedback, knowing what your use cases are, what are the common patters you encounter so that the Redis module + better serves your needs. Do contact us using the channels mentioned above, we are interested in hearing from you! +
+
\ No newline at end of file diff --git a/docs/src/reference/docbook/reference/riak.xml b/docs/src/reference/docbook/reference/riak.xml new file mode 100644 index 000000000..6c5acdc66 --- /dev/null +++ b/docs/src/reference/docbook/reference/riak.xml @@ -0,0 +1,547 @@ + + + Riak Support + + Riak is a Key/Value datastore that supports Internet-scale data replication for high performance and high availability. Spring Data Key/Value (SDKV) provides access to the Riak datastore over the HTTP REST API using a built-in driver based on Spring 3.0's RestTemplate. In addition to making Key/Value datastore access easier from Java, the RiakTemplate has been designed, from the ground up, to be used from alternative JVM languages like Groovy or JRuby. + + Since the SDKV support for Riak uses the stateless REST API, there are no connection factories to manage or other stateful objects to keep tabs on. The helper you'll spend the most time working with is likely the thread-safe RiakTemplate or RiakKeyValueTemplate. Your choice of which to use will depend on how you want to manage buckets and keys. SDKV supports two ways to interact with Riak. If you want to use the convention you're likely already familiar with, namely of storing an entry with a given key in a "bucket" by passing the bucket and key name separately, you'll want to use the RiakTemplate. If you want to use a single object to represent your bucket and key pair, you can use the RiakKeyValueTemplate. It supports a key object that is encoded using one of several different methods: + + Using a String - You can concatenate two strings, separated by a colon: "mybucket:mykey". + Using a BucketKeyPair - You can pass an instance of BucketKeyPair, like SimpleBucketKeyPair. + Using a Map - You can pass a Map with keys for "bucket" and "key". + + + +
+ Configuring the <classname>RiakTemplate</classname> + + This is likely the easiest path to using SDKV for Riak, as the bucket and key are passed separately. The examples that follow will assume you're using this version of the the template. + + There are only two options you need to set to specify the Riak server to use in your RiakTemplate object: "defaultUri" and "mapReduceUri". Encoded with the URI should be placeholders for the bucket and the key, which will be filled in by the RestTemplate when the request is made. + + You can also turn the internal, ETag-based object cache off by setting useCache="false". It's generally recommended, however, to leave the internal cache on as the ETag matching will pick up any changes made to the entry on the Riak side and your application will benefit from greatly-increased performance for often-requested objects. + + + + + + +]]> + + +
+ Advanced Template Configuration + + There are a couple additional properties on the RiakTemplate that can be changed from their defaults. If you want to specify your own ConversionService to use when converting objects for storage inside Riak, then set it on the "conversionService" property: + + + + + + + +]]> + + + Depending on the application, it might be useful to set default Quality-of-Service parameters. In Riak paralance, these are the "dw", "w", and "r" parameters. They can be set to an integer representing the number of vnodes that need to report having received the data before declaring the operation a success, or the string "one", "all", or (the default) "quorum". These values can be overridden by passing a different set of QosParameters to the set/get operation you're performing. + + + + + + + +]]> + + + You can also set a specific ClassLoader to use when loading objects from Riak. Just set the classLoader property: + + + + + + +]]> + + + +
+
+ +
+ Working with Objects using the <classname>RiakTemplate</classname> + + One of the primary goals of the SDKV project is to make accessing Key/Value stores easier for the developer by taking away the mundane tasks of basic IO, buffering, type conversion, exception handling, and sundry other logistical concerns so the developer can focus on creating great applications. SDKV for Riak works toward this goal by making basic persistence and data access as easy as using a Map. + +
+ Saving data into Riak + + To store data in Riak, use one of the six different set methods: + + + + Additionally, there is a setWithMetaData method that takes a Map of metadata that will be set as the outgoing HTTP headers. To set custom metadata, your key should be prefixed with X-Riak-Meta- e.g. X-Riak-Meta-Custom-Header. + +
+ Letting Riak generate the key + + Riak has the ability to generate random IDs for you when storing objects. The RiakTemplate exposes this capability via the put method. It will return the ID it generated for you as a String. + + +
+
+ +
+ Retrieving data from Riak + + Retrieving data from Riak is just as easy. There are actually 13 different get methods on RiakTemplate that give the developer a wide range options for accessing and converting your data. + + Assuming you've stored a POJO using an appropriate set method, you can retrieve that object from Riak using a get: + + +
+
+ + + +
+ Map/Reduce + + Riak supports Map/Reduce functionality in a couple different ways. You can specify the Javascript source to execute (termed "anonymous" Javascript), you can reference some Javascript already stored in Riak at a specfic bucket and key, or you can reference an Erlang module and function. The Map/Reduce support in SDKV covers all these bases by giving you meaningful abstractions over the Map/Reduce job that represent the various aspects of the Map/Reduce process. + + At the highest level, every Map/Reduce request is represented by a MapReduceJob. The MapReduceJob represents the inputs, the phases, and the optional arg to send to Riak to execute the Map/Reduce job. The toJson method is responsible for serializing the entire job into the appropriate JSON data to send to Riak. + +
+ Specifying Inputs + + Riak will accept either a string denoting the bucket in which to get the list of keys to operate on, or a List of Lists denoting the bucket/key pairs to operate on while executing this Map/Reduce job. If you call the addInputs method on the job passing a List with a single string entry, the job will assume you want to operate on an entire bucket. Otherwise, you'll need to pass a multi-dimensional List of bucket/key pairs. + + To operate on an entire bucket: + bucket = new ArrayList() {{ + add("mybucket"); +}}; +job.addInputs(bucket); // Will M/R entire bucket + ]]> + + + To operate on a set of keys: + pair = new ArrayList() {{ + add("mybucket"); + add("mykey"); +}}; +List> keys = new ArrayList>() {{ + add(pair); +}}; +job.addInputs(keys); // Will M/R only specified keys + ]]> + +
+ +
+ Defining Phases + + Map/Reduce operations in Riak are broken up into phases. Phases contain a MapReduceOperation. There are currently two implementations to handle Javascript or Erlang M/R operations: JavascriptMapReduceOperation and ErlangMapReduceOperation. + + An example Map/Reduce job defining a single "map" phase defined in anonymous Javascript might look like this: + bucket = new ArrayList() {{ + add("mybucket"); +}}; + +job.addInputs(bucket); // M/R the entire bucket + +MapReduceOperation mapOper = new JavascriptMapReduceOperation("function(v){ ...M/R function body... }"); +MapReducePhase mapPhase = new RiakMapReducePhase("map", "javascript", mapOper); + +job.addPhase(mapPhase); + ]]> + +
+ +
+ Executing and Working with the Result + + To execute a configured job on your Riak server, use either the synchronous execute or asynchronous submit methods of your configured RiakTemplate: + + + +...or... + +List o = riak.execute(job, MyPojo.class); // Coerce to given type + +...or... + +Future> f = riak.submit(job); // Job runs in a separate thread + ]]> + +
+
+ +
+ Managing Bucket Properties + + It's sometimes useful to manage settings like the Quality-of-Service parameters w and dw (write and durable write thresholds) and the n_val setting at the bucket level. It's also possible to list the keys in a particular bucket by calling the getBucketSchema method, passing true as the second parameter, which tells the RiakTemplate to list the keys. + + To list the keys in a bucket, you would do something like this: + + schema = riak.getBucketSchema("mybucket", true); +List keys = schema.get("keys") +for(String key : keys) { + ...do something with each key... +} + ]]> + + + To update the bucket settings, pass a Map of properties: + + props = new HashMap(); +props.put("n_val", 6); +props.put("dw", 3); + +riak.updateBucketSchema("mybucket", props); + ]]> + + Only the properties specified in the passed-in Map will be updated. Properties that have already been set in previous operations and not specified in this operation will be unaffected. + + +
+ +
+ Asynchronous Access + + SDKV for Riak also includes an asynchronous version of most of the methods available to the RiakTemplate, whose method calls are all synchronous. The asynchronous version of the template is called AsyncRiakTemplate. + +
+ Template Configuration + + The AsyncRiakTemplate has the same basic configuration properties as the synchronous RiakTemplate. The only other property specific to the AsyncRiakTemplate you might want to configure is the thread pool the template uses to execute tasks asynchronously (by default a cached ThreadPoolExecutor). Set your ExecutorService on the template's workerPool property. + +
+ +
+ Callbacks + + Using the asynchronous Riak support in SDKV means you'll be relying on callbacks to execute your business logic when the requested operation is completed. All asynchronous operations follow a similar pattern: + + They are named similarly to their synchronous counterparts. + They take a AsyncKeyValueStoreOperation<?, ?> as a final parameter. + They return a Future<?>. + + + + To perform an asynchronous get on a JSON-serialized Map object which returns a custom object from the callback, you'd do something like: + future = riak.get("mybucket", "mykey", new AsyncKeyValueStoreOperation() { + + MyObject obj = new MyObject(); + + MyObject completed(KeyValueStoreMetaData meta, Map result) { + obj.setName(result.get("name")); + return obj; + } + + MyObject failed(Throwable error) { + obj.setError(error); + return obj; + } + +}); + +// Maybe do other work while waiting... +MyObject obj = future.get(); + ]]> + +
+ +
+ +
+ Groovy Builder Support + + If your application uses Groovy, either in a standalone context, or as part of a Grails application, then you could benefit from using the Groovy RiakBuilder that comes with SDKV for Riak. Underneath, it uses the AsyncRiakTemplate. To use the RiakBuilder, pass the constructor a configured AsyncRiakTemplate. + + Instances of RiakBuilder are NOT thread-safe and should not be shared across threads. + + The RiakBuilder implements an easy-to-use DSL for interacting with Riak. It doesn't implement the full set of methods available on the underlying AsyncRiakTemplate but a subset. The methods that the RiakBuilder responds to are: + + set + setAsBytes + put + get + getAsBytes + getAsType + containsKey + delete + foreach + + + +
+ Riak DSL Usage + + The following example illustrates the different uses of the Riak DSL, including batching requests together into a logical group, using a default bucket name (the node directly beneath riak will be considered the default bucket to use for the contained operations unless a different one is specified on the operation itself): + meta.key }} + put(value: [test: "value"]) { completed { v, meta -> meta.key }} + put(value: [test: "value"]) { completed { v, meta -> meta.key }} + put(value: [test: "value"]) { completed { v, meta -> meta.key }} + + mapreduce { + query { + map(arg: [test: "arg", alist: [1, 2, 3, 4]]) { + source "function(v, keyInfo, arg){ return [1]; }" + } + reduce { + source "function(v){ return Riak.reduceSum(v); }" + } + } + failed { it.printStackTrace() } + } + } +} +def results = riak.results + +riak.foreach(bucket: "test") { + completed { v, meta -> + riak.delete(bucket: "test", key: meta.key) + } +} + ]]> + + + Some important things to note from this example: + + Each operation in the Riak DSL has two callbacks: completed and failed. + The completed closure is passed either the result object, or, if your closure is defined with two parameters, the result object and the metadata associated with that entry. + Operations can be enclosed in an arbitrarily-named closure which the builder interprets as a default bucket name (in this case, the node "test" tells the builder to use the bucket name "test" for a default, unless one is specified on one of the enclosed operations). + Each operation within a builder's execution will be accumulated inside the special results property. Code that needs to know the output of individual operations within the batch can get access to that object through this property. Note that this means that RiakBuilder instances are NOT thread-safe. + + + + Even though the Riak DSL uses an asynchronous template underneath, all operations performed through the DSL will, by default, block until complete. To get a truly asynchronous operation, pass the parameter wait: 0 (or give a meaningful timeout in milliseconds to wait for the operation to complete) on the operation. + +
+ QosParameters on Riak DSL Operations + + You can pass QosParameters to Riak DSL operations by simply defining them as parameters to the operation: + + +
+ +
+ Working with Riak DSL Output + + The output of DSL operations will either be passed to the configured completed callback, or be returned to the caller if no callback is specified. In the example above, the mapreduce operation has no completed closure. Therefore, the return of the reduce phase is simply passed back to the builder, which makes that output available on the special results property. + + To gain access to the operation's results immediately, simply assign it to a variable: + + + If you add a non-zero wait value to the operation, "myobj" will contain a Future<?> rather than the result object itself. + + +
+
+
+ +
+ Working with streams + + SDKV for Riak includes a couple of useful helper objects to make reading and writing plain text or binary data in Riak really easy. If you want to store a file in Riak, then you can create a RiakOutputStream and simply write your data to it (making sure to call the "flush" method, which actually sends the data to Riak). + + + + Reading data from Riak is similarly easy. SDKV provides a java.io.File subclass that represents a resource in Riak. There's also a Spring IO Resource abstraction called RiakResource that can be used anywhere a Resource is required. There's also an InputStream implementation called RiakInputStream. + + + + +
+
\ No newline at end of file diff --git a/docs/src/reference/resources/css/highlight.css b/docs/src/reference/resources/css/highlight.css new file mode 100644 index 000000000..ffefef72d --- /dev/null +++ b/docs/src/reference/resources/css/highlight.css @@ -0,0 +1,35 @@ +/* + code highlight CSS resemblign the Eclipse IDE default color schema + @author Costin Leau +*/ + +.hl-keyword { + color: #7F0055; + font-weight: bold; +} + +.hl-comment { + color: #3F5F5F; + font-style: italic; +} + +.hl-multiline-comment { + color: #3F5FBF; + font-style: italic; +} + +.hl-tag { + color: #3F7F7F; +} + +.hl-attribute { + color: #7F007F; +} + +.hl-value { + color: #2A00FF; +} + +.hl-string { + color: #2A00FF; +} \ No newline at end of file diff --git a/docs/src/reference/resources/css/manual.css b/docs/src/reference/resources/css/manual.css new file mode 100644 index 000000000..77569070a --- /dev/null +++ b/docs/src/reference/resources/css/manual.css @@ -0,0 +1,99 @@ +@IMPORT url("highlight.css"); + +html { + padding: 0pt; + margin: 0pt; +} + +body { + margin-left: 10%; + margin-right: 10%; + font-family: Arial, Sans-serif; +} + +div { + margin: 0pt; +} + +p { + text-align: justify; +} + +hr { + border: 1px solid gray; + background: gray; +} + +h1,h2,h3,h4 { + color: #234623; + font-family: Arial, Sans-serif; +} + +pre { + line-height: 1.0; + color: black; +} + +pre.programlisting { + font-size: 10pt; + padding: 7pt 3pt; + border: 1pt solid black; + background: #eeeeee; + clear: both; +} + +div.table { + margin: 1em; + padding: 0.5em; + text-align: center; +} + +div.table table { + display: table; + width: 100%; +} + +div.table td { + padding-left: 7px; + padding-right: 7px; +} + +.sidebar { + float: right; + margin: 10px 0 10px 30px; + padding: 10px 20px 20px 20px; + width: 33%; + border: 1px solid black; + background-color: #F4F4F4; + font-size: 14px; +} + +.mediaobject { + padding-top: 30px; + padding-bottom: 30px; +} + +.legalnotice { + font-family: Verdana, Arial, helvetica, sans-serif; + font-size: 12px; + font-style: italic; +} + +p.releaseinfo { + font-size: 100%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; + padding-top: 10px; +} + +p.pubdate { + font-size: 120%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; +} + +span.productname { + font-size: 200%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; +} diff --git a/docs/src/reference/resources/images/admon/blank.png b/docs/src/reference/resources/images/admon/blank.png new file mode 100644 index 000000000..764bf4f0c Binary files /dev/null and b/docs/src/reference/resources/images/admon/blank.png differ diff --git a/docs/src/reference/resources/images/admon/caution.gif b/docs/src/reference/resources/images/admon/caution.gif new file mode 100644 index 000000000..d9f5e5b1b Binary files /dev/null and b/docs/src/reference/resources/images/admon/caution.gif differ diff --git a/docs/src/reference/resources/images/admon/caution.png b/docs/src/reference/resources/images/admon/caution.png new file mode 100644 index 000000000..5b7809ca4 Binary files /dev/null and b/docs/src/reference/resources/images/admon/caution.png differ diff --git a/docs/src/reference/resources/images/admon/caution.tif b/docs/src/reference/resources/images/admon/caution.tif new file mode 100644 index 000000000..4a282948c Binary files /dev/null and b/docs/src/reference/resources/images/admon/caution.tif differ diff --git a/docs/src/reference/resources/images/admon/draft.png b/docs/src/reference/resources/images/admon/draft.png new file mode 100644 index 000000000..0084708c9 Binary files /dev/null and b/docs/src/reference/resources/images/admon/draft.png differ diff --git a/docs/src/reference/resources/images/admon/home.gif b/docs/src/reference/resources/images/admon/home.gif new file mode 100644 index 000000000..6784f5bb0 Binary files /dev/null and b/docs/src/reference/resources/images/admon/home.gif differ diff --git a/docs/src/reference/resources/images/admon/home.png b/docs/src/reference/resources/images/admon/home.png new file mode 100644 index 000000000..cbb711de7 Binary files /dev/null and b/docs/src/reference/resources/images/admon/home.png differ diff --git a/docs/src/reference/resources/images/admon/important.gif b/docs/src/reference/resources/images/admon/important.gif new file mode 100644 index 000000000..6795d9a81 Binary files /dev/null and b/docs/src/reference/resources/images/admon/important.gif differ diff --git a/docs/src/reference/resources/images/admon/important.png b/docs/src/reference/resources/images/admon/important.png new file mode 100644 index 000000000..ad57f6f72 Binary files /dev/null and b/docs/src/reference/resources/images/admon/important.png differ diff --git a/docs/src/reference/resources/images/admon/important.tif b/docs/src/reference/resources/images/admon/important.tif new file mode 100644 index 000000000..184de6371 Binary files /dev/null and b/docs/src/reference/resources/images/admon/important.tif differ diff --git a/docs/src/reference/resources/images/admon/next.gif b/docs/src/reference/resources/images/admon/next.gif new file mode 100644 index 000000000..aa1516e69 Binary files /dev/null and b/docs/src/reference/resources/images/admon/next.gif differ diff --git a/docs/src/reference/resources/images/admon/next.png b/docs/src/reference/resources/images/admon/next.png new file mode 100644 index 000000000..45835bf89 Binary files /dev/null and b/docs/src/reference/resources/images/admon/next.png differ diff --git a/docs/src/reference/resources/images/admon/note.gif b/docs/src/reference/resources/images/admon/note.gif new file mode 100644 index 000000000..f329d359e Binary files /dev/null and b/docs/src/reference/resources/images/admon/note.gif differ diff --git a/docs/src/reference/resources/images/admon/note.png b/docs/src/reference/resources/images/admon/note.png new file mode 100644 index 000000000..ad57f6f72 Binary files /dev/null and b/docs/src/reference/resources/images/admon/note.png differ diff --git a/docs/src/reference/resources/images/admon/note.tif b/docs/src/reference/resources/images/admon/note.tif new file mode 100644 index 000000000..08644d6b5 Binary files /dev/null and b/docs/src/reference/resources/images/admon/note.tif differ diff --git a/docs/src/reference/resources/images/admon/prev.gif b/docs/src/reference/resources/images/admon/prev.gif new file mode 100644 index 000000000..64ca8f3c7 Binary files /dev/null and b/docs/src/reference/resources/images/admon/prev.gif differ diff --git a/docs/src/reference/resources/images/admon/prev.png b/docs/src/reference/resources/images/admon/prev.png new file mode 100644 index 000000000..cf24654f8 Binary files /dev/null and b/docs/src/reference/resources/images/admon/prev.png differ diff --git a/docs/src/reference/resources/images/admon/tip.gif b/docs/src/reference/resources/images/admon/tip.gif new file mode 100644 index 000000000..823f2b417 Binary files /dev/null and b/docs/src/reference/resources/images/admon/tip.gif differ diff --git a/docs/src/reference/resources/images/admon/tip.png b/docs/src/reference/resources/images/admon/tip.png new file mode 100644 index 000000000..ad57f6f72 Binary files /dev/null and b/docs/src/reference/resources/images/admon/tip.png differ diff --git a/docs/src/reference/resources/images/admon/tip.tif b/docs/src/reference/resources/images/admon/tip.tif new file mode 100644 index 000000000..4a3d8c75f Binary files /dev/null and b/docs/src/reference/resources/images/admon/tip.tif differ diff --git a/docs/src/reference/resources/images/admon/toc-blank.png b/docs/src/reference/resources/images/admon/toc-blank.png new file mode 100644 index 000000000..6ffad17a0 Binary files /dev/null and b/docs/src/reference/resources/images/admon/toc-blank.png differ diff --git a/docs/src/reference/resources/images/admon/toc-minus.png b/docs/src/reference/resources/images/admon/toc-minus.png new file mode 100644 index 000000000..abbb020c8 Binary files /dev/null and b/docs/src/reference/resources/images/admon/toc-minus.png differ diff --git a/docs/src/reference/resources/images/admon/toc-plus.png b/docs/src/reference/resources/images/admon/toc-plus.png new file mode 100644 index 000000000..941312ce0 Binary files /dev/null and b/docs/src/reference/resources/images/admon/toc-plus.png differ diff --git a/docs/src/reference/resources/images/admon/up.gif b/docs/src/reference/resources/images/admon/up.gif new file mode 100644 index 000000000..aabc2d016 Binary files /dev/null and b/docs/src/reference/resources/images/admon/up.gif differ diff --git a/docs/src/reference/resources/images/admon/up.png b/docs/src/reference/resources/images/admon/up.png new file mode 100644 index 000000000..07634de26 Binary files /dev/null and b/docs/src/reference/resources/images/admon/up.png differ diff --git a/docs/src/reference/resources/images/admon/warning.gif b/docs/src/reference/resources/images/admon/warning.gif new file mode 100644 index 000000000..c6acdec60 Binary files /dev/null and b/docs/src/reference/resources/images/admon/warning.gif differ diff --git a/docs/src/reference/resources/images/admon/warning.png b/docs/src/reference/resources/images/admon/warning.png new file mode 100644 index 000000000..ef3e10f40 Binary files /dev/null and b/docs/src/reference/resources/images/admon/warning.png differ diff --git a/docs/src/reference/resources/images/admon/warning.tif b/docs/src/reference/resources/images/admon/warning.tif new file mode 100644 index 000000000..7b6611ec7 Binary files /dev/null and b/docs/src/reference/resources/images/admon/warning.tif differ diff --git a/docs/src/reference/resources/images/callouts/1.png b/docs/src/reference/resources/images/callouts/1.png new file mode 100644 index 000000000..7d473430b Binary files /dev/null and b/docs/src/reference/resources/images/callouts/1.png differ diff --git a/docs/src/reference/resources/images/callouts/10.png b/docs/src/reference/resources/images/callouts/10.png new file mode 100644 index 000000000..997bbc824 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/10.png differ diff --git a/docs/src/reference/resources/images/callouts/11.png b/docs/src/reference/resources/images/callouts/11.png new file mode 100644 index 000000000..ce47dac3f Binary files /dev/null and b/docs/src/reference/resources/images/callouts/11.png differ diff --git a/docs/src/reference/resources/images/callouts/12.png b/docs/src/reference/resources/images/callouts/12.png new file mode 100644 index 000000000..31daf4e2f Binary files /dev/null and b/docs/src/reference/resources/images/callouts/12.png differ diff --git a/docs/src/reference/resources/images/callouts/13.png b/docs/src/reference/resources/images/callouts/13.png new file mode 100644 index 000000000..14021a89c Binary files /dev/null and b/docs/src/reference/resources/images/callouts/13.png differ diff --git a/docs/src/reference/resources/images/callouts/14.png b/docs/src/reference/resources/images/callouts/14.png new file mode 100644 index 000000000..64014b75f Binary files /dev/null and b/docs/src/reference/resources/images/callouts/14.png differ diff --git a/docs/src/reference/resources/images/callouts/15.png b/docs/src/reference/resources/images/callouts/15.png new file mode 100644 index 000000000..0d65765fc Binary files /dev/null and b/docs/src/reference/resources/images/callouts/15.png differ diff --git a/docs/src/reference/resources/images/callouts/2.png b/docs/src/reference/resources/images/callouts/2.png new file mode 100644 index 000000000..5d09341b2 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/2.png differ diff --git a/docs/src/reference/resources/images/callouts/3.png b/docs/src/reference/resources/images/callouts/3.png new file mode 100644 index 000000000..ef7b70047 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/3.png differ diff --git a/docs/src/reference/resources/images/callouts/4.png b/docs/src/reference/resources/images/callouts/4.png new file mode 100644 index 000000000..adb8364eb Binary files /dev/null and b/docs/src/reference/resources/images/callouts/4.png differ diff --git a/docs/src/reference/resources/images/callouts/5.png b/docs/src/reference/resources/images/callouts/5.png new file mode 100644 index 000000000..4d7eb4600 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/5.png differ diff --git a/docs/src/reference/resources/images/callouts/6.png b/docs/src/reference/resources/images/callouts/6.png new file mode 100644 index 000000000..0ba694af6 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/6.png differ diff --git a/docs/src/reference/resources/images/callouts/7.png b/docs/src/reference/resources/images/callouts/7.png new file mode 100644 index 000000000..472e96f8a Binary files /dev/null and b/docs/src/reference/resources/images/callouts/7.png differ diff --git a/docs/src/reference/resources/images/callouts/8.png b/docs/src/reference/resources/images/callouts/8.png new file mode 100644 index 000000000..5e60973c2 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/8.png differ diff --git a/docs/src/reference/resources/images/callouts/9.png b/docs/src/reference/resources/images/callouts/9.png new file mode 100644 index 000000000..a0676d26c Binary files /dev/null and b/docs/src/reference/resources/images/callouts/9.png differ diff --git a/docs/src/reference/resources/images/logo.png b/docs/src/reference/resources/images/logo.png new file mode 100644 index 000000000..a9f6d959e Binary files /dev/null and b/docs/src/reference/resources/images/logo.png differ diff --git a/docs/src/reference/resources/images/xdev-spring_logo.jpg b/docs/src/reference/resources/images/xdev-spring_logo.jpg new file mode 100644 index 000000000..622962ee3 Binary files /dev/null and b/docs/src/reference/resources/images/xdev-spring_logo.jpg differ diff --git a/docs/src/reference/resources/xsl/fopdf.xsl b/docs/src/reference/resources/xsl/fopdf.xsl new file mode 100644 index 000000000..4b3692f19 --- /dev/null +++ b/docs/src/reference/resources/xsl/fopdf.xsl @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + + + + + ( + + ) + + + + Copyright © 2010-2011 + + + + + + + + + + + + + + + + + + + + + + + + + + + + -5em + -5em + + + + + + + + + + + Spring Data Key Value () + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + 1 + 1 + 0 + + + + + + book toc + + + + 2 + + + + + + + + + + 0 + 0 + 0 + + + 5mm + 10mm + 10mm + + 15mm + 10mm + 0mm + + 18mm + 18mm + + + 0pc + + + + + justify + false + + + 11 + 8 + + + 1.4 + + + + + + + 0.8em + + + + + + 17.4cm + + + + 4pt + 4pt + 4pt + 4pt + + + + 0.1pt + 0.1pt + + + + + 1 + + + + + + + + left + bold + + + pt + + + + + + + + + + + + + + + 0.8em + 0.8em + 0.8em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.6em + 0.6em + 0.6em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.4em + 0.4em + 0.4em + + + pt + + 0.1em + 0.1em + 0.1em + + + + + bold + + + pt + + false + 0.4em + 0.6em + 0.8em + + + + + + + + + pt + + + + + 1em + 1em + 1em + #444444 + solid + 0.1pt + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + + + + 1 + + #F0F0F0 + + + + + + 0 + 1 + + + 90 + + + + + '1' + src/docbkx/resources/images/admons/ + + + + + + figure after + example before + equation before + table before + procedure before + + + + 1 + + + + 0.8em + 0.8em + 0.8em + 0.1em + 0.1em + 0.1em + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/reference/resources/xsl/highlight-fo.xsl b/docs/src/reference/resources/xsl/highlight-fo.xsl new file mode 100644 index 000000000..f0b5dd941 --- /dev/null +++ b/docs/src/reference/resources/xsl/highlight-fo.xsl @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/highlight.xsl b/docs/src/reference/resources/xsl/highlight.xsl new file mode 100644 index 000000000..c63c4765c --- /dev/null +++ b/docs/src/reference/resources/xsl/highlight.xsl @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/html-custom.xsl b/docs/src/reference/resources/xsl/html-custom.xsl new file mode 100644 index 000000000..575267e3c --- /dev/null +++ b/docs/src/reference/resources/xsl/html-custom.xsl @@ -0,0 +1,145 @@ + + + + + + + + + + '5' + '1' + + + 1 + + + 1 + + + 1 + 0 + 1 + + + + images/admon/ + .png + + 120 + images/callouts/ + .png + + + css/manual.css + text/css + book toc,title + + text-align: left + + + + + + + + + + + + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Begin Google Analytics code + + + End Google Analytics code + + + + + Begin LoopFuse code + + + End LoopFuse code + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/html-single-custom.xsl b/docs/src/reference/resources/xsl/html-single-custom.xsl new file mode 100644 index 000000000..264d9a81f --- /dev/null +++ b/docs/src/reference/resources/xsl/html-single-custom.xsl @@ -0,0 +1,142 @@ + + + + + + + + + + + 1 + + + 1 + + + 1 + 0 + 1 + + + + images/admon/ + .png + + 120 + images/callouts/ + .png + + + css/manual.css + text/css + book toc,title + + text-align: left + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Begin Google Analytics code + + +End Google Analytics code + + + + +Begin LoopFuse code + + +End LoopFuse code + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/html.xsl b/docs/src/reference/resources/xsl/html.xsl new file mode 100644 index 000000000..2b0f8d6e2 --- /dev/null +++ b/docs/src/reference/resources/xsl/html.xsl @@ -0,0 +1,107 @@ + + + + + + + + + + + 0 + 0 + 1 + + + + + + book toc + + + + 3 + + + + + 1 + + + + + + + 1 + + + 90 + + + + + 1 + images/admons/ + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + () + + + + +
+

Authors

+

+ +

+
+ + + + + + +
+ diff --git a/docs/src/reference/resources/xsl/html_chunk.xsl b/docs/src/reference/resources/xsl/html_chunk.xsl new file mode 100644 index 000000000..29b35d281 --- /dev/null +++ b/docs/src/reference/resources/xsl/html_chunk.xsl @@ -0,0 +1,221 @@ + + + + + + + + + + '5' + '1' + 0 + 0 + 1 + + + + book toc + qandaset toc + + + 3 + + + 1 + + + + + 1 + 90 + + + + + 1 + images/admons/ + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + () + + + + + +
+

Authors

+

+ +

+
+ + + + + + + + 1 + + + + + + + + + + + + + +
diff --git a/docs/src/reference/resources/xsl/pdf-custom.xsl b/docs/src/reference/resources/xsl/pdf-custom.xsl new file mode 100644 index 000000000..2b2290622 --- /dev/null +++ b/docs/src/reference/resources/xsl/pdf-custom.xsl @@ -0,0 +1,522 @@ + + + + + + + + + + + '1' + images/admon/ + .png + + + + + 24pt + + + + + + + + + + + + + + + + + + + + -5em + -5em + + + + + + book toc,title + + + + + + + + + + + + + + + + + please define productname in your docbook file! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + 1 + 1 + + + + + 0 + 0 + 0 + + + + false + + + 11 + 8 + + + 1.4 + + + + left + bold + + + pt + + + + + + + + + + + + + + + 0.8em + 0.8em + 0.8em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.6em + 0.6em + 0.6em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.4em + 0.4em + 0.4em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.3em + 0.3em + 0.3em + + + pt + + 0.1em + 0.1em + 0.1em + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4pt + 4pt + 4pt + 4pt + + + + 0.1pt + 0.1pt + + + + + + + + + + + + + + + + + + pt + + + + + 1em + 1em + 1em + 0.1em + 0.1em + 0.1em + + #444444 + solid + 0.1pt + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + + + + 1 + + #F0F0F0 + + + + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + + + + 0.5em + 0.5em + 0.5em + 0.1em + 0.1em + 0.1em + always + + + + + + normal + italic + + + pt + + false + 0.1em + 0.1em + 0.1em + + + + + + 0 + 1 + + + 90 + + + + + + figure after + example after + equation before + table before + procedure before + + + + 1 + + 0pt + + + 3 + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..f5a5999b1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,20 @@ +## Dependecies Version + +# Logging +log4jVersion = 1.2.16 +slf4jVersion = 1.6.1 + +# Common libraries +springVersion = 3.0.5.RELEASE +jacksonVersion = 1.6.4 + +# Testing +junitVersion = 4.8.1 +mockitoVersion = 1.8.5 + + +# -------------------- +# Project wide version +# -------------------- +springDataKeyValueVersion=1.0.0.BUILD-SNAPSHOT +version = 'springDataKeyValueVersion' \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..d8809f151 --- /dev/null +++ b/gradlew @@ -0,0 +1,168 @@ +#!/bin/bash + +############################################################################## +## ## +## Gradle wrapper script for UN*X ## +## ## +############################################################################## + +# Uncomment those lines to set JVM options. GRADLE_OPTS and JAVA_OPTS can be used together. +# GRADLE_OPTS="$GRADLE_OPTS -Xmx512m" +# JAVA_OPTS="$JAVA_OPTS -Xmx512m" + +GRADLE_APP_NAME=Gradle + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set JAVA_HOME if it's not already set. +if [ -z "$JAVA_HOME" ] ; then + if $darwin ; then + [ -z "$JAVA_HOME" -a -d "/Library/Java/Home" ] && export JAVA_HOME="/Library/Java/Home" + [ -z "$JAVA_HOME" -a -d "/System/Library/Frameworks/JavaVM.framework/Home" ] && export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Home" + else + javaExecutable="`which javac`" + [ -z "$javaExecutable" -o "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ] && die "JAVA_HOME not set and cannot find javac to deduce location, please set JAVA_HOME." + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + [ `expr "$readLink" : '\([^ ]*\)'` = "no" ] && die "JAVA_HOME not set and readlink not available, please set JAVA_HOME." + javaExecutable="`readlink -f \"$javaExecutable\"`" + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + export JAVA_HOME="$javaHome" + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVACMD" ] && JAVACMD=`cygpath --unix "$JAVACMD"` + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +STARTER_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain +CLASSPATH=`dirname "$0"`/gradle/wrapper/gradle-wrapper.jar +WRAPPER_PROPERTIES=`dirname "$0"`/gradle/wrapper/gradle-wrapper.properties +# Determine the Java command to use to start the JVM. +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="java" + fi +fi +if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +if [ -z "$JAVA_HOME" ] ; then + warn "JAVA_HOME environment variable is not set" +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add GRADLE_APP_NAME to the JAVA_OPTS as -Xdock:name +if $darwin; then + JAVA_OPTS="$JAVA_OPTS -Xdock:name=$GRADLE_APP_NAME" +# we may also want to set -Xdock:image +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + JAVA_HOME=`cygpath --path --mixed "$JAVA_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +GRADLE_APP_BASE_NAME=`basename "$0"` + +exec "$JAVACMD" $JAVA_OPTS $GRADLE_OPTS \ + -classpath "$CLASSPATH" \ + -Dorg.gradle.appname="$GRADLE_APP_BASE_NAME" \ + -Dorg.gradle.wrapper.properties="$WRAPPER_PROPERTIES" \ + $STARTER_MAIN_CLASS \ + "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..4855abb88 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem ## +@rem Gradle startup script for Windows ## +@rem ## +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Uncomment those lines to set JVM options. GRADLE_OPTS and JAVA_OPTS can be used together. +@rem set GRADLE_OPTS=%GRADLE_OPTS% -Xmx512m +@rem set JAVA_OPTS=%JAVA_OPTS% -Xmx512m + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=.\ + +@rem Find java.exe +set JAVA_EXE=java.exe +if not defined JAVA_HOME goto init + +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. +echo. +goto end + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set STARTER_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain +set CLASSPATH=%DIRNAME%\gradle\wrapper\gradle-wrapper.jar +set WRAPPER_PROPERTIES=%DIRNAME%\gradle\wrapper\gradle-wrapper.properties + +set GRADLE_OPTS=%JAVA_OPTS% %GRADLE_OPTS% -Dorg.gradle.wrapper.properties="%WRAPPER_PROPERTIES%" + +@rem Execute Gradle +"%JAVA_EXE%" %GRADLE_OPTS% -classpath "%CLASSPATH%" %STARTER_MAIN_CLASS% %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +if not "%OS%"=="Windows_NT" echo 1 > nul | choice /n /c:1 + +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit "%ERRORLEVEL%" +exit /b "%ERRORLEVEL%" + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega \ No newline at end of file diff --git a/maven.gradle b/maven.gradle new file mode 100644 index 000000000..996d1b693 --- /dev/null +++ b/maven.gradle @@ -0,0 +1,119 @@ +apply plugin: 'maven' + +// Create a source jar for uploading +task sourceJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +// Create a javadoc jar for uploading +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourceJar + archives javadocJar +} + +// Configuration for SpringSource s3 maven deployer +configurations { + deployerJars +} +dependencies { + deployerJars "org.springframework.build.aws:org.springframework.build.aws.maven:3.0.0.RELEASE" +} + +// Remove the archive configuration from the runtime configuration, so that anything added to archives +// (such as the source jar) is no longer included in the runtime classpath +configurations.default.extendsFrom = [configurations.runtime] as Set +// Add the main jar into the default configuration +artifacts { 'default' jar } + +gradle.taskGraph.whenReady {graph -> + if (graph.hasTask(uploadArchives)) { + // check properties defined and fail early + s3AccessKey + s3SecretAccessKey + } +} + +def deployer = null + +uploadArchives { + description = "Maven deploy of archives artifacts to SpringSource Maven repos" // url appended below + group = "Distribution" + // Maven deployment + def releaseRepositoryUrl = "file://${project.properties.mavenSyncRepoDir}" + def milestoneRepositoryUrl = 's3://maven.springframework.org/milestone' + def snapshotRepositoryUrl = 's3://maven.springframework.org/snapshot' + + // add a configuration with a classpath that includes our s3 maven deployer + configurations { deployerJars } + dependencies { + deployerJars "org.springframework.build.aws:org.springframework.build.aws.maven:3.0.0.RELEASE" + } + + deployer = repositories.mavenDeployer { + configuration = configurations.deployerJars + // releaseBuild + if (releaseBuild) { + logger.info("Deploying to local Maven repo " + releaseRepositoryUrl) + // "mavenSyncRepoDir" should be set in properties + repository(url: releaseRepositoryUrl) + } else { + s3credentials = [userName: project.properties.s3AccessKey, passphrase: project.properties.s3SecretAccessKey] + repository(url: milestoneRepositoryUrl) { + authentication(s3credentials) + } + snapshotRepository(url: snapshotRepositoryUrl) { + authentication(s3credentials) + } + } + } + + customizePom(deployer.pom) +} + +install { + customizePom(repositories.mavenInstaller.pom) +} + +def customizePom(pom) { + def optionalDeps = ['log4j','jsr250-api'] + + //pom.scopeMappings.addMapping(10, configurations.provided, 'provided') + pom.whenConfigured { p -> + // Remove test scope dependencies from published poms + p.dependencies = p.dependencies.findAll {it.scope != 'test'} + + // Flag optional deps + + p.dependencies.findAll { dep -> + optionalDeps.contains(dep.artifactId) || + dep.groupId.startsWith('org.slf4j') + }*.optional = true + + } + + pom.project { + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + // similar to Spring's configuration + dependencies { + dependency { + artifactId = groupId = 'commons-logging' + scope = 'compile' + optional = 'true' + version = '1.1.1' + } + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..78739438f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +rootProject.name = 'spring-data-key-value' + +include 'docs' +include "spring-data-keyvalue-core", + "spring-data-redis", + "spring-data-riak" + +docs = findProject(':docs') \ No newline at end of file diff --git a/spring-data-keyvalue-core/.classpath b/spring-data-keyvalue-core/.classpath new file mode 100644 index 000000000..e49979c1c --- /dev/null +++ b/spring-data-keyvalue-core/.classpath @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-keyvalue-core/.project b/spring-data-keyvalue-core/.project new file mode 100644 index 000000000..145c84059 --- /dev/null +++ b/spring-data-keyvalue-core/.project @@ -0,0 +1,17 @@ + + + spring-data-core + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/spring-data-keyvalue-core/.settings/org.eclipse.jdt.core.prefs b/spring-data-keyvalue-core/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..f924903e3 --- /dev/null +++ b/spring-data-keyvalue-core/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +# +#Thu Apr 21 21:26:44 EEST 2011 +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.source=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error diff --git a/spring-data-keyvalue-core/build.gradle b/spring-data-keyvalue-core/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/spring-data-keyvalue-core/src/main/java/org/springframework/data/keyvalue/UncategorizedKeyvalueStoreException.java b/spring-data-keyvalue-core/src/main/java/org/springframework/data/keyvalue/UncategorizedKeyvalueStoreException.java new file mode 100644 index 000000000..c65d6ac1f --- /dev/null +++ b/spring-data-keyvalue-core/src/main/java/org/springframework/data/keyvalue/UncategorizedKeyvalueStoreException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue; + +import org.springframework.dao.UncategorizedDataAccessException; + +public class UncategorizedKeyvalueStoreException extends UncategorizedDataAccessException { + + public UncategorizedKeyvalueStoreException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-data-keyvalue-core/template.mf b/spring-data-keyvalue-core/template.mf new file mode 100644 index 000000000..d553c5f67 --- /dev/null +++ b/spring-data-keyvalue-core/template.mf @@ -0,0 +1,19 @@ +Bundle-SymbolicName: org.springframework.data.keyvalue +Bundle-Name: Spring data Key-Value +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Package: + sun.reflect;version="0";resolution:=optional +Import-Template: + org.springframework.beans.*;version="[3.0.0, 4.0.0)", + org.springframework.core.*;version="[3.0.0, 4.0.0)", + org.springframework.dao.*;version="[3.0.0, 4.0.0)", + org.springframework.util.*;version="[3.0.0, 4.0.0)", + org.springframework.data.core.*;version="[1.0.0, 2.0.0)", + org.springframework.data.core.*;version="[1.0.0, 2.0.0)", + org.springframework.data.persistence.*;version="[1.0.0, 2.0.0)", + org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.w3c.dom.*;version="0" + + diff --git a/spring-data-redis/.classpath b/spring-data-redis/.classpath new file mode 100644 index 000000000..3848cd547 --- /dev/null +++ b/spring-data-redis/.classpath @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-redis/.gitignore b/spring-data-redis/.gitignore new file mode 100644 index 000000000..7ee4e668b --- /dev/null +++ b/spring-data-redis/.gitignore @@ -0,0 +1,2 @@ + +*.log \ No newline at end of file diff --git a/spring-data-redis/.project b/spring-data-redis/.project new file mode 100644 index 000000000..7ab2483ad --- /dev/null +++ b/spring-data-redis/.project @@ -0,0 +1,16 @@ + + + spring-data-redis + + + + org.eclipse.jdt.core.javanature + + + + org.eclipse.jdt.core.javabuilder + + + + + diff --git a/spring-data-redis/.settings/org.eclipse.jdt.core.prefs b/spring-data-redis/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..9811d21e9 --- /dev/null +++ b/spring-data-redis/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +# +#Fri Jul 01 15:44:20 EEST 2011 +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.source=1.6 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error diff --git a/spring-data-redis/build.gradle b/spring-data-redis/build.gradle new file mode 100644 index 000000000..cd6427bf6 --- /dev/null +++ b/spring-data-redis/build.gradle @@ -0,0 +1,17 @@ +repositories { + mavenRepo name: "ext-snapshots", urls: "http://springframework.svn.sourceforge.net/svnroot/springframework/repos/repo-ext/" +} + +dependencies { + compile project(":spring-data-keyvalue-core") + compile "redis.clients:jedis:$jedisVersion" + compile("org.jredis:jredis-anthonylauzon:$jredisVersion") { optional = true } + compile("org.idevlab:rjc:$rjcVersion") { optional = true } + compile("org.springframework:spring-oxm:$springVersion") { optional = true } + compile("commons-beanutils:commons-beanutils-core:1.8.3") { optional = true } + testCompile("javax.annotation:jsr250-api:1.0") { optional = true } + testCompile("com.thoughtworks.xstream:xstream:1.3") { optional = true } +} + +sourceCompatibility = 1.6 +targetCompatibility = 1.6 \ No newline at end of file diff --git a/spring-data-redis/gradle.properties b/spring-data-redis/gradle.properties new file mode 100644 index 000000000..74a43fc38 --- /dev/null +++ b/spring-data-redis/gradle.properties @@ -0,0 +1,13 @@ +# Dependencies properties +jedisVersion = 2.0.0 +jredisVersion = 03122010 +rjcVersion= 0.6.4 + + +# Manifest properties + +## OSGi ranges +spring.range = "[3.0.0, 4.0.0)" +jedis.range = "[2.0.0, 2.0.0]" +jackson.range = "[1.6, 2.0.0)" +rjc.range = "[0.6.4, 0.6.4]" diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/RedisConnectionFailureException.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/RedisConnectionFailureException.java new file mode 100644 index 000000000..4a49658cb --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/RedisConnectionFailureException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis; + +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * Fatal exception thrown when the Redis connection fails completely. + * + * @author Mark Pollack + */ +public class RedisConnectionFailureException extends DataAccessResourceFailureException { + + public RedisConnectionFailureException(String msg) { + super(msg); + } + + public RedisConnectionFailureException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/RedisSystemException.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/RedisSystemException.java new file mode 100644 index 000000000..b72123868 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/RedisSystemException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis; + +import org.springframework.data.keyvalue.UncategorizedKeyvalueStoreException; + +/** + * Exception thrown when we can't classify a Redis exception into one of Spring generic data access exceptions. + * + * @author Costin Leau + */ +public class RedisSystemException extends UncategorizedKeyvalueStoreException { + + public RedisSystemException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisCollectionParser.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisCollectionParser.java new file mode 100644 index 000000000..c806f8dcb --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisCollectionParser.java @@ -0,0 +1,48 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.config; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.data.keyvalue.redis.support.collections.RedisCollectionFactoryBean; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * Parser for the Redis <collection> element. + * + * @author Costin Leau + */ +public class RedisCollectionParser extends AbstractSimpleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return RedisCollectionFactoryBean.class; + } + + @Override + protected void postProcess(BeanDefinitionBuilder beanDefinition, Element element) { + String template = element.getAttribute("template"); + if (StringUtils.hasText(template)) { + beanDefinition.addPropertyReference("template", template); + } + } + + @Override + protected boolean isEligibleAttribute(String attributeName) { + return super.isEligibleAttribute(attributeName) && (!"template".equals(attributeName)); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisListenerContainerParser.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisListenerContainerParser.java new file mode 100644 index 000000000..8dca98e68 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisListenerContainerParser.java @@ -0,0 +1,139 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.data.keyvalue.redis.listener.ChannelTopic; +import org.springframework.data.keyvalue.redis.listener.PatternTopic; +import org.springframework.data.keyvalue.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.keyvalue.redis.listener.Topic; +import org.springframework.data.keyvalue.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; + +/** + * Parser for the Redis <listener-container> element. + * + * @author Costin Leau + */ +class RedisListenerContainerParser extends AbstractSimpleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return RedisMessageListenerContainer.class; + } + + @SuppressWarnings("unchecked") + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + // parse attributes (but replace the value assignment with references) + NamedNodeMap attributes = element.getAttributes(); + + for (int x = 0; x < attributes.getLength(); x++) { + Attr attribute = (Attr) attributes.item(x); + if (isEligibleAttribute(attribute, parserContext)) { + String propertyName = extractPropertyName(attribute.getLocalName()); + Assert.state(StringUtils.hasText(propertyName), + "Illegal property name returned from 'extractPropertyName(String)': cannot be null or empty."); + builder.addPropertyReference(propertyName, attribute.getValue()); + } + } + + String phase = element.getAttribute("phase"); + if (StringUtils.hasText(phase)) { + builder.addPropertyValue("phase", phase); + } + + postProcess(builder, element); + + // parse nested listeners + List listDefs = DomUtils.getChildElementsByTagName(element, "listener"); + + if (!listDefs.isEmpty()) { + ManagedMap> listeners = new ManagedMap>( + listDefs.size()); + for (Element listElement : listDefs) { + Object[] listenerDefinition = parseListener(listElement); + listeners.put((BeanDefinition) listenerDefinition[0], + (Collection) listenerDefinition[1]); + } + + builder.addPropertyValue("messageListeners", listeners); + } + } + + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (!"phase".equals(attributeName)); + } + + /** + * Parses a listener definition. Returns the listener bean reference definition (as the array first entry) and its associated topics (also as bean definitions). + * + * @param element + * @return + */ + private Object[] parseListener(Element element) { + Object[] ret = new Object[2]; + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MessageListenerAdapter.class); + builder.addConstructorArgReference(element.getAttribute("ref")); + + String method = element.getAttribute("method"); + if (StringUtils.hasText(method)){ + builder.addPropertyValue("defaultListenerMethod", method); + } + + String serializer = element.getAttribute("serializer"); + if (StringUtils.hasText(serializer)){ + builder.addPropertyReference("serializer", serializer); + } + + // assemble topics + Collection topics = new ArrayList(); + + // get topic + String xTopics = element.getAttribute("topic"); + if (StringUtils.hasText(xTopics)) { + String[] array = StringUtils.delimitedListToStringArray(xTopics, " "); + + for (String string : array) { + topics.add(string.contains("*") ? new PatternTopic(string) : new ChannelTopic(string)); + } + } + ret[0] = builder.getBeanDefinition(); + ret[1] = topics; + + return ret; + } + + @Override + protected boolean shouldGenerateId() { + return true; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisNamespaceHandler.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisNamespaceHandler.java new file mode 100644 index 000000000..2a136f377 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/config/RedisNamespaceHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.config; + +import org.springframework.beans.factory.xml.NamespaceHandler; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * {@link NamespaceHandler} for Spring Data Redis namespace. + * + * @author Costin Leau + */ +class RedisNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("listener-container", new RedisListenerContainerParser()); + registerBeanDefinitionParser("collection", new RedisCollectionParser()); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DataType.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DataType.java new file mode 100644 index 000000000..f1db9881e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DataType.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import java.util.EnumSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Enumeration of the Redis data types. + * + * @author Costin Leau + */ +public enum DataType { + + NONE("none"), STRING("string"), LIST("list"), SET("set"), ZSET("zset"), HASH("hash"); + + private static final Map codeLookup = new ConcurrentHashMap(6); + + static { + for (DataType type : EnumSet.allOf(DataType.class)) + codeLookup.put(type.code, type); + + } + + private final String code; + + DataType(String name) { + this.code = name; + } + + /** + * Returns the code associated with the current enum. + * + * @return code of this enum + */ + public String code() { + return code; + } + + /** + * Utility method for converting an enum code to an actual enum. + * + * @param code enum code + * @return actual enum corresponding to the given code + */ + public static DataType fromCode(String code) { + DataType data = codeLookup.get(code); + if (data == null) + throw new IllegalArgumentException("unknown data type code"); + return data; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultMessage.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultMessage.java new file mode 100644 index 000000000..1fd622577 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultMessage.java @@ -0,0 +1,52 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + + +/** + * Default message implementation. + * + * @author Costin Leau + */ +public class DefaultMessage implements Message { + + private final byte[] channel; + private final byte[] body; + private String toString; + + public DefaultMessage(byte[] channel, byte[] body) { + this.body = body; + this.channel = channel; + } + + @Override + public byte[] getChannel() { + return (channel != null ? channel.clone() : null); + } + + @Override + public byte[] getBody() { + return (body != null ? body.clone() : null); + } + + @Override + public String toString() { + if (toString == null){ + toString = new String(body); + } + return toString; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultSortParameters.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultSortParameters.java new file mode 100644 index 000000000..62a34bba1 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultSortParameters.java @@ -0,0 +1,167 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required byPattern applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Default implementation for {@link SortParameters}. + * + * @author Costin Leau + */ +public class DefaultSortParameters implements SortParameters { + + private byte[] byPattern; + private Range limit; + private final List getPattern = new ArrayList(4); + private Order order; + private Boolean alphabetic; + + /** + * Constructs a new DefaultSortParameters instance. + */ + public DefaultSortParameters() { + this(null, null, null, null, null); + } + + /** + * Constructs a new DefaultSortParameters instance. + * + * @param limit + * @param order + * @param alphabetic + */ + public DefaultSortParameters(Range limit, Order order, Boolean alphabetic) { + this(null, limit, null, order, alphabetic); + } + + /** + * Constructs a new DefaultSortParameters instance. + * + * @param byPattern + * @param limit + * @param getPattern + * @param order + * @param alphabetic + */ + public DefaultSortParameters(byte[] byPattern, Range limit, byte[][] getPattern, Order order, Boolean alphabetic) { + super(); + this.byPattern = byPattern; + this.limit = limit; + this.order = order; + this.alphabetic = alphabetic; + setGetPattern(getPattern); + } + + @Override + public byte[] getByPattern() { + return byPattern; + } + + public void setByPattern(byte[] byPattern) { + this.byPattern = byPattern; + } + + @Override + public Range getLimit() { + return limit; + } + + public void setLimit(Range limit) { + this.limit = limit; + } + + @Override + public byte[][] getGetPattern() { + return getPattern.toArray(new byte[getPattern.size()][]); + } + + public void addGetPattern(byte[] gPattern) { + getPattern.add(gPattern); + } + + public void setGetPattern(byte[][] gPattern) { + getPattern.clear(); + + for (byte[] bs : gPattern) { + getPattern.add(bs); + } + } + + @Override + public Order getOrder() { + return order; + } + + public void setOrder(Order order) { + this.order = order; + } + + @Override + public Boolean isAlphabetic() { + return alphabetic; + } + + public void setAlphabetic(Boolean alphabetic) { + this.alphabetic = alphabetic; + } + + // + // builder like methods + // + + public DefaultSortParameters order(Order order) { + setOrder(order); + return this; + } + + public DefaultSortParameters alpha() { + setAlphabetic(true); + return this; + } + + public DefaultSortParameters asc() { + setOrder(Order.ASC); + return this; + } + + public DefaultSortParameters desc() { + setOrder(Order.DESC); + return this; + } + + public DefaultSortParameters numeric() { + setAlphabetic(false); + return this; + } + + public DefaultSortParameters get(byte[] pattern) { + addGetPattern(pattern); + return this; + } + + public DefaultSortParameters by(byte[] pattern) { + setByPattern(pattern); + return this; + } + + public DefaultSortParameters limit(long start, long count) { + setLimit(new Range(start, count)); + return this; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultStringRedisConnection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultStringRedisConnection.java new file mode 100644 index 000000000..61b973ed3 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultStringRedisConnection.java @@ -0,0 +1,1155 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.RedisSystemException; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.SerializationUtils; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link StringRedisConnection}. + * + * @author Costin Leau + */ +public class DefaultStringRedisConnection implements StringRedisConnection { + + private final RedisConnection delegate; + private final RedisSerializer serializer; + + /** + * Constructs a new DefaultStringRedisConnection instance. + * Uses {@link StringRedisSerializer} as underlying serializer. + * + * @param connection Redis connection + */ + public DefaultStringRedisConnection(RedisConnection connection) { + Assert.notNull(connection, "connection is required"); + this.delegate = connection; + this.serializer = new StringRedisSerializer(); + } + + /** + * Constructs a new DefaultStringRedisConnection instance. + * + * @param connection Redis connection + * @param serializer String serializer + */ + public DefaultStringRedisConnection(RedisConnection connection, RedisSerializer serializer) { + Assert.notNull(connection, "connection is required"); + Assert.notNull(connection, "serializer is required"); + this.delegate = connection; + this.serializer = serializer; + } + + public Long append(byte[] key, byte[] value) { + return delegate.append(key, value); + } + + public void bgSave() { + delegate.bgSave(); + } + + public void bgWriteAof() { + delegate.bgWriteAof(); + } + + public List bLPop(int timeout, byte[]... keys) { + return delegate.bLPop(timeout, keys); + } + + public List bRPop(int timeout, byte[]... keys) { + return delegate.bRPop(timeout, keys); + } + + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + return delegate.bRPopLPush(timeout, srcKey, dstKey); + } + + public void close() throws RedisSystemException { + delegate.close(); + } + + public Long dbSize() { + return delegate.dbSize(); + } + + public Long decr(byte[] key) { + return delegate.decr(key); + } + + public Long decrBy(byte[] key, long value) { + return delegate.decrBy(key, value); + } + + public Long del(byte[]... keys) { + return delegate.del(keys); + } + + public void discard() { + delegate.discard(); + } + + public byte[] echo(byte[] message) { + return delegate.echo(message); + } + + public List exec() { + return delegate.exec(); + } + + public Boolean exists(byte[] key) { + return delegate.exists(key); + } + + public Boolean expire(byte[] key, long seconds) { + return delegate.expire(key, seconds); + } + + public Boolean expireAt(byte[] key, long unixTime) { + return delegate.expireAt(key, unixTime); + } + + public void flushAll() { + delegate.flushAll(); + } + + public void flushDb() { + delegate.flushDb(); + } + + public byte[] get(byte[] key) { + return delegate.get(key); + } + + public Boolean getBit(byte[] key, long offset) { + return delegate.getBit(key, offset); + } + + public List getConfig(String pattern) { + return delegate.getConfig(pattern); + } + + public Object getNativeConnection() { + return delegate.getNativeConnection(); + } + + public byte[] getRange(byte[] key, long start, long end) { + return delegate.getRange(key, start, end); + } + + public byte[] getSet(byte[] key, byte[] value) { + return delegate.getSet(key, value); + } + + public Subscription getSubscription() { + return delegate.getSubscription(); + } + + public Boolean hDel(byte[] key, byte[] field) { + return delegate.hDel(key, field); + } + + public Boolean hExists(byte[] key, byte[] field) { + return delegate.hExists(key, field); + } + + public byte[] hGet(byte[] key, byte[] field) { + return delegate.hGet(key, field); + } + + public Map hGetAll(byte[] key) { + return delegate.hGetAll(key); + } + + public Long hIncrBy(byte[] key, byte[] field, long delta) { + return delegate.hIncrBy(key, field, delta); + } + + public Set hKeys(byte[] key) { + return delegate.hKeys(key); + } + + public Long hLen(byte[] key) { + return delegate.hLen(key); + } + + public List hMGet(byte[] key, byte[]... fields) { + return delegate.hMGet(key, fields); + } + + public void hMSet(byte[] key, Map hashes) { + delegate.hMSet(key, hashes); + } + + public Boolean hSet(byte[] key, byte[] field, byte[] value) { + return delegate.hSet(key, field, value); + } + + public Boolean hSetNX(byte[] key, byte[] field, byte[] value) { + return delegate.hSetNX(key, field, value); + } + + public List hVals(byte[] key) { + return delegate.hVals(key); + } + + public Long incr(byte[] key) { + return delegate.incr(key); + } + + public Long incrBy(byte[] key, long value) { + return delegate.incrBy(key, value); + } + + public Properties info() { + return delegate.info(); + } + + public boolean isClosed() { + return delegate.isClosed(); + } + + public boolean isQueueing() { + return delegate.isQueueing(); + } + + public boolean isSubscribed() { + return delegate.isSubscribed(); + } + + public Set keys(byte[] pattern) { + return delegate.keys(pattern); + } + + public Long lastSave() { + return delegate.lastSave(); + } + + public byte[] lIndex(byte[] key, long index) { + return delegate.lIndex(key, index); + } + + public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { + return delegate.lInsert(key, where, pivot, value); + } + + public Long lLen(byte[] key) { + return delegate.lLen(key); + } + + public byte[] lPop(byte[] key) { + return delegate.lPop(key); + } + + public Long lPush(byte[] key, byte[] value) { + return delegate.lPush(key, value); + } + + public Long lPushX(byte[] key, byte[] value) { + return delegate.lPushX(key, value); + } + + public List lRange(byte[] key, long start, long end) { + return delegate.lRange(key, start, end); + } + + public Long lRem(byte[] key, long count, byte[] value) { + return delegate.lRem(key, count, value); + } + + public void lSet(byte[] key, long index, byte[] value) { + delegate.lSet(key, index, value); + } + + public void lTrim(byte[] key, long start, long end) { + delegate.lTrim(key, start, end); + } + + public List mGet(byte[]... keys) { + return delegate.mGet(keys); + } + + public void mSet(Map tuple) { + delegate.mSet(tuple); + } + + public void mSetNX(Map tuple) { + delegate.mSetNX(tuple); + } + + public void multi() { + delegate.multi(); + } + + public Boolean persist(byte[] key) { + return delegate.persist(key); + } + + public Boolean move(byte[] key, int dbIndex) { + return delegate.move(key, dbIndex); + } + + public String ping() { + return delegate.ping(); + } + + public void pSubscribe(MessageListener listener, byte[]... patterns) { + delegate.pSubscribe(listener, patterns); + } + + public Long publish(byte[] channel, byte[] message) { + return delegate.publish(channel, message); + } + + public byte[] randomKey() { + return delegate.randomKey(); + } + + public void rename(byte[] oldName, byte[] newName) { + delegate.rename(oldName, newName); + } + + public Boolean renameNX(byte[] oldName, byte[] newName) { + return delegate.renameNX(oldName, newName); + } + + public void resetConfigStats() { + delegate.resetConfigStats(); + } + + public byte[] rPop(byte[] key) { + return delegate.rPop(key); + } + + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + return delegate.rPopLPush(srcKey, dstKey); + } + + public Long rPush(byte[] key, byte[] value) { + return delegate.rPush(key, value); + } + + public Long rPushX(byte[] key, byte[] value) { + return delegate.rPushX(key, value); + } + + public Boolean sAdd(byte[] key, byte[] value) { + return delegate.sAdd(key, value); + } + + public void save() { + delegate.save(); + } + + public Long sCard(byte[] key) { + return delegate.sCard(key); + } + + public Set sDiff(byte[]... keys) { + return delegate.sDiff(keys); + } + + public void sDiffStore(byte[] destKey, byte[]... keys) { + delegate.sDiffStore(destKey, keys); + } + + public void select(int dbIndex) { + delegate.select(dbIndex); + } + + public void set(byte[] key, byte[] value) { + delegate.set(key, value); + } + + public void setBit(byte[] key, long offset, boolean value) { + delegate.setBit(key, offset, value); + } + + public void setConfig(String param, String value) { + delegate.setConfig(param, value); + } + + public void setEx(byte[] key, long seconds, byte[] value) { + delegate.setEx(key, seconds, value); + } + + public Boolean setNX(byte[] key, byte[] value) { + return delegate.setNX(key, value); + } + + public void setRange(byte[] key, byte[] value, long start) { + delegate.setRange(key, value, start); + } + + public void shutdown() { + delegate.shutdown(); + } + + public Set sInter(byte[]... keys) { + return delegate.sInter(keys); + } + + public void sInterStore(byte[] destKey, byte[]... keys) { + delegate.sInterStore(destKey, keys); + } + + public Boolean sIsMember(byte[] key, byte[] value) { + return delegate.sIsMember(key, value); + } + + public Set sMembers(byte[] key) { + return delegate.sMembers(key); + } + + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + return delegate.sMove(srcKey, destKey, value); + } + + public Long sort(byte[] key, SortParameters params, byte[] storeKey) { + return delegate.sort(key, params, storeKey); + } + + public List sort(byte[] key, SortParameters params) { + return delegate.sort(key, params); + } + + public byte[] sPop(byte[] key) { + return delegate.sPop(key); + } + + public byte[] sRandMember(byte[] key) { + return delegate.sRandMember(key); + } + + public Boolean sRem(byte[] key, byte[] value) { + return delegate.sRem(key, value); + } + + public Long strLen(byte[] key) { + return delegate.strLen(key); + } + + public void subscribe(MessageListener listener, byte[]... channels) { + delegate.subscribe(listener, channels); + } + + public Set sUnion(byte[]... keys) { + return delegate.sUnion(keys); + } + + public void sUnionStore(byte[] destKey, byte[]... keys) { + delegate.sUnionStore(destKey, keys); + } + + public Long ttl(byte[] key) { + return delegate.ttl(key); + } + + public DataType type(byte[] key) { + return delegate.type(key); + } + + public void unwatch() { + delegate.unwatch(); + } + + public void watch(byte[]... keys) { + delegate.watch(keys); + } + + public Boolean zAdd(byte[] key, double score, byte[] value) { + return delegate.zAdd(key, score, value); + } + + public Long zCard(byte[] key) { + return delegate.zCard(key); + } + + public Long zCount(byte[] key, double min, double max) { + return delegate.zCount(key, min, max); + } + + public Double zIncrBy(byte[] key, double increment, byte[] value) { + return delegate.zIncrBy(key, increment, value); + } + + public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + return delegate.zInterStore(destKey, aggregate, weights, sets); + } + + public Long zInterStore(byte[] destKey, byte[]... sets) { + return delegate.zInterStore(destKey, sets); + } + + public Set zRange(byte[] key, long start, long end) { + return delegate.zRange(key, start, end); + } + + public Set zRangeByScore(byte[] key, double min, double max, long offset, long count) { + return delegate.zRangeByScore(key, min, max, offset, count); + } + + public Set zRangeByScore(byte[] key, double min, double max) { + return delegate.zRangeByScore(key, min, max); + } + + public Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + return delegate.zRangeByScoreWithScores(key, min, max, offset, count); + } + + public Set zRangeByScoreWithScores(byte[] key, double min, double max) { + return delegate.zRangeByScoreWithScores(key, min, max); + } + + public Set zRangeWithScores(byte[] key, long start, long end) { + return delegate.zRangeWithScores(key, start, end); + } + + public Set zRevRangeByScore(byte[] key, double min, double max, long offset, long count) { + return delegate.zRevRangeByScore(key, min, max, offset, count); + } + + public Set zRevRangeByScore(byte[] key, double min, double max) { + return delegate.zRevRangeByScore(key, min, max); + } + + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + return delegate.zRevRangeByScoreWithScores(key, min, max, offset, count); + } + + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max) { + return delegate.zRevRangeByScoreWithScores(key, min, max); + } + + public Long zRank(byte[] key, byte[] value) { + return delegate.zRank(key, value); + } + + public Boolean zRem(byte[] key, byte[] value) { + return delegate.zRem(key, value); + } + + public Long zRemRange(byte[] key, long start, long end) { + return delegate.zRemRange(key, start, end); + } + + public Long zRemRangeByScore(byte[] key, double min, double max) { + return delegate.zRemRangeByScore(key, min, max); + } + + public Set zRevRange(byte[] key, long start, long end) { + return delegate.zRevRange(key, start, end); + } + + public Set zRevRangeWithScores(byte[] key, long start, long end) { + return delegate.zRevRangeWithScores(key, start, end); + } + + public Long zRevRank(byte[] key, byte[] value) { + return delegate.zRevRank(key, value); + } + + public Double zScore(byte[] key, byte[] value) { + return delegate.zScore(key, value); + } + + public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + return delegate.zUnionStore(destKey, aggregate, weights, sets); + } + + public Long zUnionStore(byte[] destKey, byte[]... sets) { + return delegate.zUnionStore(destKey, sets); + } + + // + // String methods + // + + private byte[] serialize(String data) { + return serializer.serialize(data); + } + + private byte[][] serializeMulti(String... keys) { + byte[][] ret = new byte[keys.length][]; + + for (int i = 0; i < ret.length; i++) { + ret[i] = serializer.serialize(keys[i]); + } + + return ret; + } + + private Map serialize(Map hashes) { + Map ret = new LinkedHashMap(hashes.size()); + + for (Map.Entry entry : hashes.entrySet()) { + ret.put(serializer.serialize(entry.getKey()), serializer.serialize(entry.getValue())); + } + + return ret; + } + + + private List deserialize(List data) { + return SerializationUtils.deserialize(data, serializer); + } + + private Set deserialize(Set data) { + return SerializationUtils.deserialize(data, serializer); + } + + private String deserialize(byte[] data) { + return serializer.deserialize(data); + } + + private Set deserializeTuple(Set data) { + if (data == null) { + return null; + } + Set result = new LinkedHashSet(data.size()); + for (Tuple raw : data) { + result.add(new DefaultStringTuple(raw, serializer.deserialize(raw.getValue()))); + } + + return result; + } + + @Override + public Long append(String key, String value) { + return delegate.append(serialize(key), serialize(value)); + } + + @Override + public List bLPop(int timeout, String... keys) { + return deserialize(delegate.bLPop(timeout, serializeMulti(keys))); + } + + @Override + public List bRPop(int timeout, String... keys) { + return deserialize(delegate.bRPop(timeout, serializeMulti(keys))); + } + + @Override + public String bRPopLPush(int timeout, String srcKey, String dstKey) { + return deserialize(delegate.bRPopLPush(timeout, serialize(srcKey), serialize(dstKey))); + } + + @Override + public Long decr(String key) { + return delegate.decr(serialize(key)); + } + + @Override + public Long decrBy(String key, long value) { + return delegate.decrBy(serialize(key), value); + } + + @Override + public Long del(String... keys) { + return delegate.del(serializeMulti(keys)); + } + + @Override + public String echo(String message) { + return deserialize(delegate.echo(serialize(message))); + } + + @Override + public Boolean exists(String key) { + return delegate.exists(serialize(key)); + } + + @Override + public Boolean expire(String key, long seconds) { + return delegate.expire(serialize(key), seconds); + } + + @Override + public Boolean expireAt(String key, long unixTime) { + return delegate.expireAt(serialize(key), unixTime); + } + + @Override + public String get(String key) { + return deserialize(delegate.get(serialize(key))); + } + + @Override + public Boolean getBit(String key, long offset) { + return delegate.getBit(serialize(key), offset); + } + + @Override + public String getRange(String key, long start, long end) { + return deserialize(delegate.getRange(serialize(key), start, end)); + } + + @Override + public String getSet(String key, String value) { + return deserialize(delegate.getSet(serialize(key), serialize(value))); + } + + @Override + public Boolean hDel(String key, String field) { + return delegate.hDel(serialize(key), serialize(field)); + } + + @Override + public Boolean hExists(String key, String field) { + return delegate.hExists(serialize(key), serialize(field)); + } + + @Override + public String hGet(String key, String field) { + return deserialize(delegate.hGet(serialize(key), serialize(field))); + } + + @Override + public Map hGetAll(String key) { + throw new UnsupportedOperationException(); + } + + @Override + public Long hIncrBy(String key, String field, long delta) { + return delegate.hIncrBy(serialize(key), serialize(field), delta); + } + + @Override + public Set hKeys(String key) { + return deserialize(delegate.hKeys(serialize(key))); + } + + @Override + public Long hLen(String key) { + return delegate.hLen(serialize(key)); + } + + @Override + public List hMGet(String key, String... fields) { + return deserialize(delegate.hMGet(serialize(key), serializeMulti(fields))); + } + + + @Override + public void hMSet(String key, Map hashes) { + delegate.hMSet(serialize(key), serialize(hashes)); + } + + @Override + public Boolean hSet(String key, String field, String value) { + return delegate.hSet(serialize(key), serialize(field), serialize(value)); + } + + @Override + public Boolean hSetNX(String key, String field, String value) { + return delegate.hSetNX(serialize(key), serialize(field), serialize(value)); + } + + @Override + public List hVals(String key) { + return deserialize(delegate.hVals(serialize(key))); + } + + @Override + public Long incr(String key) { + return delegate.incr(serialize(key)); + } + + @Override + public Long incrBy(String key, long value) { + return delegate.incrBy(serialize(key), value); + } + + @Override + public Collection keys(String pattern) { + return deserialize(delegate.keys(serialize(pattern))); + } + + @Override + public String lIndex(String key, long index) { + return deserialize(delegate.lIndex(serialize(key), index)); + } + + @Override + public Long lInsert(String key, Position where, String pivot, String value) { + return delegate.lInsert(serialize(key), where, serialize(pivot), serialize(value)); + } + + @Override + public Long lLen(String key) { + return delegate.lLen(serialize(key)); + } + + @Override + public String lPop(String key) { + return deserialize(delegate.lPop(serialize(key))); + } + + @Override + public Long lPush(String key, String value) { + return delegate.lPush(serialize(key), serialize(value)); + } + + @Override + public Long lPushX(String key, String value) { + return delegate.lPushX(serialize(key), serialize(value)); + } + + @Override + public List lRange(String key, long start, long end) { + return deserialize(delegate.lRange(serialize(key), start, end)); + } + + @Override + public Long lRem(String key, long count, String value) { + return delegate.lRem(serialize(key), count, serialize(value)); + } + + @Override + public void lSet(String key, long index, String value) { + delegate.lSet(serialize(key), index, serialize(value)); + } + + @Override + public void lTrim(String key, long start, long end) { + delegate.lTrim(serialize(key), start, end); + } + + @Override + public List mGet(String... keys) { + return deserialize(delegate.mGet(serializeMulti(keys))); + } + + @Override + public void mSetNXString(Map tuple) { + delegate.mSetNX(serialize(tuple)); + } + + @Override + public void mSetString(Map tuple) { + delegate.mSet(serialize(tuple)); + } + + @Override + public Boolean persist(String key) { + return delegate.persist(serialize(key)); + } + + @Override + public Boolean move(String key, int dbIndex) { + return delegate.move(serialize(key), dbIndex); + } + + @Override + public void pSubscribe(MessageListener listener, String... patterns) { + delegate.pSubscribe(listener, serializeMulti(patterns)); + } + + @Override + public Long publish(String channel, String message) { + return delegate.publish(serialize(channel), serialize(message)); + } + + @Override + public void rename(String oldName, String newName) { + delegate.rename(serialize(oldName), serialize(newName)); + } + + @Override + public Boolean renameNX(String oldName, String newName) { + return delegate.renameNX(serialize(oldName), serialize(newName)); + } + + @Override + public String rPop(String key) { + return deserialize(delegate.rPop(serialize(key))); + } + + @Override + public String rPopLPush(String srcKey, String dstKey) { + return deserialize(delegate.rPopLPush(serialize(srcKey), serialize(dstKey))); + } + + @Override + public Long rPush(String key, String value) { + return delegate.rPush(serialize(key), serialize(value)); + } + + @Override + public Long rPushX(String key, String value) { + return delegate.rPushX(serialize(key), serialize(value)); + } + + @Override + public Boolean sAdd(String key, String value) { + return delegate.sAdd(serialize(key), serialize(value)); + } + + @Override + public Long sCard(String key) { + return delegate.sCard(serialize(key)); + } + + @Override + public Set sDiff(String... keys) { + return deserialize(delegate.sDiff(serializeMulti(keys))); + } + + @Override + public void sDiffStore(String destKey, String... keys) { + delegate.sDiffStore(serialize(destKey), serializeMulti(keys)); + } + + @Override + public void set(String key, String value) { + delegate.set(serialize(key), serialize(value)); + } + + @Override + public void setBit(String key, long offset, boolean value) { + delegate.setBit(serialize(key), offset, value); + } + + @Override + public void setEx(String key, long seconds, String value) { + delegate.setEx(serialize(key), seconds, serialize(value)); + } + + @Override + public Boolean setNX(String key, String value) { + return delegate.setNX(serialize(key), serialize(value)); + } + + @Override + public void setRange(String key, String value, long start) { + delegate.setRange(serialize(key), serialize(value), start); + } + + @Override + public Set sInter(String... keys) { + return deserialize(delegate.sInter(serializeMulti(keys))); + } + + @Override + public void sInterStore(String destKey, String... keys) { + delegate.sInterStore(serialize(destKey), serializeMulti(keys)); + } + + @Override + public Boolean sIsMember(String key, String value) { + return delegate.sIsMember(serialize(key), serialize(value)); + } + + @Override + public Set sMembers(String key) { + return deserialize(delegate.sMembers(serialize(key))); + } + + @Override + public Boolean sMove(String srcKey, String destKey, String value) { + return delegate.sMove(serialize(srcKey), serialize(destKey), serialize(value)); + } + + @Override + public Long sort(String key, SortParameters params, String storeKey) { + return delegate.sort(serialize(key), params, serialize(storeKey)); + } + + @Override + public List sort(String key, SortParameters params) { + return deserialize(delegate.sort(serialize(key), params)); + } + + @Override + public String sPop(String key) { + return deserialize(delegate.sPop(serialize(key))); + } + + @Override + public String sRandMember(String key) { + return deserialize(delegate.sRandMember(serialize(key))); + } + + @Override + public Boolean sRem(String key, String value) { + return delegate.sRem(serialize(key), serialize(value)); + } + + @Override + public Long strLen(String key) { + return delegate.strLen(serialize(key)); + } + + @Override + public void subscribe(MessageListener listener, String... channels) { + delegate.subscribe(listener, serializeMulti(channels)); + } + + @Override + public Set sUnion(String... keys) { + return deserialize(delegate.sUnion(serializeMulti(keys))); + } + + @Override + public void sUnionStore(String destKey, String... keys) { + delegate.sUnionStore(serialize(destKey), serializeMulti(keys)); + } + + @Override + public Long ttl(String key) { + return delegate.ttl(serialize(key)); + } + + @Override + public DataType type(String key) { + return delegate.type(serialize(key)); + } + + @Override + public Boolean zAdd(String key, double score, String value) { + return delegate.zAdd(serialize(key), score, serialize(value)); + } + + @Override + public Long zCard(String key) { + return delegate.zCard(serialize(key)); + } + + @Override + public Long zCount(String key, double min, double max) { + return delegate.zCount(serialize(key), min, max); + } + + @Override + public Double zIncrBy(String key, double increment, String value) { + return delegate.zIncrBy(serialize(key), increment, serialize(value)); + } + + @Override + public Long zInterStore(String destKey, Aggregate aggregate, int[] weights, String... sets) { + return delegate.zInterStore(serialize(destKey), aggregate, weights, serializeMulti(sets)); + } + + @Override + public Long zInterStore(String destKey, String... sets) { + return delegate.zInterStore(serialize(destKey), serializeMulti(sets)); + } + + @Override + public Set zRange(String key, long start, long end) { + return deserialize(delegate.zRange(serialize(key), start, end)); + } + + @Override + public Set zRangeByScore(String key, double min, double max, long offset, long count) { + return deserialize(delegate.zRangeByScore(serialize(key), min, max, offset, count)); + } + + @Override + public Set zRangeByScore(String key, double min, double max) { + return deserialize(delegate.zRangeByScore(serialize(key), min, max)); + } + + @Override + public Set zRangeByScoreWithScores(String key, double min, double max, long offset, long count) { + return deserializeTuple(delegate.zRangeByScoreWithScores(serialize(key), min, max, offset, count)); + } + + @Override + public Set zRangeByScoreWithScores(String key, double min, double max) { + return deserializeTuple(delegate.zRangeByScoreWithScores(serialize(key), min, max)); + } + + @Override + public Set zRangeWithScores(String key, long start, long end) { + return deserializeTuple(delegate.zRangeWithScores(serialize(key), start, end)); + } + + @Override + public Long zRank(String key, String value) { + return delegate.zRank(serialize(key), serialize(value)); + } + + @Override + public Boolean zRem(String key, String value) { + return delegate.zRem(serialize(key), serialize(value)); + } + + @Override + public Long zRemRange(String key, long start, long end) { + return delegate.zRemRange(serialize(key), start, end); + } + + @Override + public Long zRemRangeByScore(String key, double min, double max) { + return delegate.zRemRangeByScore(serialize(key), min, max); + } + + @Override + public Set zRevRange(String key, long start, long end) { + return deserialize(delegate.zRevRange(serialize(key), start, end)); + } + + @Override + public Set zRevRangeWithScores(String key, long start, long end) { + return deserializeTuple(delegate.zRevRangeWithScores(serialize(key), start, end)); + } + + @Override + public Long zRevRank(String key, String value) { + return delegate.zRevRank(serialize(key), serialize(value)); + } + + @Override + public Double zScore(String key, String value) { + return delegate.zScore(serialize(key), serialize(value)); + } + + @Override + public Long zUnionStore(String destKey, Aggregate aggregate, int[] weights, String... sets) { + return delegate.zUnionStore(serialize(destKey), aggregate, weights, serializeMulti(sets)); + } + + @Override + public Long zUnionStore(String destKey, String... sets) { + return delegate.zUnionStore(serialize(destKey), serializeMulti(sets)); + } + + @Override + public List closePipeline() { + return delegate.closePipeline(); + } + + @Override + public boolean isPipelined() { + return delegate.isPipelined(); + } + + @Override + public void openPipeline() { + delegate.openPipeline(); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultStringTuple.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultStringTuple.java new file mode 100644 index 000000000..9ed234216 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultStringTuple.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import org.springframework.data.keyvalue.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.keyvalue.redis.connection.StringRedisConnection.StringTuple; + +/** + * Default implementation for {@link StringTuple} interface. + * + * @author Costin Leau + */ +public class DefaultStringTuple extends DefaultTuple implements StringTuple { + + private final String valueAsString; + + /** + * Constructs a new DefaultStringTuple instance. + * + * @param value + * @param score + */ + public DefaultStringTuple(byte[] value, String valueAsString, Double score) { + super(value, score); + this.valueAsString = valueAsString; + + } + + /** + * Constructs a new DefaultStringTuple instance. + * + * @param tuple + * @param valueAsString + */ + public DefaultStringTuple(Tuple tuple, String valueAsString) { + super(tuple.getValue(), tuple.getScore()); + this.valueAsString = valueAsString; + } + + @Override + public String getValueAsString() { + return valueAsString; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultTuple.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultTuple.java new file mode 100644 index 000000000..e9c366fda --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultTuple.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import org.springframework.data.keyvalue.redis.connection.RedisZSetCommands.Tuple; + +/** + * Default implementation for {@link Tuple} interface. + * + * @author Costin Leau + */ +public class DefaultTuple implements Tuple { + + private final Double score; + private final byte[] value; + + + /** + * Constructs a new DefaultTuple instance. + * + * @param value + * @param score + */ + public DefaultTuple(byte[] value, Double score) { + this.score = score; + this.value = value; + } + + @Override + public Double getScore() { + return score; + } + + @Override + public byte[] getValue() { + return value; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/Message.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/Message.java new file mode 100644 index 000000000..0a1b17010 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/Message.java @@ -0,0 +1,40 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.io.Serializable; + +/** + * Class encapsulating a Redis message body and its properties. + * + * @author Costin Leau + */ +public interface Message extends Serializable { + + /** + * Returns the body (or the payload) of the message. + * + * @return message body + */ + byte[] getBody(); + + /** + * Returns the channel associated with the message. + * + * @return message channel. + */ + byte[] getChannel(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/MessageListener.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/MessageListener.java new file mode 100644 index 000000000..6e75495f0 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/MessageListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +/** + * Listener of messages published in Redis. + * + * @author Costin Leau + */ +public interface MessageListener { + + /** + * Callback for processing received objects through Redis. + * + * @param message message + * @param pattern pattern matching the channel (if specified) - can be null + */ + void onMessage(Message message, byte[] pattern); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisCommands.java new file mode 100644 index 000000000..072439633 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisCommands.java @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + + +/** + * Interface for the commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisCommands extends RedisKeyCommands, RedisStringCommands, RedisListCommands, RedisSetCommands, + RedisZSetCommands, RedisHashCommands, RedisTxCommands, RedisPubSubCommands, RedisConnectionCommands, + RedisServerCommands { +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnection.java new file mode 100644 index 000000000..6f9f0a2fd --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnection.java @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import java.util.List; + +import org.springframework.dao.DataAccessException; + +/** + * A connection to a Redis server. Acts as an common abstraction across various + * Redis client libraries (or drivers). Additionally performs exception translation + * between the underlying Redis client library and Spring DAO exceptions. + * + * The methods follow as much as possible the Redis names and conventions. + * + * @author Costin Leau + */ +public interface RedisConnection extends RedisCommands { + + /** + * Closes (or quits) the connection. + * + * @throws DataAccessException + */ + void close() throws DataAccessException; + + /** + * Indicates whether the underlying connection is closed or not. + * + * @return true if the connection is closed, false otherwise. + */ + boolean isClosed(); + + /** + * Returns the native connection (the underlying library/driver object). + * + * @return underlying, native object + */ + Object getNativeConnection(); + + /** + * Indicates whether the connection is in "queue"(or "MULTI") mode or not. + * When queueing, all commands are postponed until EXEC or DISCARD commands + * are issued. + * Since in queueing no results are returned, the connection will return NULL + * on all operations that interact with the data. + * + * @return true if the connection is in queue/MULTI mode, false otherwise + */ + boolean isQueueing(); + + /** + * Indicates whether the connection is currently pipelined or not. + * + * @return true if the connection is pipelined, false otherwise + * @see #openPipeline() + * @see #isQueueing() + */ + boolean isPipelined(); + + /** + * Activates the pipeline mode for this connection. When pipelined, all commands return null + * (the reply is read at the end through {@link #closePipeline()}. + * Calling this method when the connection is already pipelined has no effect. + * + * Pipelining is used for issuing commands without requesting the response right away but rather + * at the end of the batch. While somewhat similar to MULTI, pipelining does not + * guarantee atomicity - it only tries to improve performance when issuing a lot of + * commands (such as in batching scenarios). + * + *

Note:

Consider doing some performance testing before using this feature since + * in many cases the performance benefits are minimal yet the impact on usage are not. + * + * @see #multi() + */ + void openPipeline(); + + /** + * Executes the commands in the pipeline and returns their result. + * If the connection is not pipelined, an empty collection is returned. + * + * @return the result of the executed commands. + */ + List closePipeline(); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnectionCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnectionCommands.java new file mode 100644 index 000000000..f3a3ac104 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnectionCommands.java @@ -0,0 +1,32 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + + +/** + * Connection-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisConnectionCommands { + + public abstract void select(int dbIndex); + + public abstract byte[] echo(byte[] message); + + public abstract String ping(); + +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnectionFactory.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnectionFactory.java new file mode 100644 index 000000000..e7ff2beff --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisConnectionFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import org.springframework.dao.support.PersistenceExceptionTranslator; + +/** + * Thread-safe factory of Redis connections. + * + * @author Costin Leau + */ +public interface RedisConnectionFactory extends PersistenceExceptionTranslator { + + /** + * Provides a suitable connection for interacting with Redis. + * + * @return connection for interacting with Redis. + */ + RedisConnection getConnection(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisHashCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisHashCommands.java new file mode 100644 index 000000000..6437627f9 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisHashCommands.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Hash-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisHashCommands { + + Boolean hSet(byte[] key, byte[] field, byte[] value); + + Boolean hSetNX(byte[] key, byte[] field, byte[] value); + + byte[] hGet(byte[] key, byte[] field); + + List hMGet(byte[] key, byte[]... fields); + + void hMSet(byte[] key, Map hashes); + + Long hIncrBy(byte[] key, byte[] field, long delta); + + Boolean hExists(byte[] key, byte[] field); + + Boolean hDel(byte[] key, byte[] field); + + Long hLen(byte[] key); + + Set hKeys(byte[] key); + + List hVals(byte[] key); + + Map hGetAll(byte[] key); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisInvalidSubscriptionException.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisInvalidSubscriptionException.java new file mode 100644 index 000000000..485a87bae --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisInvalidSubscriptionException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Exception thrown when subscribing to an expired/dead {@link Subscription}. + * + * @author Costin Leau + */ +public class RedisInvalidSubscriptionException extends InvalidDataAccessResourceUsageException { + + /** + * Constructs a new RedisInvalidSubscriptionException instance. + * + * @param msg + * @param cause + */ + public RedisInvalidSubscriptionException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs a new RedisInvalidSubscriptionException instance. + * + * @param msg + */ + public RedisInvalidSubscriptionException(String msg) { + super(msg); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisKeyCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisKeyCommands.java new file mode 100644 index 000000000..41d047c35 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisKeyCommands.java @@ -0,0 +1,58 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.util.List; +import java.util.Set; + + +/** + * Key-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisKeyCommands { + + public abstract Boolean exists(byte[] key); + + public abstract Long del(byte[]... keys); + + public abstract DataType type(byte[] key); + + public abstract Set keys(byte[] pattern); + + public abstract byte[] randomKey(); + + public abstract void rename(byte[] oldName, byte[] newName); + + public abstract Boolean renameNX(byte[] oldName, byte[] newName); + + public abstract Boolean expire(byte[] key, long seconds); + + public abstract Boolean expireAt(byte[] key, long unixTime); + + public abstract Boolean persist(byte[] key); + + public abstract Boolean move(byte[] key, int dbIndex); + + public abstract Long ttl(byte[] key); + + // sort commands + public abstract List sort(byte[] key, SortParameters params); + + public abstract Long sort(byte[] key, SortParameters params, byte[] storeKey); + +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisListCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisListCommands.java new file mode 100644 index 000000000..deee6298e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisListCommands.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import java.util.List; + +/** + * List-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisListCommands { + + /** + * List insertion position. + */ + public enum Position { + BEFORE, AFTER + } + + Long rPush(byte[] key, byte[] value); + + Long lPush(byte[] key, byte[] value); + + Long rPushX(byte[] key, byte[] value); + + Long lPushX(byte[] key, byte[] value); + + Long lLen(byte[] key); + + List lRange(byte[] key, long begin, long end); + + void lTrim(byte[] key, long begin, long end); + + byte[] lIndex(byte[] key, long index); + + Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value); + + void lSet(byte[] key, long index, byte[] value); + + Long lRem(byte[] key, long count, byte[] value); + + byte[] lPop(byte[] key); + + byte[] rPop(byte[] key); + + List bLPop(int timeout, byte[]... keys); + + List bRPop(int timeout, byte[]... keys); + + byte[] rPopLPush(byte[] srcKey, byte[] dstKey); + + byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisPubSubCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisPubSubCommands.java new file mode 100644 index 000000000..64772fe0d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisPubSubCommands.java @@ -0,0 +1,77 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +/** + * PubSub-specific Redis commands. + * + * @author Costin Leau + */ +public interface RedisPubSubCommands { + + /** + * Indicates whether the current connection is subscribed (to at least one channel) + * or not. + * + * @return true if the connection is subscribed, false otherwise + */ + boolean isSubscribed(); + + /** + * Returns the current subscription for this connection or null if the connection is + * not subscribed. + * + * @return the current subscription, null if none is available + */ + Subscription getSubscription(); + + /** + * Publishes the given message to the given channel. + * + * @param channel the channel to publish to + * @param message message to publish + * @return the number of clients that received the message + */ + Long publish(byte[] channel, byte[] message); + + /** + * Subscribes the connection to the given channels. + * Once subscribed, a connection + * enters listening mode and can only subscribe to other channels or unsubscribe. + * No other commands are accepted until the connection is unsubscribed. + *

+ * Note that this operation is blocking and the current thread starts waiting + * for new messages immediately. + * + * @param listener message listener + * @param channels channel names + */ + void subscribe(MessageListener listener, byte[]... channels); + + /** + * Subscribes the connection to all channels matching the given patterns. + * Once subscribed, a connection + * enters listening mode and can only subscribe to other channels or unsubscribe. + * No other commands are accepted until the connection is unsubscribed. + *

+ * Note that this operation is blocking and the current thread starts waiting + * for new messages immediately. + * + * @param listener message listener + * @param patterns channel name patterns + */ + void pSubscribe(MessageListener listener, byte[]... patterns); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisServerCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisServerCommands.java new file mode 100644 index 000000000..09e236225 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisServerCommands.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.util.List; +import java.util.Properties; + +/** + * Server-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisServerCommands { + + void bgWriteAof(); + + void bgSave(); + + Long lastSave(); + + void save(); + + Long dbSize(); + + void flushDb(); + + void flushAll(); + + Properties info(); + + void shutdown(); + + List getConfig(String pattern); + + void setConfig(String param, String value); + + void resetConfigStats(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisSetCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisSetCommands.java new file mode 100644 index 000000000..b7ef2401e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisSetCommands.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import java.util.Set; + +/** + * Set-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisSetCommands { + + Boolean sAdd(byte[] key, byte[] value); + + Boolean sRem(byte[] key, byte[] value); + + byte[] sPop(byte[] key); + + Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value); + + Long sCard(byte[] key); + + Boolean sIsMember(byte[] key, byte[] value); + + Set sInter(byte[]... keys); + + void sInterStore(byte[] destKey, byte[]... keys); + + Set sUnion(byte[]... keys); + + void sUnionStore(byte[] destKey, byte[]... keys); + + Set sDiff(byte[]... keys); + + void sDiffStore(byte[] destKey, byte[]... keys); + + Set sMembers(byte[] key); + + byte[] sRandMember(byte[] key); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisStringCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisStringCommands.java new file mode 100644 index 000000000..d763774ae --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisStringCommands.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import java.util.List; +import java.util.Map; + +/** + * String/Value-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisStringCommands { + + byte[] get(byte[] key); + + byte[] getSet(byte[] key, byte[] value); + + List mGet(byte[]... keys); + + void set(byte[] key, byte[] value); + + Boolean setNX(byte[] key, byte[] value); + + void setEx(byte[] key, long seconds, byte[] value); + + void mSet(Map tuple); + + void mSetNX(Map tuple); + + Long incr(byte[] key); + + Long incrBy(byte[] key, long value); + + Long decr(byte[] key); + + Long decrBy(byte[] key, long value); + + Long append(byte[] key, byte[] value); + + byte[] getRange(byte[] key, long begin, long end); + + void setRange(byte[] key, byte[] value, long offset); + + Boolean getBit(byte[] key, long offset); + + void setBit(byte[] key, long offset, boolean value); + + Long strLen(byte[] key); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisSubscribedConnectionException.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisSubscribedConnectionException.java new file mode 100644 index 000000000..bcc93bab7 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisSubscribedConnectionException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Exception thrown when issuing commands on a connection that is subscribed and waiting + * for events. + * + * @author Costin Leau + * @see org.springframework.data.keyvalue.redis.connection.RedisPubSubCommands + */ +public class RedisSubscribedConnectionException extends InvalidDataAccessApiUsageException { + + /** + * Constructs a new RedisSubscribedConnectionException instance. + * + * @param msg + * @param cause + */ + public RedisSubscribedConnectionException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs a new RedisSubscribedConnectionException instance. + * + * @param msg + */ + public RedisSubscribedConnectionException(String msg) { + super(msg); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisTxCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisTxCommands.java new file mode 100644 index 000000000..73f82f600 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisTxCommands.java @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.util.List; + + +/** + * Transaction/Batch specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisTxCommands { + + void multi(); + + List exec(); + + void discard(); + + void watch(byte[]... keys); + + void unwatch(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisZSetCommands.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisZSetCommands.java new file mode 100644 index 000000000..ff6073b8c --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/RedisZSetCommands.java @@ -0,0 +1,96 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import java.util.Set; + + +/** + * ZSet(SortedSet)-specific commands supported by Redis. + * + * @author Costin Leau + */ +public interface RedisZSetCommands { + + /** + * Sort aggregation operations. + */ + public enum Aggregate { + SUM, MIN, MAX; + } + + /** + * ZSet tuple. + */ + public interface Tuple { + byte[] getValue(); + + Double getScore(); + } + + Boolean zAdd(byte[] key, double score, byte[] value); + + Boolean zRem(byte[] key, byte[] value); + + Double zIncrBy(byte[] key, double increment, byte[] value); + + Long zRank(byte[] key, byte[] value); + + Long zRevRank(byte[] key, byte[] value); + + Set zRange(byte[] key, long begin, long end); + + Set zRangeWithScores(byte[] key, long begin, long end); + + Set zRangeByScore(byte[] key, double min, double max); + + Set zRangeByScoreWithScores(byte[] key, double min, double max); + + Set zRangeByScore(byte[] key, double min, double max, long offset, long count); + + Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count); + + Set zRevRange(byte[] key, long begin, long end); + + Set zRevRangeWithScores(byte[] key, long begin, long end); + + Set zRevRangeByScore(byte[] key, double min, double max); + + Set zRevRangeByScoreWithScores(byte[] key, double min, double max); + + Set zRevRangeByScore(byte[] key, double min, double max, long offset, long count); + + Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count); + + Long zCount(byte[] key, double min, double max); + + Long zCard(byte[] key); + + Double zScore(byte[] key, byte[] value); + + Long zRemRange(byte[] key, long begin, long end); + + Long zRemRangeByScore(byte[] key, double min, double max); + + Long zUnionStore(byte[] destKey, byte[]... sets); + + Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets); + + Long zInterStore(byte[] destKey, byte[]... sets); + + Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/SortParameters.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/SortParameters.java new file mode 100644 index 000000000..ca69f3315 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/SortParameters.java @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +/** + * Entity containing the parameters for the SORT operation. + * + * @author Costin Leau + */ +public interface SortParameters { + + /** + * Sorting order. + */ + public enum Order { + ASC, DESC + } + + /** + * Utility class wrapping the 'LIMIT' setting. + * + */ + static class Range { + private final long start; + private final long count; + + public Range(long start, long count) { + this.start = start; + this.count = count; + } + + public long getStart() { + return start; + } + + public long getCount() { + return count; + } + } + + /** + * Returns the sorting order. Can be null if nothing is specified. + * + * @return sorting order + */ + Order getOrder(); + + /** + * Indicates if the sorting is numeric (default) or alphabetical (lexicographical). + * Can be null if nothing is specified. + * + * @return the type of sorting + */ + Boolean isAlphabetic(); + + /** + * Returns the pattern (if set) for sorting by external keys (BY). + * Can be null if nothing is specified. + * + * @return BY pattern. + */ + byte[] getByPattern(); + + /** + * Returns the pattern (if set) for retrieving external keys (GET). + * Can be null if nothing is specified. + * + * @return GET pattern. + */ + byte[][] getGetPattern(); + + /** + * Returns the sorting limit (range or pagination). + * Can be null if nothing is specified. + * + * @return sorting limit/range + */ + Range getLimit(); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/StringRedisConnection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/StringRedisConnection.java new file mode 100644 index 000000000..fc7c170aa --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/StringRedisConnection.java @@ -0,0 +1,245 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.core.RedisCallback; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; + +/** + * Convenience extension of {@link RedisConnection} that accepts and returns {@link String}s instead of + * byte arrays. Uses a {@link RedisSerializer} underneath to perform the conversion. + * + * @author Costin Leau + * @see RedisCallback + * @see RedisSerializer + * @see StringRedisTemplate + */ +public interface StringRedisConnection extends RedisConnection { + + /** + * String-friendly ZSet tuple. + */ + public interface StringTuple extends Tuple { + String getValueAsString(); + } + + Boolean exists(String key); + + Long del(String... keys); + + DataType type(String key); + + Collection keys(String pattern); + + void rename(String oldName, String newName); + + Boolean renameNX(String oldName, String newName); + + Boolean expire(String key, long seconds); + + Boolean expireAt(String key, long unixTime); + + Boolean persist(String key); + + Boolean move(String key, int dbIndex); + + Long ttl(String key); + + String echo(String message); + + // sort commands + List sort(String key, SortParameters params); + + Long sort(String key, SortParameters params, String storeKey); + + String get(String key); + + String getSet(String key, String value); + + List mGet(String... keys); + + void set(String key, String value); + + Boolean setNX(String key, String value); + + void setEx(String key, long seconds, String value); + + void mSetString(Map tuple); + + void mSetNXString(Map tuple); + + Long incr(String key); + + Long incrBy(String key, long value); + + Long decr(String key); + + Long decrBy(String key, long value); + + Long append(String key, String value); + + String getRange(String key, long start, long end); + + void setRange(String key, String value, long offset); + + Boolean getBit(String key, long offset); + + void setBit(String key, long offset, boolean value); + + Long strLen(String key); + + Long rPush(String key, String value); + + Long lPush(String key, String value); + + Long rPushX(String key, String value); + + Long lPushX(String key, String value); + + Long lLen(String key); + + List lRange(String key, long start, long end); + + void lTrim(String key, long start, long end); + + String lIndex(String key, long index); + + Long lInsert(String key, Position where, String pivot, String value); + + void lSet(String key, long index, String value); + + Long lRem(String key, long count, String value); + + String lPop(String key); + + String rPop(String key); + + List bLPop(int timeout, String... keys); + + List bRPop(int timeout, String... keys); + + String rPopLPush(String srcKey, String dstKey); + + String bRPopLPush(int timeout, String srcKey, String dstKey); + + Boolean sAdd(String key, String value); + + Boolean sRem(String key, String value); + + String sPop(String key); + + Boolean sMove(String srcKey, String destKey, String value); + + Long sCard(String key); + + Boolean sIsMember(String key, String value); + + Set sInter(String... keys); + + void sInterStore(String destKey, String... keys); + + Set sUnion(String... keys); + + void sUnionStore(String destKey, String... keys); + + Set sDiff(String... keys); + + void sDiffStore(String destKey, String... keys); + + Set sMembers(String key); + + String sRandMember(String key); + + Boolean zAdd(String key, double score, String value); + + Boolean zRem(String key, String value); + + Double zIncrBy(String key, double increment, String value); + + Long zRank(String key, String value); + + Long zRevRank(String key, String value); + + Set zRange(String key, long start, long end); + + Set zRangeWithScores(String key, long start, long end); + + Set zRevRange(String key, long start, long end); + + Set zRevRangeWithScores(String key, long start, long end); + + Set zRangeByScore(String key, double min, double max); + + Set zRangeByScoreWithScores(String key, double min, double max); + + Set zRangeByScore(String key, double min, double max, long offset, long count); + + Set zRangeByScoreWithScores(String key, double min, double max, long offset, long count); + + Long zCount(String key, double min, double max); + + Long zCard(String key); + + Double zScore(String key, String value); + + Long zRemRange(String key, long start, long end); + + Long zRemRangeByScore(String key, double min, double max); + + Long zUnionStore(String destKey, String... sets); + + Long zUnionStore(String destKey, Aggregate aggregate, int[] weights, String... sets); + + Long zInterStore(String destKey, String... sets); + + Long zInterStore(String destKey, Aggregate aggregate, int[] weights, String... sets); + + Boolean hSet(String key, String field, String value); + + Boolean hSetNX(String key, String field, String value); + + String hGet(String key, String field); + + List hMGet(String key, String... fields); + + void hMSet(String key, Map hashes); + + Long hIncrBy(String key, String field, long delta); + + Boolean hExists(String key, String field); + + Boolean hDel(String key, String field); + + Long hLen(String key); + + Set hKeys(String key); + + List hVals(String key); + + Map hGetAll(String key); + + Long publish(String channel, String message); + + void subscribe(MessageListener listener, String... channels); + + void pSubscribe(MessageListener listener, String... patterns); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/Subscription.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/Subscription.java new file mode 100644 index 000000000..bdad9ae35 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/Subscription.java @@ -0,0 +1,96 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection; + +import java.util.Collection; + +/** + * Subscription for Redis channels. Just like the underlying {@link RedisConnection}, + * it should not be used by multiple threads. + * + * Note that once a subscription died, it cannot accept any more subscriptions. + * + * @author Costin Leau + */ +public interface Subscription { + + /** + * Adds the given channels to the current subscription. + * + * @param channels channel names + */ + void subscribe(byte[]... channels) throws RedisInvalidSubscriptionException; + + /** + * Adds the given channel patterns to the current subscription. + * + * @param patterns channel patterns + */ + void pSubscribe(byte[]... patterns) throws RedisInvalidSubscriptionException; + + /** + * Cancels the current subscription for all channels given by name. + */ + void unsubscribe(); + + /** + * Cancels the current subscription for all given channels. + * + * @param channels channel names + */ + void unsubscribe(byte[]... channels); + + /** + * Cancels the subscription for all channels matched by patterns. + */ + void pUnsubscribe(); + + /** + * Cancels the subscription for all channels matching the given patterns. + * + * @param patterns + */ + void pUnsubscribe(byte[]... patterns); + + /** + * Returns the (named) channels for this subscription. + * + * @return collection of named channels + */ + Collection getChannels(); + + /** + * Returns the channel patters for this subscription. + * + * @return collection of channel patterns + */ + Collection getPatterns(); + + /** + * Returns the listener used for this subscription. + * + * @return the listener used for this subscription. + */ + MessageListener getListener(); + + /** + * Indicates whether this subscription is still 'alive' + * or not. + * + * @return true if the subscription still applies, false otherwise. + */ + boolean isAlive(); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnection.java new file mode 100644 index 000000000..363e7c920 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnection.java @@ -0,0 +1,2350 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.jedis; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.UncategorizedKeyvalueStoreException; +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisSubscribedConnectionException; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.connection.Subscription; +import org.springframework.util.ReflectionUtils; + +import redis.clients.jedis.BinaryJedis; +import redis.clients.jedis.BinaryJedisPubSub; +import redis.clients.jedis.BinaryTransaction; +import redis.clients.jedis.Client; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.SortingParams; +import redis.clients.jedis.Transaction; +import redis.clients.jedis.ZParams; +import redis.clients.jedis.exceptions.JedisConnectionException; +import redis.clients.jedis.exceptions.JedisException; +import redis.clients.util.Pool; + +/** + * {@code RedisConnection} implementation on top of Jedis library. + * + * @author Costin Leau + */ +public class JedisConnection implements RedisConnection { + + private static final Field CLIENT_FIELD; + + static { + CLIENT_FIELD = ReflectionUtils.findField(BinaryJedis.class, "client", Client.class); + ReflectionUtils.makeAccessible(CLIENT_FIELD); + } + + private final Jedis jedis; + private final Client client; + private final BinaryTransaction transaction; + private final Pool pool; + /** flag indicating whether the connection needs to be dropped or not */ + private boolean broken = false; + + private volatile JedisSubscription subscription; + private volatile Pipeline pipeline; + private final int dbIndex; + + /** + * Constructs a new JedisConnection instance. + * + * @param jedis Jedis entity + */ + public JedisConnection(Jedis jedis) { + this(jedis, null, 0); + } + + /** + * + * Constructs a new JedisConnection instance backed by a jedis pool. + * + * @param jedis + * @param pool can be null, if no pool is used + */ + public JedisConnection(Jedis jedis, Pool pool, int dbIndex) { + this.jedis = jedis; + // extract underlying connection for batch operations + client = (Client) ReflectionUtils.getField(CLIENT_FIELD, jedis); + transaction = new Transaction(client); + + this.pool = pool; + + this.dbIndex = dbIndex; + + // select the db + if (dbIndex > 0) { + select(dbIndex); + } + } + + protected DataAccessException convertJedisAccessException(Exception ex) { + if (ex instanceof JedisException) { + // check connection flag + if (ex instanceof JedisConnectionException) { + broken = true; + } + return JedisUtils.convertJedisAccessException((JedisException) ex); + } + if (ex instanceof IOException) { + return JedisUtils.convertJedisAccessException((IOException) ex); + } + + return new UncategorizedKeyvalueStoreException("Unknown jedis exception", ex); + } + + @Override + public void close() throws DataAccessException { + // return the connection to the pool + try { + if (pool != null) { + if (broken) { + pool.returnBrokenResource(jedis); + } + else { + // reset the connection + if (dbIndex > 0) { + select(0); + } + + pool.returnResource(jedis); + } + } + } catch (Exception ex) { + pool.returnBrokenResource(jedis); + } + + if (pool != null) { + return; + } + + // else close the connection normally + try { + if (isQueueing()) { + client.quit(); + client.disconnect(); + return; + } + jedis.quit(); + jedis.disconnect(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Jedis getNativeConnection() { + return jedis; + } + + @Override + public boolean isClosed() { + try { + return !jedis.isConnected(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public boolean isQueueing() { + return client.isInMulti(); + } + + @Override + public boolean isPipelined() { + return (pipeline != null); + } + + @Override + public void openPipeline() { + if (pipeline == null) { + pipeline = jedis.pipelined(); + } + } + + @SuppressWarnings("unchecked") + @Override + public List closePipeline() { + if (pipeline != null) { + List execute = pipeline.syncAndReturnAll(); + if (execute != null && !execute.isEmpty()) { + return execute; + } + } + return Collections.emptyList(); + } + + @Override + public List sort(byte[] key, SortParameters params) { + + SortingParams sortParams = JedisUtils.convertSortParams(params); + + try { + if (isQueueing()) { + if (sortParams != null) { + transaction.sort(key, sortParams); + } + else { + transaction.sort(key); + } + + return null; + } + if (isPipelined()) { + if (sortParams != null) { + pipeline.sort(key, sortParams); + } + else { + pipeline.sort(key); + } + + return null; + } + return (sortParams != null ? jedis.sort(key, sortParams) : jedis.sort(key)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long sort(byte[] key, SortParameters params, byte[] sortKey) { + + SortingParams sortParams = JedisUtils.convertSortParams(params); + + try { + if (isQueueing()) { + if (sortParams != null) { + transaction.sort(key, sortParams, sortKey); + } + else { + transaction.sort(key, sortKey); + } + + return null; + } + if (isPipelined()) { + if (sortParams != null) { + pipeline.sort(key, sortParams, sortKey); + } + else { + pipeline.sort(key, sortKey); + } + + return null; + } + return (sortParams != null ? jedis.sort(key, sortParams, sortKey) : jedis.sort(key, sortKey)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long dbSize() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + return jedis.dbSize(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + + @Override + public void flushDb() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + jedis.flushDB(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void flushAll() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + jedis.flushAll(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void bgSave() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.bgsave(); + return; + } + jedis.bgsave(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void bgWriteAof() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.bgrewriteaof(); + return; + } + jedis.bgrewriteaof(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void save() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.save(); + return; + } + jedis.save(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List getConfig(String param) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.configGet(param); + return null; + } + return jedis.configGet(param); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Properties info() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + return JedisUtils.info(jedis.info()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long lastSave() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.lastsave(); + return null; + } + return jedis.lastsave(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void setConfig(String param, String value) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.configSet(param, value); + return; + } + jedis.configSet(param, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + + @Override + public void resetConfigStats() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.configResetStat(); + return; + } + jedis.configResetStat(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void shutdown() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + jedis.shutdown(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] echo(byte[] message) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.echo(message); + return null; + } + return jedis.echo(message); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public String ping() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + return jedis.ping(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long del(byte[]... keys) { + try { + if (isQueueing()) { + transaction.del(keys); + return null; + } + if (isPipelined()) { + pipeline.del(keys); + return null; + } + return jedis.del(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void discard() { + try { + client.discard(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List exec() { + try { + if (isPipelined()) { + pipeline.exec(); + return null; + } + return transaction.exec(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean exists(byte[] key) { + try { + if (isQueueing()) { + transaction.exists(key); + return null; + } + if (isPipelined()) { + pipeline.exists(key); + return null; + } + return jedis.exists(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean expire(byte[] key, long seconds) { + try { + if (isQueueing()) { + transaction.expire(key, (int) seconds); + return null; + } + if (isPipelined()) { + pipeline.expire(key, (int) seconds); + return null; + } + return (jedis.expire(key, (int) seconds) == 1); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean expireAt(byte[] key, long unixTime) { + try { + if (isQueueing()) { + transaction.expireAt(key, unixTime); + return null; + } + if (isPipelined()) { + pipeline.expireAt(key, unixTime); + return null; + } + return (jedis.expireAt(key, unixTime) == 1); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set keys(byte[] pattern) { + try { + if (isQueueing()) { + transaction.keys(pattern); + return null; + } + if (isPipelined()) { + pipeline.keys(pattern); + return null; + } + return (jedis.keys(pattern)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void multi() { + if (isQueueing()) { + return; + } + try { + if (isPipelined()) { + pipeline.multi(); + return; + } + jedis.multi(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean persist(byte[] key) { + try { + if (isQueueing()) { + client.persist(key); + return null; + } + if (isPipelined()) { + pipeline.persist(key); + return null; + } + return (jedis.persist(key) == 1); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean move(byte[] key, int dbIndex) { + try { + if (isQueueing()) { + client.move(key, dbIndex); + return null; + } + if (isPipelined()) { + client.move(key, dbIndex); + return null; + } + return (jedis.move(key, dbIndex) == 1); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] randomKey() { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + return jedis.randomBinaryKey(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void rename(byte[] oldName, byte[] newName) { + try { + if (isQueueing()) { + transaction.rename(oldName, newName); + return; + } + if (isPipelined()) { + pipeline.rename(oldName, newName); + return; + } + jedis.rename(oldName, newName); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean renameNX(byte[] oldName, byte[] newName) { + try { + if (isQueueing()) { + transaction.renamenx(oldName, newName); + return null; + } + if (isPipelined()) { + pipeline.renamenx(oldName, newName); + return null; + } + return (jedis.renamenx(oldName, newName) == 1); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void select(int dbIndex) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + jedis.select(dbIndex); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long ttl(byte[] key) { + try { + if (isQueueing()) { + transaction.ttl(key); + return null; + } + if (isPipelined()) { + pipeline.ttl(key); + return null; + } + return jedis.ttl(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public DataType type(byte[] key) { + try { + if (isQueueing()) { + transaction.type(key); + return null; + } + if (isPipelined()) { + pipeline.type(key); + return null; + } + return DataType.fromCode(jedis.type(key)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void unwatch() { + try { + jedis.unwatch(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void watch(byte[]... keys) { + if (isQueueing()) { + // ignore (as watch not allowed in multi) + return; + } + try { + for (byte[] key : keys) { + if (isPipelined()) { + pipeline.watch(key); + } + else { + jedis.watch(key); + } + } + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + // + // String commands + // + + @Override + public byte[] get(byte[] key) { + try { + if (isQueueing()) { + transaction.get(key); + return null; + } + if (isPipelined()) { + pipeline.get(key); + return null; + } + + return jedis.get(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void set(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.set(key, value); + return; + } + if (isPipelined()) { + pipeline.set(key, value); + return; + } + jedis.set(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + + @Override + public byte[] getSet(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.getSet(key, value); + return null; + } + if (isPipelined()) { + pipeline.getSet(key, value); + return null; + } + return jedis.getSet(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long append(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.append(key, value); + return null; + } + if (isPipelined()) { + pipeline.append(key, value); + return null; + } + return jedis.append(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List mGet(byte[]... keys) { + try { + if (isQueueing()) { + transaction.mget(keys); + return null; + } + if (isPipelined()) { + pipeline.mget(keys); + return null; + } + return jedis.mget(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void mSet(Map tuples) { + try { + if (isQueueing()) { + transaction.mset(JedisUtils.convert(tuples)); + return; + } + if (isPipelined()) { + pipeline.mset(JedisUtils.convert(tuples)); + return; + } + jedis.mset(JedisUtils.convert(tuples)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void mSetNX(Map tuples) { + try { + if (isQueueing()) { + transaction.msetnx(JedisUtils.convert(tuples)); + return; + } + if (isPipelined()) { + pipeline.msetnx(JedisUtils.convert(tuples)); + return; + } + jedis.msetnx(JedisUtils.convert(tuples)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void setEx(byte[] key, long time, byte[] value) { + try { + if (isQueueing()) { + transaction.setex(key, (int) time, value); + return; + } + if (isPipelined()) { + pipeline.setex(key, (int) time, value); + return; + } + jedis.setex(key, (int) time, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean setNX(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.setnx(key, value); + return null; + } + if (isPipelined()) { + pipeline.setnx(key, value); + return null; + } + return JedisUtils.convertCodeReply(jedis.setnx(key, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] getRange(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.substr(key, (int) start, (int) end); + return null; + } + if (isPipelined()) { + pipeline.substr(key, (int) start, (int) end); + return null; + } + return jedis.substr(key, (int) start, (int) end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long decr(byte[] key) { + try { + if (isQueueing()) { + transaction.decr(key); + return null; + } + if (isPipelined()) { + pipeline.decr(key); + return null; + } + return jedis.decr(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long decrBy(byte[] key, long value) { + try { + if (isQueueing()) { + transaction.decrBy(key, (int) value); + return null; + } + if (isPipelined()) { + pipeline.decrBy(key, (int) value); + return null; + } + return jedis.decrBy(key, (int) value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long incr(byte[] key) { + try { + if (isQueueing()) { + transaction.incr(key); + return null; + } + if (isPipelined()) { + pipeline.incr(key); + return null; + } + return jedis.incr(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long incrBy(byte[] key, long value) { + try { + if (isQueueing()) { + transaction.incrBy(key, (int) value); + return null; + } + if (isPipelined()) { + pipeline.incrBy(key, (int) value); + return null; + } + return jedis.incrBy(key, (int) value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean getBit(byte[] key, long offset) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + return (jedis.getbit(key, offset) == 0 ? Boolean.FALSE : Boolean.TRUE); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void setBit(byte[] key, long offset, boolean value) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + jedis.setbit(key, offset, JedisUtils.asBit(value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void setRange(byte[] key, byte[] value, long start) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + jedis.setrange(key, start, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long strLen(byte[] key) { + try { + if (isQueueing()) { + transaction.strlen(key); + return null; + } + if (isPipelined()) { + pipeline.strlen(key); + return null; + } + return jedis.strlen(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + // + // List commands + // + + @Override + public Long lPush(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.lpush(key, value); + return null; + } + if (isPipelined()) { + pipeline.lpush(key, value); + return null; + } + return jedis.lpush(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long rPush(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.rpush(key, value); + return null; + } + if (isPipelined()) { + pipeline.rpush(key, value); + return null; + } + return jedis.rpush(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List bLPop(int timeout, byte[]... keys) { + try { + if (isQueueing()) { + transaction.blpop(JedisUtils.bXPopArgs(timeout, keys)); + return null; + } + if (isPipelined()) { + pipeline.blpop(JedisUtils.bXPopArgs(timeout, keys)); + return null; + } + return jedis.blpop(timeout, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List bRPop(int timeout, byte[]... keys) { + try { + if (isQueueing()) { + transaction.brpop(JedisUtils.bXPopArgs(timeout, keys)); + } + if (isPipelined()) { + pipeline.brpop(JedisUtils.bXPopArgs(timeout, keys)); + return null; + } + return jedis.brpop(timeout, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] lIndex(byte[] key, long index) { + try { + if (isQueueing()) { + transaction.lindex(key, (int) index); + return null; + } + if (isPipelined()) { + pipeline.lindex(key, (int) index); + return null; + } + return jedis.lindex(key, (int) index); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { + try { + if (isQueueing()) { + transaction.linsert(key, JedisUtils.convertPosition(where), pivot, value); + return null; + } + if (isPipelined()) { + pipeline.linsert(key, JedisUtils.convertPosition(where), pivot, value); + return null; + } + return jedis.linsert(key, JedisUtils.convertPosition(where), pivot, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long lLen(byte[] key) { + try { + if (isQueueing()) { + transaction.llen(key); + return null; + } + if (isPipelined()) { + pipeline.llen(key); + return null; + } + return jedis.llen(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] lPop(byte[] key) { + try { + if (isQueueing()) { + transaction.lpop(key); + return null; + } + if (isPipelined()) { + pipeline.lpop(key); + return null; + } + return jedis.lpop(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List lRange(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.lrange(key, (int) start, (int) end); + return null; + } + if (isPipelined()) { + pipeline.lrange(key, (int) start, (int) end); + return null; + } + return jedis.lrange(key, (int) start, (int) end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long lRem(byte[] key, long count, byte[] value) { + try { + if (isQueueing()) { + transaction.lrem(key, (int) count, value); + return null; + } + if (isPipelined()) { + pipeline.lrem(key, (int) count, value); + return null; + } + return jedis.lrem(key, (int) count, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void lSet(byte[] key, long index, byte[] value) { + try { + if (isQueueing()) { + transaction.lset(key, (int) index, value); + return; + } + if (isPipelined()) { + pipeline.lset(key, (int) index, value); + return; + } + jedis.lset(key, (int) index, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void lTrim(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.ltrim(key, (int) start, (int) end); + return; + } + if (isPipelined()) { + pipeline.ltrim(key, (int) start, (int) end); + return; + } + jedis.ltrim(key, (int) start, (int) end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] rPop(byte[] key) { + try { + if (isQueueing()) { + transaction.rpop(key); + return null; + } + if (isPipelined()) { + pipeline.rpop(key); + return null; + } + return jedis.rpop(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + try { + if (isQueueing()) { + transaction.rpoplpush(srcKey, dstKey); + return null; + } + if (isPipelined()) { + pipeline.rpoplpush(srcKey, dstKey); + return null; + } + return jedis.rpoplpush(srcKey, dstKey); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + try { + if (isQueueing()) { + transaction.brpoplpush(srcKey, dstKey, timeout); + return null; + } + if (isPipelined()) { + pipeline.brpoplpush(srcKey, dstKey, timeout); + return null; + } + return jedis.brpoplpush(srcKey, dstKey, timeout); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long lPushX(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.lpushx(key, value); + return null; + } + if (isPipelined()) { + pipeline.lpushx(key, value); + return null; + } + return jedis.lpushx(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long rPushX(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.rpushx(key, value); + return null; + } + if (isPipelined()) { + pipeline.rpushx(key, value); + return null; + } + return jedis.rpushx(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + + // + // Set commands + // + + @Override + public Boolean sAdd(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.sadd(key, value); + return null; + } + if (isPipelined()) { + pipeline.sadd(key, value); + return null; + } + return (jedis.sadd(key, value) == 1); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long sCard(byte[] key) { + try { + if (isQueueing()) { + transaction.scard(key); + return null; + } + if (isPipelined()) { + pipeline.scard(key); + return null; + } + return jedis.scard(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set sDiff(byte[]... keys) { + try { + if (isQueueing()) { + transaction.sdiff(keys); + return null; + } + if (isPipelined()) { + pipeline.sdiff(keys); + return null; + } + return jedis.sdiff(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void sDiffStore(byte[] destKey, byte[]... keys) { + try { + if (isQueueing()) { + transaction.sdiffstore(destKey, keys); + return; + } + if (isPipelined()) { + pipeline.sdiffstore(destKey, keys); + return; + } + jedis.sdiffstore(destKey, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set sInter(byte[]... keys) { + try { + if (isQueueing()) { + transaction.sinter(keys); + return null; + } + if (isPipelined()) { + pipeline.sinter(keys); + return null; + } + return jedis.sinter(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void sInterStore(byte[] destKey, byte[]... keys) { + try { + if (isQueueing()) { + transaction.sinterstore(destKey, keys); + return; + } + if (isPipelined()) { + pipeline.sinterstore(destKey, keys); + return; + } + jedis.sinterstore(destKey, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean sIsMember(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.sismember(key, value); + return null; + } + if (isPipelined()) { + pipeline.sismember(key, value); + return null; + } + return jedis.sismember(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set sMembers(byte[] key) { + try { + if (isQueueing()) { + transaction.smembers(key); + return null; + } + if (isPipelined()) { + pipeline.smembers(key); + return null; + } + return jedis.smembers(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + try { + if (isQueueing()) { + transaction.smove(srcKey, destKey, value); + return null; + } + if (isPipelined()) { + pipeline.smove(srcKey, destKey, value); + return null; + } + return JedisUtils.convertCodeReply(jedis.smove(srcKey, destKey, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] sPop(byte[] key) { + try { + if (isQueueing()) { + transaction.spop(key); + return null; + } + if (isPipelined()) { + pipeline.spop(key); + return null; + } + return jedis.spop(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] sRandMember(byte[] key) { + try { + if (isQueueing()) { + transaction.srandmember(key); + return null; + } + if (isPipelined()) { + pipeline.srandmember(key); + return null; + } + return jedis.srandmember(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean sRem(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.srem(key, value); + return null; + } + if (isPipelined()) { + pipeline.srem(key, value); + return null; + } + return JedisUtils.convertCodeReply(jedis.srem(key, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set sUnion(byte[]... keys) { + try { + if (isQueueing()) { + transaction.sunion(keys); + return null; + } + if (isPipelined()) { + pipeline.sunion(keys); + return null; + } + return jedis.sunion(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void sUnionStore(byte[] destKey, byte[]... keys) { + try { + if (isQueueing()) { + transaction.sunionstore(destKey, keys); + return; + } + if (isPipelined()) { + pipeline.sunionstore(destKey, keys); + return; + } + jedis.sunionstore(destKey, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + // + // ZSet commands + // + + @Override + public Boolean zAdd(byte[] key, double score, byte[] value) { + try { + if (isQueueing()) { + transaction.zadd(key, score, value); + return null; + } + if (isPipelined()) { + pipeline.zadd(key, score, value); + return null; + } + return JedisUtils.convertCodeReply(jedis.zadd(key, score, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zCard(byte[] key) { + try { + if (isQueueing()) { + transaction.zcard(key); + return null; + } + if (isPipelined()) { + pipeline.zcard(key); + return null; + } + return jedis.zcard(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zCount(byte[] key, double min, double max) { + try { + if (isQueueing()) { + transaction.zcount(key, min, max); + return null; + } + if (isQueueing()) { + pipeline.zcount(key, min, max); + return null; + } + return jedis.zcount(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Double zIncrBy(byte[] key, double increment, byte[] value) { + try { + if (isQueueing()) { + transaction.zincrby(key, increment, value); + return null; + } + if (isPipelined()) { + pipeline.zincrby(key, increment, value); + return null; + } + return jedis.zincrby(key, increment, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + try { + ZParams zparams = new ZParams().weights(weights).aggregate( + redis.clients.jedis.ZParams.Aggregate.valueOf(aggregate.name())); + + if (isQueueing()) { + transaction.zinterstore(destKey, zparams, sets); + return null; + } + if (isPipelined()) { + pipeline.zinterstore(destKey, zparams, sets); + return null; + } + return jedis.zinterstore(destKey, zparams, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zInterStore(byte[] destKey, byte[]... sets) { + try { + if (isQueueing()) { + transaction.zinterstore(destKey, sets); + return null; + } + if (isQueueing()) { + pipeline.zinterstore(destKey, sets); + return null; + } + return jedis.zinterstore(destKey, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRange(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.zrange(key, (int) start, (int) end); + return null; + } + if (isPipelined()) { + pipeline.zrange(key, (int) start, (int) end); + return null; + } + return jedis.zrange(key, (int) start, (int) end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRangeWithScores(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.zrangeWithScores(key, (int) start, (int) end); + return null; + } + if (isPipelined()) { + pipeline.zrangeWithScores(key, (int) start, (int) end); + return null; + } + return JedisUtils.convertJedisTuple(jedis.zrangeWithScores(key, (int) start, (int) end)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRangeByScore(byte[] key, double min, double max) { + try { + if (isQueueing()) { + transaction.zrangeByScore(key, min, max); + return null; + } + if (isPipelined()) { + pipeline.zrangeByScore(key, min, max); + return null; + } + return jedis.zrangeByScore(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max) { + try { + if (isQueueing()) { + transaction.zrangeByScoreWithScores(key, min, max); + return null; + } + if (isPipelined()) { + pipeline.zrangeByScoreWithScores(key, min, max); + return null; + } + return JedisUtils.convertJedisTuple(jedis.zrangeByScoreWithScores(key, min, max)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRevRangeWithScores(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.zrevrangeWithScores(key, (int) start, (int) end); + return null; + } + if (isPipelined()) { + pipeline.zrevrangeWithScores(key, (int) start, (int) end); + return null; + } + return JedisUtils.convertJedisTuple(jedis.zrevrangeWithScores(key, (int) start, (int) end)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRangeByScore(byte[] key, double min, double max, long offset, long count) { + try { + if (isQueueing()) { + transaction.zrangeByScore(key, min, max, (int) offset, (int) count); + return null; + } + if (isPipelined()) { + pipeline.zrangeByScore(key, min, max, (int) offset, (int) count); + return null; + } + return jedis.zrangeByScore(key, min, max, (int) offset, (int) count); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + try { + if (isQueueing()) { + transaction.zrangeByScoreWithScores(key, min, max, (int) offset, (int) count); + return null; + } + if (isPipelined()) { + pipeline.zrangeByScoreWithScores(key, min, max, (int) offset, (int) count); + return null; + } + return JedisUtils.convertJedisTuple(jedis.zrangeByScoreWithScores(key, min, max, (int) offset, (int) count)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRevRangeByScore(byte[] key, double min, double max, long offset, long count) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + throw new UnsupportedOperationException(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRevRangeByScore(byte[] key, double min, double max) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + throw new UnsupportedOperationException(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + throw new UnsupportedOperationException(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + throw new UnsupportedOperationException(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zRank(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.zrank(key, value); + return null; + } + if (isPipelined()) { + pipeline.zrank(key, value); + return null; + } + return jedis.zrank(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean zRem(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.zrem(key, value); + return null; + } + if (isPipelined()) { + pipeline.zrem(key, value); + return null; + } + return JedisUtils.convertCodeReply(jedis.zrem(key, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zRemRange(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.zremrangeByRank(key, (int) start, (int) end); + return null; + } + if (isPipelined()) { + pipeline.zremrangeByRank(key, (int) start, (int) end); + return null; + } + return jedis.zremrangeByRank(key, (int) start, (int) end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zRemRangeByScore(byte[] key, double min, double max) { + try { + if (isQueueing()) { + transaction.zremrangeByScore(key, min, max); + return null; + } + if (isPipelined()) { + pipeline.zremrangeByScore(key, min, max); + return null; + } + return jedis.zremrangeByScore(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set zRevRange(byte[] key, long start, long end) { + try { + if (isQueueing()) { + transaction.zrevrange(key, (int) start, (int) end); + return null; + } + if (isPipelined()) { + pipeline.zrevrange(key, (int) start, (int) end); + return null; + } + return jedis.zrevrange(key, (int) start, (int) end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zRevRank(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.zrevrank(key, value); + return null; + } + if (isPipelined()) { + pipeline.zrevrank(key, value); + return null; + } + return jedis.zrevrank(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Double zScore(byte[] key, byte[] value) { + try { + if (isQueueing()) { + transaction.zscore(key, value); + return null; + } + if (isPipelined()) { + pipeline.zscore(key, value); + return null; + } + return jedis.zscore(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + try { + ZParams zparams = new ZParams().weights(weights).aggregate( + redis.clients.jedis.ZParams.Aggregate.valueOf(aggregate.name())); + + if (isQueueing()) { + transaction.zunionstore(destKey, zparams, sets); + return null; + } + if (isPipelined()) { + pipeline.zunionstore(destKey, zparams, sets); + return null; + } + return jedis.zunionstore(destKey, zparams, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zUnionStore(byte[] destKey, byte[]... sets) { + try { + if (isQueueing()) { + transaction.zunionstore(destKey, sets); + return null; + } + if (isPipelined()) { + pipeline.zunionstore(destKey, sets); + return null; + } + return jedis.zunionstore(destKey, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + // + // Hash commands + // + + @Override + public Boolean hSet(byte[] key, byte[] field, byte[] value) { + try { + if (isQueueing()) { + transaction.hset(key, field, value); + return null; + } + if (isPipelined()) { + pipeline.hset(key, field, value); + return null; + } + return JedisUtils.convertCodeReply(jedis.hset(key, field, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean hSetNX(byte[] key, byte[] field, byte[] value) { + try { + if (isQueueing()) { + transaction.hsetnx(key, field, value); + return null; + } + if (isPipelined()) { + pipeline.hsetnx(key, field, value); + return null; + } + return JedisUtils.convertCodeReply(jedis.hsetnx(key, field, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean hDel(byte[] key, byte[] field) { + try { + if (isQueueing()) { + transaction.hdel(key, field); + return null; + } + if (isPipelined()) { + pipeline.hdel(key, field); + return null; + } + return JedisUtils.convertCodeReply(jedis.hdel(key, field)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean hExists(byte[] key, byte[] field) { + try { + if (isQueueing()) { + transaction.hexists(key, field); + return null; + } + if (isPipelined()) { + pipeline.hexists(key, field); + return null; + } + return jedis.hexists(key, field); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public byte[] hGet(byte[] key, byte[] field) { + try { + if (isQueueing()) { + transaction.hget(key, field); + return null; + } + if (isPipelined()) { + pipeline.hget(key, field); + return null; + } + return jedis.hget(key, field); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Map hGetAll(byte[] key) { + try { + if (isQueueing()) { + transaction.hgetAll(key); + return null; + } + if (isPipelined()) { + pipeline.hgetAll(key); + return null; + } + return jedis.hgetAll(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long hIncrBy(byte[] key, byte[] field, long delta) { + try { + if (isQueueing()) { + transaction.hincrBy(key, field, (int) delta); + return null; + } + if (isPipelined()) { + pipeline.hincrBy(key, field, (int) delta); + return null; + } + return jedis.hincrBy(key, field, (int) delta); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Set hKeys(byte[] key) { + try { + if (isQueueing()) { + transaction.hkeys(key); + return null; + } + if (isPipelined()) { + pipeline.hkeys(key); + return null; + } + return jedis.hkeys(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long hLen(byte[] key) { + try { + if (isQueueing()) { + transaction.hlen(key); + return null; + } + if (isPipelined()) { + pipeline.hlen(key); + return null; + } + return jedis.hlen(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hMGet(byte[] key, byte[]... fields) { + try { + if (isQueueing()) { + transaction.hmget(key, fields); + return null; + } + if (isPipelined()) { + pipeline.hmget(key, fields); + return null; + } + return jedis.hmget(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void hMSet(byte[] key, Map tuple) { + try { + if (isQueueing()) { + transaction.hmset(key, tuple); + return; + } + if (isPipelined()) { + pipeline.hmset(key, tuple); + return; + } + jedis.hmset(key, tuple); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hVals(byte[] key) { + try { + if (isQueueing()) { + transaction.hvals(key); + return null; + } + if (isPipelined()) { + pipeline.hvals(key); + return null; + } + return new ArrayList(jedis.hvals(key)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + + // + // Pub/Sub functionality + // + @Override + public Long publish(byte[] channel, byte[] message) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + pipeline.publish(channel, message); + return null; + } + return jedis.publish(channel, message); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Subscription getSubscription() { + return subscription; + } + + @Override + public boolean isSubscribed() { + return (subscription != null && subscription.isAlive()); + } + + @Override + public void pSubscribe(MessageListener listener, byte[]... patterns) { + if (isSubscribed()) { + throw new RedisSubscribedConnectionException( + "Connection already subscribed; use the connection Subscription to cancel or add new channels"); + } + + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + + BinaryJedisPubSub jedisPubSub = JedisUtils.adaptPubSub(listener); + + subscription = new JedisSubscription(listener, jedisPubSub, null, patterns); + jedis.psubscribe(jedisPubSub, patterns); + + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void subscribe(MessageListener listener, byte[]... channels) { + if (isSubscribed()) { + throw new RedisSubscribedConnectionException( + "Connection already subscribed; use the connection Subscription to cancel or add new channels"); + } + + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + + BinaryJedisPubSub jedisPubSub = JedisUtils.adaptPubSub(listener); + + subscription = new JedisSubscription(listener, jedisPubSub, channels, null); + jedis.subscribe(jedisPubSub, channels); + + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + private void checkSubscription() { + if (isSubscribed()) { + throw new RedisSubscribedConnectionException("Cannot execute command - connection is subscribed"); + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnectionFactory.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnectionFactory.java new file mode 100644 index 000000000..1f41fbb6b --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnectionFactory.java @@ -0,0 +1,303 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection.jedis; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisShardInfo; +import redis.clients.jedis.Protocol; + +/** + * Connection factory creating Jedis based connections. + * + * @author Costin Leau + */ +public class JedisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory { + + private final static Log log = LogFactory.getLog(JedisConnectionFactory.class); + + private JedisShardInfo shardInfo; + private String hostName = "localhost"; + private int port = Protocol.DEFAULT_PORT; + private int timeout = Protocol.DEFAULT_TIMEOUT; + private String password; + + private boolean usePool = true; + private JedisPool pool = null; + private JedisPoolConfig poolConfig = new JedisPoolConfig(); + + private int dbIndex = 0; + + /** + * Constructs a new JedisConnectionFactory instance + * with default settings (default connection pooling, no shard information). + */ + public JedisConnectionFactory() { + } + + /** + * Constructs a new JedisConnectionFactory instance. + * Will override the other connection parameters passed to the factory. + * + * @param shardInfo shard information + */ + public JedisConnectionFactory(JedisShardInfo shardInfo) { + this.shardInfo = shardInfo; + } + + /** + * Constructs a new JedisConnectionFactory instance using + * the given pool configuration. + * + * @param poolConfig pool configuration + */ + public JedisConnectionFactory(JedisPoolConfig poolConfig) { + this.poolConfig = poolConfig; + } + + + /** + * Returns a Jedis instance to be used as a Redis connection. + * The instance can be newly created or retrieved from a pool. + * + * @return Jedis instance ready for wrapping into a {@link RedisConnection}. + */ + protected Jedis fetchJedisConnector() { + try { + if (usePool && pool != null) { + return pool.getResource(); + } + Jedis jedis = new Jedis(getShardInfo()); + // force initialization (see Jedis issue #82) + jedis.connect(); + return jedis; + } catch (Exception ex) { + throw new DataAccessResourceFailureException("Cannot get Jedis connection", ex); + } + } + + /** + * Post process a newly retrieved connection. Useful for decorating or executing + * initialization commands on a new connection. + * This implementation simply returns the connection. + * + * @param connection + * @return processed connection + */ + protected JedisConnection postProcessConnection(JedisConnection connection) { + return connection; + } + + public void afterPropertiesSet() { + if (shardInfo == null) { + shardInfo = new JedisShardInfo(hostName, port); + + if (StringUtils.hasLength(password)) { + shardInfo.setPassword(password); + } + + if (timeout > 0) { + shardInfo.setTimeout(timeout); + } + } + + if (usePool) { + pool = new JedisPool(poolConfig, shardInfo.getHost(), shardInfo.getPort(), shardInfo.getTimeout(), + shardInfo.getPassword()); + } + } + + public void destroy() { + if (usePool && pool != null) { + try { + pool.destroy(); + } catch (Exception ex) { + log.warn("Cannot properly close Jedis pool", ex); + } + pool = null; + } + } + + public JedisConnection getConnection() { + Jedis jedis = fetchJedisConnector(); + return postProcessConnection((usePool ? new JedisConnection(jedis, pool, dbIndex) : new JedisConnection(jedis, + null, dbIndex))); + } + + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + return JedisUtils.convertJedisAccessException(ex); + } + + /** + * Returns the Redis hostName. + * + * @return Returns the hostName + */ + public String getHostName() { + return hostName; + } + + /** + * Sets the Redis hostName. + * + * @param hostName The hostName to set. + */ + public void setHostName(String hostName) { + this.hostName = hostName; + } + + /** + * Returns the password used for authenticating with the Redis server. + * + * @return password for authentication + */ + public String getPassword() { + return password; + } + + /** + * Sets the password used for authenticating with the Redis server. + * + * @param password the password to set + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Returns the port used to connect to the Redis instance. + * + * @return Redis port. + */ + public int getPort() { + return port; + + } + + /** + * Sets the port used to connect to the Redis instance. + * + * @param port Redis port + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Returns the shardInfo. + * + * @return Returns the shardInfo + */ + public JedisShardInfo getShardInfo() { + return shardInfo; + } + + /** + * Sets the shard info for this factory. + * + * @param shardInfo The shardInfo to set. + */ + public void setShardInfo(JedisShardInfo shardInfo) { + this.shardInfo = shardInfo; + } + + /** + * Returns the timeout. + * + * @return Returns the timeout + */ + public int getTimeout() { + return timeout; + } + + /** + * @param timeout The timeout to set. + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + /** + * Indicates the use of a connection pool. + * + * @return Returns the use of connection pooling. + */ + public boolean getUsePool() { + return usePool; + } + + /** + * Turns on or off the use of connection pooling. + * + * @param usePool The usePool to set. + */ + public void setUsePool(boolean usePool) { + this.usePool = usePool; + } + + /** + * Returns the poolConfig. + * + * @return Returns the poolConfig + */ + public JedisPoolConfig getPoolConfig() { + return poolConfig; + } + + /** + * Sets the pool configuration for this factory. + * + * @param poolConfig The poolConfig to set. + */ + public void setPoolConfig(JedisPoolConfig poolConfig) { + this.poolConfig = poolConfig; + } + + + /** + * Returns the index of the database. + * + * @return Returns the database index + */ + public int getDatabase() { + return dbIndex; + } + + /** + * Sets the index of the database used by this connection factory. + * Default is 0. + * + * @param index database index + */ + public void setDatabase(int index) { + Assert.isTrue(index >= 0, "invalid DB index (a positive index required)"); + this.dbIndex = index; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisMessageListener.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisMessageListener.java new file mode 100644 index 000000000..92151968a --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisMessageListener.java @@ -0,0 +1,67 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.jedis; + +import org.springframework.data.keyvalue.redis.connection.DefaultMessage; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.util.Assert; + +import redis.clients.jedis.BinaryJedisPubSub; + +/** + * MessageListener adapter on top of Jedis. + * + * @author Costin Leau + */ +class JedisMessageListener extends BinaryJedisPubSub { + + private final MessageListener listener; + + JedisMessageListener(MessageListener listener) { + Assert.notNull(listener, "message listener is required"); + this.listener = listener; + } + + @Override + public void onMessage(byte[] channel, byte[] message) { + listener.onMessage(new DefaultMessage(channel, message), null); + } + + @Override + public void onPMessage(byte[] pattern, byte[] channel, byte[] message) { + listener.onMessage(new DefaultMessage(channel, message), pattern); + } + + @Override + public void onPSubscribe(byte[] pattern, int subscribedChannels) { + // no-op + } + + @Override + public void onPUnsubscribe(byte[] pattern, int subscribedChannels) { + // no-op + } + + @Override + public void onSubscribe(byte[] channel, int subscribedChannels) { + // no-op + } + + @Override + public void onUnsubscribe(byte[] channel, int subscribedChannels) { + // no-op + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisSubscription.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisSubscription.java new file mode 100644 index 000000000..a2be1371a --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisSubscription.java @@ -0,0 +1,72 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.jedis; + +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.util.AbstractSubscription; + +import redis.clients.jedis.BinaryJedisPubSub; + +/** + * Jedis specific subscription. + * + * @author Costin Leau + */ +class JedisSubscription extends AbstractSubscription { + + private final BinaryJedisPubSub jedisPubSub; + + JedisSubscription(MessageListener listener, BinaryJedisPubSub jedisPubSub, byte[][] channels, byte[][] patterns) { + super(listener, channels, patterns); + this.jedisPubSub = jedisPubSub; + } + + @Override + protected void doClose() { + jedisPubSub.unsubscribe(); + jedisPubSub.punsubscribe(); + } + + @Override + protected void doPsubscribe(byte[]... patterns) { + jedisPubSub.psubscribe(patterns); + } + + @Override + protected void doPUnsubscribe(boolean all, byte[]... patterns) { + if (all) { + jedisPubSub.punsubscribe(); + } + else { + jedisPubSub.punsubscribe(patterns); + } + } + + @Override + protected void doSubscribe(byte[]... channels) { + jedisPubSub.subscribe(channels); + } + + @Override + protected void doUnsubscribe(boolean all, byte[]... channels) { + if (all) { + jedisPubSub.unsubscribe(); + } + else { + jedisPubSub.unsubscribe(channels); + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisUtils.java new file mode 100644 index 000000000..f02e30fd8 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisUtils.java @@ -0,0 +1,233 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection.jedis; + +import java.io.IOException; +import java.io.StringReader; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.keyvalue.redis.RedisConnectionFailureException; +import org.springframework.data.keyvalue.redis.RedisSystemException; +import org.springframework.data.keyvalue.redis.connection.DefaultTuple; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.connection.RedisListCommands.Position; +import org.springframework.data.keyvalue.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Order; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Range; +import org.springframework.util.Assert; + +import redis.clients.jedis.BinaryJedisPubSub; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.SortingParams; +import redis.clients.jedis.BinaryClient.LIST_POSITION; +import redis.clients.jedis.exceptions.JedisConnectionException; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.exceptions.JedisException; + +/** + * Helper class featuring methods for Jedis connection handling, providing support for exception translation. + * + * @author Costin Leau + */ +public abstract class JedisUtils { + + private static final String OK_CODE = "OK"; + private static final String OK_MULTI_CODE = "+OK"; + private static final byte[] ONE = new byte[] { 1 }; + private static final byte[] ZERO = new byte[] { 0 }; + + /** + * Converts the given, native Jedis exception to Spring's DAO hierarchy. + * + * @param ex Jedis exception + * @return converted exception + */ + public static DataAccessException convertJedisAccessException(JedisException ex) { + if (ex instanceof JedisDataException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof JedisConnectionException) { + return new RedisConnectionFailureException(ex.getMessage(), ex); + } + + // fallback to invalid data exception + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + + /** + * Converts the given, native, runtime Jedis exception to Spring's DAO hierarchy. + * + * @param ex Jedis runtime/unchecked exception + * @return converted exception + */ + public static DataAccessException convertJedisAccessException(RuntimeException ex) { + if (ex instanceof JedisException) { + return convertJedisAccessException((JedisException) ex); + } + + return new RedisSystemException("Unknown exception", ex); + } + + static DataAccessException convertJedisAccessException(IOException ex) { + if (ex instanceof UnknownHostException) { + return new RedisConnectionFailureException("Unknown host " + ex.getMessage(), ex); + } + return new RedisConnectionFailureException("Could not connect to Redis server", ex); + } + + static DataAccessException convertJedisAccessException(TimeoutException ex) { + throw new RedisConnectionFailureException("Jedis pool timed out. Could not get Redis Connection", ex); + } + + static boolean isStatusOk(String status) { + return status != null && (OK_CODE.equals(status) || OK_MULTI_CODE.equals(status)); + } + + static Boolean convertCodeReply(Number code) { + return (code != null ? code.intValue() == 1 : null); + } + + static Set convertJedisTuple(Set tuples) { + Set value = new LinkedHashSet(tuples.size()); + for (redis.clients.jedis.Tuple tuple : tuples) { + value.add(new DefaultTuple(tuple.getBinaryElement(), tuple.getScore())); + } + + return value; + } + + static byte[][] convert(Map hgetAll) { + byte[][] result = new byte[hgetAll.size() * 2][]; + + int index = 0; + for (Map.Entry entry : hgetAll.entrySet()) { + result[index++] = entry.getKey(); + result[index++] = entry.getValue(); + } + return result; + } + + static Map convert(String[] fields, String[] values) { + Map result = new LinkedHashMap(fields.length); + + for (int i = 0; i < values.length; i++) { + result.put(fields[i], values[i]); + } + return result; + } + + static String[] arrange(String[] keys, String[] values) { + String[] result = new String[keys.length * 2]; + + for (int i = 0; i < keys.length; i++) { + int index = i << 1; + result[index] = keys[i]; + result[index + 1] = values[i]; + + } + return result; + } + + static SortingParams convertSortParams(SortParameters params) { + SortingParams jedisParams = null; + + if (params != null) { + jedisParams = new SortingParams(); + + byte[] byPattern = params.getByPattern(); + if (byPattern != null) { + jedisParams.by(params.getByPattern()); + } + + byte[][] getPattern = params.getGetPattern(); + if (getPattern != null) { + jedisParams.get(getPattern); + } + + Range limit = params.getLimit(); + if (limit != null) { + jedisParams.limit((int) limit.getStart(), (int) limit.getCount()); + } + Order order = params.getOrder(); + if (order != null && order.equals(Order.DESC)) { + jedisParams.desc(); + } + Boolean isAlpha = params.isAlphabetic(); + if (isAlpha != null && isAlpha) { + jedisParams.alpha(); + } + } + + return jedisParams; + } + + static byte[] asBit(boolean value) { + return (value ? ONE : ZERO); + } + + static LIST_POSITION convertPosition(Position where) { + Assert.notNull("list positions are mandatory"); + return (Position.AFTER.equals(where) ? LIST_POSITION.AFTER : LIST_POSITION.BEFORE); + } + + static Properties info(String string) { + Properties info = new Properties(); + StringReader stringReader = new StringReader(string); + try { + info.load(stringReader); + } catch (Exception ex) { + throw new RedisSystemException("Cannot read Redis info", ex); + } finally { + stringReader.close(); + } + return info; + } + + static BinaryJedisPubSub adaptPubSub(MessageListener listener) { + return new JedisMessageListener(listener); + } + + static String[] convert(byte[]... raw) { + String[] result = new String[raw.length]; + + for (int i = 0; i < raw.length; i++) { + result[i] = new String(raw[i]); + } + + return result; + } + + static byte[][] bXPopArgs(int timeout, byte[]... keys) { + final List args = new ArrayList(); + for (final byte[] arg : keys) { + args.add(arg); + } + args.add(Protocol.toByteArray(timeout)); + return args.toArray(new byte[args.size()][]); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/package-info.java new file mode 100644 index 000000000..40ce7c10d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jedis/package-info.java @@ -0,0 +1,5 @@ +/** + * Connection package for Jedis library. + */ +package org.springframework.data.keyvalue.redis.connection.jedis; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisConnection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisConnection.java new file mode 100644 index 000000000..00433eee2 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisConnection.java @@ -0,0 +1,1140 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.jredis; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.jredis.ClientRuntimeException; +import org.jredis.JRedis; +import org.jredis.RedisException; +import org.jredis.Sort; +import org.jredis.Query.Support; +import org.jredis.ri.alphazero.JRedisService; +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.UncategorizedKeyvalueStoreException; +import org.springframework.data.keyvalue.redis.RedisSystemException; +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.connection.Subscription; +import org.springframework.util.Assert; + +/** + * {@code RedisConnection} implementation on top of JRedis library. + * + * @author Costin Leau + */ +public class JredisConnection implements RedisConnection { + + private final JRedis jredis; + private final boolean isPool; + private boolean isClosed = false; + + /** + * Constructs a new JredisConnection instance. + * + * @param jredis JRedis connection + */ + public JredisConnection(JRedis jredis) { + Assert.notNull(jredis, "a not-null instance required"); + this.jredis = jredis; + // required since Jredis combines the pool and the connection under the same interface/class + this.isPool = (jredis instanceof JRedisService); + } + + protected DataAccessException convertJredisAccessException(Exception ex) { + if (ex instanceof RedisException) { + return JredisUtils.convertJredisAccessException((RedisException) ex); + } + + if (ex instanceof ClientRuntimeException) { + return JredisUtils.convertJredisAccessException((ClientRuntimeException) ex); + } + + return new UncategorizedKeyvalueStoreException("Unknown JRedis exception", ex); + } + + @Override + public void close() throws RedisSystemException { + isClosed = true; + + // don't actually close the connection + // if a pool is used + if (!isPool) { + try { + jredis.quit(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + } + + @Override + public JRedis getNativeConnection() { + return jredis; + } + + @Override + public boolean isClosed() { + return isClosed; + } + + @Override + public boolean isQueueing() { + return false; + } + + @Override + public boolean isPipelined() { + return false; + } + + @Override + public void openPipeline() { + throw new UnsupportedOperationException("Pipelining not supported by JRedis"); + } + + @Override + public List closePipeline() { + return Collections.emptyList(); + } + + @Override + public List sort(byte[] key, SortParameters params) { + Sort sort = jredis.sort(JredisUtils.decode(key)); + JredisUtils.applySortingParams(sort, params, null); + try { + return sort.exec(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long sort(byte[] key, SortParameters params, byte[] storeKey) { + Sort sort = jredis.sort(JredisUtils.decode(key)); + JredisUtils.applySortingParams(sort, params, null); + try { + return Support.unpackValue(sort.exec()); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long dbSize() { + try { + return jredis.dbsize(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void flushDb() { + try { + jredis.flushdb(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void flushAll() { + try { + jredis.flushall(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] echo(byte[] message) { + try { + return jredis.echo(message); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public String ping() { + try { + jredis.ping(); + return "PONG"; + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void bgSave() { + try { + jredis.bgsave(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void bgWriteAof() { + try { + jredis.bgrewriteaof(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void save() { + try { + jredis.save(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public List getConfig(String pattern) { + throw new UnsupportedOperationException(); + } + + @Override + public Properties info() { + try { + return JredisUtils.info(jredis.info()); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long lastSave() { + try { + return jredis.lastsave(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void setConfig(String param, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public void resetConfigStats() { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public Long del(byte[]... keys) { + try { + return jredis.del(JredisUtils.decodeMultiple(keys)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void discard() { + try { + jredis.discard(); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public List exec() { + throw new UnsupportedOperationException(); + } + + @Override + public Boolean exists(byte[] key) { + try { + return jredis.exists(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean expire(byte[] key, long seconds) { + try { + return jredis.expire(JredisUtils.decode(key), (int) seconds); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean expireAt(byte[] key, long unixTime) { + try { + return jredis.expireat(JredisUtils.decode(key), unixTime); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set keys(byte[] pattern) { + try { + return JredisUtils.convertToSet(jredis.keys(JredisUtils.decode(pattern))); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void multi() { + throw new UnsupportedOperationException(); + } + + @Override + public Boolean persist(byte[] key) { + throw new UnsupportedOperationException(); + } + + + @Override + public Boolean move(byte[] key, int dbIndex) { + try { + return jredis.move(JredisUtils.decode(key), dbIndex); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] randomKey() { + try { + return JredisUtils.encode(jredis.randomkey()); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void rename(byte[] oldName, byte[] newName) { + try { + jredis.rename(JredisUtils.decode(oldName), JredisUtils.decode(newName)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean renameNX(byte[] oldName, byte[] newName) { + try { + return jredis.renamenx(JredisUtils.decode(oldName), JredisUtils.decode(newName)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void select(int dbIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public Long ttl(byte[] key) { + try { + return jredis.ttl(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public DataType type(byte[] key) { + try { + return JredisUtils.convertDataType(jredis.type(JredisUtils.decode(key))); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void unwatch() { + throw new UnsupportedOperationException(); + } + + @Override + public void watch(byte[]... keys) { + throw new UnsupportedOperationException(); + } + + // + // String operations + // + + @Override + public byte[] get(byte[] key) { + try { + return jredis.get(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void set(byte[] key, byte[] value) { + try { + jredis.set(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] getSet(byte[] key, byte[] value) { + try { + return jredis.getset(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long append(byte[] key, byte[] value) { + try { + return jredis.append(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public List mGet(byte[]... keys) { + try { + return jredis.mget(JredisUtils.decodeMultiple(keys)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void mSet(Map tuple) { + try { + jredis.mset(JredisUtils.decodeMap(tuple)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void mSetNX(Map tuple) { + try { + jredis.msetnx(JredisUtils.decodeMap(tuple)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void setEx(byte[] key, long seconds, byte[] value) { + throw new UnsupportedOperationException(); + } + + @Override + public Boolean setNX(byte[] key, byte[] value) { + try { + return jredis.setnx(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] getRange(byte[] key, long start, long end) { + try { + return jredis.substr(JredisUtils.decode(key), start, end); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long decr(byte[] key) { + try { + return jredis.decr(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long decrBy(byte[] key, long value) { + try { + return jredis.decrby(JredisUtils.decode(key), (int) value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long incr(byte[] key) { + try { + return jredis.incr(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long incrBy(byte[] key, long value) { + try { + return jredis.incrby(JredisUtils.decode(key), (int) value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean getBit(byte[] key, long offset) { + throw new UnsupportedOperationException(); + } + + @Override + public void setBit(byte[] key, long offset, boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setRange(byte[] key, byte[] value, long start) { + throw new UnsupportedOperationException(); + } + + @Override + public Long strLen(byte[] key) { + throw new UnsupportedOperationException(); + } + + // + // List commands + // + + @Override + public List bLPop(int timeout, byte[]... keys) { + throw new UnsupportedOperationException(); + } + + @Override + public List bRPop(int timeout, byte[]... keys) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] lIndex(byte[] key, long index) { + try { + return jredis.lindex(JredisUtils.decode(key), index); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long lLen(byte[] key) { + try { + return jredis.llen(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] lPop(byte[] key) { + try { + return jredis.lpop(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long lPush(byte[] key, byte[] value) { + try { + jredis.lpush(JredisUtils.decode(key), value); + return null; + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public List lRange(byte[] key, long start, long end) { + try { + List lrange = jredis.lrange(JredisUtils.decode(key), start, end); + + return lrange; + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long lRem(byte[] key, long count, byte[] value) { + try { + return jredis.lrem(JredisUtils.decode(key), value, (int) count); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void lSet(byte[] key, long index, byte[] value) { + try { + jredis.lset(JredisUtils.decode(key), index, value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void lTrim(byte[] key, long start, long end) { + try { + jredis.ltrim(JredisUtils.decode(key), start, end); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] rPop(byte[] key) { + try { + return jredis.rpop(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + try { + return jredis.rpoplpush(JredisUtils.decode(srcKey), JredisUtils.decode(dstKey)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long rPush(byte[] key, byte[] value) { + try { + jredis.rpush(JredisUtils.decode(key), value); + return null; + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + throw new UnsupportedOperationException(); + } + + @Override + public Long lPushX(byte[] key, byte[] value) { + throw new UnsupportedOperationException(); + } + + @Override + public Long rPushX(byte[] key, byte[] value) { + throw new UnsupportedOperationException(); + } + + + // + // Set commands + // + + @Override + public Boolean sAdd(byte[] key, byte[] value) { + try { + return jredis.sadd(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long sCard(byte[] key) { + try { + return jredis.scard(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set sDiff(byte[]... keys) { + String destKey = JredisUtils.decode(keys[0]); + String[] sets = JredisUtils.decodeMultiple(Arrays.copyOfRange(keys, 1, keys.length)); + + try { + List result = jredis.sdiff(destKey, sets); + return new LinkedHashSet(result); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void sDiffStore(byte[] destKey, byte[]... keys) { + String destSet = JredisUtils.decode(destKey); + String[] sets = JredisUtils.decodeMultiple(keys); + + try { + jredis.sdiffstore(destSet, sets); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set sInter(byte[]... keys) { + String set1 = JredisUtils.decode(keys[0]); + String[] sets = JredisUtils.decodeMultiple(Arrays.copyOfRange(keys, 1, keys.length)); + + try { + List result = jredis.sinter(set1, sets); + return new LinkedHashSet(result); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void sInterStore(byte[] destKey, byte[]... keys) { + String destSet = JredisUtils.decode(destKey); + String[] sets = JredisUtils.decodeMultiple(keys); + + try { + jredis.sinterstore(destSet, sets); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean sIsMember(byte[] key, byte[] value) { + try { + return jredis.sismember(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set sMembers(byte[] key) { + try { + return new LinkedHashSet(jredis.smembers(JredisUtils.decode(key))); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + try { + return jredis.smove(JredisUtils.decode(srcKey), JredisUtils.decode(destKey), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] sPop(byte[] key) { + try { + return jredis.spop(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] sRandMember(byte[] key) { + try { + return jredis.srandmember(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean sRem(byte[] key, byte[] value) { + try { + return jredis.srem(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set sUnion(byte[]... keys) { + String set1 = JredisUtils.decode(keys[0]); + String[] sets = JredisUtils.decodeMultiple(Arrays.copyOfRange(keys, 1, keys.length)); + + try { + return new LinkedHashSet(jredis.sunion(set1, sets)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public void sUnionStore(byte[] destKey, byte[]... keys) { + String destSet = JredisUtils.decode(destKey); + String[] sets = JredisUtils.decodeMultiple(keys); + + try { + jredis.sunionstore(destSet, sets); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + + // + // ZSet commands + // + + @Override + public Boolean zAdd(byte[] key, double score, byte[] value) { + try { + return jredis.zadd(JredisUtils.decode(key), score, value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long zCard(byte[] key) { + try { + return jredis.zcard(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long zCount(byte[] key, double min, double max) { + try { + return jredis.zcount(JredisUtils.decode(key), min, max); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Double zIncrBy(byte[] key, double increment, byte[] value) { + try { + return jredis.zincrby(JredisUtils.decode(key), increment, value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + throw new UnsupportedOperationException(); + } + + @Override + public Long zInterStore(byte[] destKey, byte[]... sets) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRange(byte[] key, long start, long end) { + try { + return new LinkedHashSet(jredis.zrange(JredisUtils.decode(key), start, end)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set zRangeWithScores(byte[] key, long start, long end) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRangeByScore(byte[] key, double min, double max) { + try { + return new LinkedHashSet(jredis.zrangebyscore(JredisUtils.decode(key), min, max)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRangeByScore(byte[] key, double min, double max, long offset, long count) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRevRangeByScore(byte[] key, double min, double max, long offset, long count) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRevRangeByScore(byte[] key, double min, double max) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + throw new UnsupportedOperationException(); + } + + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max) { + throw new UnsupportedOperationException(); + } + + @Override + public Long zRank(byte[] key, byte[] value) { + try { + return jredis.zrank(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean zRem(byte[] key, byte[] value) { + try { + return jredis.zrem(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long zRemRange(byte[] key, long start, long end) { + try { + return jredis.zremrangebyrank(JredisUtils.decode(key), start, end); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long zRemRangeByScore(byte[] key, double min, double max) { + try { + return jredis.zremrangebyscore(JredisUtils.decode(key), min, max); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set zRevRange(byte[] key, long start, long end) { + try { + return new LinkedHashSet(jredis.zrevrange(JredisUtils.decode(key), start, end)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Set zRevRangeWithScores(byte[] key, long start, long end) { + throw new UnsupportedOperationException(); + } + + @Override + public Long zRevRank(byte[] key, byte[] value) { + try { + return jredis.zrevrank(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Double zScore(byte[] key, byte[] value) { + try { + return jredis.zscore(JredisUtils.decode(key), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + + // + // Hash commands + // + + @Override + public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + throw new UnsupportedOperationException(); + } + + @Override + public Long zUnionStore(byte[] destKey, byte[]... sets) { + throw new UnsupportedOperationException(); + } + + @Override + public Boolean hDel(byte[] key, byte[] field) { + try { + return jredis.hdel(JredisUtils.decode(key), JredisUtils.decode(field)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean hExists(byte[] key, byte[] field) { + try { + return jredis.hexists(JredisUtils.decode(key), JredisUtils.decode(field)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public byte[] hGet(byte[] key, byte[] field) { + try { + return jredis.hget(JredisUtils.decode(key), JredisUtils.decode(field)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Map hGetAll(byte[] key) { + try { + return JredisUtils.encodeMap(jredis.hgetall(JredisUtils.decode(key))); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long hIncrBy(byte[] key, byte[] field, long delta) { + throw new UnsupportedOperationException(); + } + + @Override + public Set hKeys(byte[] key) { + try { + return new LinkedHashSet(JredisUtils.convertToSet(jredis.hkeys(JredisUtils.decode(key)))); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Long hLen(byte[] key) { + try { + return jredis.hlen(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public List hMGet(byte[] key, byte[]... fields) { + throw new UnsupportedOperationException(); + } + + @Override + public void hMSet(byte[] key, Map values) { + throw new UnsupportedOperationException(); + } + + @Override + public Boolean hSet(byte[] key, byte[] field, byte[] value) { + try { + return jredis.hset(JredisUtils.decode(key), JredisUtils.decode(field), value); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + @Override + public Boolean hSetNX(byte[] key, byte[] field, byte[] value) { + throw new UnsupportedOperationException(); + } + + @Override + public List hVals(byte[] key) { + try { + return jredis.hvals(JredisUtils.decode(key)); + } catch (Exception ex) { + throw convertJredisAccessException(ex); + } + } + + // + // PubSub commands + // + + @Override + public Subscription getSubscription() { + return null; + } + + @Override + public boolean isSubscribed() { + return false; + } + + @Override + public void pSubscribe(MessageListener listener, byte[]... patterns) { + throw new UnsupportedOperationException(); + } + + @Override + public Long publish(byte[] channel, byte[] message) { + throw new UnsupportedOperationException(); + } + + @Override + public void subscribe(MessageListener listener, byte[]... channels) { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisConnectionFactory.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisConnectionFactory.java new file mode 100644 index 000000000..a52d377a1 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisConnectionFactory.java @@ -0,0 +1,246 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.jredis; + +import org.jredis.ClientRuntimeException; +import org.jredis.connector.Connection; +import org.jredis.connector.ConnectionSpec; +import org.jredis.connector.Connection.Socket.Property; +import org.jredis.ri.alphazero.JRedisClient; +import org.jredis.ri.alphazero.JRedisService; +import org.jredis.ri.alphazero.connection.DefaultConnectionSpec; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Connection factory using creating JRedis based connections. + * + * @author Costin Leau + */ +public class JredisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory { + + private ConnectionSpec connectionSpec; + + private String hostName = "localhost"; + private int port = DEFAULT_REDIS_PORT; + private String password = null; + private int timeout; + + private boolean usePool = true; + private int dbIndex = DEFAULT_REDIS_DB; + + private JRedisService pool = null; + // taken from JRedis code + private int poolSize = 5; + + private static final int DEFAULT_REDIS_PORT = 6379; + private static final int DEFAULT_REDIS_DB = 0; + private static final byte[] DEFAULT_REDIS_PASSWORD = null; + + + /** + * Constructs a new JredisConnectionFactory instance. + */ + public JredisConnectionFactory() { + } + + /** + * Constructs a new JredisConnectionFactory instance. + * Will override the other connection parameters passed to the factory. + * + * @param connectionSpec already configured connection. + */ + public JredisConnectionFactory(ConnectionSpec connectionSpec) { + this.connectionSpec = connectionSpec; + } + + @Override + public void afterPropertiesSet() { + if (connectionSpec == null) { + Assert.hasText(hostName); + connectionSpec = DefaultConnectionSpec.newSpec(hostName, port, dbIndex, DEFAULT_REDIS_PASSWORD); + connectionSpec.setConnectionFlag(Connection.Flag.RELIABLE, false); + + if (StringUtils.hasLength(password)) { + connectionSpec.setCredentials(password); + } + + if (timeout > 0) { + connectionSpec.setSocketProperty(Property.SO_TIMEOUT, timeout); + } + } + + if (usePool) { + int size = getPoolSize(); + pool = new JRedisService(connectionSpec, size); + } + } + + + @Override + public void destroy() { + if (usePool && pool != null) { + pool.quit(); + pool = null; + } + } + + + @Override + public RedisConnection getConnection() { + return postProcessConnection(new JredisConnection((usePool ? pool : new JRedisClient(connectionSpec)))); + } + + + /** + * Post process a newly retrieved connection. Useful for decorating or executing + * initialization commands on a new connection. + * This implementation simply returns the connection. + * + * @param connection + * @return processed connection + */ + protected RedisConnection postProcessConnection(JredisConnection connection) { + return connection; + } + + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof ClientRuntimeException) { + return JredisUtils.convertJredisAccessException((ClientRuntimeException) ex); + } + return null; + } + + + /** + * Returns the Redis host name of this factory. + * + * @return Returns the hostName + */ + public String getHostName() { + return hostName; + } + + /** + * Sets the Redis host name for this factory. + * + * @param hostName The hostName to set. + */ + public void setHostName(String hostName) { + this.hostName = hostName; + } + + + /** + * Returns the Redis port. + * + * @return Returns the port + */ + public int getPort() { + return port; + } + + /** + * Sets the Redis port. + * + * @param port The port to set. + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Returns the password used for authenticating with the Redis server. + * + * @return password for authentication + */ + public String getPassword() { + return password; + } + + /** + * Sets the password used for authenticating with the Redis server. + * + * @param password the password to set + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Indicates the use of a connection pool. + * + * @return Returns the use of connection pooling. + */ + public boolean getUsePool() { + return usePool; + } + + /** + * Turns on or off the use of connection pooling. + * + * @param usePool The usePool to set. + */ + public void setUsePool(boolean usePool) { + this.usePool = usePool; + } + + /** + * Returns the pool size of this factory. + * + * @return Returns the poolSize + */ + public int getPoolSize() { + return poolSize; + } + + /** + * Sets the connection pool size of the underlying factory. + * + * @param poolSize The poolSize to set. + */ + public void setPoolSize(int poolSize) { + Assert.isTrue(poolSize > 0, "pool size needs to be bigger then zero"); + this.poolSize = poolSize; + usePool = true; + } + + /** + * Returns the index of the database. + * + * @return Returns the database index + */ + public int getDatabase() { + return dbIndex; + } + + /** + * Sets the index of the database used by this connection factory. + * Can be between 0 (default) and 15. + * + * @param index database index + */ + public void setDatabase(int index) { + Assert.isTrue(index >= 0, "invalid DB index (a positive index required)"); + this.dbIndex = index; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisUtils.java new file mode 100644 index 000000000..9cb3dc146 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/JredisUtils.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection.jredis; + +import java.util.Collection; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.jredis.ClientRuntimeException; +import org.jredis.RedisException; +import org.jredis.RedisType; +import org.jredis.Sort; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Order; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Range; +import org.springframework.data.keyvalue.redis.connection.util.DecodeUtils; + +/** + * Helper class featuring methods for JRedis connection handling, providing support for exception translation. + * + * @author Costin Leau + */ +public abstract class JredisUtils { + + /** + * Converts the given, native JRedis exception to Spring's DAO hierarchy. + * + * @param ex JRedis exception + * @return converted exception + */ + public static DataAccessException convertJredisAccessException(RedisException ex) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + + /** + * Converts the given, native JRedis exception to Spring's DAO hierarchy. + * + * @param ex JRedis exception + * @return converted exception + */ + public static DataAccessException convertJredisAccessException(ClientRuntimeException ex) { + return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex); + } + + static DataType convertDataType(RedisType type) { + switch (type) { + case NONE: + return DataType.NONE; + case string: + return DataType.STRING; + case list: + return DataType.LIST; + case set: + return DataType.SET; + //case zset: + // return DataType.ZSET; + case hash: + return DataType.HASH; + } + + return null; + } + + static String decode(byte[] bytes) { + return DecodeUtils.decode(bytes); + } + + static byte[] encode(String string) { + return DecodeUtils.encode(string); + } + + static String[] decodeMultiple(byte[]... bytes) { + return DecodeUtils.decodeMultiple(bytes); + } + + static Map encodeMap(Map map) { + return DecodeUtils.encodeMap(map); + } + + static Map decodeMap(Map tuple) { + return DecodeUtils.decodeMap(tuple); + } + + static Set convertToSet(Collection keys) { + return DecodeUtils.convertToSet(keys); + } + + static Sort applySortingParams(Sort jredisSort, SortParameters params, byte[] storeKey) { + if (params != null) { + byte[] byPattern = params.getByPattern(); + if (byPattern != null) { + jredisSort.BY(decode(byPattern)); + } + byte[][] getPattern = params.getGetPattern(); + + if (getPattern != null && getPattern.length > 0) { + for (byte[] bs : getPattern) { + jredisSort.GET(decode(bs)); + } + } + Range limit = params.getLimit(); + if (limit != null) { + jredisSort.LIMIT(limit.getStart(), limit.getCount()); + } + Order order = params.getOrder(); + if (order != null && order.equals(Order.DESC)) { + jredisSort.DESC(); + } + Boolean isAlpha = params.isAlphabetic(); + if (isAlpha != null && isAlpha) { + jredisSort.ALPHA(); + } + } + + if (storeKey != null) { + jredisSort.STORE(decode(storeKey)); + } + + + return jredisSort; + } + + static Properties info(Map map) { + Properties info = new Properties(); + info.putAll(map); + return info; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/package-info.java new file mode 100644 index 000000000..0affe42a0 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/jredis/package-info.java @@ -0,0 +1,5 @@ +/** + * Connection package for JRedis library. + */ +package org.springframework.data.keyvalue.redis.connection.jredis; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/package-info.java new file mode 100644 index 000000000..72c48c6b4 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/package-info.java @@ -0,0 +1,8 @@ +/** + * Connection package providing low-level abstractions for interacting with + * the various Redis 'drivers'/libraries. + * + *

Performs exception translation between the underlying library exceptions to Spring's DAO hierarchy. + */ +package org.springframework.data.keyvalue.redis.connection; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/CloseSuppressingRjcConnection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/CloseSuppressingRjcConnection.java new file mode 100644 index 000000000..f5adba611 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/CloseSuppressingRjcConnection.java @@ -0,0 +1,117 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.List; + +import org.idevlab.rjc.ds.RedisConnection; +import org.idevlab.rjc.message.RedisNodeSubscriber; +import org.idevlab.rjc.protocol.Protocol.Command; + +/** + * Basic decorator suppressing close() calls to the underlying connection. + * Used for reusing arbitrary connections with {@link RedisNodeSubscriber} without + * resorting to connection pooling. + * + * @author Costin Leau + */ +class CloseSuppressingRjcConnection implements RedisConnection { + + private final RedisConnection delegate; + + /** + * Constructs a new CloseSuppressingRjcConnection instance. + * + * @param delegate + */ + CloseSuppressingRjcConnection(RedisConnection delegate) { + this.delegate = delegate; + } + + public void close() { + // no-op + } + + public void connect() throws UnknownHostException, IOException { + delegate.connect(); + } + + public List getAll() { + return delegate.getAll(); + } + + public String getBulkReply() { + return delegate.getBulkReply(); + } + + public String getHost() { + return delegate.getHost(); + } + + public Long getIntegerReply() { + return delegate.getIntegerReply(); + } + + public List getMultiBulkReply() { + return delegate.getMultiBulkReply(); + } + + public List getObjectMultiBulkReply() { + return delegate.getObjectMultiBulkReply(); + } + + public Object getOne() { + return delegate.getOne(); + } + + public int getPort() { + return delegate.getPort(); + } + + public String getStatusCodeReply() { + return delegate.getStatusCodeReply(); + } + + public int getTimeout() { + return delegate.getTimeout(); + } + + public boolean isConnected() { + return delegate.isConnected(); + } + + public void rollbackTimeout() { + delegate.rollbackTimeout(); + } + + public void sendCommand(Command arg0, byte[]... arg1) { + delegate.sendCommand(arg0, arg1); + } + + public void sendCommand(Command arg0, String... arg1) { + delegate.sendCommand(arg0, arg1); + } + + public void sendCommand(Command arg0) { + delegate.sendCommand(arg0); + } + + public void setTimeoutInfinite() { + delegate.setTimeoutInfinite(); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnection.java new file mode 100644 index 000000000..1176fc57d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnection.java @@ -0,0 +1,2144 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.idevlab.rjc.Client; +import org.idevlab.rjc.RedisException; +import org.idevlab.rjc.Session; +import org.idevlab.rjc.SessionFactoryImpl; +import org.idevlab.rjc.SortingParams; +import org.idevlab.rjc.ZParams; +import org.idevlab.rjc.message.RedisNodeSubscriber; +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.UncategorizedKeyvalueStoreException; +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisSubscribedConnectionException; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.connection.Subscription; + +/** + * {@code RedisConnection} implementation on top of rjc library. + * + * @author Costin Leau + */ +public class RjcConnection implements RedisConnection { + + private final int dbIndex; + private boolean isClosed = false; + + private final Client client; + private final Session session; + private volatile Client pipeline; + + private volatile RjcSubscription subscription; + private volatile RedisNodeSubscriber subscriber; + + public RjcConnection(org.idevlab.rjc.ds.RedisConnection connection, int dbIndex) { + SingleDataSource connectionDataSource = new SingleDataSource(connection); + session = new SessionFactoryImpl(connectionDataSource).create(); + subscriber = new RedisNodeSubscriber(); + subscriber.setDataSource(new SingleDataSource(new CloseSuppressingRjcConnection(connection))); + client = new Client(connection); + + this.dbIndex = dbIndex; + + // select the db + if (dbIndex > 0) { + select(dbIndex); + } + } + + protected DataAccessException convertRjcAccessException(Exception ex) { + if (ex instanceof RedisException) { + return RjcUtils.convertRjcAccessException((RedisException) ex); + } + return new UncategorizedKeyvalueStoreException("Unknown rjc exception", ex); + } + + @Override + public void close() throws DataAccessException { + isClosed = true; + + // reset the connection (in case a pool is being used) + if (dbIndex > 0) { + select(0); + } + + try { + subscriber.close(); + session.close(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + @Override + public boolean isClosed() { + return isClosed; + } + + @Override + public Session getNativeConnection() { + return session; + } + + @Override + public boolean isQueueing() { + return client.isInMulti(); + } + + @Override + public boolean isPipelined() { + return (pipeline != null); + } + + @Override + public void openPipeline() { + if (pipeline == null) { + pipeline = client; + } + } + + @SuppressWarnings("unchecked") + @Override + public List closePipeline() { + if (pipeline != null) { + List execute = client.getAll(); + if (execute != null && !execute.isEmpty()) { + return execute; + } + } + return Collections.emptyList(); + } + + @Override + public List sort(byte[] key, SortParameters params) { + + SortingParams sortParams = RjcUtils.convertSortParams(params); + final String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + if (sortParams != null) { + pipeline.sort(stringKey, sortParams); + } + else { + pipeline.sort(stringKey); + } + + return null; + } + return RjcUtils.convertToList((sortParams != null ? session.sort(stringKey, sortParams) + : session.sort(stringKey))); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long sort(byte[] key, SortParameters params, byte[] sortKey) { + + SortingParams sortParams = RjcUtils.convertSortParams(params); + final String stringKey = RjcUtils.decode(key); + final String stringSortKey = RjcUtils.decode(sortKey); + + try { + if (isPipelined()) { + if (sortParams != null) { + pipeline.sort(stringKey, sortParams, stringSortKey); + } + else { + pipeline.sort(stringKey, stringSortKey); + } + + return null; + } + return (sortParams != null ? session.sort(stringKey, sortParams, stringSortKey) : session.sort(stringKey, + stringSortKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long dbSize() { + try { + if (isPipelined()) { + pipeline.dbSize(); + return null; + } + return session.dbSize(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + @Override + public void flushDb() { + try { + if (isPipelined()) { + pipeline.flushDB(); + return; + } + session.flushDB(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void flushAll() { + try { + if (isPipelined()) { + pipeline.flushAll(); + return; + } + session.flushAll(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void bgSave() { + try { + if (isPipelined()) { + pipeline.bgsave(); + return; + } + session.bgsave(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void bgWriteAof() { + try { + if (isPipelined()) { + pipeline.bgrewriteaof(); + return; + } + session.bgrewriteaof(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void save() { + try { + if (isPipelined()) { + pipeline.save(); + return; + } + session.save(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List getConfig(String param) { + try { + if (isPipelined()) { + pipeline.configGet(param); + return null; + } + return session.configGet(param); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Properties info() { + try { + if (isPipelined()) { + pipeline.info(); + return null; + } + return RjcUtils.info(session.info()); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long lastSave() { + try { + if (isPipelined()) { + pipeline.lastsave(); + return null; + } + return session.lastsave(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void setConfig(String param, String value) { + try { + if (isPipelined()) { + pipeline.configSet(param, value); + return; + } + session.configSet(param, value); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + @Override + public void resetConfigStats() { + try { + if (isPipelined()) { + pipeline.configResetStat(); + return; + } + client.configResetStat(); + client.getStatusCodeReply(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void shutdown() { + try { + if (isPipelined()) { + pipeline.shutdown(); + return; + } + session.shutdown(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] echo(byte[] message) { + String stringMsg = RjcUtils.decode(message); + try { + if (isPipelined()) { + pipeline.echo(stringMsg); + return null; + } + return RjcUtils.encode(session.echo(stringMsg)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public String ping() { + try { + if (isPipelined()) { + pipeline.ping(); + } + return session.ping(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long del(byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + if (isPipelined()) { + pipeline.del(stringKeys); + return null; + } + return session.del(stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void discard() { + try { + if (isPipelined()) { + pipeline.discard(); + return; + } + + session.discard(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List exec() { + try { + if (isPipelined()) { + pipeline.exec(); + return null; + } + return session.exec(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean exists(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.exists(stringKey); + return null; + } + return session.exists(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean expire(byte[] key, long seconds) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.expire(stringKey, (int) seconds); + return null; + } + return session.expire(stringKey, (int) seconds); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean expireAt(byte[] key, long unixTime) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.expireAt(stringKey, unixTime); + return null; + } + return session.expireAt(stringKey, unixTime); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set keys(byte[] pattern) { + String stringKey = RjcUtils.decode(pattern); + + try { + if (isPipelined()) { + pipeline.keys(stringKey); + return null; + } + return RjcUtils.convertToSet(session.keys(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void multi() { + if (isQueueing()) { + return; + } + try { + if (isPipelined()) { + pipeline.multi(); + return; + } + session.multi(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean persist(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.persist(stringKey); + return null; + } + return session.persist(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean move(byte[] key, int dbIndex) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.move(stringKey, dbIndex); + return null; + } + return session.move(stringKey, dbIndex); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] randomKey() { + try { + if (isPipelined()) { + pipeline.randomKey(); + return null; + } + return RjcUtils.encode(session.randomKey()); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void rename(byte[] oldName, byte[] newName) { + String stringOldKey = RjcUtils.decode(oldName); + String stringNewKey = RjcUtils.decode(newName); + + try { + if (isPipelined()) { + pipeline.rename(stringOldKey, stringNewKey); + return; + } + session.rename(stringOldKey, stringNewKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean renameNX(byte[] oldName, byte[] newName) { + String stringOldKey = RjcUtils.decode(oldName); + String stringNewKey = RjcUtils.decode(newName); + + try { + if (isPipelined()) { + pipeline.renamenx(stringOldKey, stringNewKey); + return null; + } + return session.renamenx(stringOldKey, stringNewKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void select(int dbIndex) { + try { + if (isPipelined()) { + pipeline.select(dbIndex); + return; + } + session.select(dbIndex); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long ttl(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.ttl(stringKey); + return null; + } + return session.ttl(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public DataType type(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.type(stringKey); + return null; + } + return DataType.fromCode(session.type(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void unwatch() { + try { + if (isPipelined()) { + pipeline.unwatch(); + return; + } + + session.unwatch(); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void watch(byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + if (isQueueing()) { + return; + } + try { + if (isPipelined()) { + pipeline.watch(stringKeys); + return; + } + else { + session.watch(stringKeys); + } + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + // + // String commands + // + + @Override + public byte[] get(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.get(stringKey); + return null; + } + + return RjcUtils.encode(session.get(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void set(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.set(stringKey, stringValue); + return; + } + session.set(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + @Override + public byte[] getSet(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.getSet(stringKey, stringValue); + return null; + } + return RjcUtils.encode(session.getSet(stringKey, stringValue)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long append(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.append(stringKey, stringValue); + return null; + } + return session.append(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List mGet(byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + if (isPipelined()) { + pipeline.mget(stringKeys); + return null; + } + return RjcUtils.convertToList(session.mget(stringKeys)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void mSet(Map tuples) { + String[] decodeMap = RjcUtils.flatten(tuples); + + try { + if (isPipelined()) { + pipeline.mset(decodeMap); + return; + } + session.mset(decodeMap); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void mSetNX(Map tuples) { + String[] decodeMap = RjcUtils.flatten(tuples); + + try { + + if (isPipelined()) { + pipeline.msetnx(decodeMap); + return; + } + session.msetnx(decodeMap); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void setEx(byte[] key, long time, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.setex(stringKey, (int) time, stringValue); + return; + } + session.setex(stringKey, (int) time, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean setNX(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.setnx(stringKey, stringValue); + return null; + } + return session.setnx(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] getRange(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.getRange(stringKey, (int) start, (int) end); + return null; + } + return RjcUtils.encode(session.getRange(stringKey, (int) start, (int) end)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long decr(byte[] key) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.decr(stringKey); + return null; + } + return session.decr(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long decrBy(byte[] key, long value) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.decrBy(stringKey, (int) value); + return null; + } + return session.decrBy(stringKey, (int) value); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long incr(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + + if (isPipelined()) { + pipeline.incr(stringKey); + return null; + } + return session.incr(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long incrBy(byte[] key, long value) { + String stringKey = RjcUtils.decode(key); + + + try { + if (isPipelined()) { + pipeline.incrBy(stringKey, (int) value); + return null; + } + return session.incrBy(stringKey, (int) value); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean getBit(byte[] key, long offset) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.getbit(stringKey, (int) offset); + return null; + } + return (session.getBit(stringKey, (int) offset) == 0 ? Boolean.FALSE : Boolean.TRUE); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void setBit(byte[] key, long offset, boolean value) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.setbit(stringKey, (int) offset, RjcUtils.asBit(value)); + return; + } + session.setBit(stringKey, (int) offset, RjcUtils.asBit(value)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void setRange(byte[] key, byte[] value, long offset) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.setRange(stringKey, (int) offset, stringValue); + return; + } + session.setRange(stringKey, (int) offset, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long strLen(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.strlen(stringKey); + return null; + } + return session.strlen(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + // + // List commands + // + + @Override + public Long lPush(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.lpush(stringKey, stringValue); + return null; + } + return session.lpush(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long rPush(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + + if (isPipelined()) { + pipeline.rpush(stringKey, stringValue); + return null; + } + return session.rpush(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List bLPop(int timeout, byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + if (isPipelined()) { + pipeline.blpop(stringKeys); + return null; + } + return RjcUtils.convertToList(session.blpop(timeout, stringKeys)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List bRPop(int timeout, byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + if (isPipelined()) { + pipeline.brpop(stringKeys); + return null; + } + return RjcUtils.convertToList(session.brpop(timeout, stringKeys)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] lIndex(byte[] key, long index) { + String stringKey = RjcUtils.decode(key); + + + try { + if (isPipelined()) { + pipeline.lindex(stringKey, (int) index); + return null; + } + return RjcUtils.encode(session.lindex(stringKey, (int) index)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + String stringPivot = RjcUtils.decode(pivot); + Client.LIST_POSITION position = RjcUtils.convertPosition(where); + + try { + if (isPipelined()) { + pipeline.linsert(stringKey, position, stringPivot, stringValue); + return null; + } + return session.linsert(stringKey, position, stringPivot, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long lLen(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + + if (isPipelined()) { + pipeline.llen(stringKey); + return null; + } + return session.llen(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] lPop(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + + if (isPipelined()) { + pipeline.lpop(stringKey); + return null; + } + return RjcUtils.encode(session.lpop(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List lRange(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + + try { + + if (isPipelined()) { + pipeline.lrange(stringKey, (int) start, (int) end); + return null; + } + return RjcUtils.convertToList(session.lrange(stringKey, (int) start, (int) end)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long lRem(byte[] key, long count, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + + if (isPipelined()) { + pipeline.lrem(stringKey, (int) count, stringValue); + return null; + } + return session.lrem(stringKey, (int) count, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void lSet(byte[] key, long index, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + try { + + if (isPipelined()) { + pipeline.lset(stringKey, (int) index, stringValue); + return; + } + session.lset(stringKey, (int) index, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void lTrim(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + + try { + + if (isPipelined()) { + pipeline.ltrim(stringKey, (int) start, (int) end); + return; + } + session.ltrim(stringKey, (int) start, (int) end); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] rPop(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + + if (isPipelined()) { + pipeline.rpop(stringKey); + return null; + } + return RjcUtils.encode(session.rpop(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + String stringKey = RjcUtils.decode(srcKey); + String stringDest = RjcUtils.decode(dstKey); + + try { + + if (isPipelined()) { + pipeline.rpoplpush(stringKey, stringDest); + return null; + } + return RjcUtils.encode(session.rpoplpush(stringKey, stringDest)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + String stringKey = RjcUtils.decode(srcKey); + String stringDest = RjcUtils.decode(dstKey); + + try { + if (isPipelined()) { + pipeline.brpoplpush(stringKey, stringDest, timeout); + return null; + } + return RjcUtils.encode(session.brpoplpush(stringKey, stringDest, timeout)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long lPushX(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + try { + if (isPipelined()) { + pipeline.lpushx(stringKey, stringValue); + return null; + } + return session.lpushx(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long rPushX(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + try { + if (isPipelined()) { + pipeline.rpushx(stringKey, stringValue); + return null; + } + return session.rpushx(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + // + // Set commands + // + + @Override + public Boolean sAdd(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + + if (isPipelined()) { + pipeline.sadd(stringKey, stringValue); + return null; + } + return session.sadd(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long sCard(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + + if (isPipelined()) { + pipeline.scard(stringKey); + return null; + } + return session.scard(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set sDiff(byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + + if (isPipelined()) { + pipeline.sdiff(stringKeys); + return null; + } + return RjcUtils.convertToSet(session.sdiff(stringKeys)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void sDiffStore(byte[] destKey, byte[]... keys) { + String stringKey = RjcUtils.decode(destKey); + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + + if (isPipelined()) { + pipeline.sdiffstore(stringKey, stringKeys); + return; + } + session.sdiffstore(stringKey, stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set sInter(byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + try { + + if (isPipelined()) { + pipeline.sinter(stringKeys); + return null; + } + return RjcUtils.convertToSet(session.sinter(stringKeys)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void sInterStore(byte[] destKey, byte[]... keys) { + String stringKey = RjcUtils.decode(destKey); + String[] stringKeys = RjcUtils.decodeMultiple(keys); + try { + + if (isPipelined()) { + pipeline.sinterstore(stringKey, stringKeys); + return; + } + session.sinterstore(stringKey, stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean sIsMember(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + + if (isPipelined()) { + pipeline.sismember(stringKey, stringValue); + return null; + } + return session.sismember(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set sMembers(byte[] key) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.smembers(stringKey); + return null; + } + return RjcUtils.convertToSet(session.smembers(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + String stringSrc = RjcUtils.decode(srcKey); + String stringDest = RjcUtils.decode(destKey); + String stringValue = RjcUtils.decode(value); + + try { + + if (isPipelined()) { + pipeline.smove(stringSrc, stringDest, stringValue); + return null; + } + return session.smove(stringSrc, stringDest, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] sPop(byte[] key) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.spop(stringKey); + return null; + } + return RjcUtils.encode(session.spop(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] sRandMember(byte[] key) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.srandmember(stringKey); + return null; + } + return RjcUtils.encode(session.srandmember(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean sRem(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + + if (isPipelined()) { + pipeline.srem(stringKey, stringValue); + return null; + } + return session.srem(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set sUnion(byte[]... keys) { + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + + if (isPipelined()) { + pipeline.sunion(stringKeys); + return null; + } + return RjcUtils.convertToSet(session.sunion(stringKeys)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void sUnionStore(byte[] destKey, byte[]... keys) { + String stringKey = RjcUtils.decode(destKey); + String[] stringKeys = RjcUtils.decodeMultiple(keys); + + try { + + if (isPipelined()) { + pipeline.sunionstore(stringKey, stringKeys); + return; + } + session.sunionstore(stringKey, stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + // + // ZSet commands + // + + @Override + public Boolean zAdd(byte[] key, double score, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.zadd(stringKey, score, stringValue); + return null; + } + return session.zadd(stringKey, score, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zCard(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.zcard(stringKey); + return null; + } + return session.zcard(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zCount(byte[] key, double min, double max) { + String stringKey = RjcUtils.decode(key); + try { + if (isPipelined()) { + pipeline.zcount(stringKey, min, max); + return null; + } + + return session.zcount(stringKey, min, max); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Double zIncrBy(byte[] key, double increment, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.zincrby(stringKey, increment, stringValue); + return null; + } + return Double.valueOf(session.zincrby(stringKey, increment, stringValue)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + String stringKey = RjcUtils.decode(destKey); + String[] stringKeys = RjcUtils.decodeMultiple(sets); + + ZParams zparams = RjcUtils.toZParams(aggregate, weights); + + try { + if (isPipelined()) { + pipeline.zinterstore(stringKey, zparams, stringKeys); + return null; + } + return session.zinterstore(stringKey, zparams, stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zInterStore(byte[] destKey, byte[]... sets) { + String stringKey = RjcUtils.decode(destKey); + String[] stringKeys = RjcUtils.decodeMultiple(sets); + try { + if (isPipelined()) { + pipeline.zinterstore(stringKey, stringKeys); + return null; + } + + return session.zinterstore(stringKey, stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRange(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.zrange(stringKey, (int) start, (int) end); + return null; + } + return RjcUtils.convertToSet(session.zrange(stringKey, (int) start, (int) end)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRangeWithScores(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.zrangeWithScores(stringKey, (int) start, (int) end); + return null; + } + return RjcUtils.convertElementScore(session.zrangeWithScores(stringKey, (int) start, (int) end)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRangeByScore(byte[] key, double min, double max) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + if (isPipelined()) { + pipeline.zrangeByScore(stringKey, minString, maxString); + return null; + } + return RjcUtils.convertToSet(session.zrangeByScore(stringKey, minString, maxString)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRangeByScore(byte[] key, double min, double max, long offset, long count) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + if (isPipelined()) { + pipeline.zrangeByScore(stringKey, minString, maxString, (int) offset, (int) count); + return null; + } + return RjcUtils.convertToSet(session.zrangeByScore(stringKey, minString, maxString, (int) offset, + (int) count)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + @Override + public Set zRevRangeByScore(byte[] key, double min, double max, long offset, long count) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + if (isPipelined()) { + pipeline.zrevrangeByScore(stringKey, minString, maxString, (int) offset, (int) count); + return null; + } + return RjcUtils.convertToSet(session.zrevrangeByScore(stringKey, minString, maxString, (int) offset, + (int) count)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRevRangeByScore(byte[] key, double min, double max) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + if (isPipelined()) { + pipeline.zrevrangeByScore(stringKey, minString, maxString); + return null; + } + return RjcUtils.convertToSet(session.zrevrangeByScore(stringKey, minString, maxString)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + if (isPipelined()) { + pipeline.zrangeByScoreWithScores(stringKey, minString, maxString); + return null; + } + return RjcUtils.convertElementScore(session.zrangeByScoreWithScores(stringKey, minString, maxString)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRevRangeWithScores(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + String minString = Long.toString(start); + String maxString = Long.toString(end); + + try { + + if (isPipelined()) { + pipeline.zrevrangeByScoreWithScores(stringKey, minString, maxString); + return null; + } + return RjcUtils.convertElementScore(session.zrevrangeByScoreWithScores(stringKey, minString, maxString)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + if (isPipelined()) { + pipeline.zrangeByScoreWithScores(stringKey, minString, maxString, (int) offset, (int) count); + return null; + } + return RjcUtils.convertElementScore(session.zrangeByScoreWithScores(stringKey, minString, maxString, + (int) offset, (int) count)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + + if (isPipelined()) { + pipeline.zrevrangeByScoreWithScores(stringKey, minString, maxString, (int) offset, (int) count); + return null; + } + return RjcUtils.convertElementScore(session.zrevrangeByScoreWithScores(stringKey, minString, maxString, + (int) offset, (int) count)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + + if (isPipelined()) { + pipeline.zrevrangeByScoreWithScores(stringKey, minString, maxString); + return null; + } + return RjcUtils.convertElementScore(session.zrevrangeByScoreWithScores(stringKey, minString, maxString)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zRank(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.zrank(stringKey, stringValue); + return null; + } + return session.zrank(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean zRem(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.zrem(stringKey, stringValue); + return null; + } + return session.zrem(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zRemRange(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + try { + if (isPipelined()) { + pipeline.zremrangeByRank(stringKey, (int) start, (int) end); + return null; + } + return session.zremrangeByRank(stringKey, (int) start, (int) end); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zRemRangeByScore(byte[] key, double min, double max) { + String stringKey = RjcUtils.decode(key); + String minString = Double.toString(min); + String maxString = Double.toString(max); + + try { + if (isPipelined()) { + pipeline.zremrangeByScore(stringKey, minString, maxString); + return null; + } + return session.zremrangeByScore(stringKey, minString, maxString); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set zRevRange(byte[] key, long start, long end) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.zrevrange(stringKey, (int) start, (int) end); + return null; + } + return RjcUtils.convertToSet(session.zrevrange(stringKey, (int) start, (int) end)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zRevRank(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.zrevrank(stringKey, stringValue); + return null; + } + return session.zrevrank(stringKey, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Double zScore(byte[] key, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.zscore(stringKey, stringValue); + return null; + } + return RjcUtils.convert(session.zscore(stringKey, stringValue)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + String stringKey = RjcUtils.decode(destKey); + String[] stringKeys = RjcUtils.decodeMultiple(destKey); + + ZParams zparams = RjcUtils.toZParams(aggregate, weights); + + try { + if (isPipelined()) { + pipeline.zunionstore(stringKey, zparams, stringKeys); + return null; + } + return session.zunionstore(stringKey, zparams, stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long zUnionStore(byte[] destKey, byte[]... sets) { + String stringKey = RjcUtils.decode(destKey); + String[] stringKeys = RjcUtils.decodeMultiple(sets); + + try { + if (isPipelined()) { + pipeline.zunionstore(stringKey, stringKeys); + return null; + } + return session.zunionstore(stringKey, stringKeys); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + // + // Hash commands + // + + @Override + public Boolean hSet(byte[] key, byte[] field, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringField = RjcUtils.decode(field); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.hset(stringKey, stringField, stringValue); + return null; + } + return session.hset(stringKey, stringField, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean hSetNX(byte[] key, byte[] field, byte[] value) { + String stringKey = RjcUtils.decode(key); + String stringField = RjcUtils.decode(field); + String stringValue = RjcUtils.decode(value); + + try { + if (isPipelined()) { + pipeline.hsetnx(stringKey, stringField, stringValue); + return null; + } + return session.hsetnx(stringKey, stringField, stringValue); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean hDel(byte[] key, byte[] field) { + String stringKey = RjcUtils.decode(key); + String stringField = RjcUtils.decode(field); + + try { + if (isPipelined()) { + pipeline.hdel(stringKey, stringField); + return null; + } + return session.hdel(stringKey, stringField); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Boolean hExists(byte[] key, byte[] field) { + String stringKey = RjcUtils.decode(key); + String stringField = RjcUtils.decode(field); + + try { + if (isPipelined()) { + pipeline.hexists(stringKey, stringField); + return null; + } + return session.hexists(stringKey, stringField); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public byte[] hGet(byte[] key, byte[] field) { + String stringKey = RjcUtils.decode(key); + String stringField = RjcUtils.decode(field); + + try { + if (isPipelined()) { + pipeline.hget(stringKey, stringField); + return null; + } + return RjcUtils.encode(session.hget(stringKey, stringField)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Map hGetAll(byte[] key) { + String stringKey = RjcUtils.decode(key); + + try { + if (isPipelined()) { + pipeline.hgetAll(stringKey); + return null; + } + return RjcUtils.encodeMap(session.hgetAll(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long hIncrBy(byte[] key, byte[] field, long delta) { + String stringKey = RjcUtils.decode(key); + String stringField = RjcUtils.decode(field); + + try { + if (isPipelined()) { + pipeline.hincrBy(stringKey, stringField, (int) delta); + return null; + } + return session.hincrBy(stringKey, stringField, (int) delta); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Set hKeys(byte[] key) { + String stringKey = RjcUtils.decode(key); + try { + if (isPipelined()) { + pipeline.hkeys(stringKey); + return null; + } + return RjcUtils.convertToSet(session.hkeys(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Long hLen(byte[] key) { + String stringKey = RjcUtils.decode(key); + try { + if (isPipelined()) { + pipeline.hlen(stringKey); + return null; + } + return session.hlen(stringKey); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List hMGet(byte[] key, byte[]... fields) { + String stringKey = RjcUtils.decode(key); + String[] stringKeys = RjcUtils.decodeMultiple(fields); + + try { + if (isPipelined()) { + pipeline.hmget(stringKey, stringKeys); + return null; + } + return RjcUtils.convertToList(session.hmget(stringKey, stringKeys)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void hMSet(byte[] key, Map tuple) { + String stringKey = RjcUtils.decode(key); + Map stringTuple = RjcUtils.decodeMap(tuple); + + try { + if (isPipelined()) { + pipeline.hmset(stringKey, stringTuple); + return; + } + session.hmset(stringKey, stringTuple); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public List hVals(byte[] key) { + String stringKey = RjcUtils.decode(key); + try { + + if (isPipelined()) { + pipeline.hvals(stringKey); + return null; + } + return RjcUtils.convertToList(session.hvals(stringKey)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + + // + // Pub/Sub functionality + // + @Override + public Long publish(byte[] channel, byte[] message) { + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + return session.publish(RjcUtils.decode(channel), RjcUtils.decode(message)); + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public Subscription getSubscription() { + return subscription; + } + + @Override + public boolean isSubscribed() { + return (subscription != null && subscription.isAlive()); + } + + @Override + public void pSubscribe(MessageListener listener, byte[]... patterns) { + if (isSubscribed()) { + throw new RedisSubscribedConnectionException( + "Connection already subscribed; use the connection Subscription to cancel or add new channels"); + } + + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + + subscription = new RjcSubscription(listener, subscriber); + subscription.pSubscribe(patterns); + subscriber.runSubscription(); + + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + @Override + public void subscribe(MessageListener listener, byte[]... channels) { + if (isSubscribed()) { + throw new RedisSubscribedConnectionException( + "Connection already subscribed; use the connection Subscription to cancel or add new channels"); + } + + try { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + + subscription = new RjcSubscription(listener, subscriber); + subscription.subscribe(channels); + subscriber.runSubscription(); + + } catch (Exception ex) { + throw convertRjcAccessException(ex); + } + } + + private void checkSubscription() { + if (isSubscribed()) { + throw new RedisSubscribedConnectionException("Cannot execute command - connection is subscribed"); + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnectionFactory.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnectionFactory.java new file mode 100644 index 000000000..641c9c61f --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnectionFactory.java @@ -0,0 +1,218 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.idevlab.rjc.ds.DataSource; +import org.idevlab.rjc.ds.PoolableDataSource; +import org.idevlab.rjc.ds.SimpleDataSource; +import org.idevlab.rjc.protocol.Protocol; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.util.Assert; + +/** + * Connection factory creating rjc based connections. + * + * @author Costin Leau + */ +public class RjcConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory { + + private final static Log log = LogFactory.getLog(JedisConnectionFactory.class); + + private String hostName = "localhost"; + private int port = Protocol.DEFAULT_PORT; + private int timeout = Protocol.DEFAULT_TIMEOUT; + private String password; + + private boolean usePool = true; + private int dbIndex = 0; + private DataSource dataSource; + + + /** + * Constructs a new RjcConnectionFactory instance + * with default settings (default connection pooling, no shard information). + */ + public RjcConnectionFactory() { + } + + + public void afterPropertiesSet() { + if (usePool) { + PoolableDataSource pool = new PoolableDataSource(); + pool.setHost(hostName); + pool.setPort(port); + pool.setPassword(password); + pool.setTimeout(timeout); + + pool.init(); + + dataSource = pool; + + } + else { + dataSource = new SimpleDataSource(hostName, port, timeout, password); + } + } + + public void destroy() { + if (usePool && dataSource != null) { + try { + ((PoolableDataSource) dataSource).close(); + } catch (Exception ex) { + log.warn("Cannot properly close Rjc pool", ex); + } + dataSource = null; + } + } + + @Override + public RedisConnection getConnection() { + return postProcessConnection(new RjcConnection(dataSource.getConnection(), dbIndex)); + } + + /** + * Post process a newly retrieved connection. Useful for decorating or executing + * initialization commands on a new connection. + * This implementation simply returns the connection. + * + * @param connection + * @return processed connection + */ + protected RjcConnection postProcessConnection(RjcConnection connection) { + return connection; + } + + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + return RjcUtils.convertRjcAccessException(ex); + } + + + /** + * Returns the Redis hostName. + * + * @return Returns the hostName + */ + public String getHostName() { + return hostName; + } + + /** + * Sets the Redis hostName. + * + * @param hostName The hostName to set. + */ + public void setHostName(String hostName) { + this.hostName = hostName; + } + + /** + * Returns the password used for authenticating with the Redis server. + * + * @return password for authentication + */ + public String getPassword() { + return password; + } + + /** + * Sets the password used for authenticating with the Redis server. + * + * @param password the password to set + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Returns the port used to connect to the Redis instance. + * + * @return Redis port. + */ + public int getPort() { + return port; + + } + + /** + * Sets the port used to connect to the Redis instance. + * + * @param port Redis port + */ + public void setPort(int port) { + this.port = port; + } + /** + * Returns the timeout. + * + * @return Returns the timeout + */ + public int getTimeout() { + return timeout; + } + + /** + * @param timeout The timeout to set. + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + /** + * Indicates the use of a connection pool. + * + * @return Returns the use of connection pooling. + */ + public boolean getUsePool() { + return usePool; + } + + /** + * Turns on or off the use of connection pooling. + * + * @param usePool The usePool to set. + */ + public void setUsePool(boolean usePool) { + this.usePool = usePool; + } + + /** + * Returns the index of the database. + * + * @return Returns the database index + */ + public int getDatabase() { + return dbIndex; + } + + /** + * Sets the index of the database used by this connection factory. + * Can be between 0 (default) and 15. + * + * @param index database index + */ + public void setDatabase(int index) { + Assert.isTrue(index >= 0, "invalid DB index (a positive index required)"); + this.dbIndex = index; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcMessageListener.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcMessageListener.java new file mode 100644 index 000000000..c16a2040f --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcMessageListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import org.idevlab.rjc.message.MessageListener; +import org.idevlab.rjc.message.PMessageListener; +import org.springframework.data.keyvalue.redis.connection.DefaultMessage; + +/** + * Message listener adapter for RJC library. + * + * @author Costin Leau + */ +class RjcMessageListener implements MessageListener, PMessageListener { + + private final org.springframework.data.keyvalue.redis.connection.MessageListener listener; + + RjcMessageListener(org.springframework.data.keyvalue.redis.connection.MessageListener messageListener) { + this.listener = messageListener; + } + + @Override + public void onMessage(String channel, String message) { + listener.onMessage(new DefaultMessage(RjcUtils.encode(channel), RjcUtils.encode(message)), null); + } + + @Override + public void onMessage(String pattern, String channel, String message) { + listener.onMessage(new DefaultMessage(RjcUtils.encode(channel), RjcUtils.encode(message)), + RjcUtils.encode(pattern)); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcSubscription.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcSubscription.java new file mode 100644 index 000000000..78c5f2277 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcSubscription.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import org.idevlab.rjc.message.RedisNodeSubscriber; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.util.AbstractSubscription; + +/** + * Message subscription on top of RJC. + * + * @author Costin Leau + */ +class RjcSubscription extends AbstractSubscription { + + private final RedisNodeSubscriber subscriber; + + RjcSubscription(MessageListener listener, RedisNodeSubscriber subscriber) { + super(listener); + this.subscriber = subscriber; + subscriber.setMessageListener(new RjcMessageListener(listener)); + subscriber.setPMessageListener(new RjcMessageListener(listener)); + } + + @Override + protected void doClose() { + subscriber.close(); + } + + @Override + protected void doPsubscribe(byte[]... patterns) { + subscriber.psubscribe(RjcUtils.decodeMultiple(patterns)); + } + + @Override + protected void doPUnsubscribe(boolean all, byte[]... patterns) { + subscriber.punsubscribe(RjcUtils.decodeMultiple(patterns)); + } + + @Override + protected void doSubscribe(byte[]... channels) { + subscriber.subscribe(RjcUtils.decodeMultiple(channels)); + } + + @Override + protected void doUnsubscribe(boolean all, byte[]... channels) { + subscriber.punsubscribe(RjcUtils.decodeMultiple(channels)); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcUtils.java new file mode 100644 index 000000000..e373fe469 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcUtils.java @@ -0,0 +1,239 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import java.io.StringReader; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.idevlab.rjc.ElementScore; +import org.idevlab.rjc.RedisException; +import org.idevlab.rjc.SortingParams; +import org.idevlab.rjc.ZParams; +import org.idevlab.rjc.Client.LIST_POSITION; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.keyvalue.redis.RedisSystemException; +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.DefaultTuple; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.connection.RedisListCommands.Position; +import org.springframework.data.keyvalue.redis.connection.RedisZSetCommands.Aggregate; +import org.springframework.data.keyvalue.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Order; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Range; +import org.springframework.data.keyvalue.redis.connection.util.DecodeUtils; +import org.springframework.util.ObjectUtils; + + +/** + * Helper class featuring methods for RJC connection handling, providing support for exception translation. + * + * @author Costin Leau + */ +public abstract class RjcUtils { + + private static final String ONE = "1"; + private static final String ZERO = "0"; + + + public static DataAccessException convertRjcAccessException(RuntimeException ex) { + if (ex instanceof RedisException) { + return convertRjcAccessException((RedisException) ex); + } + + return new RedisSystemException("Unknown exception", ex); + } + + public static DataAccessException convertRjcAccessException(RedisException ex) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + + static DataType convertDataType(String type) { + if ("string".equals(type)) { + return DataType.STRING; + } + else if ("list".equals(type)) { + return DataType.LIST; + } + else if ("set".equals(type)) { + return DataType.SET; + } + else if ("zset".equals(type)) { + return DataType.ZSET; + } + else if ("hash".equals(type)) { + return DataType.HASH; + } + else if ("none".equals(type)) { + return DataType.NONE; + } + + return null; + } + + static String decode(byte[] bytes) { + return DecodeUtils.decode(bytes); + } + + static byte[] encode(String string) { + return DecodeUtils.encode(string); + } + + static String[] decodeMultiple(byte[]... bytes) { + return DecodeUtils.decodeMultiple(bytes); + } + + static String[] flatten(Map tuple) { + String[] result = new String[tuple.size() * 2]; + int index = 0; + for (Map.Entry entry : tuple.entrySet()) { + result[index++] = decode(entry.getKey()); + result[index++] = decode(entry.getValue()); + } + return result; + + } + + static Set convertToSet(Collection keys) { + if (keys == null) { + return null; + } + + return DecodeUtils.convertToSet(keys); + } + + static List convertToList(Collection keys) { + if (keys == null) { + return null; + } + return DecodeUtils.convertToList(keys); + } + + static SortingParams convertSortParams(SortParameters params) { + SortingParams rjcSort = null; + + if (params != null) { + rjcSort = new SortingParams(); + + byte[] byPattern = params.getByPattern(); + if (byPattern != null) { + rjcSort.by(DecodeUtils.decode(byPattern)); + } + byte[][] getPattern = params.getGetPattern(); + + if (getPattern != null && getPattern.length > 0) { + for (byte[] bs : getPattern) { + rjcSort.get(DecodeUtils.decode(bs)); + } + } + Range limit = params.getLimit(); + if (limit != null) { + rjcSort.limit((int) limit.getStart(), (int) limit.getCount()); + } + Order order = params.getOrder(); + if (order != null && order.equals(Order.DESC)) { + rjcSort.desc(); + } + Boolean isAlpha = params.isAlphabetic(); + if (isAlpha != null && isAlpha) { + rjcSort.alpha(); + } + } + return rjcSort; + } + + static Properties info(String string) { + Properties info = new Properties(); + StringReader stringReader = new StringReader(string); + try { + info.load(stringReader); + } catch (Exception ex) { + throw new RedisSystemException("Cannot read Redis info", ex); + } finally { + stringReader.close(); + } + return info; + } + + static String asBit(boolean value) { + return (value ? ONE : ZERO); + } + + static LIST_POSITION convertPosition(Position where) { + switch (where) { + case BEFORE: + return LIST_POSITION.BEFORE; + + case AFTER: + return LIST_POSITION.AFTER; + } + return null; + } + + static ZParams toZParams(Aggregate aggregate, int[] weights) { + return new ZParams().weights(weights).aggregate(ZParams.Aggregate.valueOf(aggregate.name())); + } + + static Set convertElementScore(List tuples) { + Set value = new LinkedHashSet(tuples.size()); + for (ElementScore tuple : tuples) { + value.add(new DefaultTuple(encode(tuple.getElement()), Double.valueOf(tuple.getScore()))); + } + + return value; + } + + static Map encodeMap(Map map) { + Map result = new LinkedHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { + result.put(encode(entry.getKey()), encode(entry.getValue())); + } + return result; + } + + static Map decodeMap(Map map) { + Map result = new LinkedHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { + result.put(decode(entry.getKey()), decode(entry.getValue())); + } + return result; + } + + static Double convert(String zscore) { + return (zscore == null ? null : Double.valueOf(zscore)); + } + + + static String[] addArray(String[] one, String[] two) { + if (ObjectUtils.isEmpty(one)) { + return two; + } + if (ObjectUtils.isEmpty(two)) { + return one; + } + + String[] result = Arrays.copyOf(one, one.length + two.length); + System.arraycopy(two, 0, result, one.length, two.length); + return result; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/SingleDataSource.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/SingleDataSource.java new file mode 100644 index 000000000..db152b72c --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/SingleDataSource.java @@ -0,0 +1,38 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import org.idevlab.rjc.ds.DataSource; +import org.idevlab.rjc.ds.RedisConnection; + +/** + * Basic data source that always returns the same connection. + * + * @author Costin Leau + */ +class SingleDataSource implements DataSource { + + private final RedisConnection connection; + + SingleDataSource(RedisConnection connection) { + this.connection = connection; + } + + @Override + public RedisConnection getConnection() { + return connection; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/package-info.java new file mode 100644 index 000000000..66a90b8ae --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/rjc/package-info.java @@ -0,0 +1,5 @@ +/** + * Connection package for RJC library. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/AbstractSubscription.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/AbstractSubscription.java new file mode 100644 index 000000000..6d20a8bf5 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/AbstractSubscription.java @@ -0,0 +1,261 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.RedisInvalidSubscriptionException; +import org.springframework.data.keyvalue.redis.connection.Subscription; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Base implementation for a subscription handling the channel/pattern registration so subclasses only have to deal + * with the actual registration/unregistration. + * + * @author Costin Leau + */ +public abstract class AbstractSubscription implements Subscription { + + private final Collection channels = new ArrayList(2); + private final Collection patterns = new ArrayList(2); + private final AtomicBoolean alive = new AtomicBoolean(true); + private final MessageListener listener; + + protected AbstractSubscription(MessageListener listener) { + this(listener, null, null); + } + + /** + * Constructs a new AbstractSubscription instance. Allows channels and patterns to be added + * to the subscription w/o triggering a subscription action (as some clients (Jedis) require an initial call + * before entering into listening mode). + * + * @param listener + * @param channels + * @param patterns + */ + protected AbstractSubscription(MessageListener listener, byte[][] channels, byte[][] patterns) { + Assert.notNull(listener); + this.listener = listener; + + synchronized (this.channels) { + add(this.channels, channels); + } + synchronized (this.patterns) { + add(this.patterns, patterns); + } + } + + /** + * Subscribe to the given channels. + * + * @param channels channels to subscribe to + */ + protected abstract void doSubscribe(byte[]... channels); + + /** + * Channel unsubscribe. + * + * @param all true if all the channels are unsubscribed (used as a hint for the underlying implementation). + * @param channels channels to be unsubscribed + */ + protected abstract void doUnsubscribe(boolean all, byte[]... channels); + + /** + * Subscribe to the given patterns + * + * @param patterns patterns to subscribe to + */ + protected abstract void doPsubscribe(byte[]... patterns); + + /** + * Pattern unsubscribe. + * + * @param all true if all the patterns are unsubscribed (used as a hint for the underlying implementation). + * @param patterns patterns to be unsubscribed + */ + protected abstract void doPUnsubscribe(boolean all, byte[]... patterns); + + /** + * Shutdown the subscription and free any resources held. + */ + protected abstract void doClose(); + + @Override + public MessageListener getListener() { + return listener; + } + + @Override + public Collection getChannels() { + synchronized (channels) { + return clone(channels); + } + } + + @Override + public Collection getPatterns() { + synchronized (patterns) { + return clone(patterns); + } + } + + @Override + public void pSubscribe(byte[]... patterns) { + checkPulse(); + + Assert.notEmpty(patterns, "at least one pattern required"); + + synchronized (this.patterns) { + add(this.patterns, patterns); + } + + doPsubscribe(patterns); + } + + @Override + public void pUnsubscribe() { + pUnsubscribe((byte[][]) null); + } + + + @Override + public void subscribe(byte[]... channels) { + checkPulse(); + + Assert.notEmpty(channels, "at least one channel required"); + + synchronized (this.channels) { + add(this.channels, channels); + } + + doSubscribe(channels); + } + + @Override + public void unsubscribe() { + unsubscribe((byte[][]) null); + } + + @Override + public void pUnsubscribe(byte[]... patts) { + if (!isAlive()) { + return; + } + + // shortcut for unsubscribing all patterns + if (ObjectUtils.isEmpty(patts)) { + if (!this.patterns.isEmpty()) { + patts = getPatterns().toArray(new byte[this.patterns.size()][]); + synchronized (this.patterns) { + this.patterns.clear(); + } + } + else { + // nothing to unsubscribe from + return; + } + } + else { + synchronized (this.patterns) { + remove(this.patterns, patts); + } + } + + if (isWorking()) { + doPUnsubscribe(this.patterns.isEmpty(), patts); + } + } + + @Override + public void unsubscribe(byte[]... chans) { + if (!isAlive()) { + return; + } + + // shortcut for unsubscribing all channels + if (ObjectUtils.isEmpty(chans)) { + if (!this.channels.isEmpty()) { + chans = getPatterns().toArray(new byte[this.channels.size()][]); + synchronized (this.channels) { + this.channels.clear(); + } + } + else { + // nothing to unsubscribe from + return; + } + } + else { + synchronized (this.channels) { + remove(this.channels, chans); + } + } + + if (isWorking()) { + doUnsubscribe(this.channels.isEmpty(), chans); + } + } + + @Override + public boolean isAlive() { + return alive.get(); + } + + private void checkPulse() { + if (!isAlive()) { + throw new RedisInvalidSubscriptionException("Subscription has been unsubscribed and cannot be used anymore"); + } + } + + private boolean isWorking() { + if (channels.isEmpty() && patterns.isEmpty()) { + alive.set(false); + doClose(); + } + return isAlive(); + } + + + private static Collection clone(Collection col) { + Collection list = new ArrayList(col.size()); + for (ByteArrayWrapper wrapper : col) { + list.add(wrapper.getArray().clone()); + } + return list; + } + + + private static void add(Collection col, byte[]... bytes) { + if (!ObjectUtils.isEmpty(bytes)) { + for (byte[] bs : bytes) { + col.add(new ByteArrayWrapper(bs)); + } + } + } + + private static void remove(Collection col, byte[]... bytes) { + if (!ObjectUtils.isEmpty(bytes)) { + for (byte[] bs : bytes) { + col.remove(new ByteArrayWrapper(bs)); + } + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/Base64.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/Base64.java new file mode 100644 index 000000000..3e99472d6 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/Base64.java @@ -0,0 +1,570 @@ +package org.springframework.data.keyvalue.redis.connection.util; + +import java.util.Arrays; + +/** + * A very fast and memory efficient class to encode and decode to and from BASE64 in full accordance + * with RFC 2045.

+ * On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is about 10 times faster + * on small arrays (10 - 1000 bytes) and 2-3 times as fast on larger arrays (10000 - 1000000 bytes) + * compared to sun.misc.Encoder()/Decoder().

+ * + * On byte arrays the encoder is about 20% faster than Jakarta Commons Base64 Codec for encode and + * about 50% faster for decoding large arrays. This implementation is about twice as fast on very small + * arrays (< 30 bytes). If source/destination is a String this + * version is about three times as fast due to the fact that the Commons Codec result has to be recoded + * to a String from byte[], which is very expensive.

+ * + * This encode/decode algorithm doesn't create any temporary arrays as many other codecs do, it only + * allocates the resulting array. This produces less garbage and it is possible to handle arrays twice + * as large as algorithms that create a temporary array. (E.g. Jakarta Commons Codec). It is unknown + * whether Sun's sun.misc.Encoder()/Decoder() produce temporary arrays but since performance + * is quite low it probably does.

+ * + * The encoder produces the same output as the Sun one except that the Sun's encoder appends + * a trailing line separator if the last character isn't a pad. Unclear why but it only adds to the + * length and is probably a side effect. Both are in conformance with RFC 2045 though.
+ * Commons codec seem to always att a trailing line separator.

+ * + * Note! + * The encode/decode method pairs (types) come in three versions with the exact same algorithm and + * thus a lot of code redundancy. This is to not create any temporary arrays for transcoding to/from different + * format types. The methods not used can simply be commented out.

+ * + * There is also a "fast" version of all decode methods that works the same way as the normal ones, but + * har a few demands on the decoded input. Normally though, these fast verions should be used if the source if + * the input is known and it hasn't bee tampered with.

+ * + * If you find the code useful or you find a bug, please send me a note at base64 @ miginfocom . com. + * + * Licence (BSD): + * ============== + * + * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * Neither the name of the MiG InfoCom AB nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * @version 2.2 + * @author Mikael Grev + * Date: 2004-aug-02 + * Time: 11:31:11 + */ + +class Base64 { + private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + private static final int[] IA = new int[256]; + static { + Arrays.fill(IA, -1); + for (int i = 0, iS = CA.length; i < iS; i++) + IA[CA[i]] = i; + IA['='] = 0; + } + + // **************************************************************************************** + // * char[] version + // **************************************************************************************** + + /** Encodes a raw byte array into a BASE64 char[] representation i accordance with RFC 2045. + * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never null. + */ + public final static char[] encodeToChar(byte[] sArr, boolean lineSep) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) + return new char[0]; + + int eLen = (sLen / 3) * 3; // Length of even 24-bits. + int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count + int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array + char[] dArr = new char[dLen]; + + // Encode even 24-bits + for (int s = 0, d = 0, cc = 0; s < eLen;) { + // Copy next three bytes into lower 24 bits of int, paying attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = CA[(i >>> 18) & 0x3f]; + dArr[d++] = CA[(i >>> 12) & 0x3f]; + dArr[d++] = CA[(i >>> 6) & 0x3f]; + dArr[d++] = CA[i & 0x3f]; + + // Add optional line separator + if (lineSep && ++cc == 19 && d < dLen - 2) { + dArr[d++] = '\r'; + dArr[d++] = '\n'; + cc = 0; + } + } + + // Pad and encode last bits if source isn't even 24 bits. + int left = sLen - eLen; // 0 - 2. + if (left > 0) { + // Prepare the int + int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[dLen - 4] = CA[i >> 12]; + dArr[dLen - 3] = CA[(i >>> 6) & 0x3f]; + dArr[dLen - 2] = left == 2 ? CA[i & 0x3f] : '='; + dArr[dLen - 1] = '='; + } + return dArr; + } + + /** Decodes a BASE64 encoded char array. All illegal characters will be ignored and can handle both arrays with + * and without line separators. + * @param sArr The source array. null or length 0 will return an empty array. + * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + */ + public final static byte[] decode(char[] sArr) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) + return new byte[0]; + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++) + // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + if (IA[sArr[i]] < 0) + sepCnt++; + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) + return null; + + int pad = 0; + for (int i = sLen; i > 1 && IA[sArr[--i]] <= 0;) + if (sArr[i] == '=') + pad++; + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len;) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. + int c = IA[sArr[s++]]; + if (c >= 0) + i |= c << (18 - j * 6); + else + j--; + } + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) + dArr[d++] = (byte) i; + } + } + return dArr; + } + + /** Decodes a BASE64 encoded char array that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(char[])}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. + */ + public final static byte[] decodeFast(char[] sArr) { + // Check special case + int sLen = sArr.length; + if (sLen == 0) + return new byte[0]; + + int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IA[sArr[sIx]] < 0) + sIx++; + + // Trim illegal chars from end + while (eIx > 0 && IA[sArr[eIx]] < 0) + eIx--; + + // get the padding count (=) (0, 1 or 2) + int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. + int cCnt = eIx - sIx + 1; // Content count including possible separators + int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { + // Assemble three bytes into an int from four "valid" characters. + int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) + i |= IA[sArr[sIx++]] << (18 - j * 6); + + for (int r = 16; d < len; r -= 8) + dArr[d++] = (byte) (i >> r); + } + + return dArr; + } + + // **************************************************************************************** + // * byte[] version + // **************************************************************************************** + + /** Encodes a raw byte array into a BASE64 byte[] representation i accordance with RFC 2045. + * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never null. + */ + public final static byte[] encodeToByte(byte[] sArr, boolean lineSep) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) + return new byte[0]; + + int eLen = (sLen / 3) * 3; // Length of even 24-bits. + int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count + int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array + byte[] dArr = new byte[dLen]; + + // Encode even 24-bits + for (int s = 0, d = 0, cc = 0; s < eLen;) { + // Copy next three bytes into lower 24 bits of int, paying attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = (byte) CA[(i >>> 18) & 0x3f]; + dArr[d++] = (byte) CA[(i >>> 12) & 0x3f]; + dArr[d++] = (byte) CA[(i >>> 6) & 0x3f]; + dArr[d++] = (byte) CA[i & 0x3f]; + + // Add optional line separator + if (lineSep && ++cc == 19 && d < dLen - 2) { + dArr[d++] = '\r'; + dArr[d++] = '\n'; + cc = 0; + } + } + + // Pad and encode last bits if source isn't an even 24 bits. + int left = sLen - eLen; // 0 - 2. + if (left > 0) { + // Prepare the int + int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[dLen - 4] = (byte) CA[i >> 12]; + dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f]; + dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '='; + dArr[dLen - 1] = '='; + } + return dArr; + } + + /** Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with + * and without line separators. + * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + */ + public final static byte[] decode(byte[] sArr) { + // Check special case + int sLen = sArr.length; + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++) + // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + if (IA[sArr[i] & 0xff] < 0) + sepCnt++; + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) + return null; + + int pad = 0; + for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;) + if (sArr[i] == '=') + pad++; + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len;) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. + int c = IA[sArr[s++] & 0xff]; + if (c >= 0) + i |= c << (18 - j * 6); + else + j--; + } + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) + dArr[d++] = (byte) i; + } + } + + return dArr; + } + + + /** Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(byte[])}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. + */ + public final static byte[] decodeFast(byte[] sArr) { + // Check special case + int sLen = sArr.length; + if (sLen == 0) + return new byte[0]; + + int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0) + sIx++; + + // Trim illegal chars from end + while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0) + eIx--; + + // get the padding count (=) (0, 1 or 2) + int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. + int cCnt = eIx - sIx + 1; // Content count including possible separators + int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { + // Assemble three bytes into an int from four "valid" characters. + int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) + i |= IA[sArr[sIx++]] << (18 - j * 6); + + for (int r = 16; d < len; r -= 8) + dArr[d++] = (byte) (i >> r); + } + + return dArr; + } + + // **************************************************************************************** + // * String version + // **************************************************************************************** + + /** Encodes a raw byte array into a BASE64 String representation i accordance with RFC 2045. + * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never null. + */ + public final static String encodeToString(byte[] sArr, boolean lineSep) { + // Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower. + return new String(encodeToChar(sArr, lineSep)); + } + + /** Decodes a BASE64 encoded String. All illegal characters will be ignored and can handle both strings with + * and without line separators.
+ * Note! It can be up to about 2x the speed to call decode(str.toCharArray()) instead. That + * will create a temporary array though. This version will use str.charAt(i) to iterate the string. + * @param str The source string. null or length 0 will return an empty array. + * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + */ + public final static byte[] decode(String str) { + // Check special case + int sLen = str != null ? str.length() : 0; + if (sLen == 0) + return new byte[0]; + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++) + // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + if (IA[str.charAt(i)] < 0) + sepCnt++; + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) + return null; + + // Count '=' at end + int pad = 0; + for (int i = sLen; i > 1 && IA[str.charAt(--i)] <= 0;) + if (str.charAt(i) == '=') + pad++; + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len;) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. + int c = IA[str.charAt(s++)]; + if (c >= 0) + i |= c << (18 - j * 6); + else + j--; + } + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) + dArr[d++] = (byte) i; + } + } + return dArr; + } + + /** Decodes a BASE64 encoded string that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(String)}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * @param s The source string. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. + */ + public final static byte[] decodeFast(String s) { + // Check special case + int sLen = s.length(); + if (sLen == 0) + return new byte[0]; + + int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IA[s.charAt(sIx) & 0xff] < 0) + sIx++; + + // Trim illegal chars from end + while (eIx > 0 && IA[s.charAt(eIx) & 0xff] < 0) + eIx--; + + // get the padding count (=) (0, 1 or 2) + int pad = s.charAt(eIx) == '=' ? (s.charAt(eIx - 1) == '=' ? 2 : 1) : 0; // Count '=' at end. + int cCnt = eIx - sIx + 1; // Content count including possible separators + int sepCnt = sLen > 76 ? (s.charAt(76) == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { + // Assemble three bytes into an int from four "valid" characters. + int i = IA[s.charAt(sIx++)] << 18 | IA[s.charAt(sIx++)] << 12 | IA[s.charAt(sIx++)] << 6 + | IA[s.charAt(sIx++)]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) + i |= IA[s.charAt(sIx++)] << (18 - j * 6); + + for (int r = 16; d < len; r -= 8) + dArr[d++] = (byte) (i >> r); + } + + return dArr; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/ByteArrayWrapper.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/ByteArrayWrapper.java new file mode 100644 index 000000000..7c708192b --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/ByteArrayWrapper.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.util; + +import java.util.Arrays; + +/** + * Simple wrapper class used for wrapping arrays so they can be used as keys inside maps. + * + * @author Costin Leau + */ +public class ByteArrayWrapper { + + private final byte[] array; + private final int hashCode; + + public ByteArrayWrapper(byte[] array) { + this.array = array; + this.hashCode = Arrays.hashCode(array); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ByteArrayWrapper) { + return Arrays.equals(array, ((ByteArrayWrapper) obj).array); + } + + return false; + } + + @Override + public int hashCode() { + return hashCode; + } + + /** + * Returns the array. + * + * @return Returns the array + */ + public byte[] getArray() { + return array; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/DecodeUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/DecodeUtils.java new file mode 100644 index 000000000..b40867607 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/DecodeUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Simple class containing various decoding utilities. + * + * @author Costin Leau + */ +public abstract class DecodeUtils { + + public static String decode(byte[] bytes) { + return Base64.encodeToString(bytes, false); + } + + public static String[] decodeMultiple(byte[]... bytes) { + String[] result = new String[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + result[i] = decode(bytes[i]); + } + return result; + } + + public static byte[] encode(String string) { + return (string == null ? null : Base64.decode(string)); + } + + public static Map encodeMap(Map map) { + Map result = new LinkedHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { + result.put(encode(entry.getKey()), entry.getValue()); + } + return result; + } + + public static Map decodeMap(Map tuple) { + Map result = new LinkedHashMap(tuple.size()); + for (Map.Entry entry : tuple.entrySet()) { + result.put(decode(entry.getKey()), entry.getValue()); + } + return result; + } + + public static Set convertToSet(Collection keys) { + Set set = new LinkedHashSet(keys.size()); + + for (String string : keys) { + set.add(encode(string)); + } + return set; + } + + public static List convertToList(Collection keys) { + List set = new ArrayList(keys.size()); + + for (String string : keys) { + set.add(encode(string)); + } + return set; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/package-info.java new file mode 100644 index 000000000..c8f07d8ce --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/util/package-info.java @@ -0,0 +1,5 @@ +/** + * Internal utility package for encoding/decoding Strings to byte[] (using Base64) library. + */ +package org.springframework.data.keyvalue.redis.connection.util; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/AbstractOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/AbstractOperations.java new file mode 100644 index 000000000..1b15b4747 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/AbstractOperations.java @@ -0,0 +1,205 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.keyvalue.redis.core.ZSetOperations.TypedTuple; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.SerializationUtils; +import org.springframework.util.Assert; + +/** + * Internal base class used by various RedisTemplate XXXOperations implementations. + * + * @author Costin Leau + */ +abstract class AbstractOperations { + + // utility methods for the template internal methods + abstract class ValueDeserializingRedisCallback implements RedisCallback { + private Object key; + + public ValueDeserializingRedisCallback(Object key) { + this.key = key; + } + + @Override + public final V doInRedis(RedisConnection connection) { + byte[] result = inRedis(rawKey(key), connection); + return deserializeValue(result); + } + + protected abstract byte[] inRedis(byte[] rawKey, RedisConnection connection); + } + + RedisSerializer keySerializer = null; + RedisSerializer valueSerializer = null; + RedisSerializer hashKeySerializer = null; + RedisSerializer hashValueSerializer = null; + RedisSerializer stringSerializer = null; + RedisTemplate template; + + AbstractOperations(RedisTemplate template) { + keySerializer = template.getKeySerializer(); + valueSerializer = template.getValueSerializer(); + hashKeySerializer = template.getHashKeySerializer(); + hashValueSerializer = template.getHashValueSerializer(); + stringSerializer = template.getStringSerializer(); + + this.template = template; + } + + + T execute(RedisCallback callback, boolean b) { + return template.execute(callback, b); + } + + public RedisOperations getOperations() { + return template; + } + + @SuppressWarnings("unchecked") + byte[] rawKey(Object key) { + Assert.notNull(key, "non null key required"); + return keySerializer.serialize(key); + } + + byte[] rawString(String key) { + return stringSerializer.serialize(key); + } + + @SuppressWarnings("unchecked") + byte[] rawValue(Object value) { + return valueSerializer.serialize(value); + } + + @SuppressWarnings("unchecked") + byte[] rawHashKey(HK hashKey) { + Assert.notNull(hashKey, "non null hash key required"); + return hashKeySerializer.serialize(hashKey); + } + + @SuppressWarnings("unchecked") + byte[] rawHashValue(HV value) { + return hashValueSerializer.serialize(value); + } + + byte[][] rawKeys(K key, K otherKey) { + final byte[][] rawKeys = new byte[2][]; + + + rawKeys[0] = rawKey(key); + rawKeys[1] = rawKey(key); + return rawKeys; + } + + byte[][] rawKeys(Collection keys) { + return rawKeys(null, keys); + } + + byte[][] rawKeys(K key, Collection keys) { + final byte[][] rawKeys = new byte[keys.size() + (key != null ? 1 : 0)][]; + + int i = 0; + + if (key != null) { + rawKeys[i++] = rawKey(key); + } + + for (K k : keys) { + rawKeys[i++] = rawKey(k); + } + + return rawKeys; + } + + @SuppressWarnings("unchecked") + Set deserializeValues(Set rawValues) { + return SerializationUtils.deserialize(rawValues, valueSerializer); + } + + @SuppressWarnings("unchecked") + Set> deserializeTupleValues(Set rawValues) { + Set> set = new LinkedHashSet>(rawValues.size()); + for (Tuple rawValue : rawValues) { + set.add(new DefaultTypedTuple(valueSerializer.deserialize(rawValue.getValue()), rawValue.getScore())); + } + return set; + } + + @SuppressWarnings("unchecked") + List deserializeValues(List rawValues) { + return SerializationUtils.deserialize(rawValues, valueSerializer); + } + + @SuppressWarnings("unchecked") + Set deserializeHashKeys(Set rawKeys) { + return SerializationUtils.deserialize(rawKeys, hashKeySerializer); + } + + @SuppressWarnings("unchecked") + List deserializeHashValues(List rawValues) { + return SerializationUtils.deserialize(rawValues, hashValueSerializer); + } + + @SuppressWarnings("unchecked") + Map deserializeHashMap(Map entries) { + // connection in pipeline/multi mode + if (entries == null) { + return null; + } + + Map map = new LinkedHashMap(entries.size()); + + for (Map.Entry entry : entries.entrySet()) { + map.put((HK) deserializeHashKey(entry.getKey()), (HV) deserializeHashValue(entry.getValue())); + } + + return map; + } + + @SuppressWarnings("unchecked") + K deserializeKey(byte[] value) { + return (K) keySerializer.deserialize(value); + } + + @SuppressWarnings("unchecked") + V deserializeValue(byte[] value) { + return (V) valueSerializer.deserialize(value); + } + + String deserializeString(byte[] value) { + return (String) stringSerializer.deserialize(value); + } + + @SuppressWarnings( { "unchecked" }) + HK deserializeHashKey(byte[] value) { + return (HK) hashKeySerializer.deserialize(value); + } + + @SuppressWarnings("unchecked") + HV deserializeHashValue(byte[] value) { + return (HV) hashValueSerializer.deserialize(value); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundHashOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundHashOperations.java new file mode 100644 index 000000000..d559ed00d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundHashOperations.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Hash operations bound to a certain key. + * + * @author Costin Leau + */ +public interface BoundHashOperations extends BoundKeyOperations { + + RedisOperations getOperations(); + + boolean hasKey(Object key); + + Long increment(HK key, long delta); + + HV get(Object key); + + void put(HK key, HV value); + + Boolean putIfAbsent(HK key, HV value); + + Collection multiGet(Collection keys); + + void putAll(Map m); + + Set keys(); + + Collection values(); + + Long size(); + + void delete(Object key); + + Map entries(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundKeyOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundKeyOperations.java new file mode 100644 index 000000000..d6791ea0f --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundKeyOperations.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; + +/** + * Operations over a Redis key. + * + * Useful for executing common key-'bound' operations to all implementations. + * + *

As the rest of the APIs, if the underlying connection is pipelined or queued/in multi mode, + * all methods will return null. + *

+ * @author Costin Leau + */ +public interface BoundKeyOperations { + + /** + * Returns the key associated with this entity. + * + * @return key associated with the implementing entity + */ + K getKey(); + + /** + * Returns the associated Redis type. + * + * @return key type + */ + DataType getType(); + + /** + * Returns the expiration of this key. + * + * @return expiration value (in seconds) + */ + Long getExpire(); + + /** + * Sets the key time-to-live/expiration. + * + * @param timeout expiration value + * @param unit expiration unit + * @return true if expiration was set, false otherwise + */ + Boolean expire(long timeout, TimeUnit unit); + + /** + * Sets the key time-to-live/expiration. + * + * @param date expiration date + * @return true if expiration was set, false otherwise + */ + Boolean expireAt(Date date); + + /** + * Removes the expiration (if any) of the key. + * @return true if expiration was removed, false otherwise + */ + Boolean persist(); + + /** + * Renames the key. + * + * @param newKey new key + */ + void rename(K newKey); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundListOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundListOperations.java new file mode 100644 index 000000000..5701587ba --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundListOperations.java @@ -0,0 +1,61 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * List operations bound to a certain key. + * + * @author Costin Leau + */ +public interface BoundListOperations extends BoundKeyOperations { + + RedisOperations getOperations(); + + List range(long start, long end); + + void trim(long start, long end); + + Long size(); + + Long leftPush(V value); + + Long leftPushIfPresent(V value); + + Long leftPush(V pivot, V value); + + Long rightPush(V value); + + Long rightPushIfPresent(V value); + + Long rightPush(V pivot, V value); + + V leftPop(); + + V leftPop(long timeout, TimeUnit unit); + + V rightPop(); + + V rightPop(long timeout, TimeUnit unit); + + Long remove(long i, Object value); + + V index(long index); + + void set(long index, V value); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundSetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundSetOperations.java new file mode 100644 index 000000000..e13520885 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundSetOperations.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Set; + +/** + * Set operations bound to a certain key. + * + * @author Costin Leau + */ +public interface BoundSetOperations extends BoundKeyOperations { + + RedisOperations getOperations(); + + Set diff(K key); + + Set diff(Collection keys); + + void diffAndStore(K key, K destKey); + + void diffAndStore(Collection keys, K destKey); + + Set intersect(K key); + + Set intersect(Collection keys); + + void intersectAndStore(K key, K destKey); + + void intersectAndStore(Collection keys, K destKey); + + Set union(K key); + + Set union(Collection keys); + + void unionAndStore(K key, K destKey); + + void unionAndStore(Collection keys, K destKey); + + Boolean add(V value); + + Boolean isMember(Object o); + + Set members(); + + Boolean move(K destKey, V value); + + V randomMember(); + + Boolean remove(Object o); + + V pop(); + + Long size(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundValueOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundValueOperations.java new file mode 100644 index 000000000..ae6267bf9 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundValueOperations.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.concurrent.TimeUnit; + +/** + * Value (or String in Redis terminology) operations bound to a certain key. + * + * @author Costin Leau + */ +public interface BoundValueOperations extends BoundKeyOperations { + + RedisOperations getOperations(); + + void set(V value); + + void set(V value, long offset); + + void set(V value, long timeout, TimeUnit unit); + + Boolean setIfAbsent(V value); + + V get(); + + String get(long start, long end); + + V getAndSet(V value); + + Long increment(long delta); + + Integer append(String value); + + Long size(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundZSetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundZSetOperations.java new file mode 100644 index 000000000..162e9dd51 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BoundZSetOperations.java @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.core.ZSetOperations.TypedTuple; + + +/** + * ZSet (or SortedSet) operations bound to a certain key. + * + * @author Costin Leau + */ +public interface BoundZSetOperations extends BoundKeyOperations { + + RedisOperations getOperations(); + + void intersectAndStore(K otherKey, K destKey); + + void intersectAndStore(Collection otherKeys, K destKey); + + Set range(long start, long end); + + Set rangeByScore(double min, double max); + + Set reverseRange(long start, long end); + + Set reverseRangeByScore(double min, double max); + + Set> rangeWithScores(long start, long end); + + Set> rangeByScoreWithScores(double min, double max); + + Set> reverseRangeWithScores(long start, long end); + + Set> reverseRangeByScoreWithScores(double min, double max); + + void removeRange(long start, long end); + + void removeRangeByScore(double min, double max); + + void unionAndStore(K otherKey, K destKey); + + void unionAndStore(Collection otherKeys, K destKey); + + Boolean add(V value, double score); + + Double incrementScore(V value, double delta); + + Long rank(Object o); + + Long reverseRank(Object o); + + Boolean remove(Object o); + + Long count(double min, double max); + + Long size(); + + Double score(Object o); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BulkMapper.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BulkMapper.java new file mode 100644 index 000000000..97d37998a --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/BulkMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.List; + +/** + * Mapper translating Redis bulk value responses (typically returned by a sort query) to actual objects. Implementations of this interface do not have to worry + * about exception or connection handling. + *

+ * Typically used by {@link RedisTemplate} sort methods. + * + * @author Costin Leau + */ +public interface BulkMapper { + + T mapBulk(List tuple); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/CloseSuppressingInvocationHandler.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/CloseSuppressingInvocationHandler.java new file mode 100644 index 000000000..a44527cb4 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/CloseSuppressingInvocationHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.springframework.data.keyvalue.redis.connection.RedisConnection; + +/** +* Invocation handler that suppresses close calls on {@link RedisConnection}. +* @see RedisConnection#close() +* @author Costin Leau +*/ +class CloseSuppressingInvocationHandler implements InvocationHandler { + + private static final String CLOSE = "close"; + private static final String HASH_CODE = "hashCode"; + private static final String EQUALS = "equals"; + + private final RedisConnection target; + + public CloseSuppressingInvocationHandler(RedisConnection target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + if (method.getName().equals(EQUALS)) { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + else if (method.getName().equals(HASH_CODE)) { + // Use hashCode of PersistenceManager proxy. + return System.identityHashCode(proxy); + } + else if (method.getName().equals(CLOSE)) { + // Handle close method: suppress, not valid. + return null; + } + + // Invoke method on target RedisConnection. + try { + Object retVal = method.invoke(this.target, args); + return retVal; + } catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundHashOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundHashOperations.java new file mode 100644 index 000000000..c8e6a531e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundHashOperations.java @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.DataType; + +/** + * Default implementation for {@link HashOperations}. + * + * @author Costin Leau + */ +class DefaultBoundHashOperations extends DefaultBoundKeyOperations implements BoundHashOperations { + + private final HashOperations ops; + + /** + * Constructs a new DefaultBoundHashOperations instance. + * + * @param key + * @param template + */ + public DefaultBoundHashOperations(H key, RedisOperations operations) { + super(key, operations); + this.ops = operations.opsForHash(); + } + + @Override + public void delete(Object key) { + ops.delete(getKey(), key); + } + + @Override + public HV get(Object key) { + return ops.get(getKey(), key); + } + + @Override + public Collection multiGet(Collection hashKeys) { + return ops.multiGet(getKey(), hashKeys); + } + + @Override + public RedisOperations getOperations() { + return ops.getOperations(); + } + + @Override + public boolean hasKey(Object key) { + return ops.hasKey(getKey(), key); + } + + @Override + public Long increment(HK key, long delta) { + return ops.increment(getKey(), key, delta); + } + + @Override + public Set keys() { + return ops.keys(getKey()); + } + + @Override + public Long size() { + return ops.size(getKey()); + } + + @Override + public void putAll(Map m) { + ops.putAll(getKey(), m); + } + + @Override + public void put(HK key, HV value) { + ops.put(getKey(), key, value); + } + + @Override + public Boolean putIfAbsent(HK key, HV value) { + return ops.putIfAbsent(getKey(), key, value); + } + + @Override + public Collection values() { + return ops.values(getKey()); + } + + @Override + public Map entries() { + return ops.entries(getKey()); + } + + @Override + public DataType getType() { + return DataType.HASH; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundKeyOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundKeyOperations.java new file mode 100644 index 000000000..b3ac53db4 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundKeyOperations.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + + +/** + * Default {@link BoundKeyOperations} implementation. + * Meant for internal usage. + * + * @author Costin Leau + */ +abstract class DefaultBoundKeyOperations implements BoundKeyOperations { + + private K key; + private final RedisOperations ops; + + public DefaultBoundKeyOperations(K key, RedisOperations operations) { + setKey(key); + this.ops = operations; + } + + @Override + public K getKey() { + return key; + } + + protected void setKey(K key) { + this.key = key; + } + + @Override + public Boolean expire(long timeout, TimeUnit unit) { + return ops.expire(key, timeout, unit); + } + + @Override + public Boolean expireAt(Date date) { + return ops.expireAt(key, date); + } + + @Override + public Long getExpire() { + return ops.getExpire(key); + } + + @Override + public Boolean persist() { + return ops.persist(key); + } + + @Override + public void rename(K newKey) { + if (ops.hasKey(key)) { + ops.rename(key, newKey); + } + key = newKey; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundListOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundListOperations.java new file mode 100644 index 000000000..45a34511c --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundListOperations.java @@ -0,0 +1,134 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; + + +/** + * Default implementation for {@link BoundListOperations}. + * + * @author Costin Leau + */ +class DefaultBoundListOperations extends DefaultBoundKeyOperations implements BoundListOperations { + + private final ListOperations ops; + + /** + * Constructs a new DefaultBoundListOperations instance. + * + * @param key + * @param operations + */ + public DefaultBoundListOperations(K key, RedisOperations operations) { + super(key, operations); + this.ops = operations.opsForList(); + } + + + @Override + public RedisOperations getOperations() { + return ops.getOperations(); + } + + @Override + public V index(long index) { + return ops.index(getKey(), index); + } + + @Override + public V leftPop() { + return ops.leftPop(getKey()); + } + + @Override + public V leftPop(long timeout, TimeUnit unit) { + return ops.leftPop(getKey(), timeout, unit); + } + + @Override + public Long leftPush(V value) { + return ops.leftPush(getKey(), value); + } + + @Override + public Long leftPushIfPresent(V value) { + return ops.leftPushIfPresent(getKey(), value); + } + + @Override + public Long leftPush(V pivot, V value) { + return ops.leftPush(getKey(), pivot, value); + } + + @Override + public Long size() { + return ops.size(getKey()); + } + + @Override + public List range(long start, long end) { + return ops.range(getKey(), start, end); + } + + @Override + public Long remove(long i, Object value) { + return ops.remove(getKey(), i, value); + } + + @Override + public V rightPop() { + return ops.rightPop(getKey()); + } + + @Override + public V rightPop(long timeout, TimeUnit unit) { + return ops.rightPop(getKey(), timeout, unit); + } + + @Override + public Long rightPushIfPresent(V value) { + return ops.rightPushIfPresent(getKey(), value); + } + + @Override + public Long rightPush(V value) { + return ops.rightPush(getKey(), value); + } + + @Override + public Long rightPush(V pivot, V value) { + return ops.rightPush(getKey(), pivot, value); + } + + @Override + public void trim(long start, long end) { + ops.trim(getKey(), start, end); + } + + @Override + public void set(long index, V value) { + ops.set(getKey(), index, value); + } + + @Override + public DataType getType() { + return DataType.LIST; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundSetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundSetOperations.java new file mode 100644 index 000000000..d0010b63a --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundSetOperations.java @@ -0,0 +1,156 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.DataType; + +/** + * Default implementation for {@link BoundSetOperations}. + * + * @author Costin Leau + */ +class DefaultBoundSetOperations extends DefaultBoundKeyOperations implements BoundSetOperations { + + private final SetOperations ops; + + + /** + * Constructs a new DefaultBoundSetOperations instance. + * + * @param key + * @param operations + */ + DefaultBoundSetOperations(K key, RedisOperations operations) { + super(key, operations); + this.ops = operations.opsForSet(); + } + + @Override + public Boolean add(V value) { + return ops.add(getKey(), value); + } + + @Override + public Set diff(K key) { + return ops.difference(getKey(), key); + } + + @Override + public Set diff(Collection keys) { + return ops.difference(getKey(), keys); + } + + + @Override + public void diffAndStore(K key, K destKey) { + ops.differenceAndStore(getKey(), key, destKey); + } + + @Override + public void diffAndStore(Collection keys, K destKey) { + ops.differenceAndStore(getKey(), keys, destKey); + } + + @Override + public RedisOperations getOperations() { + return ops.getOperations(); + } + + @Override + public Set intersect(K key) { + return ops.intersect(getKey(), key); + } + + @Override + public Set intersect(Collection keys) { + return ops.intersect(getKey(), keys); + } + + @Override + public void intersectAndStore(K key, K destKey) { + ops.intersectAndStore(getKey(), key, destKey); + } + + @Override + public void intersectAndStore(Collection keys, K destKey) { + ops.intersectAndStore(getKey(), keys, destKey); + } + + @Override + public Boolean isMember(Object o) { + return ops.isMember(getKey(), o); + } + + @Override + public Set members() { + return ops.members(getKey()); + } + + @Override + public Boolean move(K destKey, V value) { + return ops.move(getKey(), value, destKey); + } + + @Override + public V randomMember() { + return ops.randomMember(getKey()); + } + + @Override + public Boolean remove(Object o) { + return ops.remove(getKey(), o); + } + + @Override + public V pop() { + return ops.pop(getKey()); + } + + @Override + public Long size() { + return ops.size(getKey()); + } + + + @Override + public Set union(K key) { + return ops.union(getKey(), key); + } + + @Override + public Set union(Collection keys) { + return ops.union(getKey(), keys); + } + + @Override + public void unionAndStore(K key, K destKey) { + ops.unionAndStore(getKey(), key, destKey); + } + + @Override + public void unionAndStore(Collection keys, K destKey) { + ops.unionAndStore(getKey(), keys, destKey); + } + + @Override + public DataType getType() { + return DataType.SET; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundValueOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundValueOperations.java new file mode 100644 index 000000000..b9ec6b168 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundValueOperations.java @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; + +/** + * @author Costin Leau + */ +class DefaultBoundValueOperations extends DefaultBoundKeyOperations implements BoundValueOperations { + + private final ValueOperations ops; + + /** + * Constructs a new DefaultBoundValueOperations instance. + * + * @param key + * @param operations + */ + public DefaultBoundValueOperations(K key, RedisOperations operations) { + super(key, operations); + this.ops = operations.opsForValue(); + } + + @Override + public V get() { + return ops.get(getKey()); + } + + @Override + public V getAndSet(V value) { + return ops.getAndSet(getKey(), value); + } + + @Override + public Long increment(long delta) { + return ops.increment(getKey(), delta); + } + + @Override + public Integer append(String value) { + return ops.append(getKey(), value); + } + + @Override + public String get(long start, long end) { + return ops.get(getKey(), start, end); + } + + @Override + public void set(V value, long timeout, TimeUnit unit) { + ops.set(getKey(), value, timeout, unit); + } + + @Override + public void set(V value) { + ops.set(getKey(), value); + } + + @Override + public Boolean setIfAbsent(V value) { + return ops.setIfAbsent(getKey(), value); + } + + @Override + public void set(V value, long offset) { + ops.set(getKey(), value, offset); + } + + @Override + public Long size() { + return ops.size(getKey()); + } + + @Override + public RedisOperations getOperations() { + return ops.getOperations(); + } + + @Override + public DataType getType() { + return DataType.STRING; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundZSetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundZSetOperations.java new file mode 100644 index 000000000..60f847bc5 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultBoundZSetOperations.java @@ -0,0 +1,164 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.ZSetOperations.TypedTuple; + +/** + * Default implementation for {@link BoundZSetOperations}. + * + * @author Costin Leau + */ +class DefaultBoundZSetOperations extends DefaultBoundKeyOperations implements BoundZSetOperations { + + private final ZSetOperations ops; + + /** + * Constructs a new DefaultBoundZSetOperations instance. + * + * @param key + * @param oeprations + */ + public DefaultBoundZSetOperations(K key, RedisOperations operations) { + super(key, operations); + this.ops = operations.opsForZSet(); + } + + @Override + public Boolean add(V value, double score) { + return ops.add(getKey(), value, score); + } + + @Override + public Double incrementScore(V value, double delta) { + return ops.incrementScore(getKey(), value, delta); + } + + @Override + public RedisOperations getOperations() { + return ops.getOperations(); + } + + @Override + public void intersectAndStore(K destKey, K otherKey) { + ops.intersectAndStore(getKey(), otherKey, destKey); + } + + @Override + public void intersectAndStore(Collection otherKeys, K destKey) { + ops.intersectAndStore(getKey(), otherKeys, destKey); + } + + @Override + public Set range(long start, long end) { + return ops.range(getKey(), start, end); + } + + @Override + public Set rangeByScore(double min, double max) { + return ops.rangeByScore(getKey(), min, max); + } + + @Override + public Set> rangeByScoreWithScores(double min, double max) { + return ops.rangeByScoreWithScores(getKey(), min, max); + } + + @Override + public Set> rangeWithScores(long start, long end) { + return ops.rangeWithScores(getKey(), start, end); + } + + @Override + public Set reverseRangeByScore(double min, double max) { + return ops.reverseRangeByScore(getKey(), min, max); + } + + @Override + public Set> reverseRangeByScoreWithScores(double min, double max) { + return ops.reverseRangeByScoreWithScores(getKey(), min, max); + } + + @Override + public Set> reverseRangeWithScores(long start, long end) { + return ops.reverseRangeWithScores(getKey(), start, end); + } + + @Override + public Long rank(Object o) { + return ops.rank(getKey(), o); + } + + @Override + public Long reverseRank(Object o) { + return ops.reverseRank(getKey(), o); + } + + @Override + public Double score(Object o) { + return ops.score(getKey(), o); + } + + @Override + public Boolean remove(Object o) { + return ops.remove(getKey(), o); + } + + @Override + public void removeRange(long start, long end) { + ops.removeRange(getKey(), start, end); + } + + @Override + public void removeRangeByScore(double min, double max) { + ops.removeRangeByScore(getKey(), min, max); + } + + @Override + public Set reverseRange(long start, long end) { + return ops.reverseRange(getKey(), start, end); + } + + @Override + public Long count(double min, double max) { + return ops.count(getKey(), min, max); + } + + @Override + public Long size() { + return ops.size(getKey()); + } + + @Override + public void unionAndStore(K otherKey, K destKey) { + ops.unionAndStore(getKey(), otherKey, destKey); + } + + @Override + public void unionAndStore(Collection otherKeys, K destKey) { + ops.unionAndStore(getKey(), otherKeys, destKey); + } + + @Override + public DataType getType() { + return DataType.ZSET; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultHashOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultHashOperations.java new file mode 100644 index 000000000..afe1def4f --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultHashOperations.java @@ -0,0 +1,228 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.RedisConnection; + +/** + * Default implementation of {@link HashOperations}. + * + * @author Costin Leau + */ +class DefaultHashOperations extends AbstractOperations implements HashOperations { + + @SuppressWarnings("unchecked") + DefaultHashOperations(RedisTemplate template) { + super((RedisTemplate) template); + } + + @SuppressWarnings("unchecked") + @Override + public HV get(K key, Object hashKey) { + final byte[] rawKey = rawKey(key); + final byte[] rawHashKey = rawHashKey(hashKey); + + byte[] rawHashValue = execute(new RedisCallback() { + @Override + public byte[] doInRedis(RedisConnection connection) { + return connection.hGet(rawKey, rawHashKey); + } + }, true); + + return (HV) deserializeHashValue(rawHashValue); + } + + @Override + public Boolean hasKey(K key, Object hashKey) { + final byte[] rawKey = rawKey(key); + final byte[] rawHashKey = rawHashKey(hashKey); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.hExists(rawKey, rawHashKey); + } + }, true); + } + + @Override + public Long increment(K key, HK hashKey, final long delta) { + final byte[] rawKey = rawKey(key); + final byte[] rawHashKey = rawHashKey(hashKey); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.hIncrBy(rawKey, rawHashKey, delta); + } + }, true); + + } + + @Override + public Set keys(K key) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.hKeys(rawKey); + } + }, true); + + return deserializeHashKeys(rawValues); + } + + @Override + public Long size(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.hLen(rawKey); + } + }, true); + } + + @Override + public void putAll(K key, Map m) { + if (m.isEmpty()) { + return; + } + + final byte[] rawKey = rawKey(key); + + final Map hashes = new LinkedHashMap(m.size()); + + for (Map.Entry entry : m.entrySet()) { + hashes.put(rawHashKey(entry.getKey()), rawHashValue(entry.getValue())); + } + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.hMSet(rawKey, hashes); + return null; + } + }, true); + } + + + @Override + public Collection multiGet(K key, Collection fields) { + if (fields.isEmpty()) { + return Collections.emptyList(); + } + + final byte[] rawKey = rawKey(key); + + final byte[][] rawHashKeys = new byte[fields.size()][]; + + int counter = 0; + for (HK hashKey : fields) { + rawHashKeys[counter++] = rawHashKey(hashKey); + } + + List rawValues = execute(new RedisCallback>() { + @Override + public List doInRedis(RedisConnection connection) { + return connection.hMGet(rawKey, rawHashKeys); + } + }, true); + + return deserializeHashValues(rawValues); + } + + @Override + public void put(K key, HK hashKey, HV value) { + final byte[] rawKey = rawKey(key); + final byte[] rawHashKey = rawHashKey(hashKey); + final byte[] rawHashValue = rawHashValue(value); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.hSet(rawKey, rawHashKey, rawHashValue); + return null; + } + }, true); + } + + @Override + public Boolean putIfAbsent(K key, HK hashKey, HV value) { + final byte[] rawKey = rawKey(key); + final byte[] rawHashKey = rawHashKey(hashKey); + final byte[] rawHashValue = rawHashValue(value); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.hSetNX(rawKey, rawHashKey, rawHashValue); + } + }, true); + } + + + @Override + public List values(K key) { + final byte[] rawKey = rawKey(key); + + List rawValues = execute(new RedisCallback>() { + @Override + public List doInRedis(RedisConnection connection) { + return connection.hVals(rawKey); + } + }, true); + + return deserializeHashValues(rawValues); + } + + @Override + public void delete(K key, Object hashKey) { + final byte[] rawKey = rawKey(key); + final byte[] rawHashKey = rawHashKey(hashKey); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.hDel(rawKey, rawHashKey); + return null; + } + }, true); + } + + @Override + public Map entries(K key) { + final byte[] rawKey = rawKey(key); + + Map entries = execute(new RedisCallback>() { + @Override + public Map doInRedis(RedisConnection connection) { + return connection.hGetAll(rawKey); + } + }, true); + + return deserializeHashMap(entries); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultListOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultListOperations.java new file mode 100644 index 000000000..2349f58ec --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultListOperations.java @@ -0,0 +1,249 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisListCommands.Position; +import org.springframework.util.CollectionUtils; + +/** + * Default implementation of {@link ListOperations}. + * + * @author Costin Leau + */ +class DefaultListOperations extends AbstractOperations implements ListOperations { + + DefaultListOperations(RedisTemplate template) { + super(template); + } + + @Override + public V index(K key, final long index) { + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.lIndex(rawKey, index); + } + }, true); + } + + @Override + public V leftPop(K key) { + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.lPop(rawKey); + } + }, true); + } + + @Override + public V leftPop(K key, long timeout, TimeUnit unit) { + final int tm = (int) unit.toSeconds(timeout); + + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + List lPop = connection.bLPop(tm, rawKey); + return (CollectionUtils.isEmpty(lPop) ? null : lPop.get(1)); + } + }, true); + } + + @Override + public Long leftPush(K key, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.lPush(rawKey, rawValue); + } + }, true); + } + + @Override + public Long leftPushIfPresent(K key, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.lPushX(rawKey, rawValue); + } + }, true); + } + + @Override + public Long leftPush(K key, V pivot, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawPivot = rawValue(pivot); + final byte[] rawValue = rawValue(value); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.lInsert(rawKey, Position.BEFORE, rawPivot, rawValue); + } + }, true); + } + + @Override + public Long size(K key) { + final byte[] rawKey = rawKey(key); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.lLen(rawKey); + } + }, true); + } + + @Override + public List range(K key, final long start, final long end) { + final byte[] rawKey = rawKey(key); + return execute(new RedisCallback>() { + @SuppressWarnings("unchecked") + @Override + public List doInRedis(RedisConnection connection) { + return deserializeValues(connection.lRange(rawKey, start, end)); + } + }, true); + } + + @Override + public Long remove(K key, final long count, Object value) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.lRem(rawKey, count, rawValue); + } + }, true); + } + + @Override + public V rightPop(K key) { + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.rPop(rawKey); + } + }, true); + } + + @Override + public V rightPop(K key, long timeout, TimeUnit unit) { + final int tm = (int) unit.toSeconds(timeout); + + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + List bRPop = connection.bRPop(tm, rawKey); + return (CollectionUtils.isEmpty(bRPop) ? null : bRPop.get(1)); + } + }, true); + } + + @Override + public Long rightPush(K key, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.rPush(rawKey, rawValue); + } + }, true); + } + + @Override + public Long rightPushIfPresent(K key, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.rPushX(rawKey, rawValue); + } + }, true); + } + + @Override + public Long rightPush(K key, V pivot, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawPivot = rawValue(pivot); + final byte[] rawValue = rawValue(value); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.lInsert(rawKey, Position.AFTER, rawPivot, rawValue); + } + }, true); + } + + @Override + public V rightPopAndLeftPush(K sourceKey, K destinationKey) { + final byte[] rawDestKey = rawKey(destinationKey); + + return execute(new ValueDeserializingRedisCallback(sourceKey) { + @Override + protected byte[] inRedis(byte[] rawSourceKey, RedisConnection connection) { + return connection.rPopLPush(rawSourceKey, rawDestKey); + } + }, true); + } + + @Override + public V rightPopAndLeftPush(K sourceKey, K destinationKey, long timeout, TimeUnit unit) { + final int tm = (int) unit.toSeconds(timeout); + final byte[] rawDestKey = rawKey(destinationKey); + + return execute(new ValueDeserializingRedisCallback(sourceKey) { + @Override + protected byte[] inRedis(byte[] rawSourceKey, RedisConnection connection) { + return connection.bRPopLPush(tm, rawSourceKey, rawDestKey); + } + }, true); + } + + @Override + public void set(K key, final long index, V value) { + final byte[] rawValue = rawValue(value); + execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + connection.lSet(rawKey, index, rawValue); + return null; + } + }, true); + } + + @Override + public void trim(K key, final long start, final long end) { + execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + connection.lTrim(rawKey, start, end); + return null; + } + }, true); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultSetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultSetOperations.java new file mode 100644 index 000000000..a4893104f --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultSetOperations.java @@ -0,0 +1,241 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.RedisConnection; + +/** + * Default implementation of {@link SetOperations}. + * + * @author Costin Leau + */ +class DefaultSetOperations extends AbstractOperations implements SetOperations { + + public DefaultSetOperations(RedisTemplate template) { + super(template); + } + + @Override + public Boolean add(K key, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.sAdd(rawKey, rawValue); + } + }, true); + } + + @Override + public Set difference(K key, K otherKey) { + return difference(key, Collections.singleton(otherKey)); + } + + @SuppressWarnings("unchecked") + @Override + public Set difference(final K key, final Collection otherKeys) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.sDiff(rawKeys); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public void differenceAndStore(K key, K otherKey, K destKey) { + differenceAndStore(key, Collections.singleton(otherKey), destKey); + } + + @Override + public void differenceAndStore(final K key, final Collection otherKeys, K destKey) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + final byte[] rawDestKey = rawKey(destKey); + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.sDiffStore(rawDestKey, rawKeys); + return null; + } + }, true); + } + + @Override + public Set intersect(K key, K otherKey) { + return intersect(key, Collections.singleton(otherKey)); + } + + @SuppressWarnings("unchecked") + @Override + public Set intersect(K key, Collection otherKeys) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.sInter(rawKeys); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public void intersectAndStore(K key, K otherKey, K destKey) { + intersectAndStore(key, Collections.singleton(otherKey), destKey); + } + + @Override + public void intersectAndStore(K key, Collection otherKeys, K destKey) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + final byte[] rawDestKey = rawKey(destKey); + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.sInterStore(rawDestKey, rawKeys); + return null; + } + }, true); + } + + @Override + public Boolean isMember(K key, Object o) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(o); + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.sIsMember(rawKey, rawValue); + } + }, true); + } + + @SuppressWarnings("unchecked") + @Override + public Set members(K key) { + final byte[] rawKey = rawKey(key); + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.sMembers(rawKey); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public Boolean move(K key, V value, K destKey) { + final byte[] rawKey = rawKey(key); + final byte[] rawDestKey = rawKey(destKey); + final byte[] rawValue = rawValue(value); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.sMove(rawKey, rawDestKey, rawValue); + } + }, true); + } + + @Override + public V randomMember(K key) { + + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.randomKey(); + } + }, true); + } + + @Override + public Boolean remove(K key, Object o) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(o); + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.sRem(rawKey, rawValue); + } + }, true); + } + + @Override + public V pop(K key) { + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.sPop(rawKey); + } + }, true); + } + + @Override + public Long size(K key) { + final byte[] rawKey = rawKey(key); + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.sCard(rawKey); + } + }, true); + } + + @Override + public Set union(K key, K otherKey) { + return union(key, Collections.singleton(otherKey)); + } + + @SuppressWarnings("unchecked") + @Override + public Set union(K key, Collection otherKeys) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.sUnion(rawKeys); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public void unionAndStore(K key, K otherKey, K destKey) { + unionAndStore(key, Collections.singleton(otherKey), destKey); + } + + @Override + public void unionAndStore(K key, Collection otherKeys, K destKey) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + final byte[] rawDestKey = rawKey(destKey); + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.sUnionStore(rawDestKey, rawKeys); + return null; + } + }, true); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultTypedTuple.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultTypedTuple.java new file mode 100644 index 000000000..fc23e6d79 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultTypedTuple.java @@ -0,0 +1,50 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import org.springframework.data.keyvalue.redis.core.ZSetOperations.TypedTuple; + +/** + * Default implementation of TypedTuple. + * + * @author Costin Leau + */ +class DefaultTypedTuple implements TypedTuple { + + private final Double score; + private final V value; + + /** + * Constructs a new DefaultTypedTuple instance. + * + * @param value + * @param score + */ + public DefaultTypedTuple(V value, Double score) { + this.score = score; + this.value = value; + } + + @Override + public Double getScore() { + return score; + } + + @Override + public V getValue() { + return value; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultValueOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultValueOperations.java new file mode 100644 index 000000000..bc2c13d0d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultValueOperations.java @@ -0,0 +1,244 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; + +/** + * Default implementation of {@link ValueOperations}. + * + * @author Costin Leau + */ +class DefaultValueOperations extends AbstractOperations implements ValueOperations { + + DefaultValueOperations(RedisTemplate template) { + super(template); + } + + @Override + public V get(final Object key) { + + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.get(rawKey); + } + }, true); + } + + @Override + public V getAndSet(K key, V newValue) { + final byte[] rawValue = rawValue(newValue); + return execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.getSet(rawKey, rawValue); + } + }, true); + } + + @Override + public Long increment(K key, final long delta) { + final byte[] rawKey = rawKey(key); + // TODO add conversion service in here ? + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + if (delta == 1) { + return connection.incr(rawKey); + } + + if (delta == -1) { + return connection.decr(rawKey); + } + + if (delta < 0) { + return connection.decrBy(rawKey, delta); + } + + return connection.incrBy(rawKey, delta); + } + }, true); + } + + @Override + public Integer append(K key, String value) { + final byte[] rawKey = rawKey(key); + final byte[] rawString = rawString(value); + + return execute(new RedisCallback() { + @Override + public Integer doInRedis(RedisConnection connection) { + return connection.append(rawKey, rawString).intValue(); + } + }, true); + } + + @Override + public String get(K key, final long start, final long end) { + final byte[] rawKey = rawKey(key); + + byte[] rawReturn = execute(new RedisCallback() { + @Override + public byte[] doInRedis(RedisConnection connection) { + return connection.getRange(rawKey, start, end); + } + }, true); + + return deserializeString(rawReturn); + } + + @SuppressWarnings("unchecked") + @Override + public List multiGet(Collection keys) { + if (keys.isEmpty()) { + return Collections.emptyList(); + } + + final byte[][] rawKeys = new byte[keys.size()][]; + + int counter = 0; + for (K hashKey : keys) { + rawKeys[counter++] = rawKey(hashKey); + } + + List rawValues = execute(new RedisCallback>() { + @Override + public List doInRedis(RedisConnection connection) { + return connection.mGet(rawKeys); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public void multiSet(Map m) { + if (m.isEmpty()) { + return; + } + + final Map rawKeys = new LinkedHashMap(m.size()); + + for (Map.Entry entry : m.entrySet()) { + rawKeys.put(rawKey(entry.getKey()), rawValue(entry.getValue())); + } + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.mSet(rawKeys); + return null; + } + }, true); + } + + @Override + public void multiSetIfAbsent(Map m) { + if (m.isEmpty()) { + return; + } + + final Map rawKeys = new LinkedHashMap(m.size()); + + for (Map.Entry entry : m.entrySet()) { + rawKeys.put(rawKey(entry.getKey()), rawValue(entry.getValue())); + } + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.mSetNX(rawKeys); + return null; + } + }, true); + } + + @Override + public void set(K key, V value) { + final byte[] rawValue = rawValue(value); + execute(new ValueDeserializingRedisCallback(key) { + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + connection.set(rawKey, rawValue); + return null; + } + }, true); + } + + @Override + public void set(K key, V value, long timeout, TimeUnit unit) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + final long rawTimeout = unit.toSeconds(timeout); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + connection.setEx(rawKey, (int) rawTimeout, rawValue); + return null; + } + }, true); + } + + @Override + public Boolean setIfAbsent(K key, V value) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) throws DataAccessException { + return connection.setNX(rawKey, rawValue); + } + }, true); + } + + + @Override + public void set(K key, final V value, final long offset) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.setRange(rawKey, rawValue, offset); + return null; + } + }, true); + } + + @Override + public Long size(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.strLen(rawKey); + } + }, true); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultZSetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultZSetOperations.java new file mode 100644 index 000000000..c0198d010 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/DefaultZSetOperations.java @@ -0,0 +1,313 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisZSetCommands.Tuple; + +/** + * Default implementation of {@link ZSetOperations}. + * + * @author Costin Leau + */ +class DefaultZSetOperations extends AbstractOperations implements ZSetOperations { + + DefaultZSetOperations(RedisTemplate template) { + super(template); + } + + @Override + public Boolean add(final K key, final V value, final double score) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.zAdd(rawKey, score, rawValue); + } + }, true); + } + + @Override + public Double incrementScore(K key, V value, final double delta) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(value); + + return execute(new RedisCallback() { + @Override + public Double doInRedis(RedisConnection connection) { + return connection.zIncrBy(rawKey, delta, rawValue); + } + }, true); + } + + @Override + public void intersectAndStore(K key, K otherKey, K destKey) { + intersectAndStore(key, Collections.singleton(otherKey), destKey); + } + + @Override + public void intersectAndStore(K key, Collection otherKeys, K destKey) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + final byte[] rawDestKey = rawKey(destKey); + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.zInterStore(rawDestKey, rawKeys); + return null; + } + }, true); + } + + @Override + public Set range(K key, final long start, final long end) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRange(rawKey, start, end); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public Set reverseRange(K key, final long start, final long end) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRevRange(rawKey, start, end); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public Set> rangeWithScores(K key, final long start, final long end) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRangeWithScores(rawKey, start, end); + } + }, true); + + return deserializeTupleValues(rawValues); + } + + @Override + public Set> reverseRangeWithScores(K key, final long start, final long end) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRevRangeWithScores(rawKey, start, end); + } + }, true); + + return deserializeTupleValues(rawValues); + } + + @Override + public Set rangeByScore(K key, final double min, final double max) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRangeByScore(rawKey, min, max); + } + }, true); + + return deserializeValues(rawValues); + } + + + @Override + public Set reverseRangeByScore(K key, final double min, final double max) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRevRangeByScore(rawKey, min, max); + } + }, true); + + return deserializeValues(rawValues); + } + + @Override + public Set> rangeByScoreWithScores(K key, final double min, final double max) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRangeByScoreWithScores(rawKey, min, max); + } + }, true); + + return deserializeTupleValues(rawValues); + } + + @Override + public Set> reverseRangeByScoreWithScores(K key, final double min, final double max) { + final byte[] rawKey = rawKey(key); + + Set rawValues = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.zRevRangeByScoreWithScores(rawKey, min, max); + + } + }, true); + + return deserializeTupleValues(rawValues); + } + + @Override + public Long rank(K key, Object o) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(o); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + Long zRank = connection.zRank(rawKey, rawValue); + return (zRank != null && zRank.longValue() >= 0 ? zRank : null); + } + }, true); + } + + @Override + public Long reverseRank(K key, Object o) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(o); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + Long zRank = connection.zRevRank(rawKey, rawValue); + return (zRank != null && zRank.longValue() >= 0 ? zRank : null); + } + }, true); + } + + @Override + public Boolean remove(K key, Object o) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(o); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.zRem(rawKey, rawValue); + } + }, true); + } + + @Override + public void removeRange(K key, final long start, final long end) { + final byte[] rawKey = rawKey(key); + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.zRemRange(rawKey, start, end); + return null; + } + }, true); + } + + @Override + public void removeRangeByScore(K key, final double min, final double max) { + final byte[] rawKey = rawKey(key); + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.zRemRangeByScore(rawKey, min, max); + return null; + } + }, true); + } + + @Override + public Double score(K key, Object o) { + final byte[] rawKey = rawKey(key); + final byte[] rawValue = rawValue(o); + + return execute(new RedisCallback() { + @Override + public Double doInRedis(RedisConnection connection) { + return connection.zScore(rawKey, rawValue); + } + }, true); + } + + @Override + public Long count(K key, final double min, final double max) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.zCount(rawKey, min, max); + } + }, true); + } + + @Override + public Long size(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return connection.zCard(rawKey); + } + }, true); + } + + @Override + public void unionAndStore(K key, K otherKey, K destKey) { + unionAndStore(key, Collections.singleton(otherKey), destKey); + } + + @Override + public void unionAndStore(K key, Collection otherKeys, K destKey) { + final byte[][] rawKeys = rawKeys(key, otherKeys); + final byte[] rawDestKey = rawKey(destKey); + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.zUnionStore(rawDestKey, rawKeys); + return null; + } + }, true); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/HashOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/HashOperations.java new file mode 100644 index 000000000..67d1d7e28 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/HashOperations.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Redis map specific operations working on a hash. + * + * @author Costin Leau + */ +public interface HashOperations { + + void delete(H key, Object hashKey); + + Boolean hasKey(H key, Object hashKey); + + HV get(H key, Object hashKey); + + Collection multiGet(H key, Collection hashKeys); + + Long increment(H key, HK hashKey, long delta); + + Set keys(H key); + + Long size(H key); + + void putAll(H key, Map m); + + void put(H key, HK hashKey, HV value); + + Boolean putIfAbsent(H key, HK hashKey, HV value); + + Collection values(H key); + + Map entries(H key); + + RedisOperations getOperations(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/HashOperationsEditor.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/HashOperationsEditor.java new file mode 100644 index 000000000..2cb2b509d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/HashOperationsEditor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.beans.PropertyEditorSupport; + +/** + * PropertyEditor allowing for easy injection of {@link HashOperations} from + * {@link RedisOperations}. + * + * @author Costin Leau + */ +class HashOperationsEditor extends PropertyEditorSupport { + + @Override + public void setValue(Object value) { + if (value instanceof RedisOperations) { + super.setValue(((RedisOperations) value).opsForHash()); + } + else { + throw new java.lang.IllegalArgumentException("Editor supports only conversion of type " + + RedisOperations.class); + } + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ListOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ListOperations.java new file mode 100644 index 000000000..9521b6d60 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ListOperations.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Redis list specific operations. + * + * @author Costin Leau + */ +public interface ListOperations { + + List range(K key, long start, long end); + + void trim(K key, long start, long end); + + Long size(K key); + + Long leftPush(K key, V value); + + Long leftPushIfPresent(K key, V value); + + Long leftPush(K key, V pivot, V value); + + Long rightPush(K key, V value); + + Long rightPushIfPresent(K key, V value); + + Long rightPush(K key, V pivot, V value); + + void set(K key, long index, V value); + + Long remove(K key, long i, Object value); + + V index(K key, long index); + + V leftPop(K key); + + V leftPop(K key, long timeout, TimeUnit unit); + + V rightPop(K key); + + V rightPop(K key, long timeout, TimeUnit unit); + + V rightPopAndLeftPush(K sourceKey, K destinationKey); + + V rightPopAndLeftPush(K sourceKey, K destinationKey, long timeout, TimeUnit unit); + + RedisOperations getOperations(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ListOperationsEditor.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ListOperationsEditor.java new file mode 100644 index 000000000..9b1ebb663 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ListOperationsEditor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.beans.PropertyEditorSupport; + +/** + * PropertyEditor allowing for easy injection of {@link ListOperations} from + * {@link RedisOperations}. + * + * @author Costin Leau + */ +class ListOperationsEditor extends PropertyEditorSupport { + @Override + public void setValue(Object value) { + if (value instanceof RedisOperations) { + super.setValue(((RedisOperations) value).opsForList()); + } + else { + throw new java.lang.IllegalArgumentException("Editor supports only conversion of type " + + RedisOperations.class); + } + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisAccessor.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisAccessor.java new file mode 100644 index 000000000..39bb33f49 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisAccessor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.util.Assert; + +/** + * Base class for {@link RedisTemplate} defining common properties. + * Not intended to be used directly. + * + * @author Costin Leau + */ +public class RedisAccessor implements InitializingBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private RedisConnectionFactory connectionFactory; + + public void afterPropertiesSet() { + Assert.notNull(getConnectionFactory(), "RedisConnectionFactory is required"); + } + + /** + * Returns the connectionFactory. + * + * @return Returns the connectionFactory + */ + public RedisConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + /** + * Sets the connection factory. + * + * @param connectionFactory The connectionFactory to set. + */ + public void setConnectionFactory(RedisConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisCallback.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisCallback.java new file mode 100644 index 000000000..6de76002e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisCallback.java @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; + +/** + * Callback interface for Redis 'low level' code. + * To be used with {@link RedisTemplate} execution methods, often as anonymous classes within a method implementation. + * Usually, used for chaining several operations together ({@code get/set/trim etc...}. + * + * @author Costin Leau + */ +public interface RedisCallback { + + /** + * Gets called by {@link RedisTemplate} with an active Redis connection. Does not need to care about activating or + * closing the connection or handling exceptions. + * + * @param connection active Redis connection + * @return a result object or {@code null} if none + * @throws DataAccessException + */ + T doInRedis(RedisConnection connection) throws DataAccessException; +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisConnectionUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisConnectionUtils.java new file mode 100644 index 000000000..b0799b82a --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisConnectionUtils.java @@ -0,0 +1,195 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.transaction.support.ResourceHolder; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class featuring {@link RedisConnection} handling, allowing for reuse of instances within 'transactions'/scopes. + * + * @author Costin Leau + */ +public abstract class RedisConnectionUtils { + + private static final Log log = LogFactory.getLog(RedisConnectionUtils.class); + + /** + * Binds a new Redis connection (from the given factory) to the current thread, if none is already bound. + * + * @param factory connection factory + * @return a new Redis connection + */ + public static RedisConnection bindConnection(RedisConnectionFactory factory) { + return doGetConnection(factory, true, true); + } + + /** + * Gets a Redis connection from the given factory. Is aware of and will return any existing corresponding connections bound to the current thread, + * for example when using a transaction manager. Will always create a new connection otherwise. + * + * @param factory connection factory for creating the connection + * @return an active Redis connection + */ + public static RedisConnection getConnection(RedisConnectionFactory factory) { + return doGetConnection(factory, true, false); + } + + /** + * Gets a Redis connection. Is aware of and will return any existing corresponding connections bound to the current thread, + * for example when using a transaction manager. Will create a new Connection otherwise, if {@code allowCreate} is true. + * + * @param factory connection factory for creating the connection + * @param allowCreate whether a new (unbound) connection should be created when no connection can be found for the current thread + * @param bind binds the connection to the thread, in case one was created + * @return an active Redis connection + */ + public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind) { + Assert.notNull(factory, "No RedisConnectionFactory specified"); + + RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory); + //TODO: investigate tx synchronization + + if (connHolder != null) + return connHolder.getConnection(); + + if (!allowCreate) { + throw new IllegalArgumentException("No connection found and allowCreate = false"); + } + + if (log.isDebugEnabled()) + log.debug("Opening RedisConnection"); + + RedisConnection conn = factory.getConnection(); + + boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); + + if (bind || synchronizationActive) { + connHolder = new RedisConnectionHolder(conn); + if (synchronizationActive) { + TransactionSynchronizationManager.registerSynchronization(new RedisConnectionSynchronization( + connHolder, factory, true)); + } + TransactionSynchronizationManager.bindResource(factory, connHolder); + return connHolder.getConnection(); + } + return conn; + } + + /** + * Closes the given connection, created via the given factory if not managed externally (i.e. not bound to the thread). + * + * @param conn the Redis connection to close + * @param factory the Redis factory that the connection was created with + */ + public static void releaseConnection(RedisConnection conn, RedisConnectionFactory factory) { + if (conn == null) { + return; + } + // Only release non-transactional/non-bound connections. + if (!isConnectionTransactional(conn, factory)) { + if (log.isDebugEnabled()) { + log.debug("Closing Redis Connection"); + } + conn.close(); + } + } + + /** + * Unbinds and closes the connection (if any) associated with the given factory. + * + * @param factory Redis factory + */ + public static void unbindConnection(RedisConnectionFactory factory) { + RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.unbindResourceIfPossible(factory); + if (connHolder != null) { + RedisConnection connection = connHolder.getConnection(); + connection.close(); + } + } + + /** + * Return whether the given Redis connection is transactional, that is, bound to the current thread by Spring's transaction facilities. + * + * @param conn Redis connection to check + * @param connFactory Redis connection factory that the connection was created with + * @return whether the connection is transactional or not + */ + public static boolean isConnectionTransactional(RedisConnection conn, RedisConnectionFactory connFactory) { + if (connFactory == null) { + return false; + } + RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(connFactory); + return (connHolder != null && conn == connHolder.getConnection()); + } + + private static class RedisConnectionSynchronization extends + ResourceHolderSynchronization { + + private final boolean newRedisConnection; + + public RedisConnectionSynchronization(RedisConnectionHolder connHolder, RedisConnectionFactory connFactory, + boolean newRedisConnection) { + super(connHolder, connFactory); + this.newRedisConnection = newRedisConnection; + } + + @Override + protected boolean shouldUnbindAtCompletion() { + return this.newRedisConnection; + } + + @Override + protected void releaseResource(RedisConnectionHolder resourceHolder, RedisConnectionFactory resourceKey) { + releaseConnection(resourceHolder.getConnection(), resourceKey); + } + } + + private static class RedisConnectionHolder implements ResourceHolder { + + private boolean isVoid = false; + private final RedisConnection conn; + + public RedisConnectionHolder(RedisConnection conn) { + this.conn = conn; + } + + @Override + public boolean isVoid() { + return isVoid; + } + + public RedisConnection getConnection() { + return conn; + } + + @Override + public void reset() { + // no-op + } + + @Override + public void unbound() { + this.isVoid = true; + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisOperations.java new file mode 100644 index 000000000..57e51fd1e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisOperations.java @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.query.SortQuery; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; + + +/** + * Interface that specified a basic set of Redis operations, implemented by {@link RedisTemplate}. + * Not often used but a useful option for extensibility and testability (as it can be easily mocked or stubbed). + * + * @author Costin Leau + */ +public interface RedisOperations { + + /** + * Executes the given action within a Redis connection. + * + * Application exceptions thrown by the action object get propagated to the caller (can only be unchecked) whenever possible. + * Redis exceptions are transformed into appropriate DAO ones. + * Allows for returning a result object, that is a domain object or a collection of domain objects. + * Performs automatic serialization/deserialization for the given objects to and from binary data suitable for the Redis storage. + * + * Note: Callback code is not supposed to handle transactions itself! Use an appropriate transaction manager. + * Generally, callback code must not touch any Connection lifecycle methods, like close, to let the template do its work. + * + * @param return type + * @param action callback object that specifies the Redis action + * @return a result object returned by the action or null + */ + T execute(RedisCallback action); + + + /** + * Executes a Redis session. + * + * Allows multiple operations to be executed in the same session enabling 'transactional' capabilities through {@link #multi()} + * and {@link #watch(Collection)} operations. + * + * @param return type + * @param session session callback + * @return result object returned by the action or null + */ + T execute(SessionCallback session); + + // /** + // * Executes the given action object on a pipelined connection, returning the results. Note that the callback cannot + // * return a non-null value as it gets overwritten by the pipeline. + // * + // * @param list element return type + // * @param action callback object to execute + // * @return list of objects returned by the pipeline + // */ + // List executePipelined(RedisCallback action); + + + Boolean hasKey(K key); + + void delete(K key); + + void delete(Collection key); + + DataType type(K key); + + Set keys(K pattern); + + K randomKey(); + + void rename(K oldKey, K newKey); + + Boolean renameIfAbsent(K oldKey, K newKey); + + Boolean expire(K key, long timeout, TimeUnit unit); + + Boolean expireAt(K key, Date date); + + Boolean persist(K key); + + Boolean move(K key, int dbIndex); + + Long getExpire(K key); + + void watch(K keys); + + void watch(Collection keys); + + void unwatch(); + + /**' + * + */ + void multi(); + + void discard(); + + List exec(); + + // pubsub functionality on the template + void convertAndSend(String destination, Object message); + + + // operation types + /** + * Returns the operations performed on simple values (or Strings in Redis terminology). + * + * @return value operations + */ + ValueOperations opsForValue(); + + /** + * Returns the operations performed on simple values (or Strings in Redis terminology) + * bound to the given key. + * + * @param key Redis key + * @return value operations bound to the given key + */ + BoundValueOperations boundValueOps(K key); + + /** + * Returns the operations performed on list values. + * + * @return list operations + */ + ListOperations opsForList(); + + /** + * Returns the operations performed on list values bound to the given key. + * + * @param key Redis key + * @return list operations bound to the given key + */ + BoundListOperations boundListOps(K key); + + /** + * Returns the operations performed on set values. + * + * @return set operations + */ + SetOperations opsForSet(); + + /** + * Returns the operations performed on set values bound to the given key. + * + * @param key Redis key + * @return set operations bound to the given key + */ + BoundSetOperations boundSetOps(K key); + + /** + * Returns the operations performed on zset values (also known as sorted sets). + * + * @return zset operations + */ + ZSetOperations opsForZSet(); + + /** + * Returns the operations performed on zset values (also known as sorted sets) + * bound to the given key. + * + * @param key Redis key + * @return zset operations bound to the given key. + */ + BoundZSetOperations boundZSetOps(K key); + + /** + * Returns the operations performed on hash values. + * + * @param hash key (or field) type + * @param hash value type + * @return hash operations + */ + HashOperations opsForHash(); + + /** + * Returns the operations performed on hash values bound to the given key. + * + * @param hash key (or field) type + * @param hash value type + * @param key Redis key + * @return hash operations bound to the given key. + */ + BoundHashOperations boundHashOps(K key); + + + List sort(SortQuery query); + + List sort(SortQuery query, RedisSerializer resultSerializer); + + List sort(SortQuery query, BulkMapper bulkMapper); + + List sort(SortQuery query, BulkMapper bulkMapper, RedisSerializer resultSerializer); + + Long sort(SortQuery query, K storeKey); + + RedisSerializer getValueSerializer(); + + RedisSerializer getKeySerializer(); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisTemplate.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisTemplate.java new file mode 100644 index 000000000..cdf6f4364 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/RedisTemplate.java @@ -0,0 +1,794 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.core.query.QueryUtils; +import org.springframework.data.keyvalue.redis.core.query.SortQuery; +import org.springframework.data.keyvalue.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.SerializationUtils; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Helper class that simplifies Redis data access code. + *

+ * Performs automatic serialization/deserialization between the given objects and the underlying binary data in the Redis store. + * By default, it uses Java serialization for its objects (through {@link JdkSerializationRedisSerializer}). For String intensive + * operations consider the dedicated {@link StringRedisTemplate}. + *

+ * The central method is execute, supporting Redis access code implementing the {@link RedisCallback} interface. + * It provides {@link RedisConnection} handling such that neither the {@link RedisCallback} implementation nor + * the calling code needs to explicitly care about retrieving/closing Redis connections, or handling Connection + * lifecycle exceptions. For typical single step actions, there are various convenience methods. + *

+ * Once configured, this class is thread-safe. + * + *

Note that while the template is generified, it is up to the serializers/deserializers to properly convert the given Objects + * to and from binary data. + *

+ * This is the central class in Redis support. + * + * @author Costin Leau + * @param the Redis key type against which the template works (usually a String) + * @param the Redis value type against which the template works + * @see StringRedisTemplate + */ +public class RedisTemplate extends RedisAccessor implements RedisOperations { + + private boolean exposeConnection = false; + private RedisSerializer defaultSerializer = new JdkSerializationRedisSerializer(); + + private RedisSerializer keySerializer = null; + private RedisSerializer valueSerializer = null; + private RedisSerializer hashKeySerializer = null; + private RedisSerializer hashValueSerializer = null; + private RedisSerializer stringSerializer = new StringRedisSerializer(); + + // cache singleton objects (where possible) + private ValueOperations valueOps; + private ListOperations listOps; + private SetOperations setOps; + private ZSetOperations zSetOps; + + /** + * Constructs a new RedisTemplate instance. + * + */ + public RedisTemplate() { + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + boolean defaultUsed = false; + + if (keySerializer == null) { + keySerializer = defaultSerializer; + defaultUsed = true; + } + if (valueSerializer == null) { + valueSerializer = defaultSerializer; + defaultUsed = true; + } + + if (hashKeySerializer == null) { + hashKeySerializer = defaultSerializer; + defaultUsed = true; + } + + if (hashValueSerializer == null) { + hashValueSerializer = defaultSerializer; + defaultUsed = true; + } + + if (defaultUsed) { + Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized"); + } + } + + @Override + public T execute(RedisCallback action) { + return execute(action, isExposeConnection()); + } + + /** + * Executes the given action object within a connection, which can be exposed or not. + * + * @param return type + * @param action callback object that specifies the Redis action + * @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code + * @return object returned by the action + */ + public T execute(RedisCallback action, boolean exposeConnection) { + return execute(action, exposeConnection, false); + } + + /** + * Executes the given action object within a connection that can be exposed or not. Additionally, the connection + * can be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios). + * Use {@link #executePipelined(RedisCallback)} as an alternative. + * + * @param return type + * @param action callback object to execute + * @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code + * @param pipeline whether to pipeline or not the connection for the execution + * @return object returned by the action + */ + public T execute(RedisCallback action, boolean exposeConnection, boolean pipeline) { + Assert.notNull(action, "Callback object must not be null"); + + RedisConnectionFactory factory = getConnectionFactory(); + RedisConnection conn = RedisConnectionUtils.getConnection(factory); + + boolean existingConnection = TransactionSynchronizationManager.hasResource(factory); + preProcessConnection(conn, existingConnection); + + boolean pipelineStatus = conn.isPipelined(); + if (pipeline && !pipelineStatus) { + conn.openPipeline(); + } + + try { + RedisConnection connToExpose = (exposeConnection ? conn : createRedisConnectionProxy(conn)); + T result = action.doInRedis(connToExpose); + // TODO: any other connection processing? + return postProcessResult(result, conn, existingConnection); + } finally { + try { + if (pipeline && !pipelineStatus) { + conn.closePipeline(); + } + } finally { + RedisConnectionUtils.releaseConnection(conn, factory); + } + } + } + + + @Override + public T execute(SessionCallback session) { + RedisConnectionFactory factory = getConnectionFactory(); + // bind connection + RedisConnectionUtils.bindConnection(factory); + try { + return session.execute(this); + } finally { + RedisConnectionUtils.unbindConnection(factory); + } + } + + // @SuppressWarnings("unchecked") + // public List executePipelined(final RedisCallback action) { + // return executePipelined(action, valueSerializer); + // } + // + // /** + // * Executes the given action object on a pipelined connection, returning the results using a dedicated serializer. + // * Note that the callback cannot return a non-null value as it gets overwritten by the pipeline. + // * + // * @param action callback object to execute + // * @param resultSerializer + // * @return list of objects returned by the pipeline + // */ + // public List executePipelined(final RedisCallback action, final RedisSerializer resultSerializer) { + // return execute(new RedisCallback>() { + // public List doInRedis(RedisConnection connection) throws DataAccessException { + // connection.openPipeline(); + // boolean pipelinedClosed = false; + // try { + // Object result = action.doInRedis(connection); + // if (result != null) { + // throw new InvalidDataAccessApiUsageException( + // "Callback cannot returned a non-null value as it gets overwritten by the pipeline"); + // } + // List closePipeline = connection.closePipeline(); + // pipelinedClosed = true; + // //return SerializationUtils.deserialize(pipeline, resultSerializer); + // + // } finally { + // if (!pipelinedClosed) { + // connection.closePipeline(); + // } + // } + // } + // }); + // } + + protected RedisConnection createRedisConnectionProxy(RedisConnection pm) { + Class[] ifcs = ClassUtils.getAllInterfacesForClass(pm.getClass(), getClass().getClassLoader()); + return (RedisConnection) Proxy.newProxyInstance(pm.getClass().getClassLoader(), ifcs, + new CloseSuppressingInvocationHandler(pm)); + } + + /** + * Processes the connection (before any settings are executed on it). Default implementation returns the connection as is. + * + * @param connection redis connection + */ + protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) { + return connection; + } + + protected T postProcessResult(T result, RedisConnection conn, boolean existingConnection) { + return result; + } + + /** + * Returns whether to expose the native Redis connection to RedisCallback code, or rather a connection proxy (the default). + * + * @return whether to expose the native Redis connection or not + */ + public boolean isExposeConnection() { + return exposeConnection; + } + + /** + * Sets whether to expose the Redis connection to {@link RedisCallback} code. + * + * Default is "false": a proxy will be returned, suppressing quit and disconnect calls. + * + * @param exposeConnection + */ + public void setExposeConnection(boolean exposeConnection) { + this.exposeConnection = exposeConnection; + } + + /** + * Returns the default serializer used by this template. + * + * @return template default serializer + */ + public RedisSerializer getDefaultSerializer() { + return defaultSerializer; + } + + /** + * Sets the default serializer to use for this template. All serializers (expect the {@link #setStringSerializer(RedisSerializer)}) are + * initialized to this value unless explicitly set. Defaults to {@link JdkSerializationRedisSerializer}. + * + * @param serializer default serializer to use + */ + public void setDefaultSerializer(RedisSerializer serializer) { + this.defaultSerializer = serializer; + } + + /** + * Sets the key serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param serializer the key serializer to be used by this template. + */ + public void setKeySerializer(RedisSerializer serializer) { + this.keySerializer = serializer; + } + + /** + * Returns the key serializer used by this template. + * + * @return the key serializer used by this template. + */ + public RedisSerializer getKeySerializer() { + return keySerializer; + } + + /** + * Sets the value serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param serializer the value serializer to be used by this template. + */ + public void setValueSerializer(RedisSerializer serializer) { + this.valueSerializer = serializer; + } + + /** + * Returns the value serializer used by this template. + * + * @return the value serializer used by this template. + */ + public RedisSerializer getValueSerializer() { + return valueSerializer; + } + + /** + * Returns the hashKeySerializer. + * + * @return Returns the hashKeySerializer + */ + public RedisSerializer getHashKeySerializer() { + return hashKeySerializer; + } + + /** + * Sets the hash key (or field) serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param hashKeySerializer The hashKeySerializer to set. + */ + public void setHashKeySerializer(RedisSerializer hashKeySerializer) { + this.hashKeySerializer = hashKeySerializer; + } + + /** + * Returns the hashValueSerializer. + * + * @return Returns the hashValueSerializer + */ + public RedisSerializer getHashValueSerializer() { + return hashValueSerializer; + } + + /** + * Sets the hash value serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param hashValueSerializer The hashValueSerializer to set. + */ + public void setHashValueSerializer(RedisSerializer hashValueSerializer) { + this.hashValueSerializer = hashValueSerializer; + } + + /** + * Returns the stringSerializer. + * + * @return Returns the stringSerializer + */ + public RedisSerializer getStringSerializer() { + return stringSerializer; + } + + /** + * Sets the string value serializer to be used by this template (when the arguments or return types + * are always strings). Defaults to {@link StringRedisSerializer}. + * + * @see ValueOperations#get(Object, long, long) + * @param stringSerializer The stringValueSerializer to set. + */ + public void setStringSerializer(RedisSerializer stringSerializer) { + this.stringSerializer = stringSerializer; + } + + @SuppressWarnings("unchecked") + private byte[] rawKey(Object key) { + Assert.notNull(key, "non null key required"); + return keySerializer.serialize(key); + } + + private byte[] rawString(String key) { + return stringSerializer.serialize(key); + } + + @SuppressWarnings("unchecked") + private byte[] rawValue(Object value) { + return valueSerializer.serialize(value); + } + + private byte[][] rawKeys(Collection keys) { + final byte[][] rawKeys = new byte[keys.size()][]; + + int i = 0; + for (K key : keys) { + rawKeys[i++] = rawKey(key); + } + + return rawKeys; + } + + @SuppressWarnings("unchecked") + private K deserializeKey(byte[] value) { + return (K) keySerializer.deserialize(value); + } + + // + // RedisOperations + // + @Override + public List exec() { + return execute(new RedisCallback>() { + + @Override + public List doInRedis(RedisConnection connection) throws DataAccessException { + return connection.exec(); + } + }); + } + + @Override + public void delete(K key) { + final byte[] rawKey = rawKey(key); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.del(rawKey); + return null; + } + }, true); + } + + @Override + public void delete(Collection keys) { + final byte[][] rawKeys = rawKeys(keys); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.del(rawKeys); + return null; + } + }, true); + } + + @Override + public Boolean hasKey(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.exists(rawKey); + } + }, true); + } + + @Override + public Boolean expire(K key, long timeout, TimeUnit unit) { + final byte[] rawKey = rawKey(key); + final int rawTimeout = (int) unit.toSeconds(timeout); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.expire(rawKey, rawTimeout); + } + }, true); + } + + @Override + public Boolean expireAt(K key, Date date) { + final byte[] rawKey = rawKey(key); + final long rawTimeout = date.getTime(); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.expireAt(rawKey, rawTimeout); + } + }, true); + } + + @Override + public void convertAndSend(String channel, Object message) { + Assert.hasText(channel, "a non-empty channel is required"); + + final byte[] rawChannel = rawString(channel); + final byte[] rawMessage = rawValue(message); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.publish(rawChannel, rawMessage); + return null; + } + }, true); + } + + + // + // Value operations + // + + @Override + public Long getExpire(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) { + return Long.valueOf(connection.ttl(rawKey)); + } + }, true); + } + + @SuppressWarnings("unchecked") + @Override + public Set keys(K pattern) { + final byte[] rawKey = rawKey(pattern); + + Set rawKeys = execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) { + return connection.keys(rawKey); + } + }, true); + + return SerializationUtils.deserialize(rawKeys, keySerializer); + } + + @Override + public Boolean persist(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.persist(rawKey); + } + }, true); + } + + @Override + public Boolean move(K key, final int dbIndex) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.move(rawKey, dbIndex); + } + }, true); + } + + @Override + public K randomKey() { + byte[] rawKey = execute(new RedisCallback() { + @Override + public byte[] doInRedis(RedisConnection connection) { + return connection.randomKey(); + } + }, true); + + return deserializeKey(rawKey); + } + + @Override + public void rename(K oldKey, K newKey) { + final byte[] rawOldKey = rawKey(oldKey); + final byte[] rawNewKey = rawKey(newKey); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.rename(rawOldKey, rawNewKey); + return null; + } + }, true); + } + + @Override + public Boolean renameIfAbsent(K oldKey, K newKey) { + final byte[] rawOldKey = rawKey(oldKey); + final byte[] rawNewKey = rawKey(newKey); + + return execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) { + return connection.renameNX(rawOldKey, rawNewKey); + } + }, true); + } + + @Override + public DataType type(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + @Override + public DataType doInRedis(RedisConnection connection) { + return connection.type(rawKey); + } + }, true); + } + + @Override + public void multi() { + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + connection.multi(); + return null; + } + }, true); + } + + @Override + public void discard() { + execute(new RedisCallback() { + + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + connection.discard(); + return null; + } + }, true); + } + + @Override + public void watch(K key) { + final byte[] rawKey = rawKey(key); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.watch(rawKey); + return null; + } + }, true); + } + + @Override + public void watch(Collection keys) { + final byte[][] rawKeys = rawKeys(keys); + + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) { + connection.watch(rawKeys); + return null; + } + }, true); + } + + @Override + public void unwatch() { + execute(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + connection.unwatch(); + return null; + } + }, true); + } + + // Sort operations + + @SuppressWarnings("unchecked") + @Override + public List sort(SortQuery query) { + return sort(query, valueSerializer); + } + + @Override + public List sort(SortQuery query, RedisSerializer resultSerializer) { + final byte[] rawKey = rawKey(query.getKey()); + final SortParameters params = QueryUtils.convertQuery(query, stringSerializer); + + List vals = execute(new RedisCallback>() { + @Override + public List doInRedis(RedisConnection connection) throws DataAccessException { + return connection.sort(rawKey, params); + } + }, true); + + return SerializationUtils.deserialize(vals, resultSerializer); + } + + @SuppressWarnings("unchecked") + @Override + public List sort(SortQuery query, BulkMapper bulkMapper) { + return sort(query, bulkMapper, valueSerializer); + } + + @Override + public List sort(SortQuery query, BulkMapper bulkMapper, RedisSerializer resultSerializer) { + List values = sort(query, resultSerializer); + + int bulkSize = query.getGetPattern().size(); + List result = new ArrayList(values.size() / bulkSize + 1); + + List bulk = new ArrayList(bulkSize); + for (S s : values) { + + bulk.add(s); + if (bulk.size() == bulkSize) { + result.add(bulkMapper.mapBulk(Collections.unmodifiableList(bulk))); + // create a new list (we could reuse the old one but the client might hang on to it for some reason) + bulk = new ArrayList(bulkSize); + } + } + + return result; + } + + @Override + public Long sort(SortQuery query, K storeKey) { + final byte[] rawStoreKey = rawKey(storeKey); + final byte[] rawKey = rawKey(query.getKey()); + final SortParameters params = QueryUtils.convertQuery(query, stringSerializer); + + return execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) throws DataAccessException { + return connection.sort(rawKey, params, rawStoreKey); + } + }, true); + } + + @Override + public BoundValueOperations boundValueOps(K key) { + return new DefaultBoundValueOperations(key, this); + } + + @Override + public ValueOperations opsForValue() { + if (valueOps == null) { + valueOps = new DefaultValueOperations(this); + } + return valueOps; + } + + @Override + public ListOperations opsForList() { + if (listOps == null) { + listOps = new DefaultListOperations(this); + } + return listOps; + } + + @Override + public BoundListOperations boundListOps(K key) { + return new DefaultBoundListOperations(key, this); + } + + @Override + public BoundSetOperations boundSetOps(K key) { + return new DefaultBoundSetOperations(key, this); + } + + @Override + public SetOperations opsForSet() { + if (setOps == null) { + setOps = new DefaultSetOperations(this); + } + return setOps; + } + + @Override + public BoundZSetOperations boundZSetOps(K key) { + return new DefaultBoundZSetOperations(key, this); + } + + @Override + public ZSetOperations opsForZSet() { + if (zSetOps == null) { + zSetOps = new DefaultZSetOperations(this); + } + return zSetOps; + } + + @Override + public BoundHashOperations boundHashOps(K key) { + return new DefaultBoundHashOperations(key, this); + } + + @Override + public HashOperations opsForHash() { + return new DefaultHashOperations(this); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SessionCallback.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SessionCallback.java new file mode 100644 index 000000000..8af247965 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SessionCallback.java @@ -0,0 +1,35 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import org.springframework.dao.DataAccessException; + +/** + * Callback executing all operations against a surrogate 'session' (basically against the same underlying Redis connection). + * Allows 'transactions' to take place through the use of multi/discard/exec/watch/unwatch commands. + * + * @author Costin Leau + */ +public interface SessionCallback { + + /** + * Executes all the given operations inside the same session. + * + * @param operations Redis operations + * @return return value + */ + T execute(RedisOperations operations) throws DataAccessException; +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SetOperations.java new file mode 100644 index 000000000..a8145f30c --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SetOperations.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Set; + +/** + * Redis set specific operations. + * + * @author Costin Leau + */ +public interface SetOperations { + + Set difference(K key, K otherKey); + + Set difference(K key, Collection otherKeys); + + void differenceAndStore(K key, K otherKey, K destKey); + + void differenceAndStore(K key, Collection otherKeys, K destKey); + + Set intersect(K key, K otherKey); + + Set intersect(K key, Collection otherKeys); + + void intersectAndStore(K key, K otherKey, K destKey); + + void intersectAndStore(K key, Collection otherKeys, K destKey); + + Set union(K key, K otherKey); + + Set union(K key, Collection otherKeys); + + void unionAndStore(K key, K otherKey, K destKey); + + void unionAndStore(K key, Collection otherKeys, K destKey); + + Boolean add(K key, V value); + + Boolean isMember(K key, Object o); + + Set members(K key); + + Boolean move(K key, V value, K destKey); + + V randomMember(K key); + + Boolean remove(K key, Object o); + + V pop(K key); + + Long size(K key); + + RedisOperations getOperations(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SetOperationsEditor.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SetOperationsEditor.java new file mode 100644 index 000000000..2cc6704af --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/SetOperationsEditor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.beans.PropertyEditorSupport; + +/** + * PropertyEditor allowing for easy injection of {@link SetOperations} from + * {@link RedisOperations}. + * + * @author Costin Leau + */ +class SetOperationsEditor extends PropertyEditorSupport { + + @Override + public void setValue(Object value) { + if (value instanceof RedisOperations) { + super.setValue(((RedisOperations) value).opsForSet()); + } + else { + throw new java.lang.IllegalArgumentException("Editor supports only conversion of type " + + RedisOperations.class); + } + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/StringRedisTemplate.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/StringRedisTemplate.java new file mode 100644 index 000000000..29db41807 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/StringRedisTemplate.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import org.springframework.data.keyvalue.redis.connection.DefaultStringRedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.StringRedisConnection; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; + +/** + * String-focused extension of RedisTemplate. Since most operations against Redis are String based, + * this class provides a dedicated class that minimizes configuration of its more generic + * {@link RedisTemplate template} especially in terms of serializers. + * + *

Note that this template exposes the {@link RedisConnection} used by the {@link RedisCallback} + * as a {@link StringRedisConnection}. + * + * @author Costin Leau + */ +public class StringRedisTemplate extends RedisTemplate { + + /** + * Constructs a new StringRedisTemplate instance. + * {@link #setConnectionFactory(RedisConnectionFactory)} and {@link #afterPropertiesSet()} still need to be called. + * + */ + public StringRedisTemplate() { + RedisSerializer stringSerializer = new StringRedisSerializer(); + setKeySerializer(stringSerializer); + setValueSerializer(stringSerializer); + setHashKeySerializer(stringSerializer); + setHashValueSerializer(stringSerializer); + } + + /** + * Constructs a new StringRedisTemplate instance ready to be used. + * + * @param connectionFactory connection factory for creating new connections + */ + public StringRedisTemplate(RedisConnectionFactory connectionFactory) { + this(); + setConnectionFactory(connectionFactory); + afterPropertiesSet(); + } + + @Override + protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) { + return new DefaultStringRedisConnection(connection); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ValueOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ValueOperations.java new file mode 100644 index 000000000..133922952 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ValueOperations.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Redis operations for simple (or in Redis terminology 'string') values. + * + * @author Costin Leau + */ +public interface ValueOperations { + + void set(K key, V value); + + void set(K key, V value, long timeout, TimeUnit unit); + + Boolean setIfAbsent(K key, V value); + + void multiSet(Map m); + + void multiSetIfAbsent(Map m); + + V get(Object key); + + V getAndSet(K key, V value); + + List multiGet(Collection keys); + + Long increment(K key, long delta); + + Integer append(K key, String value); + + String get(K key, long start, long end); + + void set(K key, V value, long offset); + + Long size(K key); + + RedisOperations getOperations(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ValueOperationsEditor.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ValueOperationsEditor.java new file mode 100644 index 000000000..325683483 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ValueOperationsEditor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.beans.PropertyEditorSupport; + +/** + * PropertyEditor allowing for easy injection of {@link ValueOperations} from + * {@link RedisOperations}. + * + * @author Costin Leau + */ +class ValueOperationsEditor extends PropertyEditorSupport { + + @Override + public void setValue(Object value) { + if (value instanceof RedisOperations) { + super.setValue(((RedisOperations) value).opsForValue()); + } + else { + throw new java.lang.IllegalArgumentException("Editor supports only conversion of type " + + RedisOperations.class); + } + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ZSetOperations.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ZSetOperations.java new file mode 100644 index 000000000..87bf0784c --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ZSetOperations.java @@ -0,0 +1,83 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.core; + +import java.util.Collection; +import java.util.Set; + +/** + * Redis ZSet/sorted set specific operations. + * + * @author Costin Leau + */ +public interface ZSetOperations { + + /** + * Typed ZSet tuple. + */ + public interface TypedTuple { + V getValue(); + + Double getScore(); + } + + void intersectAndStore(K key, K otherKey, K destKey); + + void intersectAndStore(K key, Collection otherKeys, K destKey); + + void unionAndStore(K key, K otherKey, K destKey); + + void unionAndStore(K key, Collection otherKeys, K destKey); + + Set range(K key, long start, long end); + + Set reverseRange(K key, long start, long end); + + Set> rangeWithScores(K key, long start, long end); + + Set> reverseRangeWithScores(K key, long start, long end); + + Set rangeByScore(K key, double min, double max); + + Set reverseRangeByScore(K key, double min, double max); + + Set> rangeByScoreWithScores(K key, double min, double max); + + Set> reverseRangeByScoreWithScores(K key, double min, double max); + + Boolean add(K key, V value, double score); + + Double incrementScore(K key, V value, double delta); + + Long rank(K key, Object o); + + Long reverseRank(K key, Object o); + + Double score(K key, Object o); + + Boolean remove(K key, Object o); + + void removeRange(K key, long start, long end); + + void removeRangeByScore(K key, double min, double max); + + Long count(K key, double min, double max); + + Long size(K key); + + RedisOperations getOperations(); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ZSetOperationsEditor.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ZSetOperationsEditor.java new file mode 100644 index 000000000..902ba495e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/ZSetOperationsEditor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import java.beans.PropertyEditorSupport; + +/** + * PropertyEditor allowing for easy injection of {@link ZSetOperations} from + * {@link RedisOperations}. + * + * @author Costin Leau + */ +class ZSetOperationsEditor extends PropertyEditorSupport { + + @Override + public void setValue(Object value) { + if (value instanceof RedisOperations) { + super.setValue(((RedisOperations) value).opsForZSet()); + } + else { + throw new java.lang.IllegalArgumentException("Editor supports only conversion of type " + + RedisOperations.class); + } + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/package-info.java new file mode 100644 index 000000000..2b41724e0 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/package-info.java @@ -0,0 +1,7 @@ +/** + * Core package for integrating Redis with Spring concepts. + * + *

Provides template support and callback for low-level access. + */ +package org.springframework.data.keyvalue.redis.core; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/DefaultSortCriterion.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/DefaultSortCriterion.java new file mode 100644 index 000000000..242a7af6e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/DefaultSortCriterion.java @@ -0,0 +1,82 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core.query; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.keyvalue.redis.connection.SortParameters.Order; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Range; + +/** + * Default implementation for {@link SortCriterion}. + * + * @author Costin Leau + */ +class DefaultSortCriterion implements SortCriterion { + + private final K key; + private String by; + private final List getKeys = new ArrayList(4); + + private Range limit; + private Order order; + private Boolean alpha; + + DefaultSortCriterion(K key) { + this.key = key; + } + + @Override + public SortCriterion alphabetical(boolean alpha) { + this.alpha = Boolean.valueOf(alpha); + return this; + } + + @Override + public SortQuery build() { + return new DefaultSortQuery(key, by, limit, order, alpha, getKeys); + } + + @Override + public SortCriterion limit(long offset, long count) { + this.limit = new Range(offset, count); + return this; + } + + @Override + public SortCriterion limit(Range range) { + this.limit = range; + return this; + } + + @Override + public SortCriterion order(Order order) { + this.order = order; + return this; + } + + @Override + public SortCriterion get(String getPattern) { + this.getKeys.add(getPattern); + return this; + } + + SortCriterion addBy(String keyPattern) { + this.by = keyPattern; + return this; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/DefaultSortQuery.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/DefaultSortQuery.java new file mode 100644 index 000000000..4348e2fa2 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/DefaultSortQuery.java @@ -0,0 +1,83 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core.query; + +import java.util.List; + +import org.springframework.data.keyvalue.redis.connection.SortParameters.Order; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Range; + +/** + * Default SortQuery implementation. + * + * @author Costin Leau + */ +class DefaultSortQuery implements SortQuery { + + private final K key; + private final Boolean alpha; + private final Order order; + private final Range limit; + private final String by; + private final List gets; + + DefaultSortQuery(K key, String by, Range limit, Order order, Boolean alpha, List gets) { + this.key = key; + this.by = by; + this.limit = limit; + this.order = order; + this.alpha = alpha; + this.gets = gets; + } + + @Override + public String getBy() { + return by; + } + + @Override + public Range getLimit() { + return limit; + } + + @Override + public Order getOrder() { + return order; + } + + @Override + public Boolean isAlphabetic() { + return alpha; + } + + @Override + public K getKey() { + return key; + } + + @Override + public List getGetPattern() { + return gets; + } + + @Override + public String toString() { + return "DefaultSortQuery [alpha=" + alpha + ", by=" + by + ", gets=" + gets + ", key=" + key + ", limit=" + + limit + ", order=" + order + "]"; + } + + +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/QueryUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/QueryUtils.java new file mode 100644 index 000000000..a8b08ee42 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/QueryUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.keyvalue.redis.connection.DefaultSortParameters; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; + +/** + * Utilities for {@link SortQuery} implementations. + * + * @author Costin Leau + */ +public abstract class QueryUtils { + + public static SortParameters convertQuery(SortQuery query, RedisSerializer stringSerializer) { + + return new DefaultSortParameters(stringSerializer.serialize(query.getBy()), query.getLimit(), serialize( + query.getGetPattern(), stringSerializer), query.getOrder(), query.isAlphabetic()); + } + + private static byte[][] serialize(List strings, RedisSerializer stringSerializer) { + List raw = null; + + if (strings == null) { + raw = Collections.emptyList(); + } + else { + raw = new ArrayList(strings.size()); + for (String key : strings) { + raw.add(stringSerializer.serialize(key)); + } + } + return raw.toArray(new byte[raw.size()][]); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortCriterion.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortCriterion.java new file mode 100644 index 000000000..50929b04d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortCriterion.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core.query; + +import org.springframework.data.keyvalue.redis.connection.SortParameters.Order; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Range; + +/** + * Internal interface part of the Sort DSL. Exposes generic operations. + * + * @author Costin Leau + */ +public interface SortCriterion { + + SortCriterion limit(long offset, long count); + + SortCriterion limit(Range range); + + SortCriterion order(Order order); + + SortCriterion alphabetical(boolean alpha); + + SortCriterion get(String pattern); + + SortQuery build(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortQuery.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortQuery.java new file mode 100644 index 000000000..27643c962 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortQuery.java @@ -0,0 +1,78 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core.query; + +import java.util.List; + +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.SortParameters; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Order; +import org.springframework.data.keyvalue.redis.connection.SortParameters.Range; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; + +/** + * High-level abstraction over a Redis SORT (generified equivalent of {@link SortParameters}). To be used with {@link RedisTemplate} + * (just as {@link SortParameters} is used by {@link RedisConnection}). + * + * @author Costin Leau + */ +public interface SortQuery { + + /** + * Returns the sorting order. Can be null if nothing is specified. + * + * @return sorting order + */ + Order getOrder(); + + /** + * Indicates if the sorting is numeric (default) or alphabetical (lexicographical). + * Can be null if nothing is specified. + * + * @return the type of sorting + */ + Boolean isAlphabetic(); + + + /** + * Returns the sorting limit (range or pagination). + * Can be null if nothing is specified. + * + * @return sorting limit/range + */ + Range getLimit(); + + /** + * Return the target key for sorting. + * + * @return + */ + K getKey(); + + /** + * Returns the pattern of the external key used for sorting. + * + * @return + */ + String getBy(); + + /** + * Returns the external key(s) whose values are returned by the sort. + * + * @return + */ + List getGetPattern(); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortQueryBuilder.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortQueryBuilder.java new file mode 100644 index 000000000..588d80694 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/SortQueryBuilder.java @@ -0,0 +1,43 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core.query; + + +/** + * Simple builder class for constructing {@link SortQuery}. + * + * @author Costin Leau + */ +public class SortQueryBuilder extends DefaultSortCriterion { + + private static final String NO_SORT_KEY = "~"; + + private SortQueryBuilder(K key) { + super(key); + } + + public static SortQueryBuilder sort(K key) { + return new SortQueryBuilder(key); + } + + public SortCriterion by(String keyPattern) { + return addBy(keyPattern); + } + + public SortCriterion noSort() { + return by(NO_SORT_KEY); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/package-info.java new file mode 100644 index 000000000..3a0c87b28 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/core/query/package-info.java @@ -0,0 +1,5 @@ +/** + * Query package for Redis template. + */ +package org.springframework.data.keyvalue.redis.core.query; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/BeanUtilsHashMapper.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/BeanUtilsHashMapper.java new file mode 100644 index 000000000..1283eb26e --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/BeanUtilsHashMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.hash; + +import java.util.Map; + +import org.apache.commons.beanutils.BeanUtils; + +/** + * HashMapper based on Apache Commons BeanUtils project. Does NOT supports nested properties. + * + * @author Costin Leau + */ +public class BeanUtilsHashMapper implements HashMapper { + + private Class type; + + public BeanUtilsHashMapper(Class type) { + this.type = type; + } + + @Override + public T fromHash(Map hash) { + T instance = org.springframework.beans.BeanUtils.instantiate(type); + try { + BeanUtils.populate(instance, hash); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + return instance; + } + + @Override + public Map toHash(T object) { + try { + return BeanUtils.describe(object); + } catch (Exception ex) { + throw new IllegalArgumentException("Cannot describe object " + object); + } + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/DecoratingStringHashMapper.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/DecoratingStringHashMapper.java new file mode 100644 index 000000000..378203134 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/DecoratingStringHashMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.hash; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Delegating hash mapper used for flattening objects into Strings. + * Suitable when dealing with mappers that support Strings and type conversion. + * + * @author Costin Leau + */ +public class DecoratingStringHashMapper implements HashMapper { + + private final HashMapper delegate; + + public DecoratingStringHashMapper(HashMapper mapper) { + this.delegate = mapper; + } + + @SuppressWarnings("unchecked") + @Override + public T fromHash(Map hash) { + Map h = hash; + return delegate.fromHash(h); + } + + @Override + public Map toHash(T object) { + Map hash = delegate.toHash(object); + Map flatten = new LinkedHashMap(hash.size()); + for (Map.Entry entry : hash.entrySet()) { + flatten.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); + } + return flatten; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/HashMapper.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/HashMapper.java new file mode 100644 index 000000000..e1bafcbc9 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/HashMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.hash; + +import java.util.Map; + +/** + * Core mapping contract between Java types and Redis hashes/maps. + * It's up to the implementation to support nested objects. + * + * @author Costin Leau + */ +public interface HashMapper { + + Map toHash(T object); + + T fromHash(Map hash); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/JacksonHashMapper.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/JacksonHashMapper.java new file mode 100644 index 000000000..1f4d0d105 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/JacksonHashMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.hash; + +import java.util.Map; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.codehaus.jackson.type.JavaType; + +/** + * Mapper based on Jackson library. Supports nested properties (rich objects). + * + * @author Costin Leau + */ +public class JacksonHashMapper implements HashMapper { + + private final ObjectMapper mapper; + private final JavaType userType; + private final JavaType mapType = TypeFactory.mapType(Map.class, String.class, Object.class); + + public JacksonHashMapper(Class type) { + this(type, new ObjectMapper()); + } + + public JacksonHashMapper(Class type, ObjectMapper mapper) { + this.mapper = mapper; + this.userType = TypeFactory.type(type); + } + + @SuppressWarnings("unchecked") + @Override + public T fromHash(Map hash) { + return (T) mapper.convertValue(hash, userType); + } + + @Override + public Map toHash(T object) { + return mapper.convertValue(object, mapType); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/package-info.java new file mode 100644 index 000000000..3209d57ca --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/hash/package-info.java @@ -0,0 +1,7 @@ +/** + * Dedicated support package for Redis hashes. + * + * Provides mapping of objects to hashes/maps (and vice versa). + */ +package org.springframework.data.keyvalue.redis.hash; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/ChannelTopic.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/ChannelTopic.java new file mode 100644 index 000000000..17ebda41a --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/ChannelTopic.java @@ -0,0 +1,44 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener; + +/** + * Channel topic implementation (maps to a Redis channel). + * + * @author Costin Leau + */ +public class ChannelTopic implements Topic { + + private final String channelName; + + /** + * Constructs a new ChannelTopic instance. + * + * @param name + */ + public ChannelTopic(String name) { + this.channelName = name; + } + + /** + * Returns the topic name. + * + * @return topic name + */ + public String getTopic() { + return channelName; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/PatternTopic.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/PatternTopic.java new file mode 100644 index 000000000..f5bbcc9e7 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/PatternTopic.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener; + +/** + * Pattern topic (matching multiple channels). + * + * @author Costin Leau + */ +public class PatternTopic implements Topic { + + private final String channelPattern; + + public PatternTopic(String pattern) { + this.channelPattern = pattern; + } + + public String getTopic() { + return channelPattern; + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisMessageListenerContainer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisMessageListenerContainer.java new file mode 100644 index 000000000..0691b8363 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisMessageListenerContainer.java @@ -0,0 +1,742 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.keyvalue.redis.connection.Message; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.Subscription; +import org.springframework.data.keyvalue.redis.connection.util.ByteArrayWrapper; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ErrorHandler; + +/** + * Container providing asynchronous behaviour for Redis message listeners. + * Handles the low level details of listening, converting and message dispatching. + *

+ * As oppose to the low level Redis (one connection per subscription), the container + * uses only one connection that is 'multiplexed' for all registered listeners, + * the message dispatch being done through the task executor. + * + *

+ * Note the container uses the connection in a lazy fashion (the connection is used only if at least one listener is configured). + * + * @author Costin Leau + */ +public class RedisMessageListenerContainer implements InitializingBean, DisposableBean, BeanNameAware, SmartLifecycle { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + + /** + * Default thread name prefix: "RedisListeningContainer-". + */ + public static final String DEFAULT_THREAD_NAME_PREFIX = ClassUtils.getShortName(RedisMessageListenerContainer.class) + + "-"; + + private long initWait = TimeUnit.SECONDS.toMillis(5); + + private Executor subscriptionExecutor; + + private Executor taskExecutor; + + private RedisConnectionFactory connectionFactory; + + private String beanName; + + private ErrorHandler errorHandler; + + + private final Object monitor = new Object(); + // whether the container is running (or not) + private volatile boolean running = false; + // whether the container has been initialized + private volatile boolean initialized = false; + // whether the container uses a connection or not + // (as the container might be running but w/o listeners, it won't use any resources) + private volatile boolean listening = false; + + private volatile boolean manageExecutor = false; + + + // lookup maps + // to avoid creation of hashes for each message, the maps use raw byte arrays (wrapped to respect the equals/hashcode contract) + + // lookup map between patterns and listeners + private final Map> patternMapping = new ConcurrentHashMap>(); + // lookup map between channels and listeners + private final Map> channelMapping = new ConcurrentHashMap>(); + + private final SubscriptionTask subscriptionTask = new SubscriptionTask(); + + private volatile RedisSerializer serializer = new StringRedisSerializer(); + + + @Override + public void afterPropertiesSet() { + if (taskExecutor == null) { + manageExecutor = true; + taskExecutor = createDefaultTaskExecutor(); + } + + if (subscriptionExecutor == null) { + subscriptionExecutor = taskExecutor; + } + + initialized = true; + + start(); + } + + /** + * Creates a default TaskExecutor. Called if no explicit TaskExecutor has been specified. + *

The default implementation builds a {@link org.springframework.core.task.SimpleAsyncTaskExecutor} + * with the specified bean name (or the class name, if no bean name specified) as thread name prefix. + * @see org.springframework.core.task.SimpleAsyncTaskExecutor#SimpleAsyncTaskExecutor(String) + */ + protected TaskExecutor createDefaultTaskExecutor() { + String threadNamePrefix = (beanName != null ? beanName + "-" : DEFAULT_THREAD_NAME_PREFIX); + return new SimpleAsyncTaskExecutor(threadNamePrefix); + } + + @Override + public void destroy() throws Exception { + initialized = false; + + stop(); + + if (manageExecutor) { + if (taskExecutor instanceof DisposableBean) { + ((DisposableBean) taskExecutor).destroy(); + + if (logger.isDebugEnabled()) { + logger.debug("Stopped internally-managed task executor"); + } + } + } + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + stop(); + callback.run(); + } + + @Override + public int getPhase() { + // start the latest + return Integer.MAX_VALUE; + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public void start() { + if (!running) { + running = true; + // wait for the subscription to start before returning + // technically speaking we can only be notified right before the subscription starts + synchronized (monitor) { + lazyListen(); + try { + // wait up to 5 seconds + monitor.wait(initWait); + } catch (InterruptedException e) { + // stop waiting + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Started RedisMessageListenerContainer"); + } + } + } + + @Override + public void stop() { + if (isRunning()) { + running = false; + synchronized (monitor) { + subscriptionTask.cancel(); + if (listening) { + try { + monitor.wait(initWait); + } catch (InterruptedException ex) { + // stop waiting + } + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Stopped RedisMessageListenerContainer"); + } + } + + + /** + * Process a message received from the provider. + * + * @param message + * @param pattern + */ + protected void processMessage(MessageListener listener, Message message, byte[] pattern) { + executeListener(listener, message, pattern); + } + + + /** + * Execute the specified listener. + * + * @see #handleListenerException + */ + protected void executeListener(MessageListener listener, Message message, byte[] pattern) { + try { + listener.onMessage(message, pattern); + } catch (Throwable ex) { + handleListenerException(ex); + } + } + + /** + * Return whether this container is currently active, + * that is, whether it has been set up but not shut down yet. + */ + public final boolean isActive() { + return initialized; + } + + /** + * Handle the given exception that arose during listener execution. + *

The default implementation logs the exception at error level. + * This can be overridden in subclasses. + * @param ex the exception to handle + */ + protected void handleListenerException(Throwable ex) { + if (isActive()) { + // Regular case: failed while active. + // Invoke ErrorHandler if available. + invokeErrorHandler(ex); + } + else { + // Rare case: listener thread failed after container shutdown. + // Log at debug level, to avoid spamming the shutdown logger. + logger.debug("Listener exception after container shutdown", ex); + } + } + + /** + * Invoke the registered ErrorHandler, if any. Log at error level otherwise. + * @param ex the uncaught error that arose during message processing. + * @see #setErrorHandler + */ + protected void invokeErrorHandler(Throwable ex) { + if (this.errorHandler != null) { + this.errorHandler.handleError(ex); + } + else if (logger.isWarnEnabled()) { + logger.warn("Execution of JMS message listener failed, and no ErrorHandler has been set.", ex); + } + } + + /** + * Returns the connectionFactory. + * + * @return Returns the connectionFactory + */ + public RedisConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + /** + * @param connectionFactory The connectionFactory to set. + */ + public void setConnectionFactory(RedisConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + + /** + * Sets the task executor used for running the message listeners when messages are received. + * If no task executor is set, an instance of {@link SimpleAsyncTaskExecutor} will be used by default. + * The task executor can be adjusted depending on the work done by the listeners and the number of + * messages coming in. + * + * @param taskExecutor The taskExecutor to set. + */ + public void setTaskExecutor(Executor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * Sets the task execution used for subscribing to Redis channels. By default, if no executor is set, + * the {@link #setTaskExecutor(Executor)} will be used. In some cases, this might be undersired as + * the listening to the connection is a long running task. + * + *

Note: This implementation uses at most one long running thread (depending on whether there are any listeners registered or not) + * and up to two threads during the initial registration. + * + * @param subscriptionExecutor The subscriptionExecutor to set. + */ + public void setSubscriptionExecutor(Executor subscriptionExecutor) { + this.subscriptionExecutor = subscriptionExecutor; + } + + /** + * Sets the serializer for converting the {@link Topic}s into low-level channels and patterns. + * By default, {@link StringRedisSerializer} is used. + * + * @param serializer The serializer to set. + */ + public void setTopicSerializer(RedisSerializer serializer) { + this.serializer = serializer; + } + + /** + * Set an ErrorHandler to be invoked in case of any uncaught exceptions thrown + * while processing a Message. By default there will be no ErrorHandler + * so that error-level logging is the only result. + */ + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Attaches the given listeners (and their topics) to the container. + * + *

+ * Note: it's possible to call this method while the container is running forcing a reinitialization + * of the container. Note however that this might cause some messages to be lost (while the container + * reinitializes) - hence calling this method at runtime is considered advanced usage. + * + * @param listeners map of message listeners and their associated topics + */ + public void setMessageListeners(Map> listeners) { + initMapping(listeners); + } + + /** + * Adds a message listener to the (potentially running) container. If the container is running, + * the listener starts receiving (matching) messages as soon as possible. + * + * @param listener message listener + * @param topics message listener topic + */ + public void addMessageListener(MessageListener listener, Collection topics) { + addListener(listener, topics); + lazyListen(); + } + + /** + * Adds a message listener to the (potentially running) container. If the container is running, + * the listener starts receiving (matching) messages as soon as possible. + * + * @param listener message listener + * @param topic message topic + */ + public void addMessageListener(MessageListener listener, Topic topic) { + addMessageListener(listener, Collections.singleton(topic)); + } + + private void initMapping(Map> listeners) { + // stop the listener if currently running + if (isRunning()) { + stop(); + } + + patternMapping.clear(); + channelMapping.clear(); + + if (!CollectionUtils.isEmpty(listeners)) { + for (Map.Entry> entry : listeners.entrySet()) { + addListener(entry.getKey(), entry.getValue()); + } + } + + // resume activity + if (initialized) { + start(); + } + } + + /** + * Method inspecting whether listening for messages (and thus using a thread) is actually needed and triggering it. + */ + private void lazyListen() { + boolean debug = logger.isDebugEnabled(); + boolean started = false; + + if (isRunning()) { + if (!listening) { + synchronized (monitor) { + if (!listening) { + if (channelMapping.size() > 0 || patternMapping.size() > 0) { + subscriptionExecutor.execute(subscriptionTask); + listening = true; + started = true; + } + } + } + if (debug) { + if (started) { + logger.debug("Started listening for Redis messages"); + } + else { + logger.debug("Postpone listening for Redis messages until actual listeners are added"); + } + } + } + } + } + + private void addListener(MessageListener listener, Collection topics) { + List channels = new ArrayList(topics.size()); + List patterns = new ArrayList(topics.size()); + + boolean trace = logger.isTraceEnabled(); + + for (Topic topic : topics) { + + ByteArrayWrapper holder = new ByteArrayWrapper(serializer.serialize(topic.getTopic())); + + if (topic instanceof ChannelTopic) { + Collection collection = channelMapping.get(holder); + if (collection == null) { + collection = new CopyOnWriteArraySet(); + channelMapping.put(holder, collection); + } + collection.add(listener); + channels.add(holder.getArray()); + + if (trace) + logger.trace("Adding listener '" + listener + "' on channel '" + topic.getTopic() + "'"); + } + + else if (topic instanceof PatternTopic) { + Collection collection = patternMapping.get(holder); + if (collection == null) { + collection = new CopyOnWriteArraySet(); + patternMapping.put(holder, collection); + } + collection.add(listener); + patterns.add(holder.getArray()); + + if (trace) + logger.trace("Adding listener '" + listener + "' for pattern '" + topic.getTopic() + "'"); + } + + else { + throw new IllegalArgumentException("Unknown topic type '" + topic.getClass() + "'"); + } + } + + // check the current listening state + if (listening) { + subscriptionTask.subscribeChannel(channels.toArray(new byte[channels.size()][])); + subscriptionTask.subscribePattern(patterns.toArray(new byte[patterns.size()][])); + } + } + + + /** + * Runnable used for Redis subscription. Implemented as a dedicated class to provide as many hints + * as possible to the underlying thread pool. + * + * @author Costin Leau + */ + private class SubscriptionTask implements SchedulingAwareRunnable { + + /** + * Runnable used, on a parallel thread, to do the initial pSubscribe. + * This is required since, during initialization, both subscribe and pSubscribe + * might be needed but since the first call is blocking, the second call needs to + * executed in parallel. + * + * @author Costin Leau + */ + private class PatternSubscriptionTask implements SchedulingAwareRunnable { + + private long WAIT = 500; + private long ROUNDS = 3; + + @Override + public boolean isLongLived() { + return false; + } + + @Override + public void run() { + // wait for subscription to be initialized + boolean done = false; + // wait 3 rounds for subscription to be initialized + for (int i = 0; i < ROUNDS || done; i++) { + if (connection != null) { + synchronized (localMonitor) { + if (connection != null && connection.isSubscribed()) { + done = true; + connection.getSubscription().pSubscribe(unwrap(patternMapping.keySet())); + } + else { + try { + Thread.sleep(WAIT); + } catch (InterruptedException ex) { + done = true; + } + } + } + } + } + } + } + + private volatile RedisConnection connection; + private final Object localMonitor = new Object(); + + @Override + public boolean isLongLived() { + return true; + } + + @Override + public void run() { + connection = connectionFactory.getConnection(); + try { + if (connection.isSubscribed()) { + throw new IllegalStateException("Retrieved connection is already subscribed; aborting listening"); + } + + // NB: each Xsubscribe call blocks + + synchronized (monitor) { + monitor.notify(); + } + + // subscribe one way or the other + // and schedule the rest + if (!channelMapping.isEmpty()) { + // schedule the rest of the subscription + if (!patternMapping.isEmpty()) { + subscriptionExecutor.execute(new PatternSubscriptionTask()); + } + connection.subscribe(new DispatchMessageListener(), unwrap(channelMapping.keySet())); + } + else { + connection.pSubscribe(new DispatchMessageListener(), unwrap(patternMapping.keySet())); + } + + } finally { + // this block is executed once the subscription has ended + // meaning cleanup is required + + listening = false; + + if (connection != null) { + synchronized (localMonitor) { + if (connection != null) { + connection.close(); + connection = null; + } + } + } + + // done with the thread, app can be destroyed + synchronized (monitor) { + monitor.notify(); + } + + } + } + + private byte[][] unwrap(Collection holders) { + if (CollectionUtils.isEmpty(holders)) { + return new byte[0][]; + } + + byte[][] unwrapped = new byte[holders.size()][]; + + int index = 0; + for (ByteArrayWrapper arrayHolder : holders) { + unwrapped[index++] = arrayHolder.getArray(); + } + + return unwrapped; + } + + void cancel() { + if (connection != null) { + synchronized (localMonitor) { + if (connection != null) { + Subscription sub = connection.getSubscription(); + if (sub != null) { + sub.pUnsubscribe(); + sub.unsubscribe(); + } + } + } + } + } + + void subscribeChannel(byte[]... channels) { + if (channels != null && channels.length > 0) { + if (connection != null) { + synchronized (localMonitor) { + if (connection != null) { + Subscription sub = connection.getSubscription(); + if (sub != null) { + sub.subscribe(channels); + } + } + } + } + } + } + + void subscribePattern(byte[]... patterns) { + if (patterns != null && patterns.length > 0) { + if (connection != null) { + synchronized (localMonitor) { + if (connection != null) { + Subscription sub = connection.getSubscription(); + if (sub != null) { + sub.pSubscribe(patterns); + } + } + } + } + } + } + + void unsubscribeChannel(byte[]... channels) { + if (channels != null && channels.length > 0) { + if (connection != null) { + synchronized (localMonitor) { + if (connection != null) { + Subscription sub = connection.getSubscription(); + if (sub != null) { + sub.unsubscribe(channels); + } + } + } + } + } + } + + void unsubscribePattern(byte[]... patterns) { + if (patterns != null && patterns.length > 0) { + if (connection != null) { + synchronized (localMonitor) { + if (connection != null) { + Subscription sub = connection.getSubscription(); + if (sub != null) { + sub.pUnsubscribe(patterns); + } + } + } + } + } + } + } + + /** + * Actual message dispatcher/multiplexer. + * + * @author Costin Leau + */ + private class DispatchMessageListener implements MessageListener { + + @Override + public void onMessage(Message message, byte[] pattern) { + // do channel matching first + byte[] channel = message.getChannel(); + + Collection ch = channelMapping.get(new ByteArrayWrapper(channel)); + Collection pt = null; + + // followed by pattern matching + if (pattern != null && pattern.length > 0) { + pt = patternMapping.get(new ByteArrayWrapper(pattern)); + } + + if (!CollectionUtils.isEmpty(ch)) { + dispatchChannels(ch, message); + } + + if (!CollectionUtils.isEmpty(pt)) { + dispatchPatterns(pt, message, pattern); + } + } + + private void dispatchChannels(Collection ch, final Message message) { + for (final MessageListener messageListener : ch) { + taskExecutor.execute(new Runnable() { + @Override + public void run() { + processMessage(messageListener, message, null); + } + }); + } + } + + private void dispatchPatterns(Collection pt, final Message message, final byte[] pattern) { + for (final MessageListener messageListener : pt) { + taskExecutor.execute(new Runnable() { + @Override + public void run() { + processMessage(messageListener, message, pattern.clone()); + } + }); + } + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/Topic.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/Topic.java new file mode 100644 index 000000000..351257469 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/Topic.java @@ -0,0 +1,32 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener; + +/** + * Topic for a Redis message. Acts a high-level abstraction on top + * of Redis low-level channels or patterns. + * + * @author Costin Leau + */ +public interface Topic { + + /** + * Returns the topic (as a String). + * + * @return the topic + */ + String getTopic(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/MessageListenerAdapter.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/MessageListenerAdapter.java new file mode 100644 index 000000000..8def8aa52 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/MessageListenerAdapter.java @@ -0,0 +1,295 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener.adapter; + +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.keyvalue.redis.connection.Message; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; +import org.springframework.util.Assert; +import org.springframework.util.MethodInvoker; +import org.springframework.util.ObjectUtils; + +/** + * Message listener adapter that delegates the handling of messages to target + * listener methods via reflection, with flexible message type conversion. + * Allows listener methods to operate on message content types, completely + * independent from the Redis API. + * + *

Modeled as much as possible after the JMS MessageListenerAdapter in + * Spring Framework. + * + *

By default, the content of incoming Redis messages gets extracted before + * being passed into the target listener method, to let the target method + * operate on message content types such as String or byte array instead of + * the raw {@link Message}. Message type conversion is delegated to a Spring + * Data {@link RedisSerializer}. By default, the {@link JdkSerializationRedisSerializer} + * will be used. (If you do not want such automatic message conversion taking + * place, then be sure to set the {@link #setSerializer Serializer} + * to null.) + * + *

Find below some examples of method signatures compliant with this + * adapter class. This first example handles all Message types + * and gets passed the contents of each Message type as an + * argument. + * + *

public interface MessageContentsDelegate {
+ *    void handleMessage(String text);
+ *    void handleMessage(byte[] bytes);
+ *    void handleMessage(Person obj);
+ * }
+ * + * For further examples and discussion please do refer to the Spring Data + * reference documentation which describes this class (and its attendant + * configuration) in detail. + * + * Important: Due to the nature of messages, the default serializer used by + * the adapter is {@link StringRedisSerializer}. If the messages are of a different type, + * change them accordingly through {@link #setSerializer(RedisSerializer)}. + * + * @author Juergen Hoeller + * @author Costin Leau + * @see org.springframework.jms.listener.adapter.MessageListenerAdapter + */ +public class MessageListenerAdapter implements MessageListener { + + /** + * Out-of-the-box value for the default listener method: "handleMessage". + */ + public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage"; + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private Object delegate; + + private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD; + + private RedisSerializer serializer; + + + /** + * Create a new {@link MessageListenerAdapter} with default settings. + */ + public MessageListenerAdapter() { + initDefaultStrategies(); + this.delegate = this; + } + + /** + * Create a new {@link MessageListenerAdapter} for the given delegate. + * + * @param delegate the delegate object + */ + public MessageListenerAdapter(Object delegate) { + initDefaultStrategies(); + setDelegate(delegate); + } + + + /** + * Set a target object to delegate message listening to. + * Specified listener methods have to be present on this target object. + *

If no explicit delegate object has been specified, listener + * methods are expected to present on this adapter instance, that is, + * on a custom subclass of this adapter, defining listener methods. + * + * @param delegate delegate object + */ + public void setDelegate(Object delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + /** + * Returns the target object to delegate message listening to. + * + * @return message listening delegation + */ + public Object getDelegate() { + return this.delegate; + } + + /** + * Specify the name of the default listener method to delegate to, + * for the case where no specific listener method has been determined. + * Out-of-the-box value is {@link #ORIGINAL_DEFAULT_LISTENER_METHOD "handleMessage"}. + * @see #getListenerMethodName + */ + public void setDefaultListenerMethod(String defaultListenerMethod) { + this.defaultListenerMethod = defaultListenerMethod; + } + + /** + * Return the name of the default listener method to delegate to. + */ + protected String getDefaultListenerMethod() { + return this.defaultListenerMethod; + } + + /** + * Set the serializer that will convert incoming raw Redis messages to + * listener method arguments. + *

The default converter is a {@link StringRedisSerializer}. + */ + public void setSerializer(RedisSerializer serializer) { + this.serializer = serializer; + } + + /** + * Standard Redis {@link MessageListener} entry point. + *

Delegates the message to the target listener method, with appropriate + * conversion of the message argument. In case of an exception, the + * {@link #handleListenerException(Throwable)} method will be invoked. + * + * @param message the incoming Redis message + * @see #handleListenerException + */ + @Override + @SuppressWarnings("unchecked") + public void onMessage(Message message, byte[] pattern) { + try { + + // Check whether the delegate is a MessageListener impl itself. + // In that case, the adapter will simply act as a pass-through. + if (delegate != this) { + if (delegate instanceof MessageListener) { + ((MessageListener) delegate).onMessage(message, pattern); + } + } + + // Regular case: find a handler method reflectively. + Object convertedMessage = extractMessage(message); + String methodName = getListenerMethodName(message, convertedMessage); + if (methodName == null) { + throw new InvalidDataAccessApiUsageException("No default listener method specified: " + + "Either specify a non-null value for the 'defaultListenerMethod' property or " + + "override the 'getListenerMethodName' method."); + } + + // Invoke the handler method with appropriate arguments. + Object[] listenerArguments = buildListenerArguments(convertedMessage); + invokeListenerMethod(methodName, listenerArguments); + } catch (Throwable th) { + handleListenerException(th); + } + } + + /** + * Initialize the default implementations for the adapter's strategies. + * + * @see #setSerializer(RedisSerializer) + * @see JdkSerializationRedisSerializer + */ + protected void initDefaultStrategies() { + setSerializer(new StringRedisSerializer()); + } + + /** + * Handle the given exception that arose during listener execution. + * The default implementation logs the exception at error level. + * @param ex the exception to handle + */ + protected void handleListenerException(Throwable ex) { + logger.error("Listener execution failed", ex); + } + + /** + * Extract the message body from the given Redis message. + * @param message the Redis Message + * @return the content of the message, to be passed into the + * listener method as argument + */ + protected Object extractMessage(Message message) { + if (serializer != null) { + return serializer.deserialize(message.getBody()); + } + return message; + } + + /** + * Determine the name of the listener method that is supposed to + * handle the given message. + *

The default implementation simply returns the configured + * default listener method, if any. + * @param originalMessage the Redis request message + * @param extractedMessage the converted Redis request message, + * to be passed into the listener method as argument + * @return the name of the listener method (never null) + * @see #setDefaultListenerMethod + */ + protected String getListenerMethodName(Message originalMessage, Object extractedMessage) { + return getDefaultListenerMethod(); + } + + /** + * Build an array of arguments to be passed into the target listener method. + * Allows for multiple method arguments to be built from a single message object. + *

The default implementation builds an array with the given message object + * as sole element. This means that the extracted message will always be passed + * into a single method argument, even if it is an array, with the target + * method having a corresponding single argument of the array's type declared. + *

This can be overridden to treat special message content such as arrays + * differently, for example passing in each element of the message array + * as distinct method argument. + * @param extractedMessage the content of the message + * @return the array of arguments to be passed into the + * listener method (each element of the array corresponding + * to a distinct method argument) + */ + protected Object[] buildListenerArguments(Object extractedMessage) { + return new Object[] { extractedMessage }; + } + + /** + * Invoke the specified listener method. + * @param methodName the name of the listener method + * @param arguments the message arguments to be passed in + * @return the result returned from the listener method + * @see #getListenerMethodName + * @see #buildListenerArguments + */ + protected Object invokeListenerMethod(String methodName, Object[] arguments) { + try { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(getDelegate()); + methodInvoker.setTargetMethod(methodName); + methodInvoker.setArguments(arguments); + methodInvoker.prepare(); + return methodInvoker.invoke(); + } catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof DataAccessException) { + throw (DataAccessException) targetEx; + } + else { + throw new RedisListenerExecutionFailedException("Listener method '" + methodName + "' threw exception", + targetEx); + } + } catch (Throwable ex) { + throw new RedisListenerExecutionFailedException("Failed to invoke target method '" + methodName + + "' with arguments " + ObjectUtils.nullSafeToString(arguments), ex); + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/RedisListenerExecutionFailedException.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/RedisListenerExecutionFailedException.java new file mode 100644 index 000000000..8f94a7a95 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/RedisListenerExecutionFailedException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener.adapter; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Exception thrown when the execution of a listener method failed. + * + * @author Costin Leau + * @see MessageListenerAdapter + */ +public class RedisListenerExecutionFailedException extends InvalidDataAccessApiUsageException { + + /** + * Constructs a new RedisListenerExecutionFailedException instance. + * + * @param msg + * @param cause + */ + public RedisListenerExecutionFailedException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs a new RedisListenerExecutionFailedException instance. + * + * @param msg + */ + public RedisListenerExecutionFailedException(String msg) { + super(msg); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/package-info.java new file mode 100644 index 000000000..69f9d096a --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/adapter/package-info.java @@ -0,0 +1,7 @@ +/** + * Message listener adapter package. + * The adapter delegates to target listener methods, converting messages to appropriate message content types + * (such as String or byte array) that get passed into listener methods. + */ +package org.springframework.data.keyvalue.redis.listener.adapter; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/package-info.java new file mode 100644 index 000000000..a074feaa6 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/package-info.java @@ -0,0 +1,5 @@ +/** + * Base package for Redis message listener / pubsub container facility + */ +package org.springframework.data.keyvalue.redis.listener; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/package-info.java new file mode 100644 index 000000000..96920cf44 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/package-info.java @@ -0,0 +1,8 @@ +/** + * Root package for integrating Redis with Spring concepts. + *

+ * Provides Redis specific exception hierarchy on top of the {@code org.springframework.dao} package. + * + */ +package org.springframework.data.keyvalue.redis; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/GenericToStringSerializer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/GenericToStringSerializer.java new file mode 100644 index 000000000..b53387366 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/GenericToStringSerializer.java @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import java.nio.charset.Charset; + +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.util.Assert; + +/** + * Generic String to byte[] (and back) serializer. Relies on the Spring {@link ConversionService} + * to transform objects into String and vice versa. The Strings are convert into bytes and vice-versa + * using the specified charset (by default UTF-8). + * + * Note: The conversion service initialization happens automatically if the class is defined + * as a Spring bean. + * + * Note: Does not handle nulls in any special way delegating everything to the container. + * + * @author Costin Leau + */ +public class GenericToStringSerializer implements RedisSerializer, BeanFactoryAware { + + private final Charset charset; + private Converter converter = new Converter(ConversionServiceFactory.createDefaultConversionService()); + private Class type; + + public GenericToStringSerializer(Class type) { + this(type, Charset.forName("UTF8")); + } + + public GenericToStringSerializer(Class type, Charset charset) { + Assert.notNull(type); + this.type = type; + this.charset = charset; + } + + public void setConversionService(ConversionService conversionService) { + Assert.notNull(conversionService, "non null conversion service required"); + converter = new Converter(conversionService); + } + + public void setTypeConverter(TypeConverter typeConverter) { + Assert.notNull(typeConverter, "non null type converter required"); + converter = new Converter(typeConverter); + } + + @Override + public T deserialize(byte[] bytes) { + if (bytes == null) { + return null; + } + + String string = new String(bytes, charset); + return converter.convert(string, type); + } + + @Override + public byte[] serialize(T object) { + if (object == null) { + return null; + } + String string = converter.convert(object, String.class); + return string.getBytes(charset); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (converter == null && beanFactory instanceof ConfigurableBeanFactory) { + ConfigurableBeanFactory cFB = (ConfigurableBeanFactory) beanFactory; + ConversionService conversionService = cFB.getConversionService(); + + converter = (conversionService != null ? new Converter(conversionService) : new Converter( + cFB.getTypeConverter())); + } + } + + private class Converter { + private final ConversionService conversionService; + private final TypeConverter typeConverter; + + public Converter(ConversionService conversionService) { + this.conversionService = conversionService; + this.typeConverter = null; + } + + public Converter(TypeConverter typeConverter) { + this.conversionService = null; + this.typeConverter = typeConverter; + } + + T convert(Object value, Class targetType) { + if (conversionService != null) { + return conversionService.convert(value, targetType); + } + return typeConverter.convertIfNecessary(value, targetType); + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/JacksonJsonRedisSerializer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/JacksonJsonRedisSerializer.java new file mode 100644 index 000000000..c858cfcb2 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/JacksonJsonRedisSerializer.java @@ -0,0 +1,105 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import java.nio.charset.Charset; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.codehaus.jackson.type.JavaType; +import org.springframework.util.Assert; + +/** + * {@link RedisSerializer} that can read and write JSON using Jackson's {@link ObjectMapper}. + * + *

This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. + * + * Note:Null objects are serialized as empty arrays and vice versa. + * + * @author Costin Leau + */ +public class JacksonJsonRedisSerializer implements RedisSerializer { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private final JavaType javaType; + + private ObjectMapper objectMapper = new ObjectMapper(); + + public JacksonJsonRedisSerializer(Class type) { + this.javaType = TypeFactory.type(type); + } + + @SuppressWarnings("unchecked") + @Override + public T deserialize(byte[] bytes) throws SerializationException { + if (SerializationUtils.isEmpty(bytes)) { + return null; + } + try { + return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType); + } catch (Exception ex) { + throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); + } + } + + @Override + public byte[] serialize(Object t) throws SerializationException { + if (t == null) { + return SerializationUtils.EMPTY_ARRAY; + } + try { + return this.objectMapper.writeValueAsBytes(t); + } catch (Exception ex) { + throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex); + } + } + + /** + * Sets the {@code ObjectMapper} for this view. If not set, a default + * {@link ObjectMapper#ObjectMapper() ObjectMapper} is used. + *

Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON serialization + * process. For example, an extended {@link org.codehaus.jackson.map.SerializerFactory} can be configured that provides + * custom serializers for specific types. The other option for refining the serialization process is to use Jackson's + * provided annotations on the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary. + */ + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "'objectMapper' must not be null"); + this.objectMapper = objectMapper; + } + + /** + * Returns the Jackson {@link JavaType} for the specific class. + * + *

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

+	 * protected JavaType getJavaType(Class<?> clazz) {
+	 *   if (List.class.isAssignableFrom(clazz)) {
+	 *     return TypeFactory.collectionType(ArrayList.class, MyBean.class);
+	 *   } else {
+	 *     return super.getJavaType(clazz);
+	 *   }
+	 * }
+	 * 
+ * + * @param clazz the class to return the java type for + * @return the java type + */ + protected JavaType getJavaType(Class clazz) { + return TypeFactory.type(clazz); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/JdkSerializationRedisSerializer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/JdkSerializationRedisSerializer.java new file mode 100644 index 000000000..fe6de7886 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/JdkSerializationRedisSerializer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.serializer.support.DeserializingConverter; +import org.springframework.core.serializer.support.SerializingConverter; + +/** + * Java Serialization Redis serializer. + * Delegates to the default (Java based) serializer in Spring 3. + * + * @author Mark Pollack + * @author Costin Leau + */ +public class JdkSerializationRedisSerializer implements RedisSerializer { + + private Converter serializer = new SerializingConverter(); + private Converter deserializer = new DeserializingConverter(); + + @SuppressWarnings("unchecked") + @Override + public Object deserialize(byte[] bytes) { + if (SerializationUtils.isEmpty(bytes)) { + return null; + } + + try { + return deserializer.convert(bytes); + } catch (Exception ex) { + throw new SerializationException("Cannot deserialize", ex); + } + } + + @Override + public byte[] serialize(Object object) { + if (object == null) { + return SerializationUtils.EMPTY_ARRAY; + } + try { + return serializer.convert(object); + } catch (Exception ex) { + throw new SerializationException("Cannot serialize", ex); + } + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/OxmSerializer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/OxmSerializer.java new file mode 100644 index 000000000..b1a2354f8 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/OxmSerializer.java @@ -0,0 +1,102 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; +import org.springframework.util.Assert; + +/** + * Serializer adapter on top of Spring's O/X Mapping. + * Delegates serialization/deserialization to OXM {@link Marshaller} and + * {@link Unmarshaller}. + * + * Note:Null objects are serialized as empty arrays and vice versa. + * + * @author Costin Leau + */ +public class OxmSerializer implements InitializingBean, RedisSerializer { + + private Marshaller marshaller; + private Unmarshaller unmarshaller; + + public OxmSerializer() { + } + + public OxmSerializer(Marshaller marshaller, Unmarshaller unmarshaller) { + this.marshaller = marshaller; + this.unmarshaller = unmarshaller; + + afterPropertiesSet(); + } + + @Override + public void afterPropertiesSet() { + Assert.notNull(marshaller, "non-null marshaller required"); + Assert.notNull(unmarshaller, "non-null unmarshaller required"); + } + + /** + * @param marshaller The marshaller to set. + */ + public void setMarshaller(Marshaller marshaller) { + this.marshaller = marshaller; + } + + /** + * @param unmarshaller The unmarshaller to set. + */ + public void setUnmarshaller(Unmarshaller unmarshaller) { + this.unmarshaller = unmarshaller; + } + + @Override + public Object deserialize(byte[] bytes) throws SerializationException { + if (SerializationUtils.isEmpty(bytes)) { + return null; + } + + try { + return unmarshaller.unmarshal(new StreamSource(new ByteArrayInputStream(bytes))); + } catch (Exception ex) { + throw new SerializationException("Cannot deserialize bytes", ex); + } + } + + @Override + public byte[] serialize(Object t) throws SerializationException { + if (t == null) { + return SerializationUtils.EMPTY_ARRAY; + } + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + StreamResult result = new StreamResult(stream); + + try { + marshaller.marshal(t, result); + } catch (Exception ex) { + throw new SerializationException("Cannot serialize object", ex); + } + return stream.toByteArray(); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/RedisSerializer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/RedisSerializer.java new file mode 100644 index 000000000..18543c579 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/RedisSerializer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +/** + * Basic interface serialization and deserialization of Objects to byte arrays (binary data). + * + * It is recommended that implementations are designed to handle null objects/empty arrays on serialization and deserialization side. + * Note that Redis does not accept null keys or values but can return null replies (for non existing keys). + * + * @author Mark Pollack + * @author Costin Leau + */ +public interface RedisSerializer { + + /** + * Serialize the given object to binary data. + * + * @param t object to serialize + * @return the equivalent binary data + */ + byte[] serialize(T t) throws SerializationException; + + /** + * Deserialize an object from the given binary data. + * + * @param bytes object binary representation + * @return the equivalent object instance + */ + T deserialize(byte[] bytes) throws SerializationException; +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/SerializationException.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/SerializationException.java new file mode 100644 index 000000000..215b8ece1 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/SerializationException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import org.springframework.core.NestedRuntimeException; + +/** + * Generic exception indicating a serialization/deserialization error. + * + * @author Costin Leau + */ +public class SerializationException extends NestedRuntimeException { + + /** + * Constructs a new SerializationException instance. + * + * @param msg + * @param cause + */ + public SerializationException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs a new SerializationException instance. + * + * @param msg + */ + public SerializationException(String msg) { + super(msg); + } +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/SerializationUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/SerializationUtils.java new file mode 100644 index 000000000..fab0120d8 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/SerializationUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Utility class with various serialization-related methods. + * + * @author Costin Leau + */ +public abstract class SerializationUtils { + + static final byte[] EMPTY_ARRAY = new byte[0]; + + static boolean isEmpty(byte[] data) { + return (data == null || data.length == 0); + } + + + @SuppressWarnings("unchecked") + static > T deserializeValues(Collection rawValues, Class type, RedisSerializer redisSerializer) { + // connection in pipeline/multi mode + if (rawValues == null) { + return null; + } + + Collection values = (List.class.isAssignableFrom(type) ? new ArrayList(rawValues.size()) + : new LinkedHashSet(rawValues.size())); + for (byte[] bs : rawValues) { + values.add(redisSerializer.deserialize(bs)); + } + + return (T) values; + } + + @SuppressWarnings("unchecked") + public static Set deserialize(Set rawValues, RedisSerializer redisSerializer) { + return deserializeValues(rawValues, Set.class, redisSerializer); + } + + @SuppressWarnings("unchecked") + public static List deserialize(List rawValues, RedisSerializer redisSerializer) { + return deserializeValues(rawValues, List.class, redisSerializer); + } + + @SuppressWarnings("unchecked") + public static Collection deserialize(Collection rawValues, RedisSerializer redisSerializer) { + return deserializeValues(rawValues, List.class, redisSerializer); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/StringRedisSerializer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/StringRedisSerializer.java new file mode 100644 index 000000000..d0b361ba1 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/StringRedisSerializer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import java.nio.charset.Charset; + +import org.springframework.util.Assert; + +/** + * Simple String to byte[] (and back) serializer. Converts Strings into bytes and vice-versa + * using the specified charset (by default UTF-8). + *

+ * Useful when the interaction with the Redis happens mainly through Strings. + * + *

Does not perform any null conversion since empty strings are valid keys/values. + * + * @author Costin Leau + */ +public class StringRedisSerializer implements RedisSerializer { + + private final Charset charset; + + public StringRedisSerializer() { + this(Charset.forName("UTF8")); + } + + public StringRedisSerializer(Charset charset) { + Assert.notNull(charset); + this.charset = charset; + } + + @Override + public String deserialize(byte[] bytes) { + return (bytes == null ? null : new String(bytes, charset)); + } + + @Override + public byte[] serialize(String string) { + return (string == null ? null : string.getBytes(charset)); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/package-info.java new file mode 100644 index 000000000..244b70323 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/serializer/package-info.java @@ -0,0 +1,5 @@ +/** + * Serialization/Deserialization package for converting Object to (and from) binary data. + */ +package org.springframework.data.keyvalue.redis.serializer; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicInteger.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicInteger.java new file mode 100644 index 000000000..ca33798ba --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicInteger.java @@ -0,0 +1,263 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.atomic; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.data.keyvalue.redis.core.BoundKeyOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.core.SessionCallback; +import org.springframework.data.keyvalue.redis.core.ValueOperations; +import org.springframework.data.keyvalue.redis.serializer.GenericToStringSerializer; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; + +/** + * Atomic integer backed by Redis. + * Uses Redis atomic increment/decrement and watch/multi/exec operations for CAS operations. + * + * @see java.util.concurrent.atomic.AtomicInteger + * @author Costin Leau + */ +public class RedisAtomicInteger extends Number implements Serializable, BoundKeyOperations { + + private volatile String key; + private ValueOperations operations; + private RedisOperations generalOps; + + + /** + * Constructs a new RedisAtomicInteger instance. + * + * @param redisCounter redis counter + * @param factory connection factory + */ + public RedisAtomicInteger(String redisCounter, RedisConnectionFactory factory) { + this(redisCounter, factory, null); + } + + /** + * Constructs a new RedisAtomicInteger instance. + * + * @param redisCounter + * @param factory + * @param initialValue + */ + public RedisAtomicInteger(String redisCounter, RedisConnectionFactory factory, int initialValue) { + this(redisCounter, factory, Integer.valueOf(initialValue)); + } + + private RedisAtomicInteger(String redisCounter, RedisConnectionFactory factory, Integer initialValue) { + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericToStringSerializer(Integer.class)); + redisTemplate.setExposeConnection(true); + redisTemplate.setConnectionFactory(factory); + redisTemplate.afterPropertiesSet(); + + this.key = redisCounter; + this.generalOps = redisTemplate; + this.operations = generalOps.opsForValue(); + + if (initialValue == null) { + if (this.operations.get(redisCounter) == null) { + set(0); + } + } + else { + set(initialValue); + } + } + + /** + * Get the current value. + * + * @return the current value + */ + public int get() { + return Integer.valueOf(operations.get(key)); + } + + /** + * Set to the given value. + * + * @param newValue the new value + */ + public void set(int newValue) { + operations.set(key, newValue); + } + + /** + * Set to the give value and return the old value. + * + * @param newValue the new value + * @return the previous value + */ + public int getAndSet(int newValue) { + return operations.getAndSet(key, newValue); + } + + /** + * Atomically set the value to the given updated value + * if the current value == the expected value. + * @param expect the expected value + * @param update the new value + * @return true if successful. False return indicates that + * the actual value was not equal to the expected value. + */ + public boolean compareAndSet(final int expect, final int update) { + return generalOps.execute(new SessionCallback() { + + @SuppressWarnings("unchecked") + @Override + public Boolean execute(RedisOperations operations) { + for (;;) { + operations.watch(Collections.singleton(key)); + if (expect == get()) { + generalOps.multi(); + set(update); + if (operations.exec() != null) { + return true; + } + } + { + return false; + } + } + } + }); + } + + /** + * Atomically increment by one the current value. + * + * @return the previous value + */ + public int getAndIncrement() { + return incrementAndGet() - 1; + } + + + /** + * Atomically decrement by one the current value. + * @return the previous value + */ + public int getAndDecrement() { + return decrementAndGet() + 1; + } + + + /** + * Atomically add the given value to current value. + * @param delta the value to add + * @return the previous value + */ + public int getAndAdd(final int delta) { + return addAndGet(delta) - delta; + } + + /** + * Atomically increment by one the current value. + * @return the updated value + */ + public int incrementAndGet() { + return operations.increment(key, 1).intValue(); + } + + /** + * Atomically decrement by one the current value. + * @return the updated value + */ + public int decrementAndGet() { + return operations.increment(key, -1).intValue(); + } + + + /** + * Atomically add the given value to current value. + * @param delta the value to add + * @return the updated value + */ + public int addAndGet(int delta) { + return operations.increment(key, delta).intValue(); + } + + /** + * Returns the String representation of the current value. + * @return the String representation of the current value. + */ + public String toString() { + return Integer.toString(get()); + } + + + public int intValue() { + return get(); + } + + public long longValue() { + return (long) get(); + } + + public float floatValue() { + return (float) get(); + } + + public double doubleValue() { + return (double) get(); + } + + @Override + public String getKey() { + return key; + } + + @Override + public Boolean expire(long timeout, TimeUnit unit) { + return generalOps.expire(key, timeout, unit); + } + + @Override + public Boolean expireAt(Date date) { + return generalOps.expireAt(key, date); + } + + @Override + public Long getExpire() { + return generalOps.getExpire(key); + } + + @Override + public Boolean persist() { + return generalOps.persist(key); + } + + @Override + public void rename(String newKey) { + generalOps.rename(key, newKey); + key = newKey; + } + + @Override + public DataType getType() { + return DataType.STRING; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicLong.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicLong.java new file mode 100644 index 000000000..36502c489 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicLong.java @@ -0,0 +1,267 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.atomic; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; +import org.springframework.data.keyvalue.redis.core.BoundKeyOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.core.SessionCallback; +import org.springframework.data.keyvalue.redis.core.ValueOperations; +import org.springframework.data.keyvalue.redis.serializer.GenericToStringSerializer; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; + +/** + * Atomic long backed by Redis. + * Uses Redis atomic increment/decrement and watch/multi/exec operations for CAS operations. + * + * @see java.util.concurrent.atomic.AtomicLong + * @author Costin Leau + */ +public class RedisAtomicLong extends Number implements Serializable, BoundKeyOperations { + + private volatile String key; + private ValueOperations operations; + private RedisOperations generalOps; + + + /** + * Constructs a new RedisAtomicLong instance. + * + * @param redisCounter redis counter + * @param factory connection factory + */ + public RedisAtomicLong(String redisCounter, RedisConnectionFactory factory) { + this(redisCounter, factory, null); + } + + /** + * Constructs a new RedisAtomicLong instance. + * + * @param redisCounter + * @param factory + * @param initialValue + */ + public RedisAtomicLong(String redisCounter, RedisConnectionFactory factory, long initialValue) { + this(redisCounter, factory, Long.valueOf(initialValue)); + } + + private RedisAtomicLong(String redisCounter, RedisConnectionFactory factory, Long initialValue) { + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericToStringSerializer(Long.class)); + redisTemplate.setExposeConnection(true); + redisTemplate.setConnectionFactory(factory); + redisTemplate.afterPropertiesSet(); + + this.key = redisCounter; + this.generalOps = redisTemplate; + this.operations = generalOps.opsForValue(); + + if (initialValue == null) { + if (this.operations.get(redisCounter) == null) { + set(0); + } + } + else { + set(initialValue); + } + } + + /** + * Gets the current value. + * + * @return the current value + */ + public long get() { + return operations.get(key); + } + + /** + * Sets to the given value. + * + * @param newValue the new value + */ + public void set(long newValue) { + operations.set(key, newValue); + } + + /** + * Atomically sets to the given value and returns the old value. + * + * @param newValue the new value + * @return the previous value + */ + public long getAndSet(long newValue) { + return operations.getAndSet(key, newValue); + } + + /** + * Atomically sets the value to the given updated value + * if the current value {@code ==} the expected value. + * + * @param expect the expected value + * @param update the new value + * @return true if successful. False return indicates that + * the actual value was not equal to the expected value. + */ + public boolean compareAndSet(final long expect, final long update) { + return generalOps.execute(new SessionCallback() { + + @SuppressWarnings("unchecked") + @Override + public Boolean execute(RedisOperations operations) { + for (;;) { + operations.watch(Collections.singleton(key)); + if (expect == get()) { + generalOps.multi(); + set(update); + if (operations.exec() != null) { + return true; + } + } + { + return false; + } + } + } + }); + } + + /** + * Atomically increments by one the current value. + * + * @return the previous value + */ + public long getAndIncrement() { + return incrementAndGet() - 1; + } + + /** + * Atomically decrements by one the current value. + * + * @return the previous value + */ + public long getAndDecrement() { + return decrementAndGet() + 1; + } + + /** + * Atomically adds the given value to the current value. + * + * @param delta the value to add + * @return the previous value + */ + public long getAndAdd(final long delta) { + return addAndGet(delta) - delta; + } + + /** + * Atomically increments by one the current value. + * + * @return the updated value + */ + public long incrementAndGet() { + return operations.increment(key, 1); + } + + /** + * Atomically decrements by one the current value. + * + * @return the updated value + */ + public long decrementAndGet() { + return operations.increment(key, -1); + } + + /** + * Atomically adds the given value to the current value. + * + * @param delta the value to add + * @return the updated value + */ + public long addAndGet(long delta) { + return operations.increment(key, delta); + } + + /** + * Returns the String representation of the current value. + * + * @return the String representation of the current value. + */ + public String toString() { + return Long.toString(get()); + } + + + public int intValue() { + return (int) get(); + } + + public long longValue() { + return get(); + } + + public float floatValue() { + return (float) get(); + } + + public double doubleValue() { + return (double) get(); + } + + @Override + public String getKey() { + return key; + } + + @Override + public Boolean expire(long timeout, TimeUnit unit) { + return generalOps.expire(key, timeout, unit); + } + + @Override + public Boolean expireAt(Date date) { + return generalOps.expireAt(key, date); + } + + @Override + public Long getExpire() { + return generalOps.getExpire(key); + } + + @Override + public Boolean persist() { + return generalOps.persist(key); + } + + @Override + public void rename(String newKey) { + generalOps.rename(key, newKey); + key = newKey; + } + + @Override + public DataType getType() { + return DataType.STRING; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/package-info.java new file mode 100644 index 000000000..82425f787 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/atomic/package-info.java @@ -0,0 +1,4 @@ +/** + * Small toolkit mirroring the {@code java.util.atomic} package in Redis. + */ +package org.springframework.data.keyvalue.redis.support.atomic; diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisCollection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisCollection.java new file mode 100644 index 000000000..cb7c0c5df --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisCollection.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.core.RedisOperations; + +/** + * Base implementation for {@link RedisCollection}. + * Provides a skeletal implementation. + * + * @author Costin Leau + */ +public abstract class AbstractRedisCollection extends AbstractCollection implements RedisCollection { + + public static final String ENCODING = "UTF-8"; + + private volatile String key; + private final RedisOperations operations; + + public AbstractRedisCollection(String key, RedisOperations operations) { + this.key = key; + this.operations = operations; + } + + @Override + public String getKey() { + return key; + } + + @Override + public RedisOperations getOperations() { + return operations; + } + + @Override + public boolean addAll(Collection c) { + boolean modified = false; + for (E e : c) { + modified |= add(e); + } + return modified; + } + + public abstract boolean add(E e); + + public abstract void clear(); + + @Override + public boolean containsAll(Collection c) { + boolean contains = true; + for (Object object : c) { + contains &= contains(object); + } + return contains; + } + + public abstract boolean remove(Object o); + + + @Override + public boolean removeAll(Collection c) { + boolean modified = false; + for (Object object : c) { + modified |= remove(object); + } + return modified; + } + + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (o instanceof RedisStore) { + return key.equals(((RedisStore) o).getKey()); + } + if (o instanceof AbstractRedisCollection) { + return o.hashCode() == hashCode(); + } + + return false; + } + + @Override + public int hashCode() { + int result = 17 + getClass().hashCode(); + result = result * 31 + key.hashCode(); + return result; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("RedisStore for key:"); + sb.append(getKey()); + return sb.toString(); + } + + @Override + public Boolean expire(long timeout, TimeUnit unit) { + return operations.expire(key, timeout, unit); + } + + @Override + public Boolean expireAt(Date date) { + return operations.expireAt(key, date); + } + + @Override + public Long getExpire() { + return operations.getExpire(key); + } + + @Override + public Boolean persist() { + return operations.persist(key); + } + + @Override + public void rename(final String newKey) { + CollectionUtils.rename(key, newKey, operations); + key = newKey; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/CollectionUtils.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/CollectionUtils.java new file mode 100644 index 000000000..1e4ee5fea --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/CollectionUtils.java @@ -0,0 +1,107 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.core.RedisOperations; +import org.springframework.data.keyvalue.redis.core.SessionCallback; + +/** + * Utility class used mainly for type conversion by the default collection implementations. + * Meant for internal use. + * + * @author Costin Leau + */ +abstract class CollectionUtils { + + @SuppressWarnings("unchecked") + static Collection reverse(Collection c) { + Object[] reverse = new Object[c.size()]; + int index = c.size(); + for (E e : c) { + reverse[--index] = e; + } + + return (List) Arrays.asList(reverse); + } + + static Collection extractKeys(Collection stores) { + Collection keys = new ArrayList(stores.size()); + + for (RedisStore store : stores) { + keys.add(store.getKey()); + } + + return keys; + } + + static void rename(final K key, final K newKey, RedisOperations operations) { + operations.execute(new SessionCallback() { + @SuppressWarnings("unchecked") + @Override + public Object execute(RedisOperations operations) throws DataAccessException { + do { + operations.watch(key); + + if (operations.hasKey(key)) { + operations.multi(); + operations.rename(key, newKey); + } + else { + operations.multi(); + } + } while (operations.exec() == null); + return null; + } + }); + } + + static Boolean renameIfAbsent(final K key, final K newKey, RedisOperations operations) { + return operations.execute(new SessionCallback() { + @SuppressWarnings("unchecked") + @Override + public Boolean execute(RedisOperations operations) throws DataAccessException { + List exec = null; + do { + operations.watch(key); + + if (operations.hasKey(key)) { + operations.multi(); + operations.renameIfAbsent(key, newKey); + } + else { + operations.watch(newKey); + operations.multi(); + operations.hasKey(newKey); + operations.hasKey(newKey); + } + exec = operations.exec(); + } while (exec == null); + + boolean result = ((Long) exec.get(0) == 1); + if (exec.size() > 1) { + result = !result; + } + return result; + } + }); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisList.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisList.java new file mode 100644 index 000000000..6149838c4 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisList.java @@ -0,0 +1,507 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.BoundListOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; + +/** + * Default implementation for {@link RedisList}. + * Suitable for not just lists, but also queues (FIFO ordering) or stacks (LIFO ordering) and deques + * (or double ended queues). + * + * Allows the maximum size (or the cap) to be specified to prevent the list from over growing. + * + * Note that all write operations will execute immediately, whether a cap is specified or not - the list + * will always accept new items (trimming the tail after each insert in case of capped collections). + * + * @author Costin Leau + */ +public class DefaultRedisList extends AbstractRedisCollection implements RedisList { + + private final BoundListOperations listOps; + + private volatile int maxSize = 0; + + private volatile boolean capped = false; + + private volatile long defaultWait = 0; + + private class DefaultRedisListIterator extends RedisIterator { + + public DefaultRedisListIterator(Iterator delegate) { + super(delegate); + } + + @Override + protected void removeFromRedisStorage(E item) { + DefaultRedisList.this.remove(item); + } + } + + /** + * Constructs a new, uncapped DefaultRedisList instance. + * + * @param key + * @param operations + */ + public DefaultRedisList(String key, RedisOperations operations) { + this(operations.boundListOps(key)); + } + + /** + * Constructs a new, uncapped DefaultRedisList instance. + * + * @param boundOps + */ + public DefaultRedisList(BoundListOperations boundOps) { + this(boundOps, 0); + } + + /** + * Constructs a new DefaultRedisList instance. + * + * @param boundOps + * @param maxSize + */ + public DefaultRedisList(BoundListOperations boundOps, int maxSize) { + super(boundOps.getKey(), boundOps.getOperations()); + listOps = boundOps; + setMaxSize(maxSize); + } + + /** + * Sets the maximum size of the (capped) list. A value of 0 means unlimited. + * + * @param maxSize list maximum size + */ + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + capped = (maxSize > 0); + } + + @Override + public List range(long start, long end) { + return listOps.range(start, end); + } + + @Override + public RedisList trim(int start, int end) { + listOps.trim(start, end); + return this; + } + + private List content() { + return listOps.range(0, -1); + } + + private void cap() { + if (capped) { + listOps.trim(0, maxSize - 1); + } + } + + + @Override + public Iterator iterator() { + return new DefaultRedisListIterator(content().iterator()); + } + + @Override + public int size() { + return listOps.size().intValue(); + } + + + @Override + public boolean add(E value) { + listOps.rightPush(value); + cap(); + return true; + } + + @Override + public void clear() { + listOps.trim(size() + 1, 0); + } + + @Override + public boolean remove(Object o) { + Long result = listOps.remove(1, o); + return (result != null && result.longValue() > 0); + } + + @Override + public void add(int index, E element) { + if (index == 0) { + listOps.leftPush(element); + cap(); + return; + } + + int size = size(); + + if (index == size()) { + listOps.rightPush(element); + cap(); + return; + } + + if (index < 0 || index > size) { + throw new IndexOutOfBoundsException(); + } + + throw new IllegalArgumentException("Redis supports insertion only at the beginning or the end of the list"); + } + + @Override + public boolean addAll(int index, Collection c) { + // insert collection in reverse + if (index == 0) { + Collection reverseC = CollectionUtils.reverse(c); + + for (E e : reverseC) { + listOps.leftPush(e); + cap(); + } + return true; + } + + int size = size(); + + if (index == size()) { + for (E e : c) { + listOps.rightPush(e); + cap(); + } + return true; + } + + if (index < 0 || index > size) { + throw new IndexOutOfBoundsException(); + } + + throw new IllegalArgumentException("Redis supports insertion only at the beginning or the end of the list"); + } + + @Override + public E get(int index) { + if (index < 0 || index > size()) { + throw new IndexOutOfBoundsException(); + } + return listOps.index(index); + } + + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public E remove(int index) { + throw new UnsupportedOperationException(); + } + + + @Override + public E set(int index, E e) { + E object = get(index); + listOps.set(index, e); + return object; + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + // + // Queue methods + // + + @Override + public E element() { + E value = peek(); + if (value == null) + throw new NoSuchElementException(); + + return value; + } + + + @Override + public boolean offer(E e) { + listOps.rightPush(e); + cap(); + return true; + } + + + @Override + public E peek() { + return listOps.index(0); + } + + + @Override + public E poll() { + return listOps.leftPop(); + } + + + @Override + public E remove() { + E value = poll(); + if (value == null) + throw new NoSuchElementException(); + + return value; + } + + // + // Dequeue + // + + @Override + public void addFirst(E e) { + listOps.leftPush(e); + cap(); + } + + @Override + public void addLast(E e) { + add(e); + } + + @Override + public Iterator descendingIterator() { + List content = content(); + Collections.reverse(content); + return new DefaultRedisListIterator(content.iterator()); + } + + @Override + public E getFirst() { + return element(); + } + + @Override + public E getLast() { + E e = peekLast(); + if (e == null) { + throw new NoSuchElementException(); + } + return e; + } + + @Override + public boolean offerFirst(E e) { + addFirst(e); + return true; + } + + @Override + public boolean offerLast(E e) { + addLast(e); + return true; + } + + @Override + public E peekFirst() { + return peek(); + } + + @Override + public E peekLast() { + return listOps.index(-1); + } + + @Override + public E pollFirst() { + return poll(); + } + + @Override + public E pollLast() { + return listOps.rightPop(); + } + + @Override + public E pop() { + E e = poll(); + if (e == null) { + throw new NoSuchElementException(); + } + return e; + } + + @Override + public void push(E e) { + addFirst(e); + } + + @Override + public E removeFirst() { + return pop(); + } + + @Override + public boolean removeFirstOccurrence(Object o) { + return remove(o); + } + + @Override + public E removeLast() { + E e = pollLast(); + if (e == null) { + throw new NoSuchElementException(); + } + return e; + } + + @Override + public boolean removeLastOccurrence(Object o) { + Long result = listOps.remove(-1, o); + return (result != null && result.longValue() > 0); + } + + + // + // BlockingQueue + // + + @Override + public int drainTo(Collection c, int maxElements) { + if (this.equals(c)) { + throw new IllegalArgumentException("Cannot drain a queue to itself"); + } + + int size = size(); + int loop = (size >= maxElements ? maxElements : size); + + for (int index = 0; index < loop; index++) { + c.add(poll()); + } + + return loop; + } + + @Override + public int drainTo(Collection c) { + return drainTo(c, size()); + } + + @Override + public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { + return offer(e); + } + + @Override + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + E element = listOps.leftPop(timeout, unit); + return (element == null ? null : element); + } + + @Override + public void put(E e) throws InterruptedException { + offer(e); + } + + @Override + public int remainingCapacity() { + return Integer.MAX_VALUE; + } + + @Override + public E take() throws InterruptedException { + return poll(0, TimeUnit.SECONDS); + } + + + // + // BlockingDeque + // + + @Override + public boolean offerFirst(E e, long timeout, TimeUnit unit) throws InterruptedException { + return offerFirst(e); + } + + @Override + public boolean offerLast(E e, long timeout, TimeUnit unit) throws InterruptedException { + return offerLast(e); + } + + @Override + public E pollFirst(long timeout, TimeUnit unit) throws InterruptedException { + return poll(timeout, unit); + } + + @Override + public E pollLast(long timeout, TimeUnit unit) throws InterruptedException { + E element = listOps.rightPop(timeout, unit); + return (element == null ? null : element); + } + + @Override + public void putFirst(E e) throws InterruptedException { + add(e); + } + + @Override + public void putLast(E e) throws InterruptedException { + put(e); + } + + @Override + public E takeFirst() throws InterruptedException { + return take(); + } + + @Override + public E takeLast() throws InterruptedException { + return pollLast(0, TimeUnit.SECONDS); + } + + @Override + public DataType getType() { + return DataType.LIST; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisMap.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisMap.java new file mode 100644 index 000000000..291b6b00c --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisMap.java @@ -0,0 +1,332 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.BoundHashOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; + +/** + * Default implementation for {@link RedisMap}. + * + * @author Costin Leau + */ +public class DefaultRedisMap implements RedisMap { + + private final BoundHashOperations hashOps; + + private class DefaultRedisMapEntry implements Map.Entry { + + private K key; + private V value; + + public DefaultRedisMapEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + } + + /** + * Constructs a new DefaultRedisMap instance. + * + * @param key + * @param operations + */ + public DefaultRedisMap(String key, RedisOperations operations) { + this.hashOps = operations.boundHashOps(key); + } + + /** + * Constructs a new DefaultRedisMap instance. + * + * @param boundOps + */ + public DefaultRedisMap(BoundHashOperations boundOps) { + this.hashOps = boundOps; + } + + @Override + public Long increment(K key, long delta) { + return hashOps.increment(key, delta); + } + + @Override + public RedisOperations getOperations() { + return hashOps.getOperations(); + } + + @Override + public void clear() { + getOperations().delete(Collections.singleton(getKey())); + } + + @Override + public boolean containsKey(Object key) { + return hashOps.hasKey(key); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Set> entrySet() { + Set keySet = keySet(); + Collection multiGet = hashOps.multiGet(keySet); + + Iterator keys = keySet.iterator(); + Iterator values = multiGet.iterator(); + + Set> entries = new LinkedHashSet>(); + while (keys.hasNext()) { + entries.add(new DefaultRedisMapEntry(keys.next(), values.next())); + } + + return entries; + } + + @Override + public V get(Object key) { + return hashOps.get(key); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public Set keySet() { + return hashOps.keys(); + } + + @Override + public V put(K key, V value) { + V oldV = get(key); + hashOps.put(key, value); + return oldV; + } + + @Override + public void putAll(Map m) { + hashOps.putAll(m); + } + + @Override + public V remove(Object key) { + V v = get(key); + hashOps.delete(key); + return v; + } + + @Override + public int size() { + return hashOps.size().intValue(); + } + + @Override + public Collection values() { + return hashOps.values(); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (o instanceof RedisMap) { + return o.hashCode() == hashCode(); + } + return false; + } + + @Override + public int hashCode() { + int result = 17 + getClass().hashCode(); + result = result * 31 + getKey().hashCode(); + return result; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("RedisStore for key:"); + sb.append(getKey()); + return sb.toString(); + } + + @Override + public V putIfAbsent(K key, V value) { + throw new UnsupportedOperationException(); + + // RedisOperations ops = hashOps.getOperations(); + // + // for (;;) { + // ops.watch(getKey()); + // V v = get(key); + // if (v == null) { + // ops.multi(); + // put(key, value); + // if (ops.exec() != null) { + // return null; + // } + // } + // else { + // return v; + // } + // } + } + + @Override + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + + // if (value == null){ + // throw new NullPointerException(); + // } + // + // RedisOperations ops = hashOps.getOperations(); + // + // for (;;) { + // ops.watch(getKey()); + // V v = get(key); + // if (value.equals(v)) { + // ops.multi(); + // remove(key); + // if (ops.exec() != null) { + // return true; + // } + // } + // else { + // return false; + // } + // } + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw new UnsupportedOperationException(); + + // if (newValue == null || oldValue == null) { + // throw new NullPointerException(); + // } + // + // RedisOperations ops = hashOps.getOperations(); + // + // for (;;) { + // ops.watch(getKey()); + // V v = get(key); + // if (oldValue.equals(v)) { + // ops.multi(); + // put(key, newValue); + // if (ops.exec() != null) { + // return true; + // } + // } + // else { + // return false; + // } + // } + } + + @Override + public V replace(K key, V value) { + throw new UnsupportedOperationException(); + + + // if (value == null) { + // throw new NullPointerException(); + // } + // + // RedisOperations ops = hashOps.getOperations(); + // + // for (;;) { + // ops.watch(getKey()); + // if (containsKey(key)) { + // ops.multi(); + // V oldValue = put(key, value); + // if (ops.exec() != null) { + // return oldValue; + // } + // } + // else { + // return null; + // } + // } + } + + @Override + public Boolean expire(long timeout, TimeUnit unit) { + return hashOps.expire(timeout, unit); + } + + @Override + public Boolean expireAt(Date date) { + return hashOps.expireAt(date); + } + + @Override + public Long getExpire() { + return hashOps.getExpire(); + } + + @Override + public Boolean persist() { + return hashOps.persist(); + } + + + @Override + public String getKey() { + return hashOps.getKey(); + } + + @Override + public void rename(String newKey) { + hashOps.rename(newKey); + } + + @Override + public DataType getType() { + return hashOps.getType(); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisSet.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisSet.java new file mode 100644 index 000000000..368d7c204 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisSet.java @@ -0,0 +1,175 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.BoundSetOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; + +/** + * Default implementation for {@link RedisSet}. + * + * @author Costin Leau + */ +public class DefaultRedisSet extends AbstractRedisCollection implements RedisSet { + + private final BoundSetOperations boundSetOps; + + private class DefaultRedisSetIterator extends RedisIterator { + + public DefaultRedisSetIterator(Iterator delegate) { + super(delegate); + } + + @Override + protected void removeFromRedisStorage(E item) { + DefaultRedisSet.this.remove(item); + } + } + + /** + * Constructs a new DefaultRedisSet instance. + * + * @param key + * @param operations + */ + public DefaultRedisSet(String key, RedisOperations operations) { + super(key, operations); + boundSetOps = operations.boundSetOps(key); + } + + /** + * Constructs a new DefaultRedisSet instance. + * + * @param boundOps + */ + public DefaultRedisSet(BoundSetOperations boundOps) { + super(boundOps.getKey(), boundOps.getOperations()); + this.boundSetOps = boundOps; + } + + + @Override + public Set diff(RedisSet set) { + return boundSetOps.diff(set.getKey()); + } + + @Override + public Set diff(Collection> sets) { + return boundSetOps.diff(CollectionUtils.extractKeys(sets)); + } + + + @Override + public RedisSet diffAndStore(RedisSet set, String destKey) { + boundSetOps.diffAndStore(set.getKey(), destKey); + return new DefaultRedisSet(boundSetOps.getOperations().boundSetOps(destKey)); + } + + @Override + public RedisSet diffAndStore(Collection> sets, String destKey) { + boundSetOps.diffAndStore(CollectionUtils.extractKeys(sets), destKey); + return new DefaultRedisSet(boundSetOps.getOperations().boundSetOps(destKey)); + } + + @Override + public Set intersect(RedisSet set) { + return boundSetOps.intersect(set.getKey()); + } + + @Override + public Set intersect(Collection> sets) { + return boundSetOps.intersect(CollectionUtils.extractKeys(sets)); + } + + @Override + public RedisSet intersectAndStore(RedisSet set, String destKey) { + boundSetOps.intersectAndStore(set.getKey(), destKey); + return new DefaultRedisSet(boundSetOps.getOperations().boundSetOps(destKey)); + } + + @Override + public RedisSet intersectAndStore(Collection> sets, String destKey) { + boundSetOps.intersectAndStore(CollectionUtils.extractKeys(sets), destKey); + return new DefaultRedisSet(boundSetOps.getOperations().boundSetOps(destKey)); + } + + @Override + public Set union(RedisSet set) { + return boundSetOps.union(set.getKey()); + } + + @Override + public Set union(Collection> sets) { + return boundSetOps.union(CollectionUtils.extractKeys(sets)); + } + + @Override + public RedisSet unionAndStore(RedisSet set, String destKey) { + boundSetOps.unionAndStore(set.getKey(), destKey); + return new DefaultRedisSet(boundSetOps.getOperations().boundSetOps(destKey)); + } + + @Override + public RedisSet unionAndStore(Collection> sets, String destKey) { + boundSetOps.unionAndStore(CollectionUtils.extractKeys(sets), destKey); + return new DefaultRedisSet(boundSetOps.getOperations().boundSetOps(destKey)); + } + + @Override + public boolean add(E e) { + return boundSetOps.add(e); + } + + @Override + public void clear() { + // intersect the set with a non existing one + // TODO: find a safer way to clean the set + String randomKey = UUID.randomUUID().toString(); + boundSetOps.intersectAndStore(Collections.singleton(randomKey), getKey()); + } + + @Override + public boolean contains(Object o) { + return boundSetOps.isMember(o); + } + + @Override + public Iterator iterator() { + return new DefaultRedisSetIterator(boundSetOps.members().iterator()); + } + + @Override + public boolean remove(Object o) { + return boundSetOps.remove(o); + } + + @Override + public int size() { + return boundSetOps.size().intValue(); + } + + @Override + public DataType getType() { + return DataType.SET; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisZSet.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisZSet.java new file mode 100644 index 000000000..3da68a34d --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/DefaultRedisZSet.java @@ -0,0 +1,246 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.BoundZSetOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; +import org.springframework.data.keyvalue.redis.core.ZSetOperations.TypedTuple; + +/** + * Default implementation for {@link RedisZSet}. + * + * @author Costin Leau + */ +public class DefaultRedisZSet extends AbstractRedisCollection implements RedisZSet { + + private final BoundZSetOperations boundZSetOps; + private double defaultScore = 1; + + private class DefaultRedisSortedSetIterator extends RedisIterator { + + public DefaultRedisSortedSetIterator(Iterator delegate) { + super(delegate); + } + + @Override + protected void removeFromRedisStorage(E item) { + DefaultRedisZSet.this.remove(item); + } + } + + /** + * Constructs a new DefaultRedisZSet instance with a default score of '1'. + * + * @param key + * @param operations + */ + public DefaultRedisZSet(String key, RedisOperations operations) { + this(key, operations, 1); + } + + /** + * Constructs a new DefaultRedisSortedSet instance. + * + * @param key + * @param operations + * @param defaultScore + */ + public DefaultRedisZSet(String key, RedisOperations operations, double defaultScore) { + super(key, operations); + boundZSetOps = operations.boundZSetOps(key); + this.defaultScore = defaultScore; + } + + + /** + * Constructs a new DefaultRedisZSet instance with a default score of '1'. + * + * @param boundOps + */ + public DefaultRedisZSet(BoundZSetOperations boundOps) { + this(boundOps, 1); + } + + /** + * Constructs a new DefaultRedisZSet instance. + * + * @param boundOps + * @param defaultScore + */ + public DefaultRedisZSet(BoundZSetOperations boundOps, double defaultScore) { + super(boundOps.getKey(), boundOps.getOperations()); + this.boundZSetOps = boundOps; + this.defaultScore = defaultScore; + } + + @Override + public RedisZSet intersectAndStore(RedisZSet set, String destKey) { + boundZSetOps.intersectAndStore(set.getKey(), destKey); + return new DefaultRedisZSet(boundZSetOps.getOperations().boundZSetOps(destKey), getDefaultScore()); + } + + @Override + public RedisZSet intersectAndStore(Collection> sets, String destKey) { + boundZSetOps.intersectAndStore(CollectionUtils.extractKeys(sets), destKey); + return new DefaultRedisZSet(boundZSetOps.getOperations().boundZSetOps(destKey), getDefaultScore()); + } + + @Override + public Set range(long start, long end) { + return boundZSetOps.range(start, end); + } + + @Override + public Set reverseRange(long start, long end) { + return boundZSetOps.reverseRange(start, end); + } + + @Override + public Set rangeByScore(double min, double max) { + return boundZSetOps.rangeByScore(min, max); + } + + @Override + public Set reverseRangeByScore(double min, double max) { + return boundZSetOps.reverseRangeByScore(min, max); + } + + @Override + public Set> rangeByScoreWithScores(double min, double max) { + return boundZSetOps.rangeByScoreWithScores(min, max); + } + + @Override + public Set> rangeWithScores(long start, long end) { + return boundZSetOps.rangeWithScores(start, end); + } + + @Override + public Set> reverseRangeByScoreWithScores(double min, double max) { + return boundZSetOps.reverseRangeByScoreWithScores(min, max); + } + + @Override + public Set> reverseRangeWithScores(long start, long end) { + return boundZSetOps.reverseRangeWithScores(start, end); + } + + @Override + public RedisZSet remove(long start, long end) { + boundZSetOps.removeRange(start, end); + return this; + } + + @Override + public RedisZSet removeByScore(double min, double max) { + boundZSetOps.removeRangeByScore(min, max); + return this; + } + + @Override + public RedisZSet unionAndStore(RedisZSet set, String destKey) { + boundZSetOps.unionAndStore(set.getKey(), destKey); + return new DefaultRedisZSet(boundZSetOps.getOperations().boundZSetOps(destKey), getDefaultScore()); + } + + @Override + public RedisZSet unionAndStore(Collection> sets, String destKey) { + boundZSetOps.unionAndStore(CollectionUtils.extractKeys(sets), destKey); + return new DefaultRedisZSet(boundZSetOps.getOperations().boundZSetOps(destKey), getDefaultScore()); + } + + @Override + public boolean add(E e) { + return add(e, getDefaultScore()); + } + + @Override + public boolean add(E e, double score) { + return boundZSetOps.add(e, score); + } + + @Override + public void clear() { + boundZSetOps.removeRange(0, -1); + } + + @Override + public boolean contains(Object o) { + return (boundZSetOps.rank(o) != null); + } + + @Override + public Iterator iterator() { + return new DefaultRedisSortedSetIterator(boundZSetOps.range(0, -1).iterator()); + } + + @Override + public boolean remove(Object o) { + return boundZSetOps.remove(o); + } + + @Override + public int size() { + return boundZSetOps.size().intValue(); + } + + @Override + public Double getDefaultScore() { + return defaultScore; + } + + @Override + public E first() { + Iterator iterator = boundZSetOps.range(0, 0).iterator(); + if (iterator.hasNext()) + return iterator.next(); + throw new NoSuchElementException(); + } + + @Override + public E last() { + Iterator iterator = boundZSetOps.reverseRange(0, 0).iterator(); + if (iterator.hasNext()) + return iterator.next(); + throw new NoSuchElementException(); + } + + @Override + public Long rank(Object o) { + return boundZSetOps.rank(o); + } + + @Override + public Long reverseRank(Object o) { + return boundZSetOps.reverseRank(o); + } + + @Override + public Double score(Object o) { + return boundZSetOps.score(o); + } + + @Override + public DataType getType() { + return DataType.ZSET; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollection.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollection.java new file mode 100644 index 000000000..2bc2b3f8c --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollection.java @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Collection; + +/** + * Redis extension for the {@link Collection} contract. + * + * @author Costin Leau + */ +public interface RedisCollection extends RedisStore { + +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollectionFactoryBean.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollectionFactoryBean.java new file mode 100644 index 000000000..2ea8086f2 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollectionFactoryBean.java @@ -0,0 +1,168 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Factory bean that facilitates creation of Redis-based collections. Supports list, set, zset (or sortedSet), map (or hash) and properties. + * Will use the key type if it exists or to create a dedicated collection (Properties vs Map). + * Otherwise uses the provided type (default is list). + * + * @author Costin Leau + */ +public class RedisCollectionFactoryBean implements InitializingBean, BeanNameAware, FactoryBean { + + public enum CollectionType { + LIST { + @Override + public DataType dataType() { + return DataType.LIST; + } + }, + SET { + @Override + public DataType dataType() { + return DataType.SET; + } + }, + ZSET { + @Override + public DataType dataType() { + return DataType.ZSET; + } + }, + MAP { + @Override + public DataType dataType() { + return DataType.HASH; + } + }, + PROPERTIES { + @Override + public DataType dataType() { + return DataType.HASH; + } + }; + + abstract DataType dataType(); + } + + + private RedisStore store; + private CollectionType type = null; + private RedisTemplate template; + private String key; + private String beanName; + + @Override + public void afterPropertiesSet() { + if (!StringUtils.hasText(key)) { + key = beanName; + } + + Assert.hasText(key, "Collection key is required - no key or bean name specified"); + Assert.notNull(template, "Redis template is required"); + + DataType dt = template.type(key); + + // can't create store + Assert.isTrue(!DataType.STRING.equals(dt), "Cannot create store on keys of type 'string'"); + + store = createStore(dt); + + if (store == null) { + if (type == null) { + type = CollectionType.LIST; + } + store = createStore(type.dataType()); + } + } + + @SuppressWarnings("unchecked") + private RedisStore createStore(DataType dt) { + switch (dt) { + case LIST: + return new DefaultRedisList(key, template); + + case SET: + return new DefaultRedisSet(key, template); + + case ZSET: + return new DefaultRedisZSet(key, template); + + case HASH: + if (CollectionType.PROPERTIES.equals(type)) { + return new RedisProperties(key, template); + } + return new DefaultRedisMap(key, template); + } + return null; + } + + @Override + public RedisStore getObject() { + return store; + } + + @Override + public Class getObjectType() { + return (store != null ? store.getClass() : RedisStore.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + /** + * Sets the store type. Used if the key does not exist. + * + * @param type The type to set. + */ + public void setType(CollectionType type) { + this.type = type; + } + + /** + * Sets the template used by the resulting store. + * + * @param template The template to set. + */ + public void setTemplate(RedisTemplate template) { + this.template = template; + } + + /** + * Sets the key of the store. + * + * @param key The key to set. + */ + public void setKey(String key) { + this.key = key; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisIterator.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisIterator.java new file mode 100644 index 000000000..8ad15edf7 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisIterator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Iterator; + +/** + * Iterator extension for Redis collection removal. + * + * @author Costin Leau + */ +abstract class RedisIterator implements Iterator { + + private final Iterator delegate; + + private E item; + + /** + * Constructs a new RedisIterator instance. + * + * @param delegate + */ + RedisIterator(Iterator delegate) { + this.delegate = delegate; + } + + /** + * @return + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + return delegate.hasNext(); + } + + /** + * @return + * @see java.util.Iterator#next() + */ + public E next() { + item = delegate.next(); + return item; + } + + /** + * + * @see java.util.Iterator#remove() + */ + public void remove() { + delegate.remove(); + removeFromRedisStorage(item); + item = null; + } + + protected abstract void removeFromRedisStorage(E item); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisList.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisList.java new file mode 100644 index 000000000..846454fb5 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisList.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Deque; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.BlockingDeque; + +/** + * Redis extension for the {@link List} contract. Supports {@link List}, {@link Queue} and {@link Deque} contracts + * as well as their equivalent blocking siblings {@link BlockingDeque} and {@link BlockingDeque}. + * + * @author Costin Leau + */ +public interface RedisList extends RedisCollection, List, BlockingDeque { + + List range(long begin, long end); + + RedisList trim(int begin, int end); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisMap.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisMap.java new file mode 100644 index 000000000..1645d5c25 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisMap.java @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.concurrent.ConcurrentMap; + + +/** + * Map view of a Redis hash. + * + * @author Costin Leau + */ +public interface RedisMap extends RedisStore, ConcurrentMap { + + Long increment(K key, long delta); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisProperties.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisProperties.java new file mode 100644 index 000000000..0fec1d1b9 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisProperties.java @@ -0,0 +1,277 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.keyvalue.redis.connection.DataType; +import org.springframework.data.keyvalue.redis.core.BoundHashOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; + +/** + * {@link Properties} extension for a Redis back-store. Useful for reading (and storing) properties + * inside a Redis hash. Particularly useful inside a Spring container for hooking into Spring's property + * placeholder or {@link org.springframework.beans.factory.config.PropertiesFactoryBean}. + *

+ * Note that this implementation only accepts Strings - objects of other type are not supported. + * + * @see Properties + * @see org.springframework.core.io.support.PropertiesLoaderSupport + * @author Costin Leau + */ +public class RedisProperties extends Properties implements RedisMap { + + private final BoundHashOperations hashOps; + private final RedisMap delegate; + + /** + * Constructs a new RedisProperties instance. + * + */ + public RedisProperties(BoundHashOperations boundOps) { + this(null, boundOps); + } + + /** + * Constructs a new RedisProperties instance. + * + * @param boundOps + */ + public RedisProperties(String key, RedisOperations operations) { + this(null, operations. boundHashOps(key)); + } + + /** + * Constructs a new RedisProperties instance. + * + * @param defaults + */ + public RedisProperties(Properties defaults, BoundHashOperations boundOps) { + super(defaults); + this.hashOps = boundOps; + this.delegate = new DefaultRedisMap(boundOps); + } + + /** + * Constructs a new RedisProperties instance. + * + * @param defaults + * @param boundOps + */ + public RedisProperties(Properties defaults, String key, RedisOperations operations) { + this(defaults, operations. boundHashOps(key)); + } + + @Override + public synchronized Object get(Object key) { + return delegate.get(key); + } + + @Override + public synchronized Object put(Object key, Object value) { + return delegate.put((String) key, (String) value); + } + + @SuppressWarnings("unchecked") + @Override + public synchronized void putAll(Map t) { + delegate.putAll((Map) t); + } + + @Override + public Enumeration propertyNames() { + Set keys = new LinkedHashSet(delegate.keySet()); + keys.addAll(defaults.stringPropertyNames()); + return Collections.enumeration(keys); + } + + @Override + public synchronized void clear() { + delegate.clear(); + } + + @Override + public synchronized Object clone() { + return new RedisProperties(defaults, hashOps); + } + + @Override + public synchronized boolean contains(Object value) { + return containsValue(value); + } + + @Override + public synchronized boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @SuppressWarnings("unchecked") + @Override + public synchronized Enumeration elements() { + Collection values = delegate.values(); + return Collections.enumeration(values); + } + + @Override + @SuppressWarnings("unchecked") + public Set> entrySet() { + Set entries = delegate.entrySet(); + return entries; + } + + @Override + public synchronized boolean equals(Object o) { + if (o == this) + return true; + + if (o instanceof RedisProperties) { + return o.hashCode() == hashCode(); + } + return false; + } + + @Override + public synchronized int hashCode() { + int hash = RedisProperties.class.hashCode(); + return hash * 17 + delegate.hashCode(); + } + + @Override + public synchronized boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public synchronized Enumeration keys() { + Set keys = keySet(); + return Collections.enumeration(keys); + } + + @SuppressWarnings("unchecked") + @Override + public Set keySet() { + Set keys = delegate.keySet(); + return keys; + } + + @Override + public synchronized Object remove(Object key) { + return delegate.remove(key); + } + + @Override + public synchronized int size() { + return delegate.size(); + } + + @SuppressWarnings("unchecked") + @Override + public Collection values() { + Collection vals = delegate.values(); + return vals; + } + + @Override + public Long increment(Object key, long delta) { + return hashOps.increment((String) key, delta); + } + + @Override + public RedisOperations getOperations() { + return hashOps.getOperations(); + } + + @Override + public Boolean expire(long timeout, TimeUnit unit) { + return hashOps.expire(timeout, unit); + } + + @Override + public Boolean expireAt(Date date) { + return hashOps.expireAt(date); + } + + @Override + public Long getExpire() { + return hashOps.getExpire(); + } + + @Override + public String getKey() { + return hashOps.getKey(); + } + + @Override + public DataType getType() { + return hashOps.getType(); + } + + @Override + public Boolean persist() { + return hashOps.persist(); + } + + @Override + public void rename(String newKey) { + hashOps.rename(newKey); + } + + @Override + public Object putIfAbsent(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean replace(Object key, Object oldValue, Object newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public Object replace(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized void storeToXML(OutputStream os, String comment, String encoding) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized void storeToXML(OutputStream os, String comment) throws IOException { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisSet.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisSet.java new file mode 100644 index 000000000..78cde802b --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisSet.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Collection; +import java.util.Set; + +/** + * Redis extension for the {@link Set} contract. Supports {@link Set} specific + * operations backed by Redis operations. + * + * @author Costin Leau + */ +public interface RedisSet extends RedisCollection, Set { + + Set intersect(RedisSet set); + + Set intersect(Collection> sets); + + Set union(RedisSet set); + + Set union(Collection> sets); + + Set diff(RedisSet set); + + Set diff(Collection> sets); + + RedisSet intersectAndStore(RedisSet set, String destKey); + + RedisSet intersectAndStore(Collection> sets, String destKey); + + RedisSet unionAndStore(RedisSet set, String destKey); + + RedisSet unionAndStore(Collection> sets, String destKey); + + RedisSet diffAndStore(RedisSet set, String destKey); + + RedisSet diffAndStore(Collection> sets, String destKey); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisStore.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisStore.java new file mode 100644 index 000000000..d3c9205d8 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisStore.java @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import org.springframework.data.keyvalue.redis.core.BoundKeyOperations; +import org.springframework.data.keyvalue.redis.core.RedisOperations; + +/** + * Basic interface for Redis-based collections. + * + * Offers access to the {@link RedisOperations} entity + * used for executing commands against the backing store. + * + * @author Costin Leau + */ +public interface RedisStore extends BoundKeyOperations { + + /** + * Returns the underlying Redis operations used by the backing implementation. + * + * @return operations + */ + RedisOperations getOperations(); +} diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisZSet.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisZSet.java new file mode 100644 index 000000000..437fa5f09 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/RedisZSet.java @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Collection; +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.SortedSet; + +import org.springframework.data.keyvalue.redis.core.ZSetOperations.TypedTuple; + +/** + * Redis ZSet (or sorted set (by weight)). Acts as a {@link SortedSet} based on the given priorities or weights associated with each item. + *

+ * Since using a {@link Comparator} does not apply, a ZSet implements the {@link SortedSet} methods where applicable. + * + * @author Costin Leau + */ +public interface RedisZSet extends RedisCollection, Set { + + RedisZSet intersectAndStore(RedisZSet set, String destKey); + + RedisZSet intersectAndStore(Collection> sets, String destKey); + + RedisZSet unionAndStore(RedisZSet set, String destKey); + + RedisZSet unionAndStore(Collection> sets, String destKey); + + Set range(long start, long end); + + Set reverseRange(long start, long end); + + Set rangeByScore(double min, double max); + + Set reverseRangeByScore(double min, double max); + + Set> rangeWithScores(long start, long end); + + Set> reverseRangeWithScores(long start, long end); + + Set> rangeByScoreWithScores(double min, double max); + + Set> reverseRangeByScoreWithScores(double min, double max); + + RedisZSet remove(long start, long end); + + RedisZSet removeByScore(double min, double max); + + /** + * Adds an element to the set with the given score, or updates the score if + * the element exists. + * + * @param e element to add + * @param score element score + * @return true if a new element was added, false otherwise (only the score has been updated) + */ + boolean add(E e, double score); + + /** + * Adds an element to the set with a default score. Equivalent to + * {@code add(e, getDefaultScore())}. + * + * + * The score value is implementation specific. + * + * {@inheritDoc} + */ + boolean add(E e); + + /** + * Returns the score of the given element. Returns null if the element is not contained by the set. + * + * @param o object + * @return the score associated with the given object + */ + Double score(Object o); + + /** + * Returns the rank (position) of the given element in the set, in ascending order. + * Returns null if the element is not contained by the set. + * + * @param o object + * @return rank of the given object + */ + Long rank(Object o); + + /** + * Returns the rank (position) of the given element in the set, in descending order. + * Returns null if the element is not contained by the set. + * + * @param o object + * @return reverse rank of the given object + */ + Long reverseRank(Object o); + + /** + * Returns the default score used by this set. + * + * @return the default score used by the implementation. + */ + Double getDefaultScore(); + + /** + * Returns the first (lowest) element currently in this sorted set. + * + * @return the first (lowest) element currently in this sorted set. + * @throws NoSuchElementException sorted set is empty. + */ + E first(); + + /** + * Returns the last (highest) element currently in this sorted set. + * + * @return the last (highest) element currently in this sorted set. + * @throws NoSuchElementException sorted set is empty. + */ + E last(); +} \ No newline at end of file diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/package-info.java new file mode 100644 index 000000000..e00eeab04 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/collections/package-info.java @@ -0,0 +1,12 @@ +/** + * Package providing implementations for most of the {@code java.util} collections on top of Redis. + *

+ * For indexed collections, such as {@link java.util.List}, {@link java.util.Queue} or {@link java.util.Deque} + * consider {@link org.springframework.data.keyvalue.redis.support.collections.RedisList}.

+ * For collections without duplicates the obvious candidate is {@link org.springframework.data.keyvalue.redis.support.collections.RedisSet}. Use + * {@link org.springframework.data.keyvalue.redis.support.collections.RedisZSet} if a + * certain order is required.

+ * Lastly, for key/value associations {@link org.springframework.data.keyvalue.redis.support.collections.RedisMap} providing a Map-like abstraction on top of a Redis hash. + */ +package org.springframework.data.keyvalue.redis.support.collections; + diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/package-info.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/package-info.java new file mode 100644 index 000000000..cb235e463 --- /dev/null +++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/support/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes supporting the Redis packages, such as collection or atomic counters. + */ +package org.springframework.data.keyvalue.redis.support; + diff --git a/spring-data-redis/src/main/resources/META-INF/spring.handlers b/spring-data-redis/src/main/resources/META-INF/spring.handlers new file mode 100644 index 000000000..eebc89b3f --- /dev/null +++ b/spring-data-redis/src/main/resources/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.springframework.org/schema/redis=org.springframework.data.keyvalue.redis.config.RedisNamespaceHandler diff --git a/spring-data-redis/src/main/resources/META-INF/spring.schemas b/spring-data-redis/src/main/resources/META-INF/spring.schemas new file mode 100644 index 000000000..fea927201 --- /dev/null +++ b/spring-data-redis/src/main/resources/META-INF/spring.schemas @@ -0,0 +1,2 @@ +http\://www.springframework.org/schema/redis/spring-redis-1.0.xsd=org/springframework/data/keyvalue/redis/config/spring-redis-1.0.xsd +http\://www.springframework.org/schema/redis/spring-redis.xsd=org/springframework/data/keyvalue/redis/config/spring-redis-1.0.xsd \ No newline at end of file diff --git a/spring-data-redis/src/main/resources/META-INF/spring.tooling b/spring-data-redis/src/main/resources/META-INF/spring.tooling new file mode 100644 index 000000000..e0504f46e --- /dev/null +++ b/spring-data-redis/src/main/resources/META-INF/spring.tooling @@ -0,0 +1,4 @@ +# Tooling related information for the jms namespace +http\://www.springframework.org/schema/redis@name=redis Namespace +http\://www.springframework.org/schema/redis@prefix=redis +http\://www.springframework.org/schema/redis@icon=org/springframework/data/keyvalue/redis/config/spring-redis.gif diff --git a/spring-data-redis/src/main/resources/org/springframework/data/keyvalue/redis/config/spring-redis-1.0.xsd b/spring-data-redis/src/main/resources/org/springframework/data/keyvalue/redis/config/spring-redis-1.0.xsd new file mode 100644 index 000000000..59c815d6e --- /dev/null +++ b/spring-data-redis/src/main/resources/org/springframework/data/keyvalue/redis/config/spring-redis-1.0.xsd @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-data-redis/src/main/resources/org/springframework/data/keyvalue/redis/config/spring-redis.gif b/spring-data-redis/src/main/resources/org/springframework/data/keyvalue/redis/config/spring-redis.gif new file mode 100644 index 000000000..20ed1f9a4 Binary files /dev/null and b/spring-data-redis/src/main/resources/org/springframework/data/keyvalue/redis/config/spring-redis.gif differ diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/Address.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/Address.java new file mode 100644 index 000000000..1967c29c5 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/Address.java @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis; + +import java.io.Serializable; + +/** + * Simple serializable class. + * + * @author Costin Leau + */ +public class Address implements Serializable { + + private static final long serialVersionUID = 4924045450477798779L; + + private String street; + + private Integer number; + + public Address() { + } + + /** + * Constructs a new Address instance. + * + * @param street + * @param number + */ + public Address(String street, int number) { + super(); + this.street = street; + this.number = number; + } + + + /** + * Returns the street. + * + * @return Returns the street + */ + public String getStreet() { + return street; + } + + /** + * @param street The street to set. + */ + public void setStreet(String street) { + this.street = street; + } + + /** + * Returns the number. + * + * @return Returns the number + */ + public Integer getNumber() { + return number; + } + + /** + * @param number The number to set. + */ + public void setNumber(Integer number) { + this.number = number; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((number == null) ? 0 : number.hashCode()); + result = prime * result + ((street == null) ? 0 : street.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Address)) + return false; + Address other = (Address) obj; + if (number == null) { + if (other.number != null) + return false; + } + else if (!number.equals(other.number)) + return false; + if (street == null) { + if (other.street != null) + return false; + } + else if (!street.equals(other.street)) + return false; + return true; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/ConnectionFactoryTracker.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/ConnectionFactoryTracker.java new file mode 100644 index 000000000..634d3448a --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/ConnectionFactoryTracker.java @@ -0,0 +1,50 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; + +/** + * Basic utility to help with the destruction of {@link RedisConnectionFactory} inside JUnit 4 tests. + * Simply add the factory during setup and then call {@link #cleanUp()} through the @AfterClass method. + * + * @author Costin Leau + */ +public abstract class ConnectionFactoryTracker { + + private static Set connFactories = new LinkedHashSet(); + + public static void add(RedisConnectionFactory factory) { + connFactories.add(factory); + } + + public static void cleanUp() { + if (connFactories != null) { + for (RedisConnectionFactory connectionFactory : connFactories) { + try { + ((DisposableBean) connectionFactory).destroy(); + //System.out.println("Succesfully cleaned up factory " + connectionFactory); + } catch (Exception ex) { + System.err.println("Cannot clean factory " + connectionFactory + ex); + } + } + } + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/Person.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/Person.java new file mode 100644 index 000000000..b161c4740 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/Person.java @@ -0,0 +1,129 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis; + +import java.io.Serializable; + +/** + * Simple serializable class. + * + * @author Mark Pollack + * @author Costin Leau + */ +public class Person implements Serializable { + + private static final long serialVersionUID = 92633004015631981L; + + private String firstName; + private String lastName; + + private Integer age; + private Address address; + + public Person() { + } + + public Person(String firstName, String lastName, int age) { + this(firstName, lastName, age, null); + } + + public Person(String firstName, String lastName, int age, Address address) { + super(); + this.firstName = firstName; + this.lastName = lastName; + this.age = age; + this.address = address; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((address == null) ? 0 : address.hashCode()); + result = prime * result + ((age == null) ? 0 : age.hashCode()); + result = prime * result + ((firstName == null) ? 0 : firstName.hashCode()); + result = prime * result + ((lastName == null) ? 0 : lastName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Person)) + return false; + Person other = (Person) obj; + if (address == null) { + if (other.address != null) + return false; + } + else if (!address.equals(other.address)) + return false; + if (age == null) { + if (other.age != null) + return false; + } + else if (!age.equals(other.age)) + return false; + if (firstName == null) { + if (other.firstName != null) + return false; + } + else if (!firstName.equals(other.firstName)) + return false; + if (lastName == null) { + if (other.lastName != null) + return false; + } + else if (!lastName.equals(other.lastName)) + return false; + return true; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/PropertyEditorsTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/PropertyEditorsTest.java new file mode 100644 index 000000000..5c886e8e4 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/PropertyEditorsTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.data.keyvalue.redis.core.RedisOperations; + +/** + * @author Costin Leau + */ +public class PropertyEditorsTest { + + private GenericXmlApplicationContext ctx; + + @Before + public void setUp() { + ctx = new GenericXmlApplicationContext("/org/springframework/data/keyvalue/redis/pe.xml"); + } + + @After + public void tearDown() { + if (ctx != null) + ctx.destroy(); + } + + @Test + public void testInjection() throws Exception { + RedisViewPE bean = ctx.getBean(RedisViewPE.class); + RedisOperations ops = ctx.getBean(RedisOperations.class); + + assertSame(ops.opsForValue(), bean.getValueOps()); + assertSame(ops.opsForList(), bean.getListOps()); + assertSame(ops.opsForSet(), bean.getSetOps()); + assertSame(ops.opsForZSet(), bean.getZsetOps()); + assertSame(ops, bean.getHashOps().getOperations()); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/RedisViewPE.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/RedisViewPE.java new file mode 100644 index 000000000..3282ef2e3 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/RedisViewPE.java @@ -0,0 +1,74 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis; + +import org.springframework.data.keyvalue.redis.core.HashOperations; +import org.springframework.data.keyvalue.redis.core.ListOperations; +import org.springframework.data.keyvalue.redis.core.SetOperations; +import org.springframework.data.keyvalue.redis.core.ValueOperations; +import org.springframework.data.keyvalue.redis.core.ZSetOperations; + +/** + * @author Costin Leau + */ +public class RedisViewPE { + + private ValueOperations valueOps; + private ListOperations listOps; + private SetOperations setOps; + private ZSetOperations zsetOps; + private HashOperations hashOps; + + public ValueOperations getValueOps() { + return valueOps; + } + + public void setValueOps(ValueOperations valueOps) { + this.valueOps = valueOps; + } + + public ListOperations getListOps() { + return listOps; + } + + public void setListOps(ListOperations listOps) { + this.listOps = listOps; + } + + public SetOperations getSetOps() { + return setOps; + } + + public void setSetOps(SetOperations setOps) { + this.setOps = setOps; + } + + public ZSetOperations getZsetOps() { + return zsetOps; + } + + public void setZsetOps(ZSetOperations zsetOps) { + this.zsetOps = zsetOps; + } + + public HashOperations getHashOps() { + return hashOps; + } + + public void setHashOps(HashOperations hashOps) { + this.hashOps = hashOps; + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/SettingsUtils.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/SettingsUtils.java new file mode 100644 index 000000000..f6697b632 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/SettingsUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis; + +import java.util.Properties; + +/** + * @author Costin Leau + */ +public abstract class SettingsUtils { + private final static Properties DEFAULTS = new Properties(); + private static final Properties SETTINGS; + + static { + DEFAULTS.put("host", "localhost"); + DEFAULTS.put("port", "6379"); + + SETTINGS = new Properties(DEFAULTS); + + try { + SETTINGS.load(SettingsUtils.class.getResourceAsStream("/org/springframework/data/keyvalue/redis/test.properties")); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot read settings"); + } + } + + public static String getHost() { + return SETTINGS.getProperty("host"); + } + + public static int getPort() { + return Integer.valueOf(SETTINGS.getProperty("port")); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/config/NamespaceTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/config/NamespaceTest.java new file mode 100644 index 000000000..46b9016f3 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/config/NamespaceTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.config; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.listener.RedisMessageListenerContainer; + +/** + * @author Costin Leau + */ +public class NamespaceTest { + + private GenericXmlApplicationContext ctx; + + @Before + public void setUp() { + ctx = new GenericXmlApplicationContext("/org/springframework/data/keyvalue/redis/config/namespace.xml"); + } + + @After + public void tearDown() { + if (ctx != null) + ctx.destroy(); + } + + @Test + public void testSanityTest() throws Exception { + RedisMessageListenerContainer container = ctx.getBean(RedisMessageListenerContainer.class); + assertTrue(container.isRunning()); + //Thread.sleep(TimeUnit.SECONDS.toMillis(1)); + } + + @Test + public void testWithMessages() throws Exception { + StringRedisTemplate template = ctx.getBean(StringRedisTemplate.class); + template.convertAndSend("x1", "[X]test"); + template.convertAndSend("z1", "[Z]test"); + //Thread.sleep(TimeUnit.SECONDS.toMillis(5)); + } + + public void testErrorHandler() throws Exception { + StubErrorHandler handler = ctx.getBean(StubErrorHandler.class); + + int index = handler.throwables.size(); + StringRedisTemplate template = ctx.getBean(StringRedisTemplate.class); + template.convertAndSend("exception", "test1"); + handler.throwables.pollLast(3, TimeUnit.SECONDS); + assertEquals(index + 1, handler.throwables.size()); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/config/StubErrorHandler.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/config/StubErrorHandler.java new file mode 100644 index 000000000..a9f83609d --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/config/StubErrorHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.config; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; + +import org.springframework.util.ErrorHandler; + +/** + * @author Costin Leau + */ +public class StubErrorHandler implements ErrorHandler { + + public BlockingDeque throwables = new LinkedBlockingDeque(); + + @Override + public void handleError(Throwable t) { + throwables.add(t); + } + +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/AbstractConnectionIntegrationTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/AbstractConnectionIntegrationTests.java new file mode 100644 index 000000000..79266a5ec --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/AbstractConnectionIntegrationTests.java @@ -0,0 +1,321 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.Address; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.Person; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; + +public abstract class AbstractConnectionIntegrationTests { + + protected StringRedisConnection connection; + protected RedisSerializer serializer = new JdkSerializationRedisSerializer(); + protected RedisSerializer stringSerializer = new StringRedisSerializer(); + + private static final String listName = "test-list"; + private static final byte[] EMPTY_ARRAY = new byte[0]; + + protected abstract RedisConnectionFactory getConnectionFactory(); + + + @Before + public void setUp() { + connection = new DefaultStringRedisConnection(getConnectionFactory().getConnection()); + ConnectionFactoryTracker.add(getConnectionFactory()); + + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + + @After + public void tearDown() { + connection.close(); + connection = null; + } + + @Test + public void testLPush() throws Exception { + byte[] val = "bar".getBytes(); + Long index = connection.lPush(listName.getBytes(), val); + if (index != null) { + assertEquals((Long) (index + 1), connection.lPush(listName.getBytes(), val)); + } + } + + @Test + public void testSetAndGet() { + String key = "foo"; + String value = "blabla"; + connection.set(key.getBytes(), value.getBytes()); + assertEquals(value, new String(connection.get(key.getBytes()))); + } + + private boolean isJredis() { + return connection.getClass().getSimpleName().startsWith("Jredis"); + } + + + @Test + public void testByteValue() { + String value = UUID.randomUUID().toString(); + Person person = new Person(value, value, 1, new Address(value, 2)); + String key = getClass() + ":byteValue"; + byte[] rawKey = stringSerializer.serialize(key); + + connection.set(rawKey, serializer.serialize(person)); + byte[] rawValue = connection.get(rawKey); + assertNotNull(rawValue); + assertEquals(person, serializer.deserialize(rawValue)); + } + + @Test + public void testPingPong() throws Exception { + assertEquals("PONG", connection.ping()); + } + + @Test + public void testInfo() throws Exception { + Properties info = connection.info(); + assertNotNull(info); + assertTrue("at least 5 settings should be present", info.size() >= 5); + String version = info.getProperty("redis_version"); + assertNotNull(version); + System.out.println(info); + } + + @Test + public void testNullKey() throws Exception { + connection.decr(EMPTY_ARRAY); + try { + connection.decr((String) null); + } catch (Exception ex) { + // excepted + } + } + + @Test + public void testNullValue() throws Exception { + byte[] key = UUID.randomUUID().toString().getBytes(); + connection.append(key, EMPTY_ARRAY); + try { + connection.append(key, null); + } catch (DataAccessException ex) { + // expected + } + } + + @Test + public void testHashNullKey() throws Exception { + byte[] key = UUID.randomUUID().toString().getBytes(); + connection.hExists(key, EMPTY_ARRAY); + try { + connection.hExists(key, null); + } catch (DataAccessException ex) { + // expected + } + } + + @Test + public void testHashNullValue() throws Exception { + byte[] key = UUID.randomUUID().toString().getBytes(); + byte[] field = "random".getBytes(); + + connection.hSet(key, field, EMPTY_ARRAY); + try { + connection.hSet(key, field, null); + } catch (DataAccessException ex) { + // expected + } + } + + @Test + public void testNullSerialization() throws Exception { + String[] keys = new String[] { "~", "[" }; + List mGet = connection.mGet(keys); + assertEquals(2, mGet.size()); + assertNull(mGet.get(0)); + assertNull(mGet.get(1)); + + StringRedisTemplate stringTemplate = new StringRedisTemplate(getConnectionFactory()); + List multiGet = stringTemplate.opsForValue().multiGet(Arrays.asList(keys)); + assertEquals(2, multiGet.size()); + assertNull(multiGet.get(0)); + assertNull(multiGet.get(1)); + } + + @Test + public void testNullCollections() throws Exception { + connection.openPipeline(); + assertNull(connection.keys("~*")); + assertNull(connection.hKeys("~")); + connection.closePipeline(); + } + + // pub sub test + @Test + public void testPubSub() throws Exception { + + final BlockingDeque queue = new LinkedBlockingDeque(); + + final MessageListener ml = new MessageListener() { + @Override + public void onMessage(Message message, byte[] pattern) { + queue.add(message); + System.out.println("received message"); + } + }; + + final byte[] channel = "foo.tv".getBytes(); + final RedisConnection subConn = getConnectionFactory().getConnection(); + + assertNotSame(connection, subConn); + + + final AtomicBoolean flag = new AtomicBoolean(true); + + Runnable listener = new Runnable() { + @Override + public void run() { + subConn.subscribe(ml, channel); + System.out.println("Subscribed"); + while (flag.get()) { + try { + Thread.currentThread().sleep(2000); + } catch (Exception ex) { + return; + } + } + } + }; + + Thread th = new Thread(listener, "listener"); + th.start(); + + try { + Thread.sleep(1500); + connection.publish(channel, "one".getBytes()); + connection.publish(channel, "two".getBytes()); + connection.publish(channel, "I see you".getBytes()); + System.out.println("Done publishing..."); + Thread.sleep(5000); + System.out.println("Done waiting ..."); + } finally { + flag.set(false); + } + System.out.println(queue); + assertEquals(3, queue.size()); + } + + @Test + public void testPubSubWithNamedChannels() { + final byte[] expectedChannel = "channel1".getBytes(); + final byte[] expectedMessage = "msg".getBytes(); + + MessageListener listener = new MessageListener() { + + @Override + public void onMessage(Message message, byte[] pattern) { + assertArrayEquals(expectedChannel, message.getChannel()); + assertArrayEquals(expectedMessage, message.getBody()); + } + }; + + Thread th = new Thread(new Runnable() { + @Override + public void run() { + // sleep 1 second to let the registration happen + try { + Thread.currentThread().sleep(2000); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + + // open a new connection + RedisConnection connection2 = getConnectionFactory().getConnection(); + connection2.publish(expectedMessage, expectedChannel); + connection2.close(); + // unsubscribe connection + connection.getSubscription().unsubscribe(); + } + }); + + th.start(); + connection.subscribe(listener, expectedChannel); + } + + @Test + public void testPubSubWithPatterns() { + final byte[] expectedPattern = "channel*".getBytes(); + final byte[] expectedMessage = "msg".getBytes(); + + MessageListener listener = new MessageListener() { + + @Override + public void onMessage(Message message, byte[] pattern) { + assertArrayEquals(expectedPattern, pattern); + assertArrayEquals(expectedMessage, message.getBody()); + System.out.println("Received message '" + new String(message.getBody()) + "'"); + } + }; + + Thread th = new Thread(new Runnable() { + @Override + public void run() { + // sleep 1 second to let the registration happen + try { + Thread.currentThread().sleep(1500); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + + // open a new connection + RedisConnection connection2 = getConnectionFactory().getConnection(); + connection2.publish(expectedMessage, "channel1".getBytes()); + connection2.publish(expectedMessage, "channel2".getBytes()); + connection2.close(); + // unsubscribe connection + connection.getSubscription().pUnsubscribe(expectedPattern); + } + }); + + th.start(); + connection.pSubscribe(listener, expectedPattern); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnectionIntegrationTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnectionIntegrationTests.java new file mode 100644 index 000000000..302a94e49 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/jedis/JedisConnectionIntegrationTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection.jedis; + +import org.junit.Test; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.AbstractConnectionIntegrationTests; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; + +import redis.clients.jedis.BinaryJedis; +import redis.clients.jedis.Transaction; + +public class JedisConnectionIntegrationTests extends AbstractConnectionIntegrationTests { + + JedisConnectionFactory factory; + + public JedisConnectionIntegrationTests() { + factory = new JedisConnectionFactory(); + factory.setUsePool(true); + + factory.setPort(SettingsUtils.getPort()); + factory.setHostName(SettingsUtils.getHost()); + + factory.afterPropertiesSet(); + } + + @Override + protected RedisConnectionFactory getConnectionFactory() { + return factory; + } + + @Test + public void testMulti() throws Exception { + byte[] key = "key".getBytes(); + byte[] value = "value".getBytes(); + + BinaryJedis jedis = (BinaryJedis) connection.getNativeConnection(); + Transaction multi = jedis.multi(); + //connection.set(key, value); + multi.set(value, key); + System.out.println(multi.exec()); + + connection.multi(); + connection.set(value, key); + System.out.println(connection.exec()); + } + +// @Test +// public void setAdd() { +// connection.sadd("s1", "1"); +// connection.sadd("s1", "2"); +// connection.sadd("s1", "3"); +// connection.sadd("s2", "2"); +// connection.sadd("s2", "3"); +// Set intersection = connection.sinter("s1", "s2"); +// System.out.println(intersection); +// +// +// } +// +// @Test +// public void setIntersectionTests() { +// RedisTemplate template = new RedisTemplate(clientFactory); +// RedisSet s1 = new RedisSet(template, "s1"); +// s1.add("1"); +// s1.add("2"); +// s1.add("3"); +// RedisSet s2 = new RedisSet(template, "s2"); +// s2.add("2"); +// s2.add("3"); +// Set s3 = s1.intersection("s3", s1, s2); +// for (Object object : s3) { +// System.out.println(object); +// } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/jredis/JRedisConnectionIntegrationTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/jredis/JRedisConnectionIntegrationTests.java new file mode 100644 index 000000000..ed92f9f63 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/jredis/JRedisConnectionIntegrationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.redis.connection.jredis; + +import org.jredis.JRedis; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.AbstractConnectionIntegrationTests; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; + +public class JRedisConnectionIntegrationTests extends AbstractConnectionIntegrationTests { + + JredisConnectionFactory factory; + + public JRedisConnectionIntegrationTests() { + factory = new JredisConnectionFactory(); + factory.setPort(SettingsUtils.getPort()); + factory.setHostName(SettingsUtils.getHost()); + + factory.setUsePool(true); + factory.afterPropertiesSet(); + } + + @Override + protected RedisConnectionFactory getConnectionFactory() { + return factory; + } + + @Test + public void testRaw() throws Exception { + JRedis jr = (JRedis) factory.getConnection().getNativeConnection(); + + System.out.println(jr.dbsize()); + System.out.println(jr.exists("foobar")); + jr.set("foobar", "barfoo"); + System.out.println(jr.get("foobar")); + } + + @Ignore("JRedis does not support pipelining") + public void testNullCollections() { + } + + @Ignore + public void testNullKey() throws Exception { + } + + @Ignore + public void testNullValue() throws Exception { + } + + @Ignore + public void testHashNullKey() throws Exception { + } + + @Ignore + public void testHashNullValue() throws Exception { + } + + @Ignore + public void testNullSerialization() throws Exception { + } + + @Ignore + public void testPubSub() throws Exception { + } + + @Ignore + public void testPubSubWithPatterns() { + } + + @Ignore + public void testPubSubWithNamedChannels() { + + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnectionIntegrationTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnectionIntegrationTests.java new file mode 100644 index 000000000..4fa5b3ed8 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/connection/rjc/RjcConnectionIntegrationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.connection.rjc; + +import org.idevlab.rjc.Session; +import org.junit.Test; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.AbstractConnectionIntegrationTests; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; + +/** + * @author Costin Leau + */ +public class RjcConnectionIntegrationTests extends AbstractConnectionIntegrationTests { + + RjcConnectionFactory factory; + + public RjcConnectionIntegrationTests() { + factory = new RjcConnectionFactory(); + factory.setPort(SettingsUtils.getPort()); + factory.setHostName(SettingsUtils.getHost()); + + factory.setUsePool(false); + factory.afterPropertiesSet(); + } + + @Override + protected RedisConnectionFactory getConnectionFactory() { + return factory; + } + + @Test + public void testRaw() throws Exception { + Session jr = (Session) factory.getConnection().getNativeConnection(); + + System.out.println(jr.dbSize()); + System.out.println(jr.exists("foobar")); + jr.set("foobar", "barfoo"); + System.out.println(jr.get("foobar")); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/SessionTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/SessionTest.java new file mode 100644 index 000000000..f550facfb --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/SessionTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; + +/** + * @author Costin Leau + */ +public class SessionTest { + + @Test + public void testSession() throws Exception { + final RedisConnection conn = mock(RedisConnection.class); + RedisConnectionFactory factory = mock(RedisConnectionFactory.class); + + when(factory.getConnection()).thenReturn(conn); + final StringRedisTemplate template = new StringRedisTemplate(factory); + + template.execute(new SessionCallback() { + @Override + public Object execute(RedisOperations operations) { + checkConnection(template, conn); + template.discard(); + assertSame(template, operations); + checkConnection(template, conn); + return null; + } + }); + } + + private void checkConnection(RedisTemplate template, final RedisConnection expectedConnection) { + template.execute(new RedisCallback() { + + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + assertSame(expectedConnection, connection); + return null; + } + }, true); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/SortTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/SortTest.java new file mode 100644 index 000000000..df6fd0b95 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/SortTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + + +import org.junit.After; +import org.junit.Before; +import org.springframework.data.keyvalue.redis.core.query.SortQueryBuilder; + +public class SortTest { + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + public void testBasicDSL() throws Exception { + SortQueryBuilder.sort("list").build(); + } + +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/TemplateTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/TemplateTest.java new file mode 100644 index 000000000..96cf1d05c --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/core/TemplateTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.core; + +import static org.junit.Assert.*; + +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.support.collections.CollectionTestParams; +import org.springframework.data.keyvalue.redis.support.collections.ObjectFactory; + +/** + * @author Costin Leau + */ +@RunWith(Parameterized.class) +public class TemplateTest { + private ObjectFactory objFactory; + private RedisTemplate template; + + public TemplateTest(ObjectFactory objFactory, RedisTemplate template) { + this.objFactory = objFactory; + this.template = template; + ConnectionFactoryTracker.add(template.getConnectionFactory()); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + @Parameters + public static Collection testParams() { + return CollectionTestParams.testParams(); + } + + @Test + public void testKeys() throws Exception { + assertTrue(template.keys("*") != null); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/PubSubTestParams.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/PubSubTestParams.java new file mode 100644 index 000000000..bac0d731b --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/PubSubTestParams.java @@ -0,0 +1,72 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener; + +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.data.keyvalue.redis.Person; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.rjc.RjcConnectionFactory; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.support.collections.ObjectFactory; +import org.springframework.data.keyvalue.redis.support.collections.PersonObjectFactory; +import org.springframework.data.keyvalue.redis.support.collections.StringObjectFactory; + +/** + * @author Costin Leau + */ +public class PubSubTestParams { + + public static Collection testParams() { + // create Jedis Factory + ObjectFactory stringFactory = new StringObjectFactory(); + ObjectFactory personFactory = new PersonObjectFactory(); + + JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory(); + jedisConnFactory.setUsePool(true); + jedisConnFactory.setPort(SettingsUtils.getPort()); + jedisConnFactory.setHostName(SettingsUtils.getHost()); + jedisConnFactory.setDatabase(2); + + jedisConnFactory.afterPropertiesSet(); + + RedisTemplate stringTemplate = new StringRedisTemplate(jedisConnFactory); + RedisTemplate personTemplate = new RedisTemplate(); + personTemplate.setConnectionFactory(jedisConnFactory); + personTemplate.afterPropertiesSet(); + + // create RJC + + RjcConnectionFactory rjcConnFactory = new RjcConnectionFactory(); + rjcConnFactory.setUsePool(false); + rjcConnFactory.setPort(SettingsUtils.getPort()); + rjcConnFactory.setHostName(SettingsUtils.getHost()); + rjcConnFactory.afterPropertiesSet(); + + RedisTemplate stringTemplateRJC = new StringRedisTemplate(rjcConnFactory); + RedisTemplate personTemplateRJC = new RedisTemplate(); + personTemplateRJC.setConnectionFactory(rjcConnFactory); + personTemplateRJC.afterPropertiesSet(); + + + return Arrays.asList(new Object[][] { { stringFactory, stringTemplate }, { personFactory, personTemplate }, + { stringFactory, stringTemplateRJC }, { personFactory, personTemplateRJC } + }); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/PubSubTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/PubSubTests.java new file mode 100644 index 000000000..a61f923f0 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/PubSubTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.data.keyvalue.redis.support.collections.ObjectFactory; + +/** + * Base test class for PubSub integration tests + * + * @author Costin Leau + */ +@RunWith(Parameterized.class) +public class PubSubTests { + + private static final String CHANNEL = "pubsub::test"; + + protected RedisMessageListenerContainer container; + protected ObjectFactory factory; + protected RedisTemplate template; + + private final BlockingDeque bag = new LinkedBlockingDeque(99); + + private final Object handler = new Object() { + void handleMessage(String message) { + bag.add(message); + } + }; + + private final MessageListenerAdapter adapter = new MessageListenerAdapter(handler); + + @Before + public void setUp() throws Exception { + adapter.setSerializer(template.getValueSerializer()); + + container = new RedisMessageListenerContainer(); + container.setConnectionFactory(template.getConnectionFactory()); + container.setBeanName("container"); + container.addMessageListener(adapter, Arrays.asList(new ChannelTopic(CHANNEL))); + container.afterPropertiesSet(); + + Thread.sleep(1000); + } + + @After + public void tearDown() throws Exception { + container.destroy(); + } + + public PubSubTests(ObjectFactory factory, RedisTemplate template) { + this.factory = factory; + this.template = template; + ConnectionFactoryTracker.add(template.getConnectionFactory()); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + @Parameters + public static Collection testParams() { + return PubSubTestParams.testParams(); + } + + /** + * Return a new instance of T + * @return + */ + protected T getT() { + return factory.instance(); + } + + @Test + public void testContainerSubscribe() throws Exception { + String payload1 = "do"; + String payload2 = "re mi"; + + template.convertAndSend(CHANNEL, payload1); + template.convertAndSend(CHANNEL, payload2); + + Set set = new LinkedHashSet(); + set.add(bag.poll(1, TimeUnit.SECONDS)); + set.add(bag.poll(1, TimeUnit.SECONDS)); + + System.out.println(set); + + assertTrue(set.contains(payload1)); + assertTrue(set.contains(payload2)); + } + + @Test + public void testMessageBatch() throws Exception { + int COUNT = 10; + for (int i = 0; i < COUNT; i++) { + template.convertAndSend(CHANNEL, "message=" + i); + } + + Thread.sleep(1000); + assertEquals(COUNT, bag.size()); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/ContainerXmlSetupTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/ContainerXmlSetupTest.java new file mode 100644 index 000000000..8b180ea3e --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/ContainerXmlSetupTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener.adapter; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.data.keyvalue.redis.listener.RedisMessageListenerContainer; + +/** + * @author Costin Leau + */ +public class ContainerXmlSetupTest { + + @Test + public void testContainerSetup() throws Exception { + GenericXmlApplicationContext ctx = new GenericXmlApplicationContext( + "/org/springframework/data/keyvalue/redis/listener/container.xml"); + RedisMessageListenerContainer container = ctx.getBean("redisContainer", RedisMessageListenerContainer.class); + assertTrue(container.isRunning()); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/MessageListenerTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/MessageListenerTest.java new file mode 100644 index 000000000..4fec13737 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/MessageListenerTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener.adapter; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.keyvalue.redis.connection.DefaultMessage; +import org.springframework.data.keyvalue.redis.connection.Message; +import org.springframework.data.keyvalue.redis.connection.MessageListener; +import org.springframework.data.keyvalue.redis.serializer.RedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer; + +/** + * Unit test for MessageListenerAdapter. + * + * @author Costin Leau + */ +public class MessageListenerTest { + + private static final RedisSerializer serializer = new StringRedisSerializer(); + private static final String CHANNEL = "some::test:"; + private static final byte[] RAW_CHANNEL = serializer.serialize(CHANNEL); + private static final String PAYLOAD = "do re mi"; + private static final byte[] RAW_PAYLOAD = serializer.serialize(PAYLOAD); + private static final Message STRING_MSG = new DefaultMessage(RAW_CHANNEL, RAW_PAYLOAD); + + private MessageListenerAdapter adapter; + + public static interface Delegate { + void handleMessage(String argument); + + void customMethod(String arg); + } + + @Mock + private Delegate target; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + this.adapter = new MessageListenerAdapter(); + } + + @Test + public void testThatWhenNoDelegateIsSuppliedTheDelegateIsAssumedToBeTheMessageListenerAdapterItself() + throws Exception { + assertSame(adapter, adapter.getDelegate()); + } + + @Test + public void testThatTheDefaultMessageHandlingMethodNameIsTheConstantDefault() throws Exception { + assertEquals(MessageListenerAdapter.ORIGINAL_DEFAULT_LISTENER_METHOD, adapter.getDefaultListenerMethod()); + } + + @Test + public void testAdapterWithListenerAndDefaultMessage() throws Exception { + MessageListener mock = mock(MessageListener.class); + + MessageListenerAdapter adapter = new MessageListenerAdapter(mock); + adapter.onMessage(STRING_MSG, null); + verify(mock).onMessage(STRING_MSG, null); + } + + + public void testRawMessage() throws Exception { + MessageListenerAdapter adapter = new MessageListenerAdapter(target); + adapter.onMessage(STRING_MSG, null); + + verify(target).handleMessage(PAYLOAD); + } + + + public void testCustomMethod() throws Exception { + MessageListenerAdapter adapter = new MessageListenerAdapter(target); + adapter.setDefaultListenerMethod("customMethod"); + adapter.onMessage(STRING_MSG, null); + + verify(target).customMethod(PAYLOAD); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/RedisMDP.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/RedisMDP.java new file mode 100644 index 000000000..f47a62693 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/RedisMDP.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener.adapter; + +/** + * @author Costin Leau + */ +public class RedisMDP { + + public void handleMessage(String message) { + System.out.println("Received message " + message); + } + + public void anotherHandle(String message) { + System.out.println("[*] Received message " + message); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/ThrowableMessageListener.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/ThrowableMessageListener.java new file mode 100644 index 000000000..2f2903e22 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/listener/adapter/ThrowableMessageListener.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.listener.adapter; + +import org.springframework.data.keyvalue.redis.connection.Message; +import org.springframework.data.keyvalue.redis.connection.MessageListener; + +/** + * + * @author Costin Leau + */ +public class ThrowableMessageListener implements MessageListener { + + @Override + public void onMessage(Message message, byte[] pattern) { + throw new IllegalStateException("throwing exception for message " + message); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/AbstractHashMapperTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/AbstractHashMapperTest.java new file mode 100644 index 000000000..6a6823987 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/AbstractHashMapperTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.mapping; + +import static org.junit.Assert.*; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.data.keyvalue.redis.Address; +import org.springframework.data.keyvalue.redis.Person; +import org.springframework.data.keyvalue.redis.hash.HashMapper; + +/** + * @author Costin Leau + */ +public abstract class AbstractHashMapperTest { + protected abstract HashMapper mapperFor(Class t); + + private void test(Object o) { + HashMapper mapper = mapperFor(o.getClass()); + Map hash = mapper.toHash(o); + System.out.println("object hash " + hash.size() + " is " + hash); + assertEquals(o, mapper.fromHash(hash)); + } + + @Test + public void testSimpleBean() throws Exception { + test(new Address("Broadway", 1)); + } + + @Test + public void testNestedBean() throws Exception { + test(new Person("George", "Enescu", 74, new Address("liveni", 19))); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/BeanUtilsHashMapperTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/BeanUtilsHashMapperTest.java new file mode 100644 index 000000000..bb094c2ca --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/BeanUtilsHashMapperTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.mapping; + +import org.junit.Test; +import org.springframework.data.keyvalue.redis.hash.BeanUtilsHashMapper; +import org.springframework.data.keyvalue.redis.hash.HashMapper; + +/** + * @author Costin Leau + */ +public class BeanUtilsHashMapperTest extends AbstractHashMapperTest { + + @Override + protected HashMapper mapperFor(Class t) { + return new BeanUtilsHashMapper(t); + } + + @Test(expected = Exception.class) + public void testNestedBean() throws Exception { + super.testNestedBean(); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/JacksonHashMapperTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/JacksonHashMapperTest.java new file mode 100644 index 000000000..18b8d5951 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/mapping/JacksonHashMapperTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.mapping; + +import org.springframework.data.keyvalue.redis.hash.HashMapper; +import org.springframework.data.keyvalue.redis.hash.JacksonHashMapper; + +public class JacksonHashMapperTest extends AbstractHashMapperTest { + + @Override + protected HashMapper mapperFor(Class t) { + return new JacksonHashMapper(t); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/serializer/SimpleRedisSerializerTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/serializer/SimpleRedisSerializerTests.java new file mode 100644 index 000000000..01f767ecb --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/serializer/SimpleRedisSerializerTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.serializer; + +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.util.UUID; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.keyvalue.redis.Address; +import org.springframework.data.keyvalue.redis.Person; +import org.springframework.oxm.xstream.XStreamMarshaller; + + +public class SimpleRedisSerializerTests { + + private static class A implements Serializable { + private Integer value = Integer.valueOf(30); + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + A other = (A) obj; + if (value == null) { + if (other.value != null) + return false; + } + else if (!value.equals(other.value)) + return false; + return true; + } + } + + private static class B implements Serializable { + private String name = getClass().getName(); + private A a = new A(); + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((a == null) ? 0 : a.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + B other = (B) obj; + if (a == null) { + if (other.a != null) + return false; + } + else if (!a.equals(other.a)) + return false; + if (name == null) { + if (other.name != null) + return false; + } + else if (!name.equals(other.name)) + return false; + return true; + } + } + + private RedisSerializer serializer; + + @Before + public void setUp() { + serializer = new JdkSerializationRedisSerializer(); + } + + @After + public void tearDown() { + serializer = null; + } + + @Test + public void testBasicSerializationRoundtrip() throws Exception { + Integer integer = new Integer(300); + verifySerializedObjects(new Integer(300), new Double(200), new B()); + } + + private void verifySerializedObjects(Object... objects) { + for (Object object : objects) { + assertEquals("Incorrectly (de)serialized object " + object, object, + serializer.deserialize(serializer.serialize(object))); + } + } + + @Test + public void testStringEncodedSerialization() { + String value = UUID.randomUUID().toString(); + assertEquals(value, serializer.deserialize(serializer.serialize(value))); + assertEquals(value, serializer.deserialize(serializer.serialize(value))); + assertEquals(value, serializer.deserialize(serializer.serialize(value))); + } + + @Test + public void testPersonSerialization() throws Exception { + String value = UUID.randomUUID().toString(); + Person p1 = new Person(value, value, 1, new Address(value, 2)); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + } + + @Test + public void testOxmSerializer() throws Exception { + XStreamMarshaller xstream = new XStreamMarshaller(); + xstream.afterPropertiesSet(); + + OxmSerializer serializer = new OxmSerializer(xstream, xstream); + + String value = UUID.randomUUID().toString(); + Person p1 = new Person(value, value, 1, new Address(value, 2)); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + } + + @Test + public void testJsonSerializer() throws Exception { + JacksonJsonRedisSerializer serializer = new JacksonJsonRedisSerializer(Person.class); + String value = UUID.randomUUID().toString(); + Person p1 = new Person(value, value, 1, new Address(value, 2)); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/BoundKeyOperationsTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/BoundKeyOperationsTest.java new file mode 100644 index 000000000..a7626125f --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/BoundKeyOperationsTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support; + +import static org.junit.Assert.*; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.core.BoundKeyOperations; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.support.collections.ObjectFactory; + +/** + * @author Costin Leau + */ +@RunWith(Parameterized.class) +public class BoundKeyOperationsTest { + private BoundKeyOperations keyOps; + private ObjectFactory objFactory; + private RedisTemplate template; + + public BoundKeyOperationsTest(BoundKeyOperations keyOps, ObjectFactory objFactory, + RedisTemplate template) { + this.objFactory = objFactory; + this.keyOps = keyOps; + this.template = template; + ConnectionFactoryTracker.add(template.getConnectionFactory()); + } + + @After + public void stop() { + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + @Parameters + public static Collection testParams() { + return BoundKeyParams.testParams(); + } + + @Test + public void testRename() throws Exception { + Object key = keyOps.getKey(); + assertNotNull(key); + Object newName = objFactory.instance(); + keyOps.rename(newName); + assertEquals(newName, keyOps.getKey()); + keyOps.rename(key); + assertEquals(key, keyOps.getKey()); + } + @Test + public void testExpire() throws Exception { + assertEquals(Long.valueOf(-1), keyOps.getExpire()); + if (keyOps.expire(10, TimeUnit.SECONDS)) { + long expire = keyOps.getExpire().longValue(); + assertTrue(expire <= 10 && expire > 5); + } + } + + @Test + public void testPersist() throws Exception { + keyOps.persist(); + assertEquals(Long.valueOf(-1), keyOps.getExpire()); + if (keyOps.expire(10, TimeUnit.SECONDS)) { + assertTrue(keyOps.getExpire().longValue() > 0); + } + keyOps.persist(); + assertEquals(-1, keyOps.getExpire().longValue()); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/BoundKeyParams.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/BoundKeyParams.java new file mode 100644 index 000000000..df2156509 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/BoundKeyParams.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support; + +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.jredis.JredisConnectionFactory; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.support.atomic.RedisAtomicInteger; +import org.springframework.data.keyvalue.redis.support.atomic.RedisAtomicLong; +import org.springframework.data.keyvalue.redis.support.collections.DefaultRedisList; +import org.springframework.data.keyvalue.redis.support.collections.DefaultRedisMap; +import org.springframework.data.keyvalue.redis.support.collections.DefaultRedisSet; +import org.springframework.data.keyvalue.redis.support.collections.RedisList; +import org.springframework.data.keyvalue.redis.support.collections.StringObjectFactory; + +/** + * @author Costin Leau + */ +public class BoundKeyParams { + + public static Collection testParams() { + // create Jedis Factory + JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory(); + jedisConnFactory.setPort(SettingsUtils.getPort()); + jedisConnFactory.setHostName(SettingsUtils.getHost()); + jedisConnFactory.afterPropertiesSet(); + + // jredis factory + JredisConnectionFactory jredisConnFactory = new JredisConnectionFactory(); + jredisConnFactory.setUsePool(true); + jredisConnFactory.setPort(SettingsUtils.getPort()); + jredisConnFactory.setHostName(SettingsUtils.getHost()); + jredisConnFactory.afterPropertiesSet(); + + StringRedisTemplate templateJS = new StringRedisTemplate(jedisConnFactory); + StringRedisTemplate templateJR = new StringRedisTemplate(jredisConnFactory); + + StringObjectFactory sof = new StringObjectFactory(); + + DefaultRedisMap mapJS = new DefaultRedisMap("bound:key:map", templateJS); + + DefaultRedisSet setJS = new DefaultRedisSet("bound:key:set", templateJS); + + RedisList list = new DefaultRedisList("bound:key:list", templateJS); + + return Arrays.asList(new Object[][] { + { new RedisAtomicInteger("bound:key:int", jedisConnFactory), sof, templateJS }, + { new RedisAtomicLong("bound:key:long", jedisConnFactory), sof, templateJS }, + { list, sof, templateJS }, { setJS, sof, templateJS }, { mapJS, sof, templateJS } }); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/atomic/AtomicCountersParam.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/atomic/AtomicCountersParam.java new file mode 100644 index 000000000..babd1948f --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/atomic/AtomicCountersParam.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.atomic; + +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; + +/** + * @author Costin Leau + */ +public abstract class AtomicCountersParam { + + public static Collection testParams() { + // create Jedis Factory + JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory(); + jedisConnFactory.setPort(SettingsUtils.getPort()); + jedisConnFactory.setHostName(SettingsUtils.getHost()); + jedisConnFactory.afterPropertiesSet(); + + // jredis factory + // JredisConnectionFactory jredisConnFactory = new JredisConnectionFactory(); + // jredisConnFactory.setUsePool(true); + // jredisConnFactory.setPort(SettingsUtils.getPort()); + // jredisConnFactory.setHostName(SettingsUtils.getHost()); + // jredisConnFactory.afterPropertiesSet(); + + return Arrays.asList(new Object[][] { { jedisConnFactory } }); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicTests.java new file mode 100644 index 000000000..d69d19add --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/atomic/RedisAtomicTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.atomic; + +import static org.junit.Assert.*; + +import java.util.Collection; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory; + +/** + * @author Costin Leau + */ +@RunWith(Parameterized.class) +public class RedisAtomicTests { + + private RedisAtomicInteger intCounter; + private RedisAtomicLong longCounter; + private RedisConnectionFactory factory; + + + public RedisAtomicTests(RedisConnectionFactory factory) { + intCounter = new RedisAtomicInteger(getClass().getSimpleName() + ":int", factory); + longCounter = new RedisAtomicLong(getClass().getSimpleName() + ":long", factory); + this.factory = factory; + ConnectionFactoryTracker.add(factory); + } + + @After + public void stop() { + RedisConnection connection = factory.getConnection(); + connection.flushDb(); + connection.close(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + @Parameters + public static Collection testParams() { + return AtomicCountersParam.testParams(); + } + + @Test + public void testIntCheckAndSet() throws Exception { + intCounter.set(0); + assertFalse(intCounter.compareAndSet(1, 10)); + assertTrue(intCounter.compareAndSet(0, 10)); + assertTrue(intCounter.compareAndSet(10, 0)); + } + + @Test + public void testLongCheckAndSet() throws Exception { + longCounter.set(0); + assertFalse(longCounter.compareAndSet(1, 10)); + assertTrue(longCounter.compareAndSet(0, 10)); + assertTrue(longCounter.compareAndSet(10, 0)); + } + + @Test + public void testLongIncrement() throws Exception { + longCounter.set(0); + assertEquals(1, longCounter.incrementAndGet()); + } + + @Test + public void testIntIncrement() throws Exception { + intCounter.set(0); + assertEquals(1, intCounter.incrementAndGet()); + } + + @Test + public void testLongCustomIncrement() throws Exception { + longCounter.set(0); + long delta = 5; + assertEquals(delta, longCounter.addAndGet(delta)); + } + + @Test + public void testIntCustomIncrement() throws Exception { + intCounter.set(0); + int delta = 5; + assertEquals(delta, intCounter.addAndGet(delta)); + } + + @Test + public void testReadExistingValue() throws Exception { + longCounter.set(5); + RedisAtomicLong keyCopy = new RedisAtomicLong(longCounter.getKey(), factory); + assertEquals(longCounter.get(), keyCopy.get()); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisCollectionTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisCollectionTests.java new file mode 100644 index 000000000..291f60665 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisCollectionTests.java @@ -0,0 +1,305 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.junit.matchers.JUnitMatchers.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.core.RedisCallback; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; + + +/** + * Base test for Redis collections. + * + * @author Costin Leau + */ +@RunWith(Parameterized.class) +public abstract class AbstractRedisCollectionTests { + + protected AbstractRedisCollection collection; + protected ObjectFactory factory; + protected RedisTemplate template; + + @Before + public void setUp() throws Exception { + collection = createCollection(); + } + + abstract AbstractRedisCollection createCollection(); + + abstract RedisStore copyStore(RedisStore store); + + + public AbstractRedisCollectionTests(ObjectFactory factory, RedisTemplate template) { + this.factory = factory; + this.template = template; + ConnectionFactoryTracker.add(template.getConnectionFactory()); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + @Parameters + public static Collection testParams() { + return CollectionTestParams.testParams(); + } + + /** + * Return a new instance of T + * @return + */ + protected T getT() { + return factory.instance(); + } + + @After + public void tearDown() throws Exception { + // remove the collection entirely since clear() doesn't always work + collection.getOperations().delete(Collections.singleton(collection.getKey())); + template.execute(new RedisCallback() { + + @Override + public Object doInRedis(RedisConnection connection) { + connection.flushDb(); + return null; + } + }); + } + + @Test + public void testAdd() { + T t1 = getT(); + assertThat(collection.add(t1), is(true)); + assertThat(collection, hasItem(t1)); + assertEquals(1, collection.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void testAddAll() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + List list = Arrays.asList(t1, t2, t3); + + assertThat(collection.addAll(list), is(true)); + assertThat(collection, hasItem(t1)); + assertThat(collection, hasItem(t2)); + assertThat(collection, hasItem(t3)); + assertEquals(collection.size(), 3); + } + + @Test + public void testClear() { + T t1 = getT(); + assertEquals(0, collection.size()); + collection.add(t1); + assertEquals(1, collection.size()); + collection.clear(); + assertEquals(0, collection.size()); + } + + @Test + public void testContainsObject() { + T t1 = getT(); + assertThat(collection, not(hasItem(t1))); + assertThat(collection.add(t1), is(true)); + assertThat(collection, hasItem(t1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testContainsAll() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + List list = Arrays.asList(t1, t2, t3); + + assertThat(collection.addAll(list), is(true)); + assertThat(collection.containsAll(list), is(true)); + assertThat(collection, hasItems(t1, t2, t3)); + } + + @Test + public void testEquals() { + //assertEquals(collection, copyStore(collection)); + } + + @Test + public void testHashCode() { + assertThat(collection.hashCode(), not(equalTo(collection.getKey().hashCode()))); + } + + @Test + public void testIsEmpty() { + assertEquals(0, collection.size()); + assertTrue(collection.isEmpty()); + collection.add(getT()); + assertEquals(1, collection.size()); + assertFalse(collection.isEmpty()); + collection.clear(); + assertTrue(collection.isEmpty()); + } + + @Test + public void testIterator() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + List list = Arrays.asList(t1, t2, t3, t4); + + assertThat(collection.addAll(list), is(true)); + Iterator iterator = collection.iterator(); + + assertEquals(t1, iterator.next()); + assertEquals(t2, iterator.next()); + assertEquals(t3, iterator.next()); + assertEquals(t4, iterator.next()); + assertFalse(iterator.hasNext()); + } + + @Test + public void testRemoveObject() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + assertEquals(0, collection.size()); + assertThat(collection.add(t1), is(true)); + assertThat(collection.add(t2), is(true)); + assertEquals(2, collection.size()); + assertThat(collection.remove(t3), is(false)); + assertThat(collection.remove(t2), is(true)); + assertThat(collection.remove(t2), is(false)); + assertEquals(1, collection.size()); + assertThat(collection.remove(t1), is(true)); + assertEquals(0, collection.size()); + } + + @Test + public void removeAll() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + List list = Arrays.asList(t1, t2, t3); + + assertThat(collection.addAll(list), is(true)); + assertThat(collection.containsAll(list), is(true)); + assertThat(collection, hasItems(t1, t2, t3)); + + List newList = Arrays.asList(getT(), getT()); + List partialList = Arrays.asList(getT(), t1, getT()); + + assertThat(collection.removeAll(newList), is(false)); + assertThat(collection.removeAll(partialList), is(true)); + assertThat(collection, not(hasItem(t1))); + assertThat(collection, hasItems(t2, t3)); + assertThat(collection.removeAll(list), is(true)); + assertThat(collection, not(hasItems(t2, t3))); + } + + @Test(expected = UnsupportedOperationException.class) + public void testRetainAll() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + List list = Arrays.asList(t1, t2); + List newList = Arrays.asList(t2, t3); + + assertThat(collection.addAll(list), is(true)); + assertThat(collection, hasItems(t1, t2)); + assertThat(collection.retainAll(newList), is(true)); + assertThat(collection, not(hasItem(t1))); + assertThat(collection, hasItem(t2)); + } + + @Test + public void testSize() { + assertEquals(0, collection.size()); + assertTrue(collection.isEmpty()); + collection.add(getT()); + assertEquals(1, collection.size()); + collection.add(getT()); + collection.add(getT()); + assertEquals(3, collection.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void testToArray() { + Object[] expectedArray = new Object[] { getT(), getT(), getT() }; + List list = (List) Arrays.asList(expectedArray); + + assertThat(collection.addAll(list), is(true)); + + Object[] array = collection.toArray(); + assertArrayEquals(expectedArray, array); + } + + @SuppressWarnings("unchecked") + @Test + public void testToArrayWithGenerics() { + Object[] expectedArray = new Object[] { getT(), getT(), getT() }; + List list = (List) Arrays.asList(expectedArray); + + assertThat(collection.addAll(list), is(true)); + + Object[] array = collection.toArray(new Object[expectedArray.length]); + assertArrayEquals(expectedArray, array); + } + + @Test + public void testToString() { + String name = collection.toString(); + collection.add(getT()); + assertEquals(name, collection.toString()); + } + + @Test + public void testGetKey() throws Exception { + assertNotNull(collection.getKey()); + } + + protected boolean isJredis() { + return template.getConnectionFactory().getClass().getSimpleName().startsWith("Jredis"); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisListTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisListTests.java new file mode 100644 index 000000000..4078e5a15 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisListTests.java @@ -0,0 +1,498 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import static org.junit.Assert.*; +import static org.junit.matchers.JUnitMatchers.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; + +/** + * Integration test for RedisList + * + * @author Costin Leau + */ +public abstract class AbstractRedisListTests extends AbstractRedisCollectionTests { + + protected RedisList list; + + /** + * Constructs a new AbstractRedisListTests instance. + * + * @param factory + * @param template + */ + public AbstractRedisListTests(ObjectFactory factory, RedisTemplate template) { + super(factory, template); + } + + @SuppressWarnings("unchecked") + @Before + public void setUp() throws Exception { + super.setUp(); + list = (RedisList) collection; + } + + @Test + public void testAddIndexObjectHead() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + + assertEquals(t1, list.get(0)); + list.add(0, t3); + assertEquals(t3, list.get(0)); + } + + @Test + public void testAddIndexObjectTail() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + + assertEquals(t2, list.get(1)); + list.add(2, t3); + assertEquals(t3, list.get(2)); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddIndexObjectMiddle() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + + assertEquals(t1, list.get(0)); + list.add(1, t3); + } + + @Test + public void addAllIndexCollectionHead() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + list.add(t1); + list.add(t2); + + List asList = Arrays.asList(t3, t4); + + assertEquals(t1, list.get(0)); + list.addAll(0, asList); + // verify insertion order + assertEquals(t3, list.get(0)); + assertEquals(t4, list.get(1)); + } + + @Test + public void addAllIndexCollectionTail() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + list.add(t1); + list.add(t2); + + List asList = Arrays.asList(t3, t4); + + assertEquals(t1, list.get(0)); + assertTrue(list.addAll(2, asList)); + + // verify insertion order + assertEquals(t3, list.get(2)); + assertEquals(t4, list.get(3)); + } + + @Test(expected = IllegalArgumentException.class) + public void addAllIndexCollectionMiddle() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + list.add(t1); + list.add(t2); + + List asList = Arrays.asList(t3, t4); + + assertEquals(t1, list.get(0)); + assertTrue(list.addAll(1, asList)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIndexOfObject() { + T t1 = getT(); + T t2 = getT(); + + assertEquals(-1, list.indexOf(t1)); + list.add(t1); + assertEquals(0, list.indexOf(t1)); + + assertEquals(-1, list.indexOf(t2)); + list.add(t2); + assertEquals(1, list.indexOf(t1)); + } + + @Test + public void testOffer() { + T t1 = getT(); + + assertTrue(list.offer(t1)); + assertTrue(list.contains(t1)); + } + + @Test + public void testPeek() { + assertNull(list.peek()); + T t1 = getT(); + list.add(t1); + assertEquals(t1, list.peek()); + list.clear(); + assertNull(list.peek()); + } + + @Test + public void testElement() { + try { + list.element(); + fail(); + } catch (NoSuchElementException nse) { + // expected + } + + T t1 = getT(); + list.add(t1); + assertEquals(t1, list.element()); + list.clear(); + try { + list.element(); + fail(); + } catch (NoSuchElementException nse) { + // expected + } + } + + @Test + public void testPop() { + testPoll(); + } + + @Test + public void testPoll() { + assertNull(list.poll()); + T t1 = getT(); + list.add(t1); + assertEquals(t1, list.poll()); + assertNull(list.poll()); + } + + @Test + public void testRemove() { + try { + list.remove(); + fail(); + } catch (NoSuchElementException nse) { + // expected + } + + T t1 = getT(); + list.add(t1); + assertEquals(t1, list.remove()); + try { + list.remove(); + fail(); + } catch (NoSuchElementException nse) { + // expected + } + } + + @Test + public void testRange() { + T t1 = getT(); + T t2 = getT(); + + assertTrue(list.range(0, -1).isEmpty()); + list.add(t1); + list.add(t2); + assertEquals(2, list.range(0, -1).size()); + assertEquals(t1, list.range(0, 0).get(0)); + assertEquals(t2, list.range(1, 1).get(0)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testRemoveIndex() { + T t1 = getT(); + T t2 = getT(); + + assertNull(list.remove(0)); + list.add(t1); + list.add(t2); + assertNull(list.remove(2)); + assertEquals(t2, list.remove(1)); + assertEquals(t1, list.remove(0)); + } + + @Test + public void testTrim() { + T t1 = getT(); + T t2 = getT(); + + assertTrue(list.trim(0, 0).isEmpty()); + list.add(t1); + list.add(t2); + assertEquals(2, list.size()); + assertEquals(1, list.trim(0, 0).size()); + assertEquals(1, list.size()); + assertEquals(t1, list.get(0)); + } + + @Test + public void testCappedCollection() throws Exception { + RedisList cappedList = new DefaultRedisList(template.boundListOps(collection.getKey() + ":capped"), 1); + T first = getT(); + cappedList.offer(first); + assertEquals(1, cappedList.size()); + cappedList.add(getT()); + assertEquals(1, cappedList.size()); + T last = getT(); + cappedList.add(last); + assertEquals(1, cappedList.size()); + assertEquals(first, cappedList.get(0)); + } + + @Test + public void testAddFirst() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.addFirst(t1); + list.addFirst(t2); + list.addFirst(t3); + + Iterator iterator = list.iterator(); + assertEquals(t3, iterator.next()); + assertEquals(t2, iterator.next()); + assertEquals(t1, iterator.next()); + } + + @Test + public void testAddLast() { + testAdd(); + } + + @Test + public void testDescendingIterator() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + list.add(t3); + + Iterator iterator = list.descendingIterator(); + assertEquals(t3, iterator.next()); + assertEquals(t2, iterator.next()); + assertEquals(t1, iterator.next()); + + } + + @Test + public void testDrainToCollectionWithMaxElements() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + list.add(t3); + + List c = new ArrayList(); + + list.drainTo(c, 2); + assertEquals(1, list.size()); + assertThat(list, hasItem(t3)); + assertEquals(2, c.size()); + assertThat(c, hasItems(t1, t2)); + } + + @Test + public void testDrainToCollection() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + list.add(t3); + + List c = new ArrayList(); + + list.drainTo(c); + assertTrue(list.isEmpty()); + assertEquals(3, c.size()); + assertThat(c, hasItems(t1, t2, t3)); + } + + @Test + public void testGetFirst() { + T t1 = getT(); + T t2 = getT(); + + list.add(t1); + list.add(t2); + + assertEquals(t1, list.getFirst()); + } + + @Test + public void testLast() { + testAdd(); + } + + @Test + public void testOfferFirst() { + testAddFirst(); + } + + @Test + public void testOfferLast() { + testAddLast(); + } + + @Test + public void testPeekFirst() { + testPeek(); + } + + @Test + public void testPeekLast() { + T t1 = getT(); + T t2 = getT(); + + list.add(t1); + list.add(t2); + + assertEquals(t2, list.peekLast()); + assertEquals(2, list.size()); + } + + @Test + public void testPollFirst() { + testPoll(); + } + + @Test + public void testPollLast() { + T t1 = getT(); + T t2 = getT(); + + list.add(t1); + list.add(t2); + + T last = list.pollLast(); + assertEquals(t2, last); + assertEquals(1, list.size()); + assertThat(list, hasItem(t1)); + } + + @Test + public void testPut() { + testOffer(); + } + + @Test + public void testPutFirst() { + testAdd(); + } + + @Test + public void testPutLast() { + testPut(); + } + + @Test + public void testRemainingCapacity() { + assertEquals(Integer.MAX_VALUE, list.remainingCapacity()); + } + + @Test + public void testRemoveFirst() { + testPop(); + } + + @Test + public void testRemoveFirstOccurrence() { + testRemove(); + } + + @Test + public void testRemoveLast() { + testPollLast(); + } + + @Test + public void testRmoveLastOccurrence() { + T t1 = getT(); + T t2 = getT(); + + list.add(t1); + list.add(t2); + list.add(t1); + list.add(t2); + + list.removeLastOccurrence(t2); + assertEquals(3, list.size()); + Iterator iterator = list.iterator(); + assertEquals(t1, iterator.next()); + assertEquals(t2, iterator.next()); + assertEquals(t1, iterator.next()); + } + + @Test + public void testTake() { + testPoll(); + } + + @Test + public void testTakeFirst() { + testTake(); + } + + @Test + public void testTakeLast() { + testPollLast(); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisMapTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisMapTests.java new file mode 100644 index 000000000..f9d266355 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisMapTests.java @@ -0,0 +1,418 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import static org.junit.matchers.JUnitMatchers.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.core.RedisCallback; +import org.springframework.data.keyvalue.redis.core.RedisOperations; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; + +/** + * Integration test for Redis Map. + * + * @author Costin Leau + */ +@RunWith(Parameterized.class) +public abstract class AbstractRedisMapTests { + + protected RedisMap map; + protected ObjectFactory keyFactory; + protected ObjectFactory valueFactory; + protected RedisTemplate template; + + abstract RedisMap createMap(); + + @Before + public void setUp() throws Exception { + map = createMap(); + } + + public AbstractRedisMapTests(ObjectFactory keyFactory, ObjectFactory valueFactory, RedisTemplate template) { + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + this.template = template; + ConnectionFactoryTracker.add(template.getConnectionFactory()); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + protected K getKey() { + return keyFactory.instance(); + } + + protected V getValue() { + return valueFactory.instance(); + } + + protected RedisStore copyStore(RedisStore store) { + return new DefaultRedisMap(store.getKey(), store.getOperations()); + } + + @After + public void tearDown() throws Exception { + // remove the collection entirely since clear() doesn't always work + map.getOperations().delete(Collections.singleton(map.getKey())); + template.execute(new RedisCallback() { + + @Override + public Object doInRedis(RedisConnection connection) { + connection.flushDb(); + return null; + } + }); + } + + @Test + public void testClear() { + map.clear(); + assertEquals(0, map.size()); + map.put(getKey(), getValue()); + assertEquals(1, map.size()); + map.clear(); + assertEquals(0, map.size()); + } + + @Test + public void testContainsKey() { + K k1 = getKey(); + K k2 = getKey(); + + assertFalse(map.containsKey(k1)); + assertFalse(map.containsKey(k2)); + map.put(k1, getValue()); + assertTrue(map.containsKey(k1)); + map.put(k2, getValue()); + assertTrue(map.containsKey(k2)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testContainsValue() { + V v1 = getValue(); + V v2 = getValue(); + + assertFalse(map.containsValue(v1)); + assertFalse(map.containsValue(v2)); + map.put(getKey(), v1); + assertTrue(map.containsValue(v1)); + map.put(getKey(), v2); + assertTrue(map.containsValue(v2)); + } + + public Set> entrySet() { + return map.entrySet(); + } + + @Test + public void testEquals() { + RedisStore clone = copyStore(map); + assertEquals(clone, map); + assertEquals(clone, clone); + assertEquals(map, map); + } + + @Test + public void testNotEquals() { + RedisOperations ops = map.getOperations(); + RedisStore newInstance = new DefaultRedisMap(ops. boundHashOps(map.getKey() + ":new")); + assertFalse(map.equals(newInstance)); + assertFalse(newInstance.equals(map)); + } + + @Test + public void testGet() { + K k1 = getKey(); + V v1 = getValue(); + + assertNull(map.get(UUID.randomUUID().toString())); + assertNull(map.get(k1)); + map.put(k1, v1); + assertEquals(v1, map.get(k1)); + } + + @Test + public void testGetKey() { + assertNotNull(map.getKey()); + } + + @Test + public void testGetOperations() { + assertEquals(template, map.getOperations()); + } + + @Test + public void testHashCode() { + assertThat(map.hashCode(), not(equalTo(map.getKey().hashCode()))); + assertEquals(map.hashCode(), copyStore(map).hashCode()); + } + + @Test + public void testIncrement() { + assumeTrue(!isJredis()); + K k1 = getKey(); + V v1 = getValue(); + + map.put(k1, v1); + try { + Long value = map.increment(k1, 1); + System.out.println("Value is " + value); + } catch (InvalidDataAccessApiUsageException ex) { + // expected + } + } + + @Test + public void testIsEmpty() { + map.clear(); + assertTrue(map.isEmpty()); + map.put(getKey(), getValue()); + assertFalse(map.isEmpty()); + map.clear(); + assertTrue(map.isEmpty()); + } + + @Test + public void testKeySet() { + map.clear(); + assertTrue(map.keySet().isEmpty()); + K k1 = getKey(); + K k2 = getKey(); + K k3 = getKey(); + + map.put(k1, getValue()); + map.put(k2, getValue()); + map.put(k3, getValue()); + + Set keySet = map.keySet(); + assertThat(keySet, hasItems(k1, k2, k3)); + assertEquals(3, keySet.size()); + } + + @Test + public void testPut() { + K k1 = getKey(); + K k2 = getKey(); + V v1 = getValue(); + V v2 = getValue(); + + map.put(k1, v1); + map.put(k2, v2); + + assertEquals(v1, map.get(k1)); + assertEquals(v2, map.get(k2)); + } + + @Test + public void testPutAll() { + assumeTrue(!isJredis()); + Map m = new LinkedHashMap(); + K k1 = getKey(); + K k2 = getKey(); + + V v1 = getValue(); + V v2 = getValue(); + + m.put(k1, v1); + m.put(k2, v2); + + assertNull(map.get(k1)); + assertNull(map.get(k2)); + + map.putAll(m); + + assertEquals(v1, map.get(k1)); + assertEquals(v2, map.get(k2)); + } + + @Test + public void testRemove() { + K k1 = getKey(); + K k2 = getKey(); + + V v1 = getValue(); + V v2 = getValue(); + + assertNull(map.remove(k1)); + assertNull(map.remove(k2)); + + map.put(k1, v1); + map.put(k2, v2); + + assertEquals(v1, map.remove(k1)); + assertNull(map.remove(k1)); + assertNull(map.get(k1)); + + assertEquals(v2, map.remove(k2)); + assertNull(map.remove(k2)); + assertNull(map.get(k2)); + } + + @Test + public void testSize() { + assertEquals(0, map.size()); + map.put(getKey(), getValue()); + assertEquals(1, map.size()); + K k = getKey(); + map.put(k, getValue()); + assertEquals(2, map.size()); + map.remove(k); + assertEquals(1, map.size()); + + map.clear(); + assertEquals(0, map.size()); + } + + @Test + public void testValues() { + V v1 = getValue(); + V v2 = getValue(); + V v3 = getValue(); + + map.put(getKey(), v1); + map.put(getKey(), v2); + + Collection values = map.values(); + assertEquals(2, values.size()); + assertThat(values, hasItems(v1, v2)); + + map.put(getKey(), v3); + values = map.values(); + assertEquals(3, values.size()); + assertThat(values, hasItems(v1, v2, v3)); + } + + @Test + public void testEntrySet() { + assumeTrue(!isJredis()); + Set> entries = map.entrySet(); + assertTrue(entries.isEmpty()); + + K k1 = getKey(); + K k2 = getKey(); + + V v1 = getValue(); + V v2 = getValue(); + + map.put(k1, v1); + map.put(k2, v1); + + entries = map.entrySet(); + + Set keys = new LinkedHashSet(); + Collection values = new ArrayList(); + + for (Entry entry : entries) { + keys.add(entry.getKey()); + values.add(entry.getValue()); + } + + assertEquals(2, keys.size()); + + assertThat(keys, hasItems(k1, k2)); + assertThat(values, hasItem(v1)); + assertThat(values, not(hasItem(v2))); + } + + + @Test(expected = UnsupportedOperationException.class) + public void testConcurrentPutIfAbsent() { + K k1 = getKey(); + K k2 = getKey(); + + V v1 = getValue(); + V v2 = getValue(); + + assertNull(map.get(k1)); + assertNull(map.putIfAbsent(k1, v1)); + assertEquals(v1, map.putIfAbsent(k1, v2)); + assertEquals(v1, map.get(k1)); + + assertNull(map.putIfAbsent(k2, v2)); + assertEquals(v2, map.putIfAbsent(k2, v1)); + + assertEquals(v2, map.get(k2)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testConcurrentRemove() { + K k1 = getKey(); + V v1 = getValue(); + V v2 = getValue(); + + map.put(k1, v1); + assertFalse(map.remove(k1, v1)); + assertEquals(v1, map.get(k1)); + assertTrue(map.remove(k1, v1)); + assertNull(map.get(k1)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testConcurrentReplaceTwoArgs() { + K k1 = getKey(); + V v1 = getValue(); + V v2 = getValue(); + + map.put(k1, v1); + + assertFalse(map.replace(k1, v2, v1)); + assertEquals(v1, map.get(k1)); + assertTrue(map.replace(k1, v1, v2)); + assertEquals(v2, map.get(k1)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testConcurrentReplaceOneArg() { + K k1 = getKey(); + V v1 = getValue(); + V v2 = getValue(); + + assertNull(map.replace(k1, v1)); + map.put(k1, v1); + assertNull(map.replace(getKey(), v1)); + assertEquals(v1, map.replace(k1, v2)); + assertEquals(v2, map.get(k1)); + + } + + private boolean isJredis() { + return template.getConnectionFactory().getClass().getSimpleName().startsWith("Jredis"); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisSetTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisSetTests.java new file mode 100644 index 000000000..b06c1b139 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisSetTests.java @@ -0,0 +1,265 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.junit.matchers.JUnitMatchers.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.keyvalue.redis.core.BoundSetOperations; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; + +/** + * Integration test for Redis set. + * + * @author Costin Leau + */ +public abstract class AbstractRedisSetTests extends AbstractRedisCollectionTests { + + protected RedisSet set; + + + /** + * Constructs a new AbstractRedisSetTests instance. + * + * @param factory + * @param template + */ + public AbstractRedisSetTests(ObjectFactory factory, RedisTemplate template) { + super(factory, template); + } + + + @SuppressWarnings("unchecked") + @Before + public void setUp() throws Exception { + super.setUp(); + set = (RedisSet) collection; + } + + private RedisSet createSetFor(String key) { + return new DefaultRedisSet((BoundSetOperations) set.getOperations().boundSetOps(key)); + } + + @Test + public void testDiff() { + RedisSet diffSet1 = createSetFor("test:set:diff1"); + RedisSet diffSet2 = createSetFor("test:set:diff2"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + set.add(t1); + set.add(t2); + set.add(t3); + + diffSet1.add(t2); + diffSet2.add(t3); + + Set diff = set.diff(Arrays.asList(diffSet1, diffSet2)); + assertEquals(1, diff.size()); + assertThat(diff, hasItem(t1)); + } + + @Test + public void testDiffAndStore() { + RedisSet diffSet1 = createSetFor("test:set:diff1"); + RedisSet diffSet2 = createSetFor("test:set:diff2"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + set.add(t1); + set.add(t2); + set.add(t3); + + diffSet1.add(t2); + diffSet2.add(t3); + diffSet2.add(t4); + + String resultName = "test:set:diff:result:1"; + RedisSet diff = set.diffAndStore(Arrays.asList(diffSet1, diffSet2), resultName); + + assertEquals(1, diff.size()); + assertThat(diff, hasItem(t1)); + assertEquals(resultName, diff.getKey()); + } + + @Test + public void testIntersect() { + RedisSet intSet1 = createSetFor("test:set:int1"); + RedisSet intSet2 = createSetFor("test:set:int2"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + set.add(t1); + set.add(t2); + set.add(t3); + + intSet1.add(t2); + intSet1.add(t4); + intSet2.add(t2); + intSet2.add(t3); + + Set inter = set.intersect(Arrays.asList(intSet1, intSet2)); + assertEquals(1, inter.size()); + assertThat(inter, hasItem(t2)); + } + + + public void testIntersectAndStore() { + RedisSet intSet1 = createSetFor("test:set:int1"); + RedisSet intSet2 = createSetFor("test:set:int2"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + set.add(t1); + set.add(t2); + set.add(t3); + + intSet1.add(t2); + intSet1.add(t4); + intSet2.add(t2); + intSet2.add(t3); + + String resultName = "test:set:intersect:result:1"; + RedisSet inter = set.intersectAndStore(Arrays.asList(intSet1, intSet2), resultName); + assertEquals(1, inter.size()); + assertThat(inter, hasItem(t2)); + assertEquals(resultName, inter.getKey()); + } + + @Test + public void testUnion() { + RedisSet unionSet1 = createSetFor("test:set:union1"); + RedisSet unionSet2 = createSetFor("test:set:union2"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + set.add(t1); + set.add(t2); + + unionSet1.add(t2); + unionSet1.add(t4); + unionSet2.add(t3); + + Set union = set.union(Arrays.asList(unionSet1, unionSet2)); + assertEquals(4, union.size()); + assertThat(union, hasItems(t1, t2, t3, t4)); + } + + @Test + public void testUnionAndStore() { + RedisSet unionSet1 = createSetFor("test:set:union1"); + RedisSet unionSet2 = createSetFor("test:set:union2"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + set.add(t1); + set.add(t2); + + unionSet1.add(t2); + unionSet1.add(t4); + unionSet2.add(t3); + + String resultName = "test:set:union:result:1"; + RedisSet union = set.unionAndStore(Arrays.asList(unionSet1, unionSet2), resultName); + assertEquals(4, union.size()); + assertThat(union, hasItems(t1, t2, t3, t4)); + assertEquals(resultName, union.getKey()); + } + + @Test + public void testIterator() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + List list = Arrays.asList(t1, t2, t3, t4); + + assertThat(collection.addAll(list), is(true)); + Iterator iterator = collection.iterator(); + + List result = new ArrayList(list); + + while (iterator.hasNext()) { + result.remove(iterator.next()); + } + + assertEquals(0, result.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void testToArray() { + Object[] expectedArray = new Object[] { getT(), getT(), getT() }; + List list = (List) Arrays.asList(expectedArray); + + assertThat(collection.addAll(list), is(true)); + + Object[] array = collection.toArray(); + + List result = new ArrayList(list); + + for (int i = 0; i < array.length; i++) { + result.remove(array[i]); + } + + assertEquals(0, result.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void testToArrayWithGenerics() { + Object[] expectedArray = new Object[] { getT(), getT(), getT() }; + List list = (List) Arrays.asList(expectedArray); + + assertThat(collection.addAll(list), is(true)); + + Object[] array = collection.toArray(new Object[expectedArray.length]); + List result = new ArrayList(list); + + for (int i = 0; i < array.length; i++) { + result.remove(array[i]); + } + + assertEquals(0, result.size()); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisZSetTest.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisZSetTest.java new file mode 100644 index 000000000..6a96f001c --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/AbstractRedisZSetTest.java @@ -0,0 +1,395 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import static org.junit.matchers.JUnitMatchers.*; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.keyvalue.redis.core.BoundZSetOperations; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; + +/** + * Integration test for Redis ZSet. + * + * @author Costin Leau + */ +public abstract class AbstractRedisZSetTest extends AbstractRedisCollectionTests { + + protected RedisZSet zSet; + + /** + * Constructs a new AbstractRedisZSetTest instance. + * + * @param factory + * @param template + */ + public AbstractRedisZSetTest(ObjectFactory factory, RedisTemplate template) { + super(factory, template); + } + + @SuppressWarnings("unchecked") + @Before + public void setUp() throws Exception { + super.setUp(); + zSet = (RedisZSet) collection; + } + + @Test + public void testAddWithScore() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + Iterator iterator = zSet.iterator(); + assertEquals(t1, iterator.next()); + assertEquals(t2, iterator.next()); + assertEquals(t3, iterator.next()); + assertFalse(iterator.hasNext()); + } + + @Test + public void testAdd() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1); + zSet.add(t2); + zSet.add(t3); + + Double d = new Double("1"); + + assertEquals(d, zSet.score(t1)); + assertEquals(d, zSet.score(t2)); + assertEquals(d, zSet.score(t3)); + } + + + @Test + public void testFirst() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + assertEquals(3, zSet.size()); + assertEquals(t1, zSet.first()); + } + + @Test(expected = NoSuchElementException.class) + public void testFirstException() throws Exception { + zSet.first(); + } + + @Test + public void testLast() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + assertEquals(3, zSet.size()); + assertEquals(t3, zSet.last()); + } + + @Test(expected = NoSuchElementException.class) + public void testLastException() throws Exception { + zSet.last(); + } + + @Test + public void testRank() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + assertEquals(Long.valueOf(0), zSet.rank(t1)); + assertEquals(Long.valueOf(1), zSet.rank(t2)); + assertEquals(Long.valueOf(2), zSet.rank(t3)); + assertNull(zSet.rank(getT())); + //assertNull(); + } + + @Test + public void testReverseRank() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + assertEquals(Long.valueOf(0), zSet.reverseRank(t3)); + assertEquals(Long.valueOf(1), zSet.reverseRank(t2)); + assertEquals(Long.valueOf(2), zSet.reverseRank(t1)); + assertNull(zSet.rank(getT())); + } + + @Test + public void testScore() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + assertNull(zSet.score(getT())); + assertEquals(Double.valueOf(3), zSet.score(t1)); + assertEquals(Double.valueOf(4), zSet.score(t2)); + assertEquals(Double.valueOf(5), zSet.score(t3)); + } + + @Test + public void testDefaultScore() { + assertEquals(1, zSet.getDefaultScore(), 0); + } + + private RedisZSet createZSetFor(String key) { + return new DefaultRedisZSet((BoundZSetOperations) zSet.getOperations().boundZSetOps(key)); + } + + @Test + public void testIntersectAndStore() { + assumeTrue(!isJredis()); + RedisZSet interSet1 = createZSetFor("test:zset:inter1"); + RedisZSet interSet2 = createZSetFor("test:zset:inter"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + + interSet1.add(t2, 2); + interSet1.add(t4, 3); + interSet2.add(t2, 2); + interSet2.add(t3, 3); + + String resultName = "test:zset:inter:result:1"; + RedisZSet inter = zSet.intersectAndStore(Arrays.asList(interSet1, interSet2), resultName); + + assertEquals(1, inter.size()); + assertThat(inter, hasItem(t2)); + assertEquals(Double.valueOf(6), inter.score(t2)); + assertEquals(resultName, inter.getKey()); + } + + @Test + public void testRange() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + + Set range = zSet.range(1, 2); + assertEquals(2, range.size()); + Iterator iterator = range.iterator(); + assertEquals(t2, iterator.next()); + assertEquals(t3, iterator.next()); + } + + @Test + public void testReverseRange() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + + Set range = zSet.reverseRange(1, 2); + assertEquals(2, range.size()); + Iterator iterator = range.iterator(); + assertEquals(t2, iterator.next()); + assertEquals(t1, iterator.next()); + } + + public void testRangeByScore() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + + Set range = zSet.rangeByScore(1.5, 3.5); + assertEquals(2, range.size()); + assertThat(range, hasItems(t2, t3)); + + Iterator iterator = range.iterator(); + assertEquals(t2, iterator.next()); + assertEquals(t3, iterator.next()); + } + + @Test + public void testRemove() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + zSet.add(t4, 4); + + zSet.remove(1, 2); + + assertEquals(2, zSet.size()); + Iterator iterator = zSet.iterator(); + assertEquals(t1, iterator.next()); + assertEquals(t4, iterator.next()); + } + + @Test + public void testRemoveByScore() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + zSet.add(t4, 4); + + zSet.removeByScore(1.5, 2.5); + + assertEquals(3, zSet.size()); + Iterator iterator = zSet.iterator(); + assertEquals(t1, iterator.next()); + assertEquals(t3, iterator.next()); + assertEquals(t4, iterator.next()); + } + + @Test + public void testUnionAndStore() { + assumeTrue(!isJredis()); + RedisZSet unionSet1 = createZSetFor("test:zset:union1"); + RedisZSet unionSet2 = createZSetFor("test:zset:union2"); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + + unionSet1.add(t2, 2); + unionSet1.add(t4, 5); + unionSet2.add(t3, 6); + + String resultName = "test:zset:union:result:1"; + RedisZSet union = zSet.unionAndStore(Arrays.asList(unionSet1, unionSet2), resultName); + assertEquals(4, union.size()); + assertThat(union, hasItems(t1, t2, t3, t4)); + assertEquals(resultName, union.getKey()); + + assertEquals(Double.valueOf(1), union.score(t1)); + assertEquals(Double.valueOf(4), union.score(t2)); + assertEquals(Double.valueOf(6), union.score(t3)); + assertEquals(Double.valueOf(5), union.score(t4)); + } + + @Test + public void testIterator() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + zSet.add(t4, 4); + + Iterator iterator = collection.iterator(); + + assertEquals(t1, iterator.next()); + assertEquals(t2, iterator.next()); + assertEquals(t3, iterator.next()); + assertEquals(t4, iterator.next()); + assertFalse(iterator.hasNext()); + } + + @SuppressWarnings("unchecked") + @Test + public void testToArray() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + zSet.add(t4, 4); + + Object[] array = collection.toArray(); + assertArrayEquals(new Object[] { t1, t2, t3, t4 }, array); + } + + @SuppressWarnings("unchecked") + @Test + public void testToArrayWithGenerics() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + + zSet.add(t1, 1); + zSet.add(t2, 2); + zSet.add(t3, 3); + zSet.add(t4, 4); + + Object[] array = collection.toArray(new Object[zSet.size()]); + assertArrayEquals(new Object[] { t1, t2, t3, t4 }, array); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/CollectionTestParams.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/CollectionTestParams.java new file mode 100644 index 000000000..4464c94ae --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/CollectionTestParams.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.data.keyvalue.redis.Person; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.jredis.JredisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.rjc.RjcConnectionFactory; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.serializer.JacksonJsonRedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.OxmSerializer; +import org.springframework.oxm.xstream.XStreamMarshaller; + +/** + * @author Costin Leau + */ +public abstract class CollectionTestParams { + + public static Collection testParams() { + // XStream serializer + XStreamMarshaller xstream = new XStreamMarshaller(); + try { + xstream.afterPropertiesSet(); + } catch (Exception ex) { + throw new RuntimeException("Cannot init XStream", ex); + } + OxmSerializer serializer = new OxmSerializer(xstream, xstream); + JacksonJsonRedisSerializer jsonSerializer = new JacksonJsonRedisSerializer(Person.class); + + // create Jedis Factory + ObjectFactory stringFactory = new StringObjectFactory(); + ObjectFactory personFactory = new PersonObjectFactory(); + + JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory(); + jedisConnFactory.setUsePool(true); + + jedisConnFactory.setPort(SettingsUtils.getPort()); + jedisConnFactory.setHostName(SettingsUtils.getHost()); + + jedisConnFactory.afterPropertiesSet(); + + RedisTemplate stringTemplate = new StringRedisTemplate(jedisConnFactory); + RedisTemplate personTemplate = new RedisTemplate(); + personTemplate.setConnectionFactory(jedisConnFactory); + personTemplate.afterPropertiesSet(); + + RedisTemplate xstreamStringTemplate = new RedisTemplate(); + xstreamStringTemplate.setConnectionFactory(jedisConnFactory); + xstreamStringTemplate.setDefaultSerializer(serializer); + xstreamStringTemplate.afterPropertiesSet(); + + RedisTemplate xstreamPersonTemplate = new RedisTemplate(); + xstreamPersonTemplate.setConnectionFactory(jedisConnFactory); + xstreamPersonTemplate.setValueSerializer(serializer); + xstreamPersonTemplate.afterPropertiesSet(); + + // json + RedisTemplate jsonPersonTemplate = new RedisTemplate(); + jsonPersonTemplate.setConnectionFactory(jedisConnFactory); + jsonPersonTemplate.setValueSerializer(jsonSerializer); + jsonPersonTemplate.afterPropertiesSet(); + + // jredis + JredisConnectionFactory jredisConnFactory = new JredisConnectionFactory(); + jredisConnFactory.setUsePool(true); + + jredisConnFactory.setPort(SettingsUtils.getPort()); + jredisConnFactory.setHostName(SettingsUtils.getHost()); + + jredisConnFactory.afterPropertiesSet(); + + RedisTemplate stringTemplateJR = new StringRedisTemplate(jredisConnFactory); + RedisTemplate personTemplateJR = new RedisTemplate(); + personTemplateJR.setConnectionFactory(jredisConnFactory); + personTemplateJR.afterPropertiesSet(); + + RedisTemplate xstreamStringTemplateJR = new RedisTemplate(); + xstreamStringTemplateJR.setConnectionFactory(jredisConnFactory); + xstreamStringTemplateJR.setDefaultSerializer(serializer); + xstreamStringTemplateJR.afterPropertiesSet(); + + RedisTemplate xstreamPersonTemplateJR = new RedisTemplate(); + xstreamPersonTemplateJR.setValueSerializer(serializer); + xstreamPersonTemplateJR.setConnectionFactory(jredisConnFactory); + xstreamPersonTemplateJR.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplateJR = new RedisTemplate(); + jsonPersonTemplateJR.setValueSerializer(jsonSerializer); + jsonPersonTemplateJR.setConnectionFactory(jredisConnFactory); + jsonPersonTemplateJR.afterPropertiesSet(); + + + // rjc + RjcConnectionFactory rjcConnFactory = new RjcConnectionFactory(); + rjcConnFactory.setUsePool(true); + rjcConnFactory.setPort(SettingsUtils.getPort()); + rjcConnFactory.setHostName(SettingsUtils.getHost()); + rjcConnFactory.afterPropertiesSet(); + + RedisTemplate stringTemplateRJC = new StringRedisTemplate(rjcConnFactory); + RedisTemplate personTemplateRJC = new RedisTemplate(); + personTemplateRJC.setConnectionFactory(rjcConnFactory); + personTemplateRJC.afterPropertiesSet(); + + RedisTemplate xstreamStringTemplateRJC = new RedisTemplate(); + xstreamStringTemplateRJC.setConnectionFactory(rjcConnFactory); + xstreamStringTemplateRJC.setDefaultSerializer(serializer); + xstreamStringTemplateRJC.afterPropertiesSet(); + + RedisTemplate xstreamPersonTemplateRJC = new RedisTemplate(); + xstreamPersonTemplateRJC.setValueSerializer(serializer); + xstreamPersonTemplateRJC.setConnectionFactory(rjcConnFactory); + xstreamPersonTemplateRJC.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplateRJC = new RedisTemplate(); + jsonPersonTemplateRJC.setValueSerializer(jsonSerializer); + jsonPersonTemplateRJC.setConnectionFactory(rjcConnFactory); + jsonPersonTemplateRJC.afterPropertiesSet(); + + return Arrays.asList(new Object[][] { { stringFactory, stringTemplateRJC }, + { personFactory, personTemplateRJC }, { stringFactory, stringTemplateJR }, + { personFactory, personTemplateJR }, { stringFactory, stringTemplate }, + { personFactory, personTemplate }, { stringFactory, xstreamStringTemplate }, + { personFactory, xstreamPersonTemplate }, { stringFactory, xstreamStringTemplateJR }, + { personFactory, xstreamPersonTemplateJR }, { personFactory, jsonPersonTemplate }, + { personFactory, jsonPersonTemplateJR }, { stringFactory, xstreamStringTemplateRJC }, + { personFactory, xstreamPersonTemplateRJC }, { personFactory, jsonPersonTemplateRJC } }); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/ObjectFactory.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/ObjectFactory.java new file mode 100644 index 000000000..78848f127 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/ObjectFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +/** + * Simple object factory. + * + * @author Costin Leau + */ +public interface ObjectFactory { + + T instance(); +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/PersonObjectFactory.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/PersonObjectFactory.java new file mode 100644 index 000000000..6e4dfe931 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/PersonObjectFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.UUID; + +import org.springframework.data.keyvalue.redis.Address; +import org.springframework.data.keyvalue.redis.Person; + +/** + * @author Costin Leau + */ +public class PersonObjectFactory implements ObjectFactory { + + private int counter = 0; + + @Override + public Person instance() { + String uuid = UUID.randomUUID().toString(); + return new Person(uuid, uuid, ++counter, new Address(uuid, counter)); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollectionFactoryBeanTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollectionFactoryBeanTests.java new file mode 100644 index 000000000..3f6742d40 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisCollectionFactoryBeanTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; +import org.springframework.data.keyvalue.redis.ConnectionFactoryTracker; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.RedisConnection; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.keyvalue.redis.core.RedisCallback; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.support.collections.RedisCollectionFactoryBean.CollectionType; + +/** + * @author Costin Leau + */ +public class RedisCollectionFactoryBeanTests { + + protected ObjectFactory factory = new StringObjectFactory(); + protected StringRedisTemplate template; + protected RedisStore col; + + public RedisCollectionFactoryBeanTests() { + JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory(); + jedisConnFactory.setUsePool(true); + + jedisConnFactory.setPort(SettingsUtils.getPort()); + jedisConnFactory.setHostName(SettingsUtils.getHost()); + + jedisConnFactory.afterPropertiesSet(); + + this.template = new StringRedisTemplate(jedisConnFactory); + ConnectionFactoryTracker.add(jedisConnFactory); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + @After + public void tearDown() throws Exception { + // clean up the whole db + template.execute(new RedisCallback() { + + @Override + public Object doInRedis(RedisConnection connection) { + connection.flushDb(); + return null; + } + }); + } + + private RedisStore createCollection(String key) { + return createCollection(key, null); + } + + private RedisStore createCollection(String key, CollectionType type) { + RedisCollectionFactoryBean fb = new RedisCollectionFactoryBean(); + fb.setKey(key); + fb.setTemplate(template); + fb.setType(type); + fb.afterPropertiesSet(); + + return fb.getObject(); + } + + @Test + public void testNone() throws Exception { + RedisStore store = createCollection("nosrt", CollectionType.PROPERTIES); + assertThat(store, instanceOf(RedisProperties.class)); + + store = createCollection("nosrt", CollectionType.MAP); + assertThat(store, instanceOf(DefaultRedisMap.class)); + + store = createCollection("nosrt", CollectionType.SET); + assertThat(store, instanceOf(DefaultRedisSet.class)); + + store = createCollection("nosrt", CollectionType.LIST); + assertThat(store, instanceOf(DefaultRedisList.class)); + + store = createCollection("nosrt"); + assertThat(store, instanceOf(DefaultRedisList.class)); + } + + + @Test + public void testExistingCol() throws Exception { + String key = "set"; + String val = "value"; + + template.boundSetOps(key).add(val); + RedisStore col = createCollection(key); + assertThat(col, is(DefaultRedisSet.class)); + + key = "map"; + template.boundHashOps(key).put(val, val); + col = createCollection(key); + assertThat(col, is(DefaultRedisMap.class)); + + col = createCollection(key, CollectionType.PROPERTIES); + assertThat(col, is(RedisProperties.class)); + + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisListTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisListTests.java new file mode 100644 index 000000000..d6361bed4 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisListTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.support.collections.AbstractRedisCollection; +import org.springframework.data.keyvalue.redis.support.collections.DefaultRedisList; +import org.springframework.data.keyvalue.redis.support.collections.RedisStore; + +/** + * Parameterized instance of Redis tests. + * + * @author Costin Leau + */ +public class RedisListTests extends AbstractRedisListTests { + + /** + * Constructs a new RedisListTests instance. + * + * @param factory + * @param connFactory + */ + public RedisListTests(ObjectFactory factory, RedisTemplate template) { + super(factory, template); + } + + @Override + RedisStore copyStore(RedisStore store) { + return new DefaultRedisList(store.getKey().toString(), store.getOperations()); + } + + @Override + AbstractRedisCollection createCollection() { + String redisName = getClass().getName(); + return new DefaultRedisList(redisName, template); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisMapTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisMapTests.java new file mode 100644 index 000000000..e9807ef68 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisMapTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.keyvalue.redis.Person; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.jredis.JredisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.rjc.RjcConnectionFactory; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.serializer.JacksonJsonRedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.OxmSerializer; +import org.springframework.oxm.xstream.XStreamMarshaller; + +/** + * Integration test for RedisMap. + * + * @author Costin Leau + */ +public class RedisMapTests extends AbstractRedisMapTests { + + public RedisMapTests(ObjectFactory keyFactory, ObjectFactory valueFactory, RedisTemplate template) { + super(keyFactory, valueFactory, template); + } + + @Override + RedisMap createMap() { + String redisName = getClass().getSimpleName(); + return new DefaultRedisMap(redisName, template); + } + + @Parameters + public static Collection testParams() { + // XStream serializer + XStreamMarshaller xstream = new XStreamMarshaller(); + try { + xstream.afterPropertiesSet(); + } catch (Exception ex) { + throw new RuntimeException("Cannot init XStream", ex); + } + OxmSerializer serializer = new OxmSerializer(xstream, xstream); + JacksonJsonRedisSerializer jsonSerializer = new JacksonJsonRedisSerializer(Person.class); + JacksonJsonRedisSerializer jsonStringSerializer = new JacksonJsonRedisSerializer(String.class); + + // create Jedis Factory + ObjectFactory stringFactory = new StringObjectFactory(); + ObjectFactory personFactory = new PersonObjectFactory(); + + JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory(); + jedisConnFactory.setUsePool(false); + jedisConnFactory.setPort(SettingsUtils.getPort()); + jedisConnFactory.setHostName(SettingsUtils.getHost()); + jedisConnFactory.afterPropertiesSet(); + + RedisTemplate genericTemplate = new RedisTemplate(); + genericTemplate.setConnectionFactory(jedisConnFactory); + genericTemplate.afterPropertiesSet(); + + RedisTemplate xstreamGenericTemplate = new RedisTemplate(); + xstreamGenericTemplate.setConnectionFactory(jedisConnFactory); + xstreamGenericTemplate.setDefaultSerializer(serializer); + xstreamGenericTemplate.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplate = new RedisTemplate(); + jsonPersonTemplate.setConnectionFactory(jedisConnFactory); + jsonPersonTemplate.setDefaultSerializer(jsonSerializer); + jsonPersonTemplate.setHashKeySerializer(jsonSerializer); + jsonPersonTemplate.setHashValueSerializer(jsonStringSerializer); + jsonPersonTemplate.afterPropertiesSet(); + + // JRedis + JredisConnectionFactory jredisConnFactory = new JredisConnectionFactory(); + jredisConnFactory.setUsePool(true); + jredisConnFactory.setPort(SettingsUtils.getPort()); + jredisConnFactory.setHostName(SettingsUtils.getHost()); + jredisConnFactory.afterPropertiesSet(); + + RedisTemplate genericTemplateJR = new RedisTemplate(); + genericTemplateJR.setConnectionFactory(jredisConnFactory); + genericTemplateJR.afterPropertiesSet(); + + RedisTemplate xGenericTemplateJR = new RedisTemplate(); + xGenericTemplateJR.setConnectionFactory(jredisConnFactory); + xGenericTemplateJR.setDefaultSerializer(serializer); + xGenericTemplateJR.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplateJR = new RedisTemplate(); + jsonPersonTemplateJR.setConnectionFactory(jredisConnFactory); + jsonPersonTemplateJR.setDefaultSerializer(jsonSerializer); + jsonPersonTemplateJR.setHashKeySerializer(jsonSerializer); + jsonPersonTemplateJR.setHashValueSerializer(jsonStringSerializer); + jsonPersonTemplateJR.afterPropertiesSet(); + + // RJC + + // rjc + RjcConnectionFactory rjcConnFactory = new RjcConnectionFactory(); + rjcConnFactory.setUsePool(true); + rjcConnFactory.setPort(SettingsUtils.getPort()); + rjcConnFactory.setHostName(SettingsUtils.getHost()); + rjcConnFactory.afterPropertiesSet(); + + RedisTemplate genericTemplateRJC = new RedisTemplate(); + genericTemplateRJC.setConnectionFactory(rjcConnFactory); + genericTemplateRJC.afterPropertiesSet(); + + RedisTemplate xGenericTemplateRJC = new RedisTemplate(); + xGenericTemplateRJC.setConnectionFactory(rjcConnFactory); + xGenericTemplateRJC.setDefaultSerializer(serializer); + xGenericTemplateRJC.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplateRJC = new RedisTemplate(); + jsonPersonTemplateRJC.setConnectionFactory(rjcConnFactory); + jsonPersonTemplateRJC.setDefaultSerializer(jsonSerializer); + jsonPersonTemplateRJC.setHashKeySerializer(jsonSerializer); + jsonPersonTemplateRJC.setHashValueSerializer(jsonStringSerializer); + jsonPersonTemplateRJC.afterPropertiesSet(); + + + return Arrays.asList(new Object[][] { { stringFactory, stringFactory, genericTemplate }, + { personFactory, personFactory, genericTemplate }, { stringFactory, personFactory, genericTemplate }, + { personFactory, stringFactory, genericTemplate }, + { personFactory, stringFactory, xstreamGenericTemplate }, + { stringFactory, stringFactory, genericTemplateJR }, + { personFactory, personFactory, genericTemplateJR }, + { stringFactory, personFactory, genericTemplateJR }, + { personFactory, stringFactory, genericTemplateJR }, + { personFactory, stringFactory, xGenericTemplateJR }, + { personFactory, stringFactory, jsonPersonTemplate }, + { personFactory, stringFactory, jsonPersonTemplateJR }, + { stringFactory, stringFactory, genericTemplateRJC }, + { personFactory, personFactory, genericTemplateRJC }, + { stringFactory, personFactory, genericTemplateRJC }, + { personFactory, stringFactory, genericTemplateRJC }, + { personFactory, stringFactory, xGenericTemplateRJC }, + { personFactory, stringFactory, jsonPersonTemplateRJC } }); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisPropertiesTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisPropertiesTests.java new file mode 100644 index 000000000..b8e058e05 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisPropertiesTests.java @@ -0,0 +1,311 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import static org.junit.Assert.*; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Properties; +import java.util.Set; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.keyvalue.redis.Person; +import org.springframework.data.keyvalue.redis.SettingsUtils; +import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.jredis.JredisConnectionFactory; +import org.springframework.data.keyvalue.redis.connection.rjc.RjcConnectionFactory; +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.core.StringRedisTemplate; +import org.springframework.data.keyvalue.redis.serializer.JacksonJsonRedisSerializer; +import org.springframework.data.keyvalue.redis.serializer.OxmSerializer; +import org.springframework.oxm.xstream.XStreamMarshaller; + +/** + * @author Costin Leau + */ +public class RedisPropertiesTests extends RedisMapTests { + + protected Properties defaults = new Properties(); + protected RedisProperties props; + + /** + * Constructs a new RedisPropertiesTests instance. + * + * @param keyFactory + * @param valueFactory + * @param template + */ + public RedisPropertiesTests(ObjectFactory keyFactory, ObjectFactory valueFactory, + RedisTemplate template) { + super(keyFactory, valueFactory, template); + } + + @Override + RedisMap createMap() { + String redisName = getClass().getSimpleName(); + props = new RedisProperties(defaults, redisName, new StringRedisTemplate(template.getConnectionFactory())); + return props; + } + + @Override + protected RedisStore copyStore(RedisStore store) { + return new RedisProperties(store.getKey(), store.getOperations()); + } + + @Test + public void testGetOperations() { + assertTrue(map.getOperations() instanceof StringRedisTemplate); + } + + @Test + public void testPropertiesLoad() throws Exception { + InputStream stream = getClass().getResourceAsStream( + "/org/springframework/data/keyvalue/redis/support/collections/props.properties"); + + assertNotNull(stream); + + int size = props.size(); + + try { + props.load(stream); + } finally { + stream.close(); + } + + assertEquals("bar", props.get("foo")); + assertEquals("head", props.get("bucket")); + assertEquals("island", props.get("lotus")); + assertEquals(size + 3, props.size()); + } + + @Test + @Ignore + public void testPropertiesLoadXml() throws Exception { + InputStream stream = getClass().getResourceAsStream( + "/org/springframework/data/keyvalue/redis/support/collections/props.properties"); + + assertNotNull(stream); + + int size = props.size(); + + try { + props.loadFromXML(stream); + } finally { + stream.close(); + } + + assertEquals("bar", props.get("foo")); + assertEquals("head", props.get("bucket")); + assertEquals("island", props.get("lotus")); + assertEquals(size + 3, props.size()); + } + + @Test + public void testPropertiesSave() throws Exception { + props.setProperty("x", "y"); + props.setProperty("a", "b"); + + StringWriter writer = new StringWriter(); + props.store(writer, "no-comment"); + //System.out.println(writer.toString()); + } + + @Test + @Ignore + public void testPropertiesSaveXml() throws Exception { + props.setProperty("x", "y"); + props.setProperty("a", "b"); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + props.storeToXML(bos, "comment"); + System.out.println(bos.toString()); + } + + @Test + public void testGetProperty() throws Exception { + String property = props.getProperty("a"); + assertNull(property); + defaults.put("a", "x"); + assertEquals("x", props.getProperty("a")); + } + + @Test + public void testGetPropertyDefault() throws Exception { + assertEquals("x", props.getProperty("a", "x")); + } + + @Test + public void testSetProperty() throws Exception { + assertNull(props.getProperty("a")); + defaults.setProperty("a", "x"); + assertEquals("x", props.getProperty("a")); + } + + @Test + public void testPropertiesList() throws Exception { + defaults.setProperty("a", "b"); + props.setProperty("x", "y"); + StringWriter wr = new StringWriter(); + props.list(new PrintWriter(wr)); + } + + @Test + public void testPropertyNames() throws Exception { + String key1="foo"; + String key2="x"; + String key3 = "d"; + + String val ="o"; + + defaults.setProperty(key3, val); + props.setProperty(key1, val); + props.setProperty(key2, val); + + Enumeration names = props.propertyNames(); + Set keys = new LinkedHashSet(); + keys.add(names.nextElement()); + keys.add(names.nextElement()); + keys.add(names.nextElement()); + + assertFalse(names.hasMoreElements()); + } + + @Test + public void testStringPropertyNames() throws Exception { + String key1 = "foo"; + String key2 = "x"; + String key3 = "d"; + + String val = "o"; + + defaults.setProperty(key3, val); + props.setProperty(key1, val); + props.setProperty(key2, val); + + Set keys = props.stringPropertyNames(); + assertTrue(keys.contains(key1)); + assertTrue(keys.contains(key2)); + assertTrue(keys.contains(key3)); + } + + @Parameters + public static Collection testParams() { + // XStream serializer + XStreamMarshaller xstream = new XStreamMarshaller(); + try { + xstream.afterPropertiesSet(); + } catch (Exception ex) { + throw new RuntimeException("Cannot init XStream", ex); + } + OxmSerializer serializer = new OxmSerializer(xstream, xstream); + JacksonJsonRedisSerializer jsonSerializer = new JacksonJsonRedisSerializer(Person.class); + JacksonJsonRedisSerializer jsonStringSerializer = new JacksonJsonRedisSerializer(String.class); + + // create Jedis Factory + ObjectFactory stringFactory = new StringObjectFactory(); + + JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory(); + jedisConnFactory.setUsePool(false); + + jedisConnFactory.setPort(SettingsUtils.getPort()); + jedisConnFactory.setHostName(SettingsUtils.getHost()); + + jedisConnFactory.afterPropertiesSet(); + + RedisTemplate genericTemplate = new StringRedisTemplate(jedisConnFactory); + + RedisTemplate xstreamGenericTemplate = new RedisTemplate(); + xstreamGenericTemplate.setConnectionFactory(jedisConnFactory); + xstreamGenericTemplate.setDefaultSerializer(serializer); + xstreamGenericTemplate.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplate = new RedisTemplate(); + jsonPersonTemplate.setConnectionFactory(jedisConnFactory); + jsonPersonTemplate.setDefaultSerializer(jsonSerializer); + jsonPersonTemplate.setHashKeySerializer(jsonSerializer); + jsonPersonTemplate.setHashValueSerializer(jsonStringSerializer); + jsonPersonTemplate.afterPropertiesSet(); + + // JRedis + JredisConnectionFactory jredisConnFactory = new JredisConnectionFactory(); + jredisConnFactory.setUsePool(true); + jredisConnFactory.setPort(SettingsUtils.getPort()); + jredisConnFactory.setHostName(SettingsUtils.getHost()); + jredisConnFactory.afterPropertiesSet(); + + RedisTemplate genericTemplateJR = new StringRedisTemplate(jredisConnFactory); + RedisTemplate xGenericTemplateJR = new RedisTemplate(); + xGenericTemplateJR.setConnectionFactory(jredisConnFactory); + xGenericTemplateJR.setDefaultSerializer(serializer); + xGenericTemplateJR.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplateJR = new RedisTemplate(); + jsonPersonTemplateJR.setConnectionFactory(jredisConnFactory); + jsonPersonTemplateJR.setDefaultSerializer(jsonSerializer); + jsonPersonTemplateJR.setHashKeySerializer(jsonSerializer); + jsonPersonTemplateJR.setHashValueSerializer(jsonStringSerializer); + jsonPersonTemplateJR.afterPropertiesSet(); + + // RJC + + // rjc + RjcConnectionFactory rjcConnFactory = new RjcConnectionFactory(); + rjcConnFactory.setUsePool(true); + rjcConnFactory.setPort(SettingsUtils.getPort()); + rjcConnFactory.setHostName(SettingsUtils.getHost()); + rjcConnFactory.afterPropertiesSet(); + + RedisTemplate genericTemplateRJC = new StringRedisTemplate(jredisConnFactory); + RedisTemplate xGenericTemplateRJC = new RedisTemplate(); + xGenericTemplateRJC.setConnectionFactory(rjcConnFactory); + xGenericTemplateRJC.setDefaultSerializer(serializer); + xGenericTemplateRJC.afterPropertiesSet(); + + RedisTemplate jsonPersonTemplateRJC = new RedisTemplate(); + jsonPersonTemplateRJC.setConnectionFactory(rjcConnFactory); + jsonPersonTemplateRJC.setDefaultSerializer(jsonSerializer); + jsonPersonTemplateRJC.setHashKeySerializer(jsonSerializer); + jsonPersonTemplateRJC.setHashValueSerializer(jsonStringSerializer); + jsonPersonTemplateRJC.afterPropertiesSet(); + + + return Arrays.asList(new Object[][] { { stringFactory, stringFactory, genericTemplate }, + { stringFactory, stringFactory, genericTemplate }, { stringFactory, stringFactory, genericTemplate }, + { stringFactory, stringFactory, genericTemplate }, + { stringFactory, stringFactory, xstreamGenericTemplate }, + { stringFactory, stringFactory, genericTemplateJR }, + { stringFactory, stringFactory, genericTemplateJR }, + { stringFactory, stringFactory, genericTemplateJR }, + { stringFactory, stringFactory, genericTemplateJR }, + { stringFactory, stringFactory, xGenericTemplateJR }, + { stringFactory, stringFactory, jsonPersonTemplate }, + { stringFactory, stringFactory, jsonPersonTemplateJR }, + { stringFactory, stringFactory, genericTemplateRJC }, + { stringFactory, stringFactory, genericTemplateRJC }, + { stringFactory, stringFactory, genericTemplateRJC }, + { stringFactory, stringFactory, genericTemplateRJC }, + { stringFactory, stringFactory, xGenericTemplateRJC }, + { stringFactory, stringFactory, jsonPersonTemplateRJC } }); + } +} \ No newline at end of file diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisSetTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisSetTests.java new file mode 100644 index 000000000..ad10d7d40 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisSetTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import org.springframework.data.keyvalue.redis.core.RedisTemplate; +import org.springframework.data.keyvalue.redis.support.collections.AbstractRedisCollection; +import org.springframework.data.keyvalue.redis.support.collections.DefaultRedisSet; +import org.springframework.data.keyvalue.redis.support.collections.RedisStore; + +/** + * Parameterized instance of Redis tests. + * + * @author Costin Leau + */ +public class RedisSetTests extends AbstractRedisSetTests { + + /** + * Constructs a new RedisSetTests instance. + * + * @param factory + * @param template + */ + public RedisSetTests(ObjectFactory factory, RedisTemplate template) { + super(factory, template); + } + + @Override + RedisStore copyStore(RedisStore store) { + return new DefaultRedisSet(store.getKey().toString(), store.getOperations()); + } + + @Override + AbstractRedisCollection createCollection() { + String redisName = getClass().getName(); + return new DefaultRedisSet(redisName, template); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisZSetTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisZSetTests.java new file mode 100644 index 000000000..3d1dd65a1 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/RedisZSetTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import org.springframework.data.keyvalue.redis.core.RedisTemplate; + +/** + * Parameterized instance of Redis sorted set tests. + * + * @author Costin Leau + */ +public class RedisZSetTests extends AbstractRedisZSetTest { + + /** + * Constructs a new RedisZSetTests instance. + * + * @param factory + * @param template + */ + public RedisZSetTests(ObjectFactory factory, RedisTemplate template) { + super(factory, template); + } + + @Override + RedisStore copyStore(RedisStore store) { + return new DefaultRedisZSet(store.getKey().toString(), store.getOperations()); + } + + @Override + AbstractRedisCollection createCollection() { + String redisName = getClass().getName(); + return new DefaultRedisZSet(redisName, template); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/StringObjectFactory.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/StringObjectFactory.java new file mode 100644 index 000000000..6669ca873 --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/StringObjectFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.UUID; + +/** + * String object factory based on UUID. + * + * @author Costin Leau + */ +public class StringObjectFactory implements ObjectFactory { + + @Override + public String instance() { + return UUID.randomUUID().toString(); + } +} diff --git a/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/SupportXmlTests.java b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/SupportXmlTests.java new file mode 100644 index 000000000..026074fbd --- /dev/null +++ b/spring-data-redis/src/test/java/org/springframework/data/keyvalue/redis/support/collections/SupportXmlTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.redis.support.collections; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.context.support.GenericXmlApplicationContext; + +/** + * @author Costin Leau + */ +public class SupportXmlTests { + + @Test + public void testContainerSetup() throws Exception { + GenericXmlApplicationContext ctx = new GenericXmlApplicationContext( + "/org/springframework/data/keyvalue/redis/support/collections/container.xml"); + + RedisList list = ctx.getBean("non-existing", RedisList.class); + RedisProperties props = ctx.getBean("props", RedisProperties.class); + Map map = ctx.getBean("map", Map.class); + } +} diff --git a/spring-data-redis/src/test/resources/log4j.properties b/spring-data-redis/src/test/resources/log4j.properties new file mode 100644 index 000000000..945449482 --- /dev/null +++ b/spring-data-redis/src/test/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.springframework.data.keyvalue.redis.listener=TRACE + +# for debugging datasource initialization +# log4j.category.test.jdbc=DEBUG diff --git a/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/config/namespace.xml b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/config/namespace.xml new file mode 100644 index 000000000..ab17a299e --- /dev/null +++ b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/config/namespace.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/listener/container.xml b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/listener/container.xml new file mode 100644 index 000000000..f5f3d67ed --- /dev/null +++ b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/listener/container.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/pe.xml b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/pe.xml new file mode 100644 index 000000000..50680e0a8 --- /dev/null +++ b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/pe.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/container.xml b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/container.xml new file mode 100644 index 000000000..410c81422 --- /dev/null +++ b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/container.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/props.properties b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/props.properties new file mode 100644 index 000000000..aad78142d --- /dev/null +++ b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/props.properties @@ -0,0 +1,4 @@ +# redis connection properties +foo=bar +bucket=head +lotus=island \ No newline at end of file diff --git a/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/props.xml b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/props.xml new file mode 100644 index 000000000..2e49de5b7 --- /dev/null +++ b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/support/collections/props.xml @@ -0,0 +1,6 @@ + +Hi +bar +head +island + \ No newline at end of file diff --git a/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/test.properties b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/test.properties new file mode 100644 index 000000000..ea1fbe81a --- /dev/null +++ b/spring-data-redis/src/test/resources/org/springframework/data/keyvalue/redis/test.properties @@ -0,0 +1,3 @@ +# redis connection properties +host=localhost +port=6379 \ No newline at end of file diff --git a/spring-data-redis/template.mf b/spring-data-redis/template.mf new file mode 100644 index 000000000..10749ee20 --- /dev/null +++ b/spring-data-redis/template.mf @@ -0,0 +1,29 @@ +Bundle-SymbolicName: org.springframework.data.keyvalue.redis +Bundle-Name: Spring Data Redis Support +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Export-Template: org.springframework.data.keyvalue.redis.*;version=${version} +Import-Package: + sun.reflect;version="0";resolution:=optional +Import-Template: + org.springframework.beans.*;version=${spring.range}, + org.springframework.context.*;version=${spring.range}, + org.springframework.core.*;version=${spring.range}, + org.springframework.dao.*;version=${spring.range}, + org.springframework.scheduling.*;resolution:="optional";version=${spring.range}, + org.springframework.util.*;version=${spring.range}, + org.springframework.oxm.*;resolution:="optional";version=${spring.range}, + org.springframework.transaction.support.*;version=${spring.range}, + org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.springframework.data.keyvalue.*;version=${version}, + org.w3c.dom.*;version="0", + javax.xml.transform.*;resolution:="optional";version="0", + org.jredis.*;version="[1.0.0, 2.0.0)", + org.jredis.ri.alphazero.*;version="[1.0.0, 2.0.0)", + redis.clients.jedis.*;version=${jedis.range}, + redis.clients.util.*;version=${jedis.range}, + org.idevlab.rjc.*;version=${rjc.range}, + org.apache.commons.pool.impl.*;version="[1.0.0, 3.0.0)", + org.codehaus.jackson.*;version=${jackson.range}, + org.apache.commons.beanutils.*;version=1.8.5 \ No newline at end of file diff --git a/spring-data-riak/README.md b/spring-data-riak/README.md new file mode 100644 index 000000000..33f5303de --- /dev/null +++ b/spring-data-riak/README.md @@ -0,0 +1,107 @@ +# Spring Data support for Riak + +The spring-data-riak module strives to make working with Riak painless by providing +the developer several different ways to easily access or store data using the Riak +Key/Value store. + +## Recent Changes: + +* 12/22/2010: Added async Map/Reduce support to AsyncRiakTemplate and Groovy DSL +* 12/20/2010: AsyncRiakTemplate and Groovy DSL + +### Groovy DSL + +One cool new feature just added is a Groovy DSL for data access using SDKV/Riak: + + def riak = new RiakBuilder(riakTemplate) + def result = null + + riak.set(bucket: "test", key: "test", qos: [dw: "all"], value: obj, wait: 3000L) { + completed(when: { it.integer == 12 }) { result = it.test } + completed { result = "otherwise" } + + failed { it.printStackTrace() } + } + +The Groovy DSL will respond to the following methods: + +* set +* setAsBytes +* put +* get +* getAsBytes +* getAsType +* containsKey +* delete +* foreach + +Each completed or failed closure can be accompanied by a "guard" closure. For example, +to process an entry differently, based on the type: + + riak.get(bucket: "test", key: "test") { + completed(when: { it instanceof Map }) { processMap(it) } + completed(when: { it instanceof String }) { processString(it) } + completed(when: { it instanceof byte[] }) { processBytes(it) } + completed { result = "otherwise" } + + failed { it.printStackTrace() } + } + +You can nest them, of course. To insert data and then delete all keys from a bucket: + + riak { + put(bucket: "test", value: [test: "value 1"]) + put(bucket: "test", value: [test: "value 2"]) + put(bucket: "test", value: [test: "value 3"]) + + foreach(bucket: "test") { + completed { v, meta -> + delete(bucket: meta.bucket, key: meta.key) + } + failed { it.printStackTrace() } + } + } + +You can also use a "default" bucket by nesting your operations inside an arbitrary block. In +the example below, the `test{}` closure sets a default bucket of "test" and all the +subsequent operations check for this if a `bucket` is not specified (you can override the +default by specifying a `bucket` property on the operation itself). + +The Groovy DSL for Riak now has Map/Reduce support. You build up a Map/Reduce job using the +closures shown in the example. You can pass static arguments to the phases, as well. You can +also specify a `wait` timeout on the `mapreduce` closure, just like with the other operations. + + riak { + test { + put(value: [test: "value"]) + put(value: [test: "value"]) + put(value: [test: "value"]) + put(value: [test: "value"]) + + mapreduce { + query { + map(arg: [test: "arg", alist: [1, 2, 3, 4]]) { + source "function(v){ return [1]; }" + } + reduce { + source "function(v){ return Riak.reduceSum(v); }" + } + } + completed { println "result $it" } + failed { it.printStackTrace() } + } + } + } + +Some things to note here: + +* The Groovy DSL utilizes the new AsyncRiakTemplate, so all closure calls happen + asynchronously. By default, the operation will block indefinitely. To not block at all + and continue on immediately, set the `wait` to `0`. To block until a specified timeout, + set the `wait` to the number of milliseconds to wait for the operation to complete before + timing out and throwing an exception. +* Callbacks are defined as either `completed` or `failed` closures. In addition to the + closure, you can define a "guard" closure, which is called before the main closure and + should return non-null or Boolean `true` if the closure should be executed or null or + Boolean `false` if the closure is to be skipped. This functionality is inspired by the + use of [the guard expression in Erlang case statements](http://en.wikibooks.org/wiki/Erlang_Programming/guards). \ No newline at end of file diff --git a/spring-data-riak/build.gradle b/spring-data-riak/build.gradle new file mode 100644 index 000000000..0610d27c2 --- /dev/null +++ b/spring-data-riak/build.gradle @@ -0,0 +1,9 @@ +dependencies { + compile project(":spring-data-keyvalue-core") + compile "org.codehaus.groovy:groovy-all:1.7.6" + compile "javax.mail:mail:1.4.1" + compile "javax.activation:activation:1.1.1" + compile "commons-cli:commons-cli:1.2" + + compile "org.springframework:spring-web:$springVersion" +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/DataStoreConnectionFailureException.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/DataStoreConnectionFailureException.java new file mode 100644 index 000000000..b9c56ad5c --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/DataStoreConnectionFailureException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak; + +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * @author J. Brisbin + */ +public class DataStoreConnectionFailureException extends DataAccessResourceFailureException { + + public static final long serialVersionUID = 1L; + + public DataStoreConnectionFailureException(String msg) { + super(msg); + } + + public DataStoreConnectionFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/DataStoreOperationException.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/DataStoreOperationException.java new file mode 100644 index 000000000..9dfe893a2 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/DataStoreOperationException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak; + +import org.springframework.dao.DataAccessException; + +/** + * @author J. Brisbin + */ +public class DataStoreOperationException extends DataAccessException { + + public static final long serialVersionUID = 1L; + + public DataStoreOperationException(String msg) { + super(msg); + } + + public DataStoreOperationException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/convert/KeyValueStoreMetaData.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/convert/KeyValueStoreMetaData.java new file mode 100644 index 000000000..40f8d66f5 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/convert/KeyValueStoreMetaData.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.convert; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Specify the bucket in which to store the annotated object, overriding the + * default classname method of deriving bucket name. + * + * @author J. Brisbin + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface KeyValueStoreMetaData { + + /** + * The bucket in which to store an instance of this object. + * + * @return + */ + String bucket(); + + /** + * The media type in which to covert and store this object. + * + * @return + */ + String mediaType() default "application/json"; + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AbstractRiakTemplate.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AbstractRiakTemplate.java new file mode 100644 index 000000000..85b50f641 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AbstractRiakTemplate.java @@ -0,0 +1,540 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.codehaus.groovy.runtime.GStringImpl; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.ser.CustomSerializerFactory; +import org.codehaus.jackson.map.ser.ToStringSerializer; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.data.keyvalue.riak.DataStoreOperationException; +import org.springframework.data.keyvalue.riak.convert.KeyValueStoreMetaData; +import org.springframework.data.keyvalue.riak.util.Ignore404sErrorHandler; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.support.RestGatewaySupport; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Base class for RiakTemplates that defines basic behaviour common to both kinds of templates + * (Key/Value and Bucket/Key/Value). + * + * @author J. Brisbin + */ +public abstract class AbstractRiakTemplate extends RestGatewaySupport implements InitializingBean, BeanClassLoaderAware { + + protected static final String RIAK_META_CLASSNAME = "X-Riak-Meta-ClassName"; + protected static final String RIAK_VCLOCK = "X-Riak-Vclock"; + + /** + * Regex used to extract host, port, and prefix from the given URI. + */ + protected static final Pattern prefix = Pattern.compile( + "http[s]?://(\\S+):([0-9]+)/(\\S+)/\\{bucket\\}(\\S+)"); + /** + * Do we need to handle Groovy strings in the Jackson JSON processor? + */ + protected final boolean groovyPresent = ClassUtils.isPresent( + "org.codehaus.groovy.runtime.GStringImpl", + getClass().getClassLoader()); + /** + * For getting a java.util.Date from the Last-Modified header. + */ + protected static SimpleDateFormat httpDate = new SimpleDateFormat( + "EEE, d MMM yyyy HH:mm:ss z"); + + protected final Log log = LogFactory.getLog(getClass()); + + /** + * Client ID used by Riak to correlate updates. + */ + protected final String RIAK_CLIENT_ID = getClass().getName() + "/1.0"; + + /** + * For converting objects to/from other kinds of objects. + */ + protected ConversionService conversionService = ConversionServiceFactory + .createDefaultConversionService(); + /** + * For caching objects based on ETags. + */ + protected ConcurrentSkipListMap> cache = new ConcurrentSkipListMap>(); + /** + * Whether or not to use the ETag-based cache. + */ + protected boolean useCache = true; + /** + * The URI to use inside the RestTemplate. + */ + protected String defaultUri = "http://localhost:8098/riak/{bucket}/{key}"; + /** + * The URI for the Riak Map/Reduce API. + */ + protected String mapReduceUri = "http://localhost:8098/mapred"; + /** + * A list of resolvers to turn a single object into a {@link BucketKeyPair}. + */ + protected List bucketKeyResolvers = new ArrayList(); + /** + * The default QosParameters to use for all operations through this template. + */ + protected QosParameters defaultQosParameters = null; + /** + * {@link java.util.concurrent.ExecutorService} to use for running asynchronous jobs. + */ + protected ExecutorService workerPool = Executors.newCachedThreadPool(); + /** + * Default type to use when trying to deserialize objects and we can't otherwise tell what to + * do. + */ + protected Class defaultType = String.class; + /** + * ClassLoader to use for saving/loading objects using the automatic converters. + */ + protected ClassLoader classLoader = null; + + /** + * Take all the defaults. + */ + public AbstractRiakTemplate() { + setRestTemplate(new RestTemplate()); + } + + /** + * Use the specified {@link org.springframework.http.client.ClientHttpRequestFactory}. + * + * @param requestFactory + */ + public AbstractRiakTemplate(ClientHttpRequestFactory requestFactory) { + super(requestFactory); + setRestTemplate(new RestTemplate()); + } + + public ConversionService getConversionService() { + return conversionService; + } + + /** + * Specify the conversion service to use. + * + * @param conversionService + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public String getDefaultUri() { + return defaultUri; + } + + public void setDefaultUri(String defaultUri) { + this.defaultUri = defaultUri; + } + + public String getMapReduceUri() { + return mapReduceUri; + } + + public void setMapReduceUri(String mapReduceUri) { + this.mapReduceUri = mapReduceUri; + } + + public boolean isUseCache() { + return useCache; + } + + public void setUseCache(boolean useCache) { + this.useCache = useCache; + } + + public QosParameters getDefaultQosParameters() { + return defaultQosParameters; + } + + public void setDefaultQosParameters(QosParameters defaultQosParameters) { + this.defaultQosParameters = defaultQosParameters; + } + + public ExecutorService getWorkerPool() { + return workerPool; + } + + public void setWorkerPool(ExecutorService workerPool) { + this.workerPool = workerPool; + } + + public void setIgnoreNotFound(boolean b) { + if (b) { + getRestTemplate().setErrorHandler(new Ignore404sErrorHandler()); + } else { + if (getRestTemplate().getErrorHandler() instanceof Ignore404sErrorHandler) { + getRestTemplate().setErrorHandler(new DefaultResponseErrorHandler()); + } + } + } + + public boolean getIgnoreNotFound() { + return (getRestTemplate().getErrorHandler() instanceof Ignore404sErrorHandler); + } + + /** + * Get the default type to use if none can be inferred. + * + * @return + */ + public Class getDefaultType() { + return defaultType; + } + + /** + * Set the default type to use if none can be inferred. + * + * @param defaultType + */ + public void setDefaultType(Class defaultType) { + this.defaultType = defaultType; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public String getHost() { + Matcher m = prefix.matcher(defaultUri); + if (m.matches()) { + return m.group(1); + } + return "localhost"; + } + + public Integer getPort() { + Matcher m = prefix.matcher(defaultUri); + if (m.matches()) { + return new Integer(m.group(2)); + } + return 8098; + } + + /** + * Extract the prefix from the URI for use in creating links. + * + * @return + */ + public String getPrefix() { + Matcher m = prefix.matcher(defaultUri); + if (m.matches()) { + return "/" + m.group(3); + } + return "/riak"; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(conversionService, + "Must specify a valid ConversionService."); + + List> converters = getRestTemplate().getMessageConverters(); + ObjectMapper mapper = new ObjectMapper(); + CustomSerializerFactory fac = new CustomSerializerFactory(); + if (groovyPresent) { + // Native conversion for Groovy GString objects + fac.addSpecificMapping(GStringImpl.class, ToStringSerializer.instance); + } + mapper.setSerializerFactory(fac); + for (HttpMessageConverter converter : converters) { + if (converter instanceof MappingJacksonHttpMessageConverter) { + ((MappingJacksonHttpMessageConverter) converter).setObjectMapper( + mapper); + } + } + } + + /*----------------- Utilities -----------------*/ + + @SuppressWarnings({"unchecked"}) + protected BucketKeyPair resolveBucketKeyPair(Object key, Object val) { + BucketKeyResolver resolver = null; + for (BucketKeyResolver r : bucketKeyResolvers) { + if (r.canResolve(key)) { + resolver = r; + break; + } + } + if (null == resolver) { + resolver = new SimpleBucketKeyResolver(); + } + + BucketKeyPair bucketKeyPair = resolver.resolve(key); + if (null == bucketKeyPair.getBucket() && null != val) { + // No bucket specified, check for an annotation that specified bucket name. + Annotation meta = (val instanceof Class ? (Class) val : val.getClass()).getAnnotation( + org.springframework.data.keyvalue.riak.convert.KeyValueStoreMetaData.class); + if (null != meta) { + String bucket = ((KeyValueStoreMetaData) meta).bucket(); + if (null != bucket) { + return new SimpleBucketKeyPair(bucket, + bucketKeyPair.getKey()); + } + } + } + return bucketKeyPair; + } + + protected MediaType extractMediaType(Object value) { + MediaType mediaType = (value instanceof byte[] ? MediaType.APPLICATION_OCTET_STREAM : MediaType.APPLICATION_JSON); + if (null != value && value.getClass().getAnnotations().length > 0) { + KeyValueStoreMetaData meta = value.getClass() + .getAnnotation(KeyValueStoreMetaData.class); + if (null != meta) { + // Use the media type specified on the annotation. + mediaType = MediaType.parseMediaType(meta.mediaType()); + } + } + return mediaType; + } + + protected RiakMetaData extractMetaData(HttpHeaders headers) throws + IOException { + Map props = new LinkedHashMap(); + for (Map.Entry> entry : headers.entrySet()) { + List val = entry.getValue(); + Object prop = (1 == val.size() ? val.get(0) : val); + try { + if (entry.getKey().equals("Last-Modified") || entry.getKey() + .equals("Date")) { + prop = httpDate.parse(val.get(0)); + } + } catch (ParseException e) { + log.error(e.getMessage(), e); + } + + if (entry.getKey().equals("Link")) { + List links = new ArrayList(); + for (String link : entry.getValue()) { + String[] parts = link.split(","); + for (String part : parts) { + String s = part.replaceAll("<(.+)>; rel=\"(\\S+)\"[,]?", "").trim(); + if (!"".equals(s)) { + links.add(s); + } + } + } + props.put("Link", links); + } else { + props.put(entry.getKey().toString(), prop); + } + } + props.put("ETag", headers.getETag()); + RiakMetaData meta = new RiakMetaData(headers.getContentType(), props); + + return meta; + } + + @SuppressWarnings({"unchecked"}) + protected RiakValue extractValue(final ResponseEntity response, Class origType, + Class requiredType) throws + IOException { + if (response.hasBody()) { + RiakMetaData meta = extractMetaData(response.getHeaders()); + Object o = response.getBody(); + if (!origType.equals(requiredType)) { + if (conversionService.canConvert(origType, requiredType)) { + o = conversionService.convert(o, requiredType); + } else { + if (o instanceof byte[] || o instanceof String) { + // Peek inside, see if it's a string of something we recognize + String s = (o instanceof byte[] ? new String((byte[]) o) : (String) o); + if (s.charAt(0) == '{' || s.charAt(0) == '[') { + // Looks like it might be a JSON string. Use the JSON converter + for (HttpMessageConverter conv : getRestTemplate().getMessageConverters()) { + if (conv instanceof MappingJacksonHttpMessageConverter) { + o = conv.read(requiredType, new HttpInputMessage() { + public InputStream getBody() throws IOException { + Object body = response.getBody(); + return new ByteArrayInputStream( + (body instanceof byte[] ? (byte[]) body : ((String) body) + .getBytes())); + } + + public HttpHeaders getHeaders() { + return response.getHeaders(); + } + }); + break; + } + } + + } + } else { + throw new DataStoreOperationException( + "Cannot convert object of type " + origType + " to type " + requiredType); + } + } + } + return new RiakValue((T) o, meta); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + protected T checkCache(K key, Class requiredType) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, requiredType); + RiakValue obj = cache.get(bucketKeyPair); + if (null != obj) { + String bucketName = (null != bucketKeyPair.getBucket() ? bucketKeyPair.getBucket() + .toString() : requiredType.getName()); + RestTemplate restTemplate = getRestTemplate(); + try { + HttpHeaders resp = restTemplate.headForHeaders(defaultUri, + bucketName, + bucketKeyPair.getKey()); + if (!obj.getMetaData() + .getProperties() + .get("ETag") + .toString() + .equals(resp.getETag())) { + obj = null; + } else { + if (log.isDebugEnabled()) { + log.debug("Returning CACHED object: " + obj); + } + } + } catch (ResourceAccessException ignored) { + return null; + } + } + + if (null != obj && obj.getClass() == requiredType) { + return (T) obj.get(); + } else { + return null; + } + } + + /** + * Get a string that represents the QOS parameters, taken either from the specified object or + * from the template defaults. + * + * @param qosParams + * @return + */ + protected String extractQosParameters(QosParameters qosParams) { + List params = new LinkedList(); + if (null != qosParams.getReadThreshold()) { + params.add(String.format("r=%s", qosParams.getReadThreshold())); + } else if (null != defaultQosParameters && null != defaultQosParameters + .getReadThreshold()) { + params.add(String.format("r=%s", defaultQosParameters.getReadThreshold())); + } + if (null != qosParams.getWriteThreshold()) { + params.add(String.format("w=%s", qosParams.getWriteThreshold())); + } else if (null != defaultQosParameters && null != defaultQosParameters + .getWriteThreshold()) { + params.add(String.format("w=%s", defaultQosParameters.getWriteThreshold())); + } + if (null != qosParams.getDurableWriteThreshold()) { + params.add(String.format("dw=%s", qosParams.getDurableWriteThreshold())); + } else if (null != defaultQosParameters && null != defaultQosParameters + .getDurableWriteThreshold()) { + params.add(String.format("dw=%s", defaultQosParameters.getDurableWriteThreshold())); + } + + return (params.size() > 0 ? "?" + StringUtils.collectionToDelimitedString( + params, + "&") : ""); + } + + protected HttpHeaders defaultHeaders(Map metadata) { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Riak-ClientId", RIAK_CLIENT_ID); + if (null != metadata) { + for (Map.Entry entry : metadata.entrySet()) { + Object o = entry.getValue(); + headers.set(entry.getKey(), (null != o ? o.toString() : null)); + } + } + return headers; + } + + protected Class getType(B bucket, K key) { + return getType(bucket, key, getClass().getClassLoader()); + } + + protected Class getType(B bucket, K key, ClassLoader classLoader) { + Class clazz = null; + try { + HttpHeaders headers = getRestTemplate().headForHeaders(defaultUri, bucket, key); + if (null != headers) { + String s = headers.getFirst(RIAK_META_CLASSNAME); + if (null != s) { + try { + if (null != classLoader) { + clazz = Class.forName(s, false, classLoader); + } else { + clazz = Class.forName(s); + } + } catch (ClassNotFoundException ignored) { + } + } + } + if (null == clazz) { + if (headers.getContentType().equals(MediaType.APPLICATION_JSON)) { + clazz = Map.class; + } else if (headers.getContentType().equals(MediaType.TEXT_PLAIN)) { + clazz = String.class; + } else { + // handle as bytes + log.error("Need to handle bytes!"); + clazz = byte[].class; + } + } + } catch (ResourceAccessException notFound) { + clazz = String.class; + } + return clazz; + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncBucketKeyValueStoreOperations.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncBucketKeyValueStoreOperations.java new file mode 100644 index 000000000..0b222d855 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncBucketKeyValueStoreOperations.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import java.util.Map; +import java.util.concurrent.Future; + +/** + * An asynchronous version of {@link BucketKeyValueStoreOperations}. + * + * @author J. Brisbin + */ +public interface AsyncBucketKeyValueStoreOperations { + + /** + * Put an object in Riak at a specific bucket and key and invoke callback with the value + * pulled back out of Riak after the update, which contains full headers and metadata. + * + * @param bucket + * @param key + * @param value + * @param callback Called with the update value pulled from Riak + */ + Future set(B bucket, K key, V value, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @param qosParams + * @return + */ + Future set(B bucket, K key, V value, QosParameters qosParams, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @return + */ + Future setAsBytes(B bucket, K key, byte[] value, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @param qosParams + * @return + */ + Future setAsBytes(B bucket, K key, byte[] value, QosParameters qosParams, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @param metaData + * @return + */ + Future setWithMetaData(B bucket, K key, V value, Map metaData, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @param metaData + * @param qosParams + * @return + */ + Future setWithMetaData(B bucket, K key, V value, Map metaData, QosParameters qosParams, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @return + */ + Future get(B bucket, K key, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @return + */ + Future getAsBytes(B bucket, K key, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param requiredType + * @return + */ + Future getAsType(B bucket, K key, Class requiredType, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @return + */ + Future getAndSet(B bucket, K key, V value, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @return + */ + Future getAndSetAsBytes(B bucket, K key, byte[] value, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @param requiredType + * @return + */ + Future getAndSetAsType(B bucket, K key, V value, Class requiredType, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @return + */ + Future setIfKeyNonExistent(B bucket, K key, V value, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @param value + * @return + */ + Future setIfKeyNonExistentAsBytes(B bucket, K key, byte[] value, AsyncKeyValueStoreOperation callback); + + /** + * @param bucket + * @param key + * @return + */ + Future containsKey(B bucket, K key, AsyncKeyValueStoreOperation callback); + + /** + * Delete a specific entry from this data store. + * + * @param bucket + * @param key + * @return + */ + Future delete(B bucket, K key, AsyncKeyValueStoreOperation callback); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncKeyValueStoreOperation.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncKeyValueStoreOperation.java new file mode 100644 index 000000000..291d8d126 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncKeyValueStoreOperation.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * @author J. Brisbin + */ +public interface AsyncKeyValueStoreOperation { + + T completed(KeyValueStoreMetaData meta, V result); + + T failed(Throwable error); +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncRiakTemplate.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncRiakTemplate.java new file mode 100644 index 000000000..b96a82765 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/AsyncRiakTemplate.java @@ -0,0 +1,630 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.data.keyvalue.riak.DataStoreOperationException; +import org.springframework.data.keyvalue.riak.mapreduce.AsyncMapReduceOperations; +import org.springframework.data.keyvalue.riak.mapreduce.MapReduceJob; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.util.Assert; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * An implementation of {@link AsyncBucketKeyValueStoreOperations} and {@link + * AsyncMapReduceOperations} for the Riak datastore. + *

+ * To use the AsyncRiakTemplate, create a singleton in your Spring application-context.xml: + *


+ * <bean id="riak" class="org.springframework.data.keyvalue.riak.core.AsyncRiakTemplate"
+ *     p:defaultUri="http://localhost:8098/riak/{bucket}/{key}"
+ *     p:mapReduceUri="http://localhost:8098/mapred"/>
+ * 
+ * To store and retrieve objects in Riak, use the setXXX and getXXX methods (example in + * Groovy): + *

+ * def callback = [
+ *   completed: { v, meta ->
+ *     ... do something with results ...
+ *   },
+ *   failed: { err ->
+ *   }
+ * ] as AsyncKeyValueStoreOperation
+ * def obj = new TestObject(name: "My Name", age: 40)
+ * def future = riak.set("mybucket", "mykey", obj, callback)
+ * ... this runs asynchronously, so do other work ...
+ * def name = future.get().name
+ * println "Hello $name!"
+ * 
+ * + * @author J. Brisbin + */ +public class AsyncRiakTemplate extends AbstractRiakTemplate implements AsyncBucketKeyValueStoreOperations, AsyncMapReduceOperations { + + protected AsyncKeyValueStoreOperation defaultErrorHandler = new LoggingErrorHandler(); + + public AsyncRiakTemplate() { + super(); + } + + public AsyncRiakTemplate(ClientHttpRequestFactory requestFactory) { + super(requestFactory); + } + + public AsyncKeyValueStoreOperation getDefaultErrorHandler() { + return defaultErrorHandler; + } + + public void setDefaultErrorHandler( + AsyncKeyValueStoreOperation defaultErrorHandler) { + this.defaultErrorHandler = defaultErrorHandler; + } + + public Future set(B bucket, K key, V value, + AsyncKeyValueStoreOperation callback) { + return setWithMetaData(bucket, key, value, null, null, callback); + } + + public Future set(B bucket, K key, V value, QosParameters qosParams, + AsyncKeyValueStoreOperation callback) { + return setWithMetaData(bucket, key, value, null, qosParams, callback); + } + + public Future setAsBytes(B bucket, K key, byte[] value, + AsyncKeyValueStoreOperation callback) { + return setWithMetaData(bucket, key, value, null, null, callback); + } + + @SuppressWarnings({"unchecked"}) + public Future setWithMetaData(B bucket, K key, V value, + Map metaData, + QosParameters qosParams, + AsyncKeyValueStoreOperation callback) { + String bucketName = (null != bucket ? bucket.toString() : value.getClass().getName()); + // Get a key name that may or may not include the QOS parameters. + Assert.notNull(key, "Cannot use a key."); + String keyName = (null != qosParams ? key.toString() + extractQosParameters(qosParams) : key + .toString()); + + KeyValueStoreMetaData origMeta = getMetaData(bucket, keyName); + String vclock = null; + if (null != origMeta) { + Map mprops = origMeta.getProperties(); + if (null != mprops) { + Object o = mprops.get(RIAK_VCLOCK); + if (null != o) { + vclock = o.toString(); + } + } + } + + HttpHeaders headers = defaultHeaders(metaData); + headers.setContentType(extractMediaType(value)); + if (null != vclock) { + headers.set(RIAK_VCLOCK, vclock); + } + headers.set(RIAK_META_CLASSNAME, value.getClass().getName()); + HttpEntity entity = new HttpEntity(value, headers); + return (Future) workerPool.submit(new AsyncPost(bucketName, + keyName, + entity, + callback)); + } + + public Future put(B bucket, V value, + AsyncKeyValueStoreOperation callback) { + return put(bucket, value, null, null, callback); + } + + public Future put(B bucket, V value, Map metaData, + AsyncKeyValueStoreOperation callback) { + return put(bucket, value, metaData, null, callback); + } + + @SuppressWarnings({"unchecked"}) + public Future put(B bucket, V value, Map metaData, + QosParameters qosParams, + AsyncKeyValueStoreOperation callback) { + Assert.notNull(bucket, "Bucket cannot be null"); + String bucketName = (null != qosParams ? bucket.toString() + extractQosParameters( + qosParams) : bucket + .toString()); + + HttpHeaders headers = defaultHeaders(metaData); + headers.setContentType(extractMediaType(value)); + headers.set(RIAK_META_CLASSNAME, value.getClass().getName()); + HttpEntity entity = new HttpEntity(value, headers); + return (Future) workerPool.submit(new AsyncPut(bucketName, entity, callback)); + } + + public Future get(B bucket, K key, + AsyncKeyValueStoreOperation callback) { + return getWithMetaData(bucket, key, null, callback); + } + + public RiakMetaData getMetaData(B bucket, K key) { + RestTemplate restTemplate = getRestTemplate(); + HttpHeaders headers; + try { + headers = restTemplate.headForHeaders(defaultUri, bucket, key); + RiakMetaData meta = extractMetaData(headers); + meta.setBucket((null != bucket ? bucket.toString() : null)); + meta.setKey((null != key ? key.toString() : null)); + return meta; + } catch (ResourceAccessException e) { + } catch (IOException e) { + throw new DataAccessResourceFailureException(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + public Future getBucketSchema(B bucket, QosParameters qosParams, + final AsyncKeyValueStoreOperation, R> callback) { + Assert.notNull(bucket, "Bucket cannot be null"); + Assert.notNull(callback, "Callback cannot be null"); + + String bucketName = (null != qosParams ? bucket.toString() + extractQosParameters( + qosParams) : bucket + .toString()); + + return workerPool.submit(new AsyncGet(bucketName, + "?keys=true", + Map.class, + new AsyncKeyValueStoreOperation() { + @SuppressWarnings({"unchecked"}) + public Object completed(KeyValueStoreMetaData meta, Object result) { + return callback.completed(meta, (Map) result); + } + + public Object failed(Throwable error) { + return callback.failed(error); + } + })); + } + + @SuppressWarnings({"unchecked"}) + public Future getWithMetaData(B bucket, K key, Class requiredType, + AsyncKeyValueStoreOperation callback) { + Assert.notNull(key, "Cannot use a null key."); + Assert.notNull(callback, "Callback cannot be null"); + + String bucketName = (null != bucket ? bucket.toString() : requiredType.getName()); + + if (null == requiredType) { + requiredType = (Class) getType(bucketName, key.toString()); + } + return workerPool.submit(new AsyncGet(bucketName, + key.toString(), + requiredType, + callback)); + } + + public Future getAsBytes(B bucket, K key, + AsyncKeyValueStoreOperation callback) { + return getWithMetaData(bucket, key, byte[].class, callback); + } + + public Future getAsType(B bucket, K key, Class requiredType, + AsyncKeyValueStoreOperation callback) { + return getWithMetaData(bucket, key, requiredType, callback); + } + + public Future getAndSet(final B bucket, final K key, final V value, + final AsyncKeyValueStoreOperation callback) { + final List> futures = new ArrayList>(); + try { + getWithMetaData(bucket, key, null, new AsyncKeyValueStoreOperation() { + @SuppressWarnings({"unchecked"}) + public Object completed(KeyValueStoreMetaData meta, Object result) { + futures.add(setWithMetaData(bucket, key, value, null, null, null)); + return callback.completed(meta, (V) result); + } + + public Object failed(Throwable error) { + return callback.failed(error); + } + }).get(); + } catch (InterruptedException e) { + log.error(e.getMessage(), e); + } catch (ExecutionException e) { + log.error(e.getMessage(), e); + } + return futures.size() > 0 ? futures.get(0) : null; + } + + public Future getAndSetAsBytes(B bucket, K key, byte[] value, + AsyncKeyValueStoreOperation callback) { + return getAndSet(bucket, key, value, callback); + } + + public Future getAndSetAsType(final B bucket, final K key, final V value, + final Class requiredType, + final AsyncKeyValueStoreOperation callback) { + final List> futures = new ArrayList>(); + getWithMetaData(bucket, key, requiredType, new AsyncKeyValueStoreOperation() { + @SuppressWarnings({"unchecked"}) + public R completed(KeyValueStoreMetaData meta, T result) { + try { + setWithMetaData(bucket, key, value, null, null, null).get(); + return callback.completed(meta, result); + } catch (InterruptedException e) { + return callback.failed(e); + } catch (ExecutionException e) { + return callback.failed(e); + } + } + + public R failed(Throwable error) { + return callback.failed(error); + } + }); + return futures.size() > 0 ? futures.get(0) : null; + } + + public Future setIfKeyNonExistent(final B bucket, final K key, final V value, + final AsyncKeyValueStoreOperation callback) { + return containsKey(bucket, key, new AsyncKeyValueStoreOperation() { + public Object completed(KeyValueStoreMetaData meta, Boolean result) { + if (!result) { + return setWithMetaData(bucket, key, value, null, null, callback); + } else { + return null; + } + } + + public Object failed(Throwable error) { + return callback.failed(error); + } + }); + } + + public Future setIfKeyNonExistentAsBytes(final B bucket, final K key, + final byte[] value, + final AsyncKeyValueStoreOperation callback) { + return containsKey(bucket, key, new AsyncKeyValueStoreOperation() { + public Object completed(KeyValueStoreMetaData meta, Boolean result) { + if (!result) { + return setWithMetaData(bucket, key, value, null, null, callback); + } else { + return null; + } + } + + public Object failed(Throwable error) { + return callback.failed(error); + } + }); + } + + @SuppressWarnings({"unchecked"}) + public Future containsKey(B bucket, K key, + final AsyncKeyValueStoreOperation callback) { + Assert.notNull(bucket, "Bucket cannot be null when checking for existence."); + Assert.notNull(key, "Key cannot be null when checking for existence"); + return workerPool.submit(new AsyncHead(bucket.toString(), + key.toString(), + new AsyncKeyValueStoreOperation() { + public Object completed(KeyValueStoreMetaData meta, HttpHeaders result) { + return callback.completed(null, (null != result)); + } + + public Object failed(Throwable error) { + return callback.failed(error); + } + })); + } + + @SuppressWarnings({"unchecked"}) + public Future delete(B bucket, K key, + AsyncKeyValueStoreOperation callback) { + Assert.notNull(bucket, "Bucket cannot be null when deleting."); + Assert.notNull(key, "Key cannot be null when deleting."); + return workerPool.submit(new AsyncDelete(bucket.toString(), key.toString(), callback)); + } + + public Future setAsBytes(B bucket, K key, byte[] value, QosParameters qosParams, + AsyncKeyValueStoreOperation callback) { + return setWithMetaData(bucket, key, value, null, qosParams, callback); + } + + public Future setWithMetaData(B bucket, K key, V value, + Map metaData, + AsyncKeyValueStoreOperation callback) { + return setWithMetaData(bucket, key, value, metaData, null, callback); + } + + /* ---------------- Map/Reduce ---------------- */ + + @SuppressWarnings({"unchecked"}) + public Future execute(MapReduceJob job, + AsyncKeyValueStoreOperation, R> callback) { + HttpHeaders headers = defaultHeaders(null); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity json = new HttpEntity(job.toJson(), headers); + return workerPool.submit(new AsyncMapReduce(json, callback)); + } + + /* ---------------- Runnable helpers ---------------- */ + + protected class AsyncPut implements Callable { + + private String bucket; + private HttpEntity entity = null; + private AsyncKeyValueStoreOperation callback = null; + + public AsyncPut(String bucket, HttpEntity entity, + AsyncKeyValueStoreOperation callback) { + this.bucket = bucket; + this.entity = entity; + this.callback = callback; + } + + public R call() throws Exception { + try { + URI location = getRestTemplate().postForLocation(defaultUri, entity, bucket, ""); + String path = location.getPath(); + String key = path.substring(path.lastIndexOf("/") + 1); + + HttpHeaders headers = getRestTemplate().headForHeaders(defaultUri, bucket, key); + if (null != callback) { + RiakMetaData meta = extractMetaData(headers); + meta.setBucket((null != bucket ? bucket.toString() : null)); + meta.setKey((null != key ? key.toString() : null)); + return callback.completed(meta, entity.getBody()); + } + } catch (Throwable t) { + DataStoreOperationException dsoe = new DataStoreOperationException(t.getMessage(), t); + if (null != callback) { + return callback.failed(dsoe); + } else { + defaultErrorHandler.failed(dsoe); + } + } + return null; + } + + } + + protected class AsyncPost implements Callable { + + private String bucket; + private String key; + private HttpEntity entity = null; + private AsyncKeyValueStoreOperation callback = null; + + public AsyncPost(String bucket, String key, HttpEntity entity, + AsyncKeyValueStoreOperation callback) { + this.bucket = bucket; + this.key = key; + this.entity = entity; + this.callback = callback; + } + + @SuppressWarnings({"unchecked"}) + public R call() throws Exception { + try { + HttpEntity result = getRestTemplate().postForEntity(defaultUri, + entity, + (entity.getBody() instanceof byte[] ? byte[].class : entity.getBody().getClass()), + bucket, + key + "?returnbody=true"); + if (log.isDebugEnabled()) { + log.debug(String.format("PUT object: bucket=%s, key=%s, value=%s", + bucket, + key, + entity)); + } + if (null != callback) { + RiakMetaData meta = extractMetaData(result.getHeaders()); + meta.setBucket((null != bucket ? bucket.toString() : null)); + meta.setKey((null != key ? key.toString() : null)); + return callback.completed(meta, (V) result.getBody()); + } + } catch (Throwable t) { + DataStoreOperationException dsoe = new DataStoreOperationException(t.getMessage(), t); + if (null != callback) { + return callback.failed(dsoe); + } else { + defaultErrorHandler.failed(dsoe); + } + } + return null; + } + + } + + protected class AsyncMapReduce implements Callable { + + private HttpEntity entity = null; + private AsyncKeyValueStoreOperation, R> callback = null; + + public AsyncMapReduce(HttpEntity entity, + AsyncKeyValueStoreOperation, R> callback) { + this.entity = entity; + this.callback = callback; + } + + @SuppressWarnings({"unchecked"}) + public R call() throws Exception { + try { + HttpEntity result = getRestTemplate().postForEntity(mapReduceUri, + entity, + List.class); + if (log.isDebugEnabled()) { + log.debug(String.format("M/R: json=%s", entity.getBody())); + } + if (null != callback) { + RiakMetaData meta = extractMetaData(result.getHeaders()); + return callback.completed(meta, result.getBody()); + } + } catch (Throwable t) { + DataStoreOperationException dsoe = new DataStoreOperationException(t.getMessage(), t); + if (null != callback) { + return callback.failed(dsoe); + } else { + defaultErrorHandler.failed(dsoe); + } + } + return null; + } + + } + + protected class AsyncGet implements Callable { + + private String bucket; + private String key; + private Class requiredType; + private AsyncKeyValueStoreOperation callback = null; + + public AsyncGet(String bucket, String key, Class requiredType, + AsyncKeyValueStoreOperation callback) { + this.bucket = bucket; + this.key = key; + this.requiredType = requiredType; + this.callback = callback; + } + + public R call() throws Exception { + try { + ResponseEntity result = getRestTemplate().getForEntity(defaultUri, + requiredType, + bucket, + key); + if (result.hasBody()) { + RiakMetaData meta = extractMetaData(result.getHeaders()); + meta.setBucket((null != bucket ? bucket.toString() : null)); + meta.setKey((null != key ? key.toString() : null)); + RiakValue val = new RiakValue(result.getBody(), meta); + if (useCache) { + cache.put(new SimpleBucketKeyPair(bucket, key), val); + } + if (null != callback) { + return callback.completed(meta, val.get()); + } + if (log.isDebugEnabled()) { + log.debug(String.format("GET object: bucket=%s, key=%s, type=%s", + bucket, + key, + requiredType.getName())); + } + } + } catch (Throwable t) { + DataStoreOperationException dsoe = new DataStoreOperationException(t.getMessage(), t); + if (null != callback) { + return callback.failed(dsoe); + } else { + defaultErrorHandler.failed(dsoe); + } + } + return null; + } + } + + protected class AsyncHead implements Callable { + + private String bucket; + private String key; + private AsyncKeyValueStoreOperation callback = null; + + public AsyncHead(String bucket, String key, + AsyncKeyValueStoreOperation callback) { + this.bucket = bucket; + this.key = key; + this.callback = callback; + } + + public R call() throws Exception { + try { + HttpHeaders headers = getRestTemplate().headForHeaders(defaultUri, bucket, key); + if (null != headers) { + if (null != callback) { + return callback.completed(null, headers); + } + } + } catch (Throwable t) { + DataStoreOperationException dsoe = new DataStoreOperationException(t.getMessage(), t); + if (null != callback) { + return callback.failed(dsoe); + } else { + defaultErrorHandler.failed(dsoe); + } + } + return null; + } + } + + protected class AsyncDelete implements Callable { + + private String bucket; + private String key; + private AsyncKeyValueStoreOperation callback = null; + + public AsyncDelete(String bucket, String key, + AsyncKeyValueStoreOperation callback) { + this.bucket = bucket; + this.key = key; + this.callback = callback; + } + + public R call() throws Exception { + try { + getRestTemplate().delete(defaultUri, bucket, key); + if (null != callback) { + return callback.completed(null, true); + } + } catch (Throwable t) { + DataStoreOperationException dsoe = new DataStoreOperationException(t.getMessage(), t); + if (null != callback) { + return callback.failed(dsoe); + } else { + defaultErrorHandler.failed(dsoe); + } + } + return null; + } + } + + protected class LoggingErrorHandler implements AsyncKeyValueStoreOperation { + public Object completed(KeyValueStoreMetaData meta, Throwable result) { + return null; + } + + public Object failed(Throwable error) { + log.error(error.getMessage(), error); + return null; + } + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyPair.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyPair.java new file mode 100644 index 000000000..4fe4be165 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyPair.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * A generic interface for representing composite keys in data stores that use a + * bucket and key pair. + * + * @author J. Brisbin + */ +public interface BucketKeyPair { + + /** + * Get the bucket representation. + * + * @return + */ + B getBucket(); + + /** + * Get the key representation. + * + * @return + */ + K getKey(); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyResolver.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyResolver.java new file mode 100644 index 000000000..722b5f575 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyResolver.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * A generic interface to a resolver to turn a single object into a {@link + * org.springframework.data.keyvalue.riak.core.BucketKeyPair}. + * + * @author J. Brisbin + */ +public interface BucketKeyResolver { + + /** + * Can this resolver deal with the given object? + * + * @param o + * @param + * @return + */ + boolean canResolve(V o); + + /** + * Turn the given object into a BucketKeyPair. + * + * @param o + * @return + */ + BucketKeyPair resolve(V o); +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyValueStoreOperations.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyValueStoreOperations.java new file mode 100644 index 000000000..23f370518 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/BucketKeyValueStoreOperations.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import java.util.Map; + +/** + * @author J. Brisbin + */ +public interface BucketKeyValueStoreOperations { + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#set(Object, + * Object)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @return + */ + BucketKeyValueStoreOperations set(B bucket, K key, V value); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#set(Object, + * Object, QosParameters)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @param qosParams + * @return + */ + BucketKeyValueStoreOperations set(B bucket, K key, V value, QosParameters qosParams); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#setAsBytes(Object, + * byte[])} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @return + */ + BucketKeyValueStoreOperations setAsBytes(B bucket, K key, byte[] value); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#setWithMetaData(Object, + * Object, java.util.Map, QosParameters)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @param metaData + * @param qosParams + * @return + */ + BucketKeyValueStoreOperations setWithMetaData(B bucket, K key, V value, Map metaData, QosParameters qosParams); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#get(Object)} + * that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @return + */ + V get(B bucket, K key); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#getAsBytes(Object)} + * that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @return + */ + byte[] getAsBytes(B bucket, K key); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#getAsType(Object, + * Class)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param requiredType + * @return + */ + T getAsType(B bucket, K key, Class requiredType); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#getAndSet(Object, + * Object)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @return + */ + V getAndSet(B bucket, K key, V value); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#getAndSetAsBytes(Object, + * byte[])} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @return + */ + byte[] getAndSetAsBytes(B bucket, K key, byte[] value); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#getAndSetAsType(Object, + * Object, Class)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @param requiredType + * @return + */ + T getAndSetAsType(B bucket, K key, V value, Class requiredType); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#setIfKeyNonExistent(Object, + * Object)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @return + */ + BucketKeyValueStoreOperations setIfKeyNonExistent(B bucket, K key, V value); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#setIfKeyNonExistentAsBytes(Object, + * byte[])} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @return + */ + BucketKeyValueStoreOperations setIfKeyNonExistentAsBytes(B bucket, K key, byte[] value); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#containsKey(Object)} + * that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @return + */ + boolean containsKey(B bucket, K key); + + /** + * Delete a specific entry from this data store. + * + * @param bucket + * @param key + * @return + */ + boolean delete(B bucket, K key); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#setAsBytes(Object, + * byte[], QosParameters)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @param qosParams + * @return + */ + BucketKeyValueStoreOperations setAsBytes(B bucket, K key, byte[] value, QosParameters qosParams); + + /** + * Variant of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations#setWithMetaData(Object, + * Object, java.util.Map)} that takes a discreet bucket and key pair. + * + * @param bucket + * @param key + * @param value + * @param metaData + * @return + */ + BucketKeyValueStoreOperations setWithMetaData(B bucket, K key, V value, Map metaData); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreMetaData.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreMetaData.java new file mode 100644 index 000000000..4773811cd --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreMetaData.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import org.springframework.http.MediaType; + +import java.util.Map; + +/** + * A generic interface to MetaData provided by Key/Value data stores. + * + * @author J. Brisbin + */ +public interface KeyValueStoreMetaData { + + String getBucket(); + + String getKey(); + + /** + * Get the Content-Type of this object. + * + * @return + */ + MediaType getContentType(); + + long getLastModified(); + + /** + * Get the arbitrary properties for this object. + * + * @return + */ + Map getProperties(); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreOperations.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreOperations.java new file mode 100644 index 000000000..b618bcda6 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreOperations.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.riak.core; + +import java.util.List; +import java.util.Map; + +/** + * Generic abstraction for Key/Value stores. Contains most operations that generic K/V stores + * might expose. + */ +public interface KeyValueStoreOperations { + + // Set operations + + /** + * Set a value at a specified key. + * + * @param key + * @param value + * @return This template interface + */ + KeyValueStoreOperations set(K key, V value); + + /** + * Variation on set() that allows the user to specify {@link org.springframework.data.keyvalue.riak.core.QosParameters}. + * + * @param key + * @param value + * @param qosParams + * @param + * @param + * @return + */ + KeyValueStoreOperations set(K key, V value, QosParameters qosParams); + + /** + * Set a value as a byte array at a specified key. + * + * @param key + * @param value + * @return This template interface + */ + KeyValueStoreOperations setAsBytes(K key, byte[] value); + + /** + * Variation on setWithMetaData() that allows the user to pass {@link + * org.springframework.data.keyvalue.riak.core.QosParameters}. + * + * @param key + * @param value + * @param metaData + * @param qosParams + * @param + * @param + * @return + */ + KeyValueStoreOperations setWithMetaData(K key, V value, Map metaData, QosParameters qosParams); + + // Get operations + + /** + * Get a value at the specified key, trying to infer the type from either the bucket in which + * the value was stored, or (by default) as a java.util.Map. + * + * @param key + * @return The converted value, or null if not found. + */ + V get(K key); + + /** + * Get the value at the specified key as a byte array. + * + * @param key + * @return The byte array of data, or null if not found. + */ + byte[] getAsBytes(K key); + + /** + * Get the value at the specified key and convert it into an instance of the specified type. + * + * @param key + * @param requiredType + * @return The converted value, or null if not found. + */ + T getAsType(K key, Class requiredType); + + // Get and Set operations + + /** + * Get the old value at the specified key and replace it with the given value. + * + * @param key + * @param value + * @return The old value (before it was overwritten). + */ + V getAndSet(K key, V value); + + /** + * Get the old value at the specified key as a byte array and replace it with the given + * bytes. + * + * @param key + * @param value + * @return The old byte array (before it was overwritten). + */ + byte[] getAndSetAsBytes(K key, byte[] value); + + /** + * Get the old value at the specified key and replace it with the given value, converting it + * to an instance of the given type. + * + * @param key + * @param value + * @param requiredType The type to convert the value to. + * @return The old value (before it was overwritten). + */ + T getAndSetAsType(K key, V value, Class requiredType); + + // Multi-get operations + + /** + * Get all the values at the specified keys. + * + * @param keys + * @return A list of the values retrieved or an empty list if none were found. + */ + List getValues(List keys); + + /** + * Variation on {@link KeyValueStoreOperations#getValues(java.util.List)} that uses varargs + * instead of a java.util.List. + * + * @param keys + * @return A list of the values retrieved or an empty list if none were found. + */ + List getValues(K... keys); + + /** + * Get all the values at the specified keys, converting the values into instances of the + * specified type. + * + * @param keys + * @param requiredType + * @return A list of the values retrieved or an empty list if none were found. + */ + List getValuesAsType(List keys, Class requiredType); + + /** + * A variation on {@link KeyValueStoreOperations#getValuesAsType(java.util.List, Class)} that + * takes uses varargs instead of a java.util.List. + * + * @param requiredType + * @param keys + * @return A list of the values retrieved or an empty list if none were found. + */ + List getValuesAsType(Class requiredType, K... keys); + + // Set if non-existent operations + + /** + * Set the value at the given key only if that key doesn't already exist. + * + * @param key + * @param value + * @return This template interface + */ + KeyValueStoreOperations setIfKeyNonExistent(K key, V value); + + /** + * Set the value at the given key as a byte array only if that key doesn't already exist. + * + * @param key + * @param value + * @return This template interface + */ + KeyValueStoreOperations setIfKeyNonExistentAsBytes(K key, byte[] value); + + // Multiple key-value set + + /** + * Convenience method to set multiple values as Key/Value pairs. + * + * @param keysAndValues + * @return This template interface + */ + KeyValueStoreOperations setMultiple(Map keysAndValues); + + /** + * Convenience method to set multiple values as Key/byte[] pairs. + * + * @param keysAndValues + * @return This template interface + */ + KeyValueStoreOperations setMultipleAsBytes(Map keysAndValues); + + // Multiple key-value set if non-existent + + /** + * Variation on setting multiple values only if the key doesn't already exist. + * + * @param keysAndValues + * @return This template interface + */ + KeyValueStoreOperations setMultipleIfKeysNonExistent(Map keysAndValues); + + /** + * Variation on setting multiple values as byte arrays only if the key doesn't already exist. + * + * @param keysAndValues + * @param + * @return + */ + KeyValueStoreOperations setMultipleAsBytesIfKeysNonExistent(Map keysAndValues); + + /** + * Does the store contain this key? + * + * @param key + * @return true if the key exists, false otherwise. + */ + boolean containsKey(K key); + + /** + * Delete one or more keys from the store. + * + * @param keys + * @return true if all keys were successfully deleted, false + * otherwise. + */ + boolean deleteKeys(K... keys); + + /** + * Get the properties of the specified bucket. + * + * @param bucket + * @return The bucket properties, without a list of keys in that bucket. + */ + Map getBucketSchema(B bucket); + + KeyValueStoreOperations updateBucketSchema(B bucket, Map props); + + /** + * Get the properties of the bucket and specify whether or not to list the keys in that + * bucket. + * + * @param bucket + * @param listKeys + * @return The bucket properties, with or without a list of keys in that bucket. + */ + Map getBucketSchema(B bucket, boolean listKeys); + + KeyValueStoreOperations setAsBytes(K key, byte[] value, QosParameters qosParams); + + KeyValueStoreOperations setWithMetaData(K key, V value, Map metaData); +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreValue.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreValue.java new file mode 100644 index 000000000..51d29c877 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/KeyValueStoreValue.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * A generic interface for dealing with values and their store metadata. + * + * @author J. Brisbin + */ +public interface KeyValueStoreValue { + + /** + * Get the metadata associated with this value. + * + * @return + */ + KeyValueStoreMetaData getMetaData(); + + /** + * Get the converted value itself. + * + * @return + */ + T get(); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/QosParameters.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/QosParameters.java new file mode 100644 index 000000000..e65f6f32c --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/QosParameters.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * Specify Quality Of Service parameters. + * + * @author J. Brisbin + */ +public interface QosParameters { + + /** + * Instruct the server on the read threshold. + * + * @param + * @return + */ + public T getReadThreshold(); + + /** + * Instruct the server on the normal write threshold. + * + * @param + * @return + */ + public T getWriteThreshold(); + + /** + * Instruct the server on the durable write threshold. + * + * @param + * @return + */ + public T getDurableWriteThreshold(); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakKeyValueTemplate.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakKeyValueTemplate.java new file mode 100644 index 000000000..edb9b4e44 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakKeyValueTemplate.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.keyvalue.riak.mapreduce.MapReduceJob; +import org.springframework.data.keyvalue.riak.mapreduce.MapReduceOperations; +import org.springframework.data.keyvalue.riak.mapreduce.RiakMapReduceJob; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.util.Assert; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; + +/** + * An implementation of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreOperations} + * and {@link org.springframework.data.keyvalue.riak.mapreduce.MapReduceOperations} for the Riak + * data store. + *

+ * To use the RiakTemplate, create a singleton in your Spring application-context.xml: + *


+ * <bean id="riak" class="org.springframework.data.keyvalue.riak.core.RiakTemplate"
+ *     p:defaultUri="http://localhost:8098/riak/{bucket}/{key}"
+ *     p:mapReduceUri="http://localhost:8098/mapred"/>
+ * 
+ * To store and retrieve objects in Riak, use the setXXX and getXXX methods (example in + * Groovy): + *

+ * def obj = new TestObject(name: "My Name", age: 40)
+ * riak.set([bucket: "mybucket", key: "mykey"], obj)
+ * ...
+ * def name = riak.get([bucket: "mybucket", key: "mykey"]).name
+ * println "Hello $name!"
+ * 
+ * You're key object should be one of:
  • A String encoding the bucket and key + * together, separated by a colon. e.g. "mybucket:mykey"
  • An implementation of + * BucketKeyPair (like {@link org.springframework.data.keyvalue.riak.core.SimpleBucketKeyPair})
  • + *
  • A Map with both a "bucket" and a "key" specified.
  • A + * String of only the key name, but specifying a bucket by using the {@link + * org.springframework.data.keyvalue.riak.convert.KeyValueStoreMetaData} annotation on the + * object you're storing.
+ * + * @author J. Brisbin + */ +public class RiakKeyValueTemplate extends AbstractRiakTemplate implements KeyValueStoreOperations, MapReduceOperations, InitializingBean { + + protected RiakTemplate riak; + + /** + * Take all the defaults. + */ + public RiakKeyValueTemplate() { + super(); + riak = new RiakTemplate(); + } + + /** + * Use the specified {@link org.springframework.http.client.ClientHttpRequestFactory}. + * + * @param requestFactory + */ + public RiakKeyValueTemplate(ClientHttpRequestFactory requestFactory) { + super(requestFactory); + riak = new RiakTemplate(requestFactory); + } + + /** + * Use the specified defaultUri and mapReduceUri. + * + * @param defaultUri + * @param mapReduceUri + */ + public RiakKeyValueTemplate(String defaultUri, String mapReduceUri) { + setRestTemplate(new RestTemplate()); + this.setDefaultUri(defaultUri); + this.mapReduceUri = mapReduceUri; + this.riak = new RiakTemplate(defaultUri, mapReduceUri); + } + + @Override + public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + riak.afterPropertiesSet(); + } + + /*----------------- Set Operations -----------------*/ + + public KeyValueStoreOperations set(K key, V value) { + return setWithMetaData(key, value, null, null); + } + + public KeyValueStoreOperations set(K key, V value, QosParameters qosParams) { + return setWithMetaData(key, value, null, qosParams); + } + + public KeyValueStoreOperations setAsBytes(K key, byte[] value) { + return setAsBytes(key, value, null); + } + + public KeyValueStoreOperations setAsBytes(K key, byte[] value, QosParameters qosParams) { + Assert.notNull(key, "Key cannot be null!"); + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, value); + riak.setAsBytes(bucketKeyPair.getBucket(), bucketKeyPair.getKey(), value, qosParams); + return this; + } + + public KeyValueStoreOperations setWithMetaData(K key, V value, Map metaData, QosParameters qosParams) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, value); + riak.setWithMetaData(bucketKeyPair.getBucket(), + bucketKeyPair.getKey(), + value, + metaData, + qosParams); + return this; + } + + public KeyValueStoreOperations setWithMetaData(K key, V value, Map metaData) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, value); + riak.setWithMetaData(bucketKeyPair.getBucket(), + bucketKeyPair.getKey(), + value, + metaData, + null); + return this; + } + + /*----------------- Get Operations -----------------*/ + + public RiakMetaData getMetaData(K key) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.getMetaData(bucketKeyPair.getBucket(), bucketKeyPair.getKey()); + } + + public RiakValue getWithMetaData(K key, Class requiredType) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.getWithMetaData(bucketKeyPair.getBucket(), + bucketKeyPair.getKey(), + requiredType); + } + + @SuppressWarnings({"unchecked"}) + public V get(K key) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return (V) riak.get(bucketKeyPair.getBucket(), bucketKeyPair.getKey()); + } + + public byte[] getAsBytes(K key) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + RiakValue obj = riak.getAsBytesWithMetaData(bucketKeyPair.getBucket(), + bucketKeyPair.getKey()); + return (null != obj ? obj.get() : null); + } + + public RiakValue getAsBytesWithMetaData(K key) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.getAsBytesWithMetaData(bucketKeyPair.getBucket(), bucketKeyPair.getKey()); + } + + public T getAsType(K key, Class requiredType) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.getAsType(bucketKeyPair.getBucket(), bucketKeyPair.getKey(), requiredType); + } + + public V getAndSet(K key, V value) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.getAndSet(bucketKeyPair.getBucket(), bucketKeyPair.getKey(), value); + } + + public byte[] getAndSetAsBytes(K key, byte[] value) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.getAndSetAsBytes(bucketKeyPair.getBucket(), bucketKeyPair.getKey(), value); + } + + public T getAndSetAsType(K key, V value, Class requiredType) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.getAndSetAsType(bucketKeyPair.getBucket(), + bucketKeyPair.getKey(), + value, + requiredType); + } + + @SuppressWarnings({"unchecked"}) + public List getValues(List keys) { + List results = new ArrayList(); + for (K key : keys) { + BucketKeyPair bkp = resolveBucketKeyPair(key, null); + results.add((V) riak.get(bkp.getBucket(), bkp.getKey())); + } + return results; + } + + public List getValues(K... keys) { + return getValues(keys); + } + + public List getValuesAsType(List keys, Class requiredType) { + List results = new ArrayList(); + for (K key : keys) { + BucketKeyPair bkp = resolveBucketKeyPair(key, null); + results.add(riak.getAsType(bkp.getBucket(), bkp.getKey(), requiredType)); + } + return results; + } + + public List getValuesAsType(Class requiredType, K... keys) { + return riak.getValuesAsType(requiredType, keys); + } + + /*----------------- Only-Set-Once Operations -----------------*/ + + public KeyValueStoreOperations setIfKeyNonExistent(K key, V value) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + riak.setIfKeyNonExistent(bucketKeyPair.getBucket(), bucketKeyPair.getKey(), value); + return this; + } + + public KeyValueStoreOperations setIfKeyNonExistentAsBytes(K key, byte[] value) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + riak.setIfKeyNonExistent(bucketKeyPair.getBucket(), bucketKeyPair.getKey(), value); + return this; + } + + /*----------------- Multiple Item Operations -----------------*/ + + public KeyValueStoreOperations setMultiple(Map keysAndValues) { + for (Map.Entry entry : keysAndValues.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + return this; + } + + public KeyValueStoreOperations setMultipleAsBytes(Map keysAndValues) { + for (Map.Entry entry : keysAndValues.entrySet()) { + setAsBytes(entry.getKey(), entry.getValue()); + } + return this; + } + + public KeyValueStoreOperations setMultipleIfKeysNonExistent(Map keysAndValues) { + for (Map.Entry entry : keysAndValues.entrySet()) { + setIfKeyNonExistent(entry.getKey(), entry.getValue()); + } + return this; + } + + public KeyValueStoreOperations setMultipleAsBytesIfKeysNonExistent(Map keysAndValues) { + for (Map.Entry entry : keysAndValues.entrySet()) { + setIfKeyNonExistentAsBytes(entry.getKey(), entry.getValue()); + } + return this; + } + + /*----------------- Key Operations -----------------*/ + + public boolean containsKey(K key) { + BucketKeyPair bucketKeyPair = resolveBucketKeyPair(key, null); + return riak.containsKey(bucketKeyPair.getBucket(), bucketKeyPair.getKey()); + } + + public boolean deleteKeys(K... keys) { + return riak.deleteKeys(keys); + } + + /*----------------- Map/Reduce Operations -----------------*/ + + public RiakMapReduceJob createMapReduceJob() { + return new RiakMapReduceJob(riak); + } + + public Object execute(MapReduceJob job) { + return execute(job, List.class); + } + + public T execute(MapReduceJob job, Class targetType) { + return riak.execute(job, targetType); + } + + public Future> submit(MapReduceJob job) { + // Run this job asynchronously. + return riak.submit(job); + } + + /*----------------- Link Operations -----------------*/ + + /** + * Use Riak's native Link mechanism to link two entries together. + * + * @param destination Key to the child object + * @param source Key to the parent object + * @param tag The tag for this relationship + * @return This template interface + */ + public RiakKeyValueTemplate link(K1 destination, K2 source, String tag) { + BucketKeyPair bkpFrom = resolveBucketKeyPair(source, null); + BucketKeyPair bkpTo = resolveBucketKeyPair(destination, null); + riak.link(bkpTo.getBucket(), bkpTo.getKey(), bkpFrom.getBucket(), bkpFrom.getKey(), tag); + return this; + } + + /** + * Use Riak's link walking mechanism to retrieve a multipart message that will be decoded like + * they were individual objects (e.g. using the built-in HttpMessageConverters of + * RestTemplate). + * + * @param source + * @param tag + * @return + */ + @SuppressWarnings({"unchecked"}) + public T linkWalk(K source, String tag) { + BucketKeyPair bkpSource = resolveBucketKeyPair(source, null); + return (T) riak.linkWalk(bkpSource.getBucket(), bkpSource.getKey(), tag); + } + + /*----------------- Bucket Operations -----------------*/ + + public Map getBucketSchema(B bucket) { + return riak.getBucketSchema(bucket, false); + } + + public Map getBucketSchema(B bucket, boolean listKeys) { + return riak.getBucketSchema(bucket, listKeys); + } + + public KeyValueStoreOperations updateBucketSchema(B bucket, Map props) { + riak.updateBucketSchema(bucket, props); + return this; + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakMetaData.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakMetaData.java new file mode 100644 index 000000000..12d80079c --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakMetaData.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import org.springframework.http.MediaType; + +import java.util.Date; +import java.util.Map; + +/** + * An implementation of {@link org.springframework.data.keyvalue.riak.core.KeyValueStoreMetaData} + * for Riak. + * + * @author J. Brisbin + */ +public class RiakMetaData implements KeyValueStoreMetaData { + + private MediaType mediaType = MediaType.APPLICATION_JSON; + private Map properties; + private String bucket = null; + private String key = null; + + public RiakMetaData(Map properties) { + this.properties = properties; + } + + public RiakMetaData(MediaType mediaType, Map properties) { + this.mediaType = mediaType; + this.properties = properties; + } + + public RiakMetaData(MediaType mediaType, Map properties, String bucket, String key) { + this.mediaType = mediaType; + this.properties = properties; + this.bucket = bucket; + this.key = key; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + public void setKey(String key) { + this.key = key; + } + + public String getBucket() { + return this.bucket; + } + + public String getKey() { + return this.key; + } + + public MediaType getContentType() { + return mediaType; + } + + public long getLastModified() { + return ((Date) properties.get("Last-Modified")).getTime(); + } + + public Map getProperties() { + return this.properties; + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakQosParameters.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakQosParameters.java new file mode 100644 index 000000000..a0f4a0ebe --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakQosParameters.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * A generic class for specifying Quality Of Service parameters on operations. + * + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class RiakQosParameters implements QosParameters { + + public Object readThreshold = null; + public Object writeThreshold = null; + public Object durableWriteThreshold = null; + + public void setReadThreshold(T readThreshold) { + this.readThreshold = readThreshold; + } + + public void setWriteThreshold(T writeThreshold) { + this.writeThreshold = writeThreshold; + } + + public void setDurableWriteThreshold(T durableWriteThreshold) { + this.durableWriteThreshold = durableWriteThreshold; + } + + public T getReadThreshold() { + return (T) this.readThreshold; + } + + public T getWriteThreshold() { + return (T) this.writeThreshold; + } + + public T getDurableWriteThreshold() { + return (T) this.durableWriteThreshold; + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakTemplate.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakTemplate.java new file mode 100644 index 000000000..740fb96f2 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakTemplate.java @@ -0,0 +1,781 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import org.springframework.core.convert.ConversionService; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.data.keyvalue.riak.DataStoreOperationException; +import org.springframework.data.keyvalue.riak.mapreduce.MapReduceJob; +import org.springframework.data.keyvalue.riak.mapreduce.MapReduceOperations; +import org.springframework.http.*; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.client.*; + +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMultipart; +import javax.mail.util.ByteArrayDataSource; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.*; +import java.util.concurrent.Future; + +/** + * An implementation of {@link org.springframework.data.keyvalue.riak.core.BucketKeyValueStoreOperations} + * and {@link org.springframework.data.keyvalue.riak.mapreduce.MapReduceOperations} for the Riak + * data store. + *

+ * To use the RiakTemplate, create a singleton in your Spring application-context.xml: + *


+ * <bean id="riak" class="org.springframework.data.keyvalue.riak.core.RiakTemplate"
+ *     p:defaultUri="http://localhost:8098/riak/{bucket}/{key}"
+ *     p:mapReduceUri="http://localhost:8098/mapred"/>
+ * 
+ * To store and retrieve objects in Riak, use the setXXX and getXXX methods (example in + * Groovy): + *

+ * def obj = new TestObject(name: "My Name", age: 40)
+ * riak.set("mybucket", "mykey", obj)
+ * ...
+ * def name = riak.get("mybucket", "mykey").name
+ * println "Hello $name!"
+ * 
+ * + * @author J. Brisbin + */ +public class RiakTemplate extends AbstractRiakTemplate implements BucketKeyValueStoreOperations, MapReduceOperations { + + /** + * Take all the defaults. + */ + public RiakTemplate() { + super(); + } + + /** + * Use the specified {@link org.springframework.http.client.ClientHttpRequestFactory}. + * + * @param requestFactory + */ + public RiakTemplate(ClientHttpRequestFactory requestFactory) { + super(requestFactory); + } + + /** + * Use the specified defaultUri and mapReduceUri. + * + * @param defaultUri + * @param mapReduceUri + */ + public RiakTemplate(String defaultUri, String mapReduceUri) { + setRestTemplate(new RestTemplate()); + this.setDefaultUri(defaultUri); + this.mapReduceUri = mapReduceUri; + } + +/*----------------- Set Operations -----------------*/ + + public BucketKeyValueStoreOperations set(B bucket, K key, V value) { + return setWithMetaData(bucket, key, value, null, null); + } + + public BucketKeyValueStoreOperations set(B bucket, K key, V value, + QosParameters qosParams) { + return setWithMetaData(bucket, key, value, null, qosParams); + } + + public BucketKeyValueStoreOperations setAsBytes(B bucket, K key, byte[] value) { + return setAsBytes(bucket, key, value, null); + } + + public BucketKeyValueStoreOperations setAsBytes(B bucket, K key, byte[] value, + QosParameters qosParams) { + return setWithMetaData(bucket, key, value, null, qosParams); + } + + public BucketKeyValueStoreOperations setWithMetaData(B bucket, K key, V value, + Map metaData, + QosParameters qosParams) { + Assert.notNull(key, "Key cannot be null!"); + // Get a key name that may or may not include the QOS parameters. + String keyName = (null != qosParams ? key.toString() + extractQosParameters(qosParams) : key + .toString()); + + KeyValueStoreMetaData origMeta = getMetaData(bucket, keyName); + String vclock = null; + if (null != origMeta) { + Map mprops = origMeta.getProperties(); + if (null != mprops) { + Object o = mprops.get(RIAK_VCLOCK); + if (null != o) { + vclock = o.toString(); + } + } + } + RestTemplate restTemplate = getRestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Riak-ClientId", RIAK_CLIENT_ID); + if (log.isDebugEnabled() && null == value) { + log.debug("bucket=" + bucket + ", key=" + key); + } + headers.setContentType(extractMediaType(value)); + if (null != vclock) { + headers.set(RIAK_VCLOCK, vclock); + } + if (null != metaData) { + for (Map.Entry entry : metaData.entrySet()) { + headers.set(entry.getKey(), entry.getValue()); + } + } + headers.set(RIAK_META_CLASSNAME, + (null != value ? value.getClass().getName() : defaultType.getName())); + HttpEntity entity = new HttpEntity(value, headers); + try { + restTemplate.put(defaultUri, entity, bucket, keyName); + if (log.isDebugEnabled()) { + log.debug(String.format("PUT object: bucket=%s, key=%s, value=%s", bucket, key, value)); + } + } catch (RestClientException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } + return this; + } + + public BucketKeyValueStoreOperations setWithMetaData(B bucket, K key, V value, + Map metaData) { + return setWithMetaData(bucket, key, value, metaData, null); + } + + /*----------------- Put Operations -----------------*/ + + /** + * Save an object to Riak and let it generate an ID for it. + * + * @param bucket + * @param value + * @return The generated ID + */ + public String put(B bucket, V value) { + return put(bucket, value, null); + } + + /** + * Save an object to Riak and let it generate an ID for it. + * + * @param bucket + * @param value + * @param metaData + * @return The generated ID + */ + public String put(B bucket, V value, Map metaData) { + Assert.notNull(bucket, "Bucket cannot be null."); + String bucketName = bucket.toString(); + RestTemplate restTemplate = getRestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Riak-ClientId", RIAK_CLIENT_ID); + headers.setContentType(extractMediaType(value)); + if (null != metaData) { + for (Map.Entry entry : metaData.entrySet()) { + headers.set(entry.getKey(), entry.getValue()); + } + } + headers.set(RIAK_META_CLASSNAME, value.getClass().getName()); + HttpEntity entity = new HttpEntity(value, headers); + try { + URI uri = restTemplate.postForLocation(defaultUri, entity, bucketName, ""); + String suri = uri.toString(); + String id = suri.substring(suri.lastIndexOf("/") + 1); + if (log.isDebugEnabled()) { + log.debug("New ID: " + id); + } + return id; + } catch (RestClientException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } + } + + /*----------------- Get Operations -----------------*/ + + public RiakMetaData getMetaData(B bucket, K key) { + RestTemplate restTemplate = getRestTemplate(); + HttpHeaders headers; + try { + headers = restTemplate.headForHeaders(defaultUri, bucket, key); + RiakMetaData meta = extractMetaData(headers); + meta.setBucket((null != bucket ? bucket.toString() : null)); + meta.setKey((null != key ? key.toString() : null)); + return meta; + } catch (ResourceAccessException e) { + } catch (IOException e) { + throw new DataAccessResourceFailureException(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + public RiakValue getWithMetaData(B bucket, K key, Class requiredType) { + // If no bucket name is given, infer it from the type name. + String bucketName = (null != bucket ? bucket.toString() : requiredType.getName()); + RestTemplate restTemplate = getRestTemplate(); + if (log.isDebugEnabled()) { + log.debug(String.format("GET object: bucket=%s, key=%s, type=%s", + bucketName, + key, + requiredType.getName())); + } + Class origType = getType(bucket, key, classLoader); + RiakValue val = null; + try { + ResponseEntity result = restTemplate.getForEntity(defaultUri, + requiredType, + bucketName, + key); + val = extractValue(result, requiredType, requiredType); + } catch (HttpClientErrorException e) { + switch (e.getStatusCode()) { + case NOT_ACCEPTABLE: + // Can't convert using HttpMessageConverter. Try fetching as the original type + // and using the conversion service to convert. + ResponseEntity result = restTemplate.getForEntity(defaultUri, + origType, + bucketName, + key); + try { + val = extractValue(result, origType, requiredType); + } catch (IOException ioe) { + throw new DataStoreOperationException(ioe.getMessage(), ioe); + } + break; + case NOT_FOUND: + // IGNORED + break; + default: + throw new DataStoreOperationException(e.getMessage(), e); + } + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { + throw new DataStoreOperationException(e.getMessage(), e); + } + } catch (RestClientException rce) { + if (rce.getMessage().contains("HTTP response code: 406")) { + // Can't convert using HttpMessageConverter. Try fetching as the original type + // and using the conversion service to convert. + ResponseEntity result = restTemplate.getForEntity(defaultUri, + origType, + bucketName, + key); + try { + val = extractValue(result, origType, requiredType); + } catch (IOException ioe) { + throw new DataStoreOperationException(rce.getMessage(), rce); + } + } else { + // IGNORE + if (log.isDebugEnabled()) { + log.debug("RestClientException: " + rce.getMessage()); + } + } + } catch (EOFException eof) { + // IGNORE + if (log.isDebugEnabled()) { + log.debug("EOFException: " + eof.getMessage(), eof); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + + if (null != val && useCache) { + cache.put(new SimpleBucketKeyPair(bucket, key), val); + } + return val; + } + + @SuppressWarnings({"unchecked"}) + public T get(B bucket, K key) { + Class targetClass = getType(bucket, key, classLoader); + RiakValue obj = getWithMetaData(bucket, key, targetClass); + return (null != obj ? obj.get() : null); + } + + public byte[] getAsBytes(B bucket, K key) { + RiakValue obj = getAsBytesWithMetaData(bucket, key); + return (null != obj ? obj.get() : null); + } + + @SuppressWarnings({"unchecked"}) + public RiakValue getAsBytesWithMetaData(final B bucket, final K key) { + final RestTemplate restTemplate = getRestTemplate(); + if (log.isDebugEnabled()) { + log.debug(String.format("GET object: bucket=%s, key=%s, type=byte[]", + bucket, + key)); + } + + try { + RiakValue bytes = (RiakValue) restTemplate.execute( + defaultUri, + HttpMethod.GET, + new RequestCallback() { + public void doWithRequest(ClientHttpRequest request) throws + IOException { + List mediaTypes = new ArrayList(); + mediaTypes.add(MediaType.APPLICATION_JSON); + mediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); + request.getHeaders().setAccept(mediaTypes); + } + }, + new ResponseExtractor() { + public Object extractData(ClientHttpResponse response) throws + IOException { + InputStream in = response.getBody(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buff = new byte[in.available()]; + for (int bytesRead = in.read(buff); bytesRead > 0; bytesRead = in.read( + buff)) { + out.write(buff, 0, bytesRead); + } + + HttpHeaders headers = response.getHeaders(); + RiakMetaData meta = extractMetaData(headers); + meta.setBucket((null != bucket ? bucket.toString() : null)); + meta.setKey((null != key ? key.toString() : null)); + RiakValue val = new RiakValue(out.toByteArray(), + meta); + return val; + } + }, + bucket, + key); + if (useCache) { + cache.put(new SimpleBucketKeyPair(bucket, key), bytes); + } + return bytes; + } catch (HttpClientErrorException e) { + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { + throw new DataStoreOperationException(e.getMessage(), e); + } + } catch (RestClientException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + public T getAsType(B bucket, K key, Class requiredType) { + if (useCache) { + Object obj = checkCache(new SimpleBucketKeyPair(bucket, key), requiredType); + if (null != obj) { + return (T) obj; + } + } + RiakValue obj = getWithMetaData(bucket, key, requiredType); + return (null != obj ? obj.get() : null); + } + + @SuppressWarnings({"unchecked"}) + public V getAndSet(B bucket, K key, V value) { + V old = (V) getAsType(bucket, key, value.getClass()); + set(bucket, key, value, null); + return old; + } + + public byte[] getAndSetAsBytes(B bucket, K key, byte[] value) { + byte[] old = getAsBytes(bucket, key); + setAsBytes(bucket, key, value); + return old; + } + + public T getAndSetAsType(B bucket, K key, V value, Class requiredType) { + T old = getAsType(bucket, key, requiredType); + set(bucket, key, value); + return old; + } + + @SuppressWarnings({"unchecked"}) + public List getValues(List keys) { + List results = new ArrayList(); + for (K key : keys) { + BucketKeyPair bkp = resolveBucketKeyPair(key, null); + results.add((V) get(bkp.getBucket(), bkp.getKey())); + } + return results; + } + + public List getValues(K... keys) { + return getValues(keys); + } + + public List getValuesAsType(List keys, Class requiredType) { + List results = new ArrayList(); + for (K key : keys) { + BucketKeyPair bkp = resolveBucketKeyPair(key, null); + results.add(getAsType(bkp.getBucket(), bkp.getKey(), requiredType)); + } + return results; + } + + public List getValuesAsType(Class requiredType, K... keys) { + List keyList = new ArrayList(keys.length); + return getValuesAsType(keyList, requiredType); + } + + /*----------------- Only-Set-Once Operations -----------------*/ + + public BucketKeyValueStoreOperations setIfKeyNonExistent(B bucket, K key, V value) { + if (!containsKey(bucket, key)) { + set(bucket, key, value); + } else { + if (log.isDebugEnabled()) { + log.debug(String.format("key: %s already exists. Not adding %s", + key, + value)); + } + } + return this; + } + + public BucketKeyValueStoreOperations setIfKeyNonExistentAsBytes(B bucket, K key, + byte[] value) { + if (!containsKey(bucket, key)) { + setAsBytes(bucket, key, value); + } else { + if (log.isDebugEnabled()) { + log.debug(String.format("key: %s already exists. Not adding %s", + key, + value)); + } + } + return this; + } + + /*----------------- Key Operations -----------------*/ + + public boolean containsKey(B bucket, K key) { + RestTemplate restTemplate = getRestTemplate(); + HttpHeaders headers = null; + try { + headers = restTemplate.headForHeaders(defaultUri, bucket, key); + } catch (ResourceAccessException e) { + } + return (null != headers); + } + + @SuppressWarnings({"unchecked"}) + public boolean delete(B bucket, K key) { + return deleteKeys(new SimpleBucketKeyPair(bucket, key)); + } + + public boolean deleteKeys(K... keys) { + boolean stillExists = false; + RestTemplate restTemplate = getRestTemplate(); + for (K key : keys) { + BucketKeyPair bkp = resolveBucketKeyPair(key, null); + try { + restTemplate.delete(defaultUri, bkp.getBucket(), bkp.getKey()); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { + throw new DataAccessResourceFailureException(e.getMessage(), e); + } + } + //if (!stillExists) { + //stillExists = containsKey(key); + //} + } + return !stillExists; + } + + /*----------------- Map/Reduce Operations -----------------*/ + + public Object execute(MapReduceJob job) { + return execute(job, List.class); + } + + @SuppressWarnings({"unchecked"}) + public T execute(MapReduceJob job, Class targetType) { + RestTemplate restTemplate = getRestTemplate(); + try { + ResponseEntity resp = restTemplate.postForEntity(mapReduceUri, + job.toJson(), + List.class); + if (resp.hasBody()) { + if (!targetType.isAssignableFrom(List.class)) { + // M/R jobs always return a List. Try to turn the List into something else. + List results = (List) resp.getBody(); + if (results.size() == 1) { + // A List of size 1 get's returned as the object at list[0]. + Object obj = results.get(0); + if (obj.getClass() != targetType) { + // I can't just return it as-is, I have to convert it first. + ConversionService conv = getConversionService(); + if (conv.canConvert(obj.getClass(), targetType)) { + return conv.convert(obj, targetType); + } else { + throw new DataAccessResourceFailureException( + "Can't find a converter to to convert " + obj + .getClass() + " returned from M/R job to required type " + targetType); + } + } else { + return (T) obj; + } + } + } + return (T) resp.getBody(); + } + } catch (RestClientException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + public Future> submit(MapReduceJob job) { + // Run this job asynchronously. + return workerPool.submit(job); + } + + /*----------------- Link Operations -----------------*/ + + /** + * Use Riak's native Link mechanism to link two entries together. + * + * @param destBucket Bucket of child entry + * @param destKey Key of child entry + * @param sourceBucket Bucket of parent entry + * @param sourceKey Key of parent entry + * @param tag Tag for this relationship + * @return + */ + @SuppressWarnings({"unchecked"}) + public RiakTemplate link(B1 destBucket, K1 destKey, B2 sourceBucket, + K2 sourceKey, String tag) { + RestTemplate restTemplate = getRestTemplate(); + + // Skip all conversion on the data since all we care about is the Link header. + RiakValue fromObj = getAsBytesWithMetaData(sourceBucket, sourceKey); + if (null == fromObj) { + throw new DataStoreOperationException( + "Cannot link from a non-existent source: " + sourceBucket + ":" + sourceKey); + } + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(fromObj.getMetaData().getContentType()); + headers.set(RIAK_VCLOCK, fromObj.getMetaData().getProperties().get(RIAK_VCLOCK).toString()); + Object linksObj = fromObj.getMetaData().getProperties().get("Link"); + List links = new ArrayList(); + // First add all existing links... + if (linksObj instanceof List) { + links.addAll((List) linksObj); + } else if (linksObj instanceof String) { + links.add(linksObj.toString()); + } + // ...then add the link we're creating... + links.add(String.format("<%s/%s/%s>; riaktag=\"%s\"", + getPrefix(), + destBucket, + destKey, + tag)); + String linkHeader = StringUtils.collectionToCommaDelimitedString(links); + headers.set("Link", linkHeader); + // Make sure to store the data back, otherwise it gets lost! + // Basho at some point will likely add the ability to updated metadata separate + // from the content. Until then, we have to transfer the body back-and-forth. + HttpEntity entity = new HttpEntity(fromObj.get(), headers); + restTemplate.put(defaultUri, entity, sourceBucket, sourceKey); + + return this; + } + + /** + * Use Riak's link walking mechanism to retrieve a multipart message that will be decoded like + * they were individual objects (e.g. using the built-in HttpMessageConverters of + * RestTemplate). + * + * @param bucket + * @param key + * @param tag + * @return + */ + @SuppressWarnings({"unchecked"}) + public T linkWalk(B bucket, K key, String tag) { + return (T) linkWalkAsType(bucket, key, tag, null); + } + + /** + * Use Riak's link walking mechanism to retrieve a multipart message that will be decoded like + * they were individual objects (e.g. using the built-in HttpMessageConverters of + * RestTemplate) and return the result as a list of objects of one of:
  1. The type + * specified by requiredType
  2. If that's null, try using the bucket name + * in which the object was stored
  3. If all else fails, use a {@link java.util.Map}
  4. + *
+ * + * @param bucket + * @param key + * @param tag + * @param requiredType + * @return + */ + @SuppressWarnings({"unchecked"}) + public T linkWalkAsType(B bucket, K key, String tag, final Class requiredType) { + final RestTemplate restTemplate = getRestTemplate(); + final List types = new ArrayList(); + types.add(MediaType.ALL); + T returnObj = (T) restTemplate.execute(defaultUri + "/_,{tag},_", + HttpMethod.GET, + new RequestCallback() { + public void doWithRequest(ClientHttpRequest request) throws + IOException { + // Make sure I can accept a multipart/mixed response. + request.getHeaders().setAccept(types); + } + }, + new ResponseExtractor() { + @SuppressWarnings({"unchecked"}) + public Object extractData(ClientHttpResponse response) throws + IOException { + String contentType = ((List) response.getHeaders().get("Content-Type")).get(0) + .toString(); + if (contentType.startsWith("multipart/mixed")) { + List results = new LinkedList(); + ByteArrayDataSource ds = new ByteArrayDataSource(response.getBody(), + "multipart/mixed"); + try { + // All this mess is for extracting multipart data from our response. + // I'm using the javax.mail stuff because it's the best multipart library + // that's in Maven and it's a common dependency anyway. + MimeMultipart mp = new MimeMultipart(ds); + int msgCnt = mp.getCount(); + for (int i = 0; i < msgCnt; i++) { + BodyPart bp = mp.getBodyPart(i); + if (bp.getContentType().startsWith("multipart/mixed")) { + MimeMultipart part = (MimeMultipart) bp.getContent(); + int partCnt = part.getCount(); + for (int j = 0; j < partCnt; j++) { + final BodyPart partBody = part.getBodyPart(j); + String partType = partBody.getContentType(); + String link = partBody.getHeader("Link")[0]; + String location = partBody.getHeader("Location")[0]; + String key = location.substring(location.lastIndexOf("/") + 1); + String[] links = StringUtils.delimitedListToStringArray(link, ","); + String bucketName = null; + for (String s : links) { + if (s.contains("rel=\"up\"")) { + String[] linkParts = StringUtils.delimitedListToStringArray(s, ";"); + int start = linkParts[0].lastIndexOf("/"); + bucketName = linkParts[0].substring(start + 1, + linkParts[0].length() - 1); + break; + } + } + Class clazz = requiredType; + if (null == clazz) { + clazz = getType(bucketName, key, classLoader); + } + + // Can convert message? + for (HttpMessageConverter converter : restTemplate + .getMessageConverters()) { + if (converter.canRead(clazz, MediaType.parseMediaType(partType))) { + HttpInputMessage msg = new HttpInputMessage() { + public InputStream getBody() throws IOException { + try { + return partBody.getInputStream(); + } catch (MessagingException e) { + log.error(e.getMessage(), e); + } + return null; + } + + public HttpHeaders getHeaders() { + return new HttpHeaders(); + } + }; + results.add(converter.read(clazz, msg)); + break; + } + } + + if (log.isDebugEnabled()) { + log.debug(String.format("results=%s", results)); + } + } + } + } + } catch (MessagingException e) { + log.error(e.getMessage(), e); + } + + return results; + } + return null; + } + }, + bucket, + key, + tag); + return returnObj; + } + + /*----------------- Bucket Operations -----------------*/ + + public Map getBucketSchema(B bucket) { + return getBucketSchema(bucket, false); + } + + @SuppressWarnings({"unchecked"}) + public Map getBucketSchema(B bucket, boolean listKeys) { + RestTemplate restTemplate = getRestTemplate(); + ResponseEntity resp = restTemplate.getForEntity(defaultUri, + Map.class, + bucket, + (listKeys ? "?keys=true" : "")); + if (resp.hasBody()) { + return resp.getBody(); + } else { + throw new DataStoreOperationException( + "Error encountered retrieving bucket schema (Status: " + resp.getStatusCode() + ")"); + } + } + + @SuppressWarnings({"unchecked"}) + public BucketKeyValueStoreOperations updateBucketSchema(B bucket, + Map props) { + Map bucketProps = new LinkedHashMap(); + bucketProps.put("props", props); + RestTemplate restTemplate = getRestTemplate(); + String bucketName; + if (bucket instanceof String) { + bucketName = bucket.toString(); + } else { + BucketKeyPair bkp = resolveBucketKeyPair(bucket, null); + bucketName = bkp.getBucket().toString(); + } + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity(bucketProps, headers); + restTemplate.put(defaultUri, entity, bucketName, ""); + return this; + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakValue.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakValue.java new file mode 100644 index 000000000..4ea033da7 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/RiakValue.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class RiakValue implements KeyValueStoreValue { + + private Object delegate; + private KeyValueStoreMetaData metaData; + + public RiakValue(T delegate, KeyValueStoreMetaData metaData) { + this.delegate = delegate; + this.metaData = metaData; + } + + public KeyValueStoreMetaData getMetaData() { + return this.metaData; + } + + public T get() { + return (T) delegate; + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/SimpleBucketKeyPair.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/SimpleBucketKeyPair.java new file mode 100644 index 000000000..d60a8b854 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/SimpleBucketKeyPair.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class SimpleBucketKeyPair implements BucketKeyPair, Comparable { + + private Object bucket; + private Object key; + + public SimpleBucketKeyPair(B bucket, K key) { + this.bucket = bucket; + this.key = key; + } + + public B getBucket() { + return (B) bucket; + } + + public K getKey() { + return (K) key; + } + + public int compareTo(Object o) { + if (o instanceof SimpleBucketKeyPair) { + SimpleBucketKeyPair pair = (SimpleBucketKeyPair) o; + if (pair.getBucket().equals(bucket) && pair.getKey().equals(key)) { + return 0; + } + } + return -1; + } + + @Override + public String toString() { + return String.format("{bucket=%s, key=%s}", bucket, key); + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/SimpleBucketKeyResolver.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/SimpleBucketKeyResolver.java new file mode 100644 index 000000000..5d5ea0419 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/SimpleBucketKeyResolver.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +import org.codehaus.groovy.runtime.GStringImpl; +import org.springframework.util.ClassUtils; + +import java.util.Map; +import java.util.regex.Pattern; + +/** + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class SimpleBucketKeyResolver implements BucketKeyResolver { + + private final boolean groovyPresent = ClassUtils.isPresent( + "org.codehaus.groovy.runtime.GStringImpl", + getClass().getClassLoader()); + + protected Pattern bucketColonKey = Pattern.compile("(.+):(.+)"); + + public boolean canResolve(V o) { + if (o instanceof String) { + return true; + } else if (o instanceof Map) { + Map m = (Map) o; + return (m.containsKey("bucket") && m.containsKey("key")); + } else if (o instanceof BucketKeyPair) { + return true; + } else if (groovyPresent && o instanceof GStringImpl) { + return true; + } + + return false; + } + + public BucketKeyPair resolve(V o) { + BucketKeyPair bucketKeyPair = null; + + if (o instanceof String) { + String[] s = ((String) o).split(":"); + bucketKeyPair = new SimpleBucketKeyPair((s.length == 1 ? null : s[0]), + (s.length == 1 ? s[0] : s[1])); + } else if (o instanceof Map) { + Map m = (Map) o; + Object bucket = m.get("bucket"); + Object key = m.get("key"); + bucketKeyPair = new SimpleBucketKeyPair((null != bucket ? bucket + .toString() : null), + (null != key ? key.toString() : null)); + } else if (o instanceof BucketKeyPair) { + bucketKeyPair = (BucketKeyPair) o; + } else if (groovyPresent && o instanceof GStringImpl) { + bucketKeyPair = resolve(o.toString()); + } + + return bucketKeyPair; + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakFile.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakFile.java new file mode 100644 index 000000000..1687172af --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakFile.java @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core.io; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.keyvalue.riak.DataStoreOperationException; +import org.springframework.data.keyvalue.riak.core.KeyValueStoreMetaData; +import org.springframework.data.keyvalue.riak.core.RiakTemplate; +import org.springframework.data.keyvalue.riak.core.RiakValue; + +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author J. Brisbin + */ +public class RiakFile extends File { + + private static final long serialVersionUID = 1L; + protected final Log log = LogFactory.getLog(getClass()); + + private RiakTemplate riak; + private B bucket; + private K key; + + public RiakFile(RiakTemplate riak, B bucket, K key) throws URISyntaxException { + super(riak.getDefaultUri()); + this.riak = riak; + this.bucket = bucket; + this.key = key; + } + + public String getUriAsString(boolean includeKey) { + String protocol = riak.getDefaultUri().substring(0, riak.getDefaultUri().indexOf(":")); + String uri = String.format("%s://%s:%s%s/%s/%s", + protocol, + riak.getHost(), + riak.getPort(), + riak.getPrefix(), + bucket, + (includeKey ? key : "")); + return uri; + } + + public RiakTemplate getRiak() { + return riak; + } + + public void setRiak(RiakTemplate riak) { + this.riak = riak; + } + + public B getBucket() { + return bucket; + } + + public void setBucket(B bucket) { + this.bucket = bucket; + } + + public K getKey() { + return key; + } + + public void setKey(K key) { + this.key = key; + } + + @Override + public String getName() { + return getUriAsString(true); + } + + @Override + public String getParent() { + return getUriAsString(false); + } + + @SuppressWarnings({"unchecked"}) + @Override + public File getParentFile() { + try { + return new RiakFile(riak, bucket, ""); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + return null; + } + + @Override + public String getPath() { + return getUriAsString(true); + } + + @Override + public boolean isAbsolute() { + return true; + } + + @Override + public String getAbsolutePath() { + return getUriAsString(true); + } + + @Override + public File getAbsoluteFile() { + return this; + } + + @Override + public String getCanonicalPath() throws IOException { + return getUriAsString(true); + } + + @Override + public File getCanonicalFile() throws IOException { + return this; + } + + @SuppressWarnings({"deprecation"}) + @Override + public URL toURL() throws MalformedURLException { + return new URL(getUriAsString(true)); + } + + @Override + public URI toURI() { + try { + return new URI(getUriAsString(true)); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + return null; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + @Override + public boolean exists() { + return riak.containsKey(bucket, key); + } + + @Override + public boolean isDirectory() { + return (key != null && "".equals(key)); + } + + @Override + public boolean isFile() { + return (key != null && !("".equals(key))); + } + + @Override + public boolean isHidden() { + return false; + } + + @Override + public long lastModified() { + KeyValueStoreMetaData meta = riak.getMetaData(bucket, key); + return (null != meta ? meta.getLastModified() : null); + } + + @Override + public long length() { + return riak.getAsBytes(bucket, key).length; + } + + @Override + public boolean createNewFile() throws IOException { + return true; + } + + @Override + public boolean delete() { + return riak.delete(bucket, key); + } + + @Override + public void deleteOnExit() { + // NO-OP + } + + @Override + public String[] list() { + if (isDirectory()) { + Map schema = riak.getBucketSchema(bucket, true); + String baseUri = getUriAsString(false); + List uris = new LinkedList(); + for (Object key : (List) ((Map) schema.get("props")).get("keys")) { + uris.add(baseUri + key); + } + return (String[]) uris.toArray(); + } + return new String[]{}; + } + + @Override + public String[] list(FilenameFilter filenameFilter) { + List uris = new LinkedList(); + for (String s : list()) { + if (filenameFilter.accept(this, s)) { + uris.add(s); + } + } + return (String[]) uris.toArray(); + } + + @SuppressWarnings({"unchecked"}) + @Override + public File[] listFiles() { + List uris = new LinkedList(); + for (String s : list()) { + try { + uris.add(new RiakFile(riak, bucket, s.substring(s.lastIndexOf("/")))); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + } + return (File[]) uris.toArray(); + } + + @SuppressWarnings({"unchecked"}) + @Override + public File[] listFiles(FilenameFilter filenameFilter) { + List uris = new LinkedList(); + for (String s : list(filenameFilter)) { + try { + uris.add(new RiakFile(riak, bucket, s.substring(s.lastIndexOf("/")))); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + } + return (File[]) uris.toArray(); + } + + @Override + public File[] listFiles(FileFilter fileFilter) { + return super.listFiles(fileFilter); //To change body of overridden methods use File | Settings | File Templates. + } + + @Override + public boolean mkdir() { + return true; + } + + @Override + public boolean mkdirs() { + return true; + } + + @SuppressWarnings({"unchecked"}) + @Override + public boolean renameTo(File file) { + if (file instanceof RiakFile) { + RiakFile f = (RiakFile) file; + RiakValue v = riak.getAsBytesWithMetaData(bucket, key); + try { + riak.setWithMetaData(f.getBucket(), f.getKey(), v.get(), + (Map) v.getMetaData()); + } catch (DataStoreOperationException e) { + log.error(e.getMessage(), e); + return false; + } + } else { + throw new IllegalArgumentException("Renaming to a non-Riak file is not yet supported."); + } + return true; + } + + @Override + public boolean setLastModified(long l) { + return false; + } + + @Override + public boolean setReadOnly() { + return false; + } + + @Override + public boolean setWritable(boolean b, boolean b1) { + return true; + } + + @Override + public boolean setWritable(boolean b) { + return true; + } + + @Override + public boolean setReadable(boolean b, boolean b1) { + return true; + } + + @Override + public boolean setReadable(boolean b) { + return true; + } + + @Override + public boolean setExecutable(boolean b, boolean b1) { + return false; + } + + @Override + public boolean setExecutable(boolean b) { + return false; + } + + @Override + public boolean canExecute() { + return false; + } + + @Override + public long getTotalSpace() { + return super.getTotalSpace(); + } + + @Override + public long getFreeSpace() { + return super.getFreeSpace(); + } + + @Override + public long getUsableSpace() { + return super.getUsableSpace(); + } + + @Override + public int compareTo(File file) { + if (file instanceof RiakFile) { + return 0; + } else { + return -1; + } + } + + @Override + public boolean equals(Object o) { + if (o instanceof RiakFile) { + RiakFile rf = (RiakFile) o; + return (rf.getBucket().equals(bucket) && rf.getKey().equals(key)); + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "@" + getUriAsString(true); + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakInputStream.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakInputStream.java new file mode 100644 index 000000000..279b711f7 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakInputStream.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core.io; + +import org.springframework.data.keyvalue.riak.core.RiakTemplate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An {@link java.io.InputStream} implementation that is backed by a resource residing in Riak. + * + * @author J. Brisbin + */ +public class RiakInputStream extends InputStream { + + private RiakTemplate riak; + private B bucket; + private K key; + private ByteArrayInputStream in; + + public RiakInputStream(RiakTemplate riak, B bucket, K key) { + this.riak = riak; + this.bucket = bucket; + this.key = key; + this.in = new ByteArrayInputStream(riak.getAsBytes(bucket, key)); + } + + @Override + public int read(byte[] bytes) throws IOException { + return in.read(bytes); + } + + @Override + public int read(byte[] bytes, int i, int i1) throws IOException { + return in.read(bytes, i, i1); + } + + @Override + public long skip(long l) throws IOException { + return in.skip(l); + } + + @Override + public int available() throws IOException { + return in.available(); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public void mark(int i) { + in.mark(i); + } + + @Override + public void reset() throws IOException { + in.reset(); + } + + @Override + public boolean markSupported() { + return in.markSupported(); + } + + @Override + public int read() throws IOException { + return in.read(); + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakOutputStream.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakOutputStream.java new file mode 100644 index 000000000..bb82608a0 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakOutputStream.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core.io; + +import org.springframework.data.keyvalue.riak.core.RiakTemplate; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author J. Brisbin + */ +public class RiakOutputStream extends ByteArrayOutputStream { + + private RiakTemplate riak; + private B bucket; + private K key; + + public RiakOutputStream(RiakTemplate riak, B bucket, K key) { + this.riak = riak; + this.bucket = bucket; + this.key = key; + } + + @Override + public void flush() throws IOException { + super.flush(); + riak.setAsBytes(bucket, key, toByteArray()); + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakResource.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakResource.java new file mode 100644 index 000000000..7b34e0c8a --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/RiakResource.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core.io; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.data.keyvalue.riak.core.RiakTemplate; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * An implementation of {@link org.springframework.core.io.UrlResource} that is backed by a + * resource in Riak. + * + * @author J. Brisbin + */ +public class RiakResource extends UrlResource { + + protected final Log log = LogFactory.getLog(getClass()); + + private RiakTemplate riak; + private B bucket; + private K key; + private String description; + + public RiakResource(RiakTemplate riak, B bucket, K key) throws MalformedURLException { + super(riak.getDefaultUri()); + this.bucket = bucket; + this.key = key; + } + + public RiakTemplate getRiakTemplate() { + return this.riak; + } + + public B getBucket() { + return bucket; + } + + public K getKey() { + return key; + } + + @SuppressWarnings({"unchecked"}) + @Override + public URL getURL() throws IOException { + try { + return new URL(new RiakFile(riak, bucket, key).getUriAsString(true)); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + public URI getURI() throws IOException { + try { + return new RiakFile(riak, bucket, key).toURI(); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + public Resource createRelative(String relativePath) throws MalformedURLException { + if (relativePath.startsWith("../")) { + return new RiakResource(riak, bucket, relativePath.substring(3)); + } else if (!relativePath.startsWith("/")) { + return new RiakResource(riak, bucket, relativePath); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + public String getFilename() { + try { + return new RiakFile(riak, bucket, key).getUriAsString(true); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + return riak.getDefaultUri(); + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return this.description; + } + + @SuppressWarnings({"unchecked"}) + @Override + public File getFile() throws IOException { + try { + return new RiakFile(riak, bucket, key); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + public InputStream getInputStream() throws IOException { + return new RiakInputStream(riak, bucket, key); + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/overview.html b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/overview.html new file mode 100644 index 000000000..e9d4adc0c --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/io/overview.html @@ -0,0 +1,15 @@ + + +

+ Utilities for working with resources stored in Riak as standard java.io objects. Opening a RiakInputStream to a resource will allow code that doesn't + know anything about Key/Value datastores to access resources stored within them. +

+ +

+ Alternatively, writing data to a RiakOutputStream will + create a resources in Riak without exposing any of the underlying data access code to the + calling application. +

+ + \ No newline at end of file diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/overview.html b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/overview.html new file mode 100644 index 000000000..dc82cc087 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/core/overview.html @@ -0,0 +1,7 @@ + + +

+ Root package for the core utilities that make up the Riak data access library. +

+ + \ No newline at end of file diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakBuilder.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakBuilder.java new file mode 100644 index 000000000..381465e7d --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakBuilder.java @@ -0,0 +1,463 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.groovy; + +import groovy.lang.Closure; +import groovy.util.BuilderSupport; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.keyvalue.riak.DataStoreOperationException; +import org.springframework.data.keyvalue.riak.core.AsyncRiakTemplate; +import org.springframework.data.keyvalue.riak.core.RiakQosParameters; +import org.springframework.data.keyvalue.riak.core.SimpleBucketKeyPair; +import org.springframework.data.keyvalue.riak.mapreduce.*; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A Groovy Builder that implements a powerful and syntactically succinct DSL for Riak datastore + * access using SDKV for Riak's {@link AsyncRiakTemplate}. + *

+ * The DSL responds to most of the important methods from the AsyncRiakTemplate: + *

  • set
  • setAsBytes
  • put
  • get
  • getAsBytes
  • + *
  • getAsType
  • containsKey
  • delete
  • foreach
+ *

+ * An example of DSL usage (to delete all entries in a bucket): + *

riak.foreach(bucket: "test") {
+ *   completed { v, meta ->
+ *     delete(bucket: "test", key: meta.key)
+ *   }
+ * }
+ * 
+ * + * @author J. Brisbin + */ +public class RiakBuilder extends BuilderSupport { + + private static enum NodeName { + CALL, FOREACH, MAPREDUCE, QUERY, MAP, REDUCE, INPUTS, LANGUAGE, SOURCE, KEEP, ARG, COMPLETED, FAILED + } + + protected final Log log = LogFactory.getLog(getClass()); + @Autowired(required = false) + protected AsyncRiakTemplate riak; + @Autowired(required = false) + protected ExecutorService workerPool = Executors.newCachedThreadPool(); + protected String defaultBucketName; + protected List results = new LinkedList(); + + public RiakBuilder() { + } + + public RiakBuilder(AsyncRiakTemplate riak) { + this.riak = riak; + } + + public RiakBuilder(AsyncRiakTemplate riak, ExecutorService workerPool) { + this.riak = riak; + this.workerPool = workerPool; + } + + public RiakBuilder(BuilderSupport proxyBuilder, AsyncRiakTemplate riak) { + super(proxyBuilder); + this.riak = riak; + } + + public RiakBuilder(Closure nameMappingClosure, BuilderSupport proxyBuilder, + AsyncRiakTemplate riak) { + super(nameMappingClosure, proxyBuilder); + this.riak = riak; + } + + public AsyncRiakTemplate getAsyncTemplate() { + return riak; + } + + public void setAsyncTemplate(AsyncRiakTemplate riak) { + this.riak = riak; + } + + public ExecutorService getWorkerPool() { + return workerPool; + } + + public void setWorkerPool(ExecutorService workerPool) { + this.workerPool = workerPool; + } + + @Override + protected void setParent(Object parent, Object child) { +// log.debug("setParent/2 " + parent + " " + child); + } + + @SuppressWarnings({"unchecked"}) + @Override + protected Object createNode(Object name) { +// log.debug("createNode/1 " + name); + NodeName nodeName = null; + try { + nodeName = NodeName.valueOf(name.toString().toUpperCase()); + } catch (IllegalArgumentException e) { + // IGNORED + } + if (null != nodeName) { + QueryPhase p; + switch (nodeName) { + case CALL: + break; + case FOREACH: + RiakOperation op = new RiakOperation(riak, + RiakOperation.Type.FOREACH); + op.setBucket(defaultBucketName); + return op; + case MAPREDUCE: + return createMapReduceJob(); + case QUERY: + p = new QueryPhase(); + p.job = ((RiakMapReduceOperation) getCurrent()).getJob(); + return getCurrent(); + case REDUCE: + case MAP: + p = new QueryPhase(); + p.job = ((RiakMapReduceOperation) getCurrent()).getJob(); + p.phase = name.toString(); + return p; + } + } else { + defaultBucketName = name.toString(); + } + + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + protected Object createNode(Object name, Object value) { +// log.debug("createNode/2 " + name + " " + value); + NodeName nodeName = null; + try { + nodeName = NodeName.valueOf(name.toString().toUpperCase()); + } catch (IllegalArgumentException e) { + // IGNORED + } + if (null != nodeName) { + QueryPhase p; + switch (nodeName) { + case INPUTS: + AsyncRiakMapReduceJob job = ((RiakMapReduceOperation) getCurrent()).getJob(); + if (null != value && value instanceof String) { + List keys = new ArrayList(); + keys.add(value.toString()); + job.addInputs(keys); + } else if (value instanceof List) { + job.addInputs((List) value); + } + return job; + case LANGUAGE: + p = (QueryPhase) getCurrent(); + p.language = value.toString(); + return p; + case SOURCE: + p = (QueryPhase) getCurrent(); + p.source = value.toString(); + return p; + case KEEP: + p = (QueryPhase) getCurrent(); + p.keep = (value instanceof Boolean ? (Boolean) value : new Boolean(value.toString())); + return p; + case ARG: + p = (QueryPhase) getCurrent(); + p.arg = value; + return p; + } + } + + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + protected Object createNode(Object name, Map attributes) { +// log.debug("createNode/2 (Map) " + name + " " + attributes); + NodeName nodeName = null; + try { + nodeName = NodeName.valueOf(name.toString().toUpperCase()); + } catch (IllegalArgumentException e) { + // IGNORED + } + if (null != nodeName) { + switch (nodeName) { + case MAPREDUCE: + RiakMapReduceOperation oper = createMapReduceJob(); + // Set timeout + Object o = attributes.get("wait"); + if (null != o) { + if (o instanceof Long) { + oper.setTimeout((Long) o); + } else if (o instanceof String) { + oper.setTimeout(new Long(o.toString())); + } else if (o instanceof Integer) { + oper.setTimeout(new Long((Integer) o)); + } else { + throw new IllegalArgumentException( + "Timeout should be an Integer, a Long, or a String denoting milliseconds"); + } + } + return oper; + case MAP: + case REDUCE: + QueryPhase p = new QueryPhase(); + p.job = ((RiakMapReduceOperation) getCurrent()).getJob(); + p.phase = name.toString(); + // Set arg + p.arg = attributes.get("arg"); + return p; + } + } + + RiakOperation.Type type = null; + try { + type = RiakOperation.Type.valueOf(name.toString().toUpperCase()); + } catch (IllegalArgumentException ignored) { + // IGNORED + } + if (null != type) { + RiakOperation op = new RiakOperation(riak, type); + // Set a bucket name + Object o = attributes.get("bucket"); + if (null == o && null != defaultBucketName) { + op.setBucket(defaultBucketName); + } else { + op.setBucket((null != o ? o.toString() : null)); + } + // Set the object's key + o = attributes.get("key"); + op.setKey((null != o ? o.toString() : null)); + // Set the value + o = attributes.get("value"); + op.setValue(o); + // Set the type of object (for getAsType) + o = attributes.get("type"); + if (null != o) { + if (o instanceof Class) { + op.setRequiredType((Class) o); + } else if (o instanceof String) { + try { + op.setRequiredType(Class.forName((String) o)); + } catch (ClassNotFoundException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } + } else { + op.setRequiredType(o.getClass()); + } + } + // Set QOS parameters + o = attributes.get("qos"); + if (null != o) { + RiakQosParameters qos = new RiakQosParameters(); + Map qosParams = (Map) o; + if (qosParams.containsKey("dw")) { + qos.setDurableWriteThreshold(qosParams.get("dw")); + } + if (qosParams.containsKey("w")) { + qos.setWriteThreshold(qosParams.get("w")); + } + if (qosParams.containsKey("r")) { + qos.setReadThreshold(qosParams.get("r")); + } + op.setQosParameters(qos); + } + // Set timeout + o = attributes.get("wait"); + if (null != o) { + if (o instanceof Long) { + op.setTimeout((Long) o); + } else if (o instanceof String) { + op.setTimeout(new Long(o.toString())); + } else if (o instanceof Integer) { + op.setTimeout(new Long((Integer) o)); + } else { + throw new IllegalArgumentException( + "Timeout should be an Integer, a Long, or a String denoting milliseconds"); + } + } + + return op; + } + + return null; + } + + @Override + protected Object createNode(Object name, Map attributes, Object value) { +// log.debug("createNode/3"); + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + public Object invokeMethod(String methodName, Object arg) { +// if (log.isDebugEnabled()) { +// log.debug("invokeMethod/2: " + methodName + " " + arg); +// } + NodeName nodeName = null; + try { + nodeName = NodeName.valueOf(methodName.toString().toUpperCase()); + } catch (IllegalArgumentException e) { + // IGNORED + } + if (null != nodeName) { + switch (nodeName) { + case COMPLETED: + case FAILED: + if (getCurrent() instanceof RiakOperation) { + RiakOperation op = (RiakOperation) getCurrent(); + Object[] args = (Object[]) arg; + Map params; + Closure handler = null; + Closure guard = null; + for (Object o : args) { + if (o instanceof Map) { + params = (Map) o; + if (params.containsKey("when")) { + guard = (Closure) params.get("when"); + } + } else if (o instanceof Closure) { + handler = (Closure) o; + } + } + op.addHandler(methodName, handler, guard); + return op; + } else if (getCurrent() instanceof RiakMapReduceOperation) { + RiakMapReduceOperation oper = (RiakMapReduceOperation) getCurrent(); + Object[] args = (Object[]) arg; + if ("completed".equals(methodName)) { + oper.setCompleted((Closure) args[0]); + } else if ("failed".equals(methodName)) { + oper.setFailed((Closure) args[0]); + } + return oper; + } + break; + case CALL: + results.clear(); + defaultBucketName = null; + } + } + + // By default + return super.invokeMethod(methodName, arg); + } + + @SuppressWarnings({"unchecked"}) + @Override + protected void nodeCompleted(Object parent, Object node) { +// if (log.isDebugEnabled()) { +// log.debug("nodeCompleted: parent=" + parent + ", node=" + node); +// } + if (parent instanceof RiakMapReduceOperation && node instanceof QueryPhase) { + QueryPhase p = (QueryPhase) node; + MapReduceOperation oper = null; + if ("javascript".equals(p.language)) { + if (null != p.source) { + oper = new JavascriptMapReduceOperation(p.source); + } else if (null != p.bucket && null != p.key) { + oper = new JavascriptMapReduceOperation(new SimpleBucketKeyPair(p.bucket, p.key)); + } + } else { + oper = new ErlangMapReduceOperation(p.module, p.func); + } + if (null != oper) { + RiakMapReducePhase phase = new RiakMapReducePhase(p.phase, p.language, oper); + if (null != p.keep) { + phase.setKeepResults(p.keep); + } + phase.setArg(p.arg); + p.job.addPhase(phase); + } + } else { + super.nodeCompleted(parent, node); + } + } + + @SuppressWarnings({"unchecked"}) + @Override + protected Object postNodeCompletion(Object parent, Object node) { +// if (log.isDebugEnabled()) { +// log.debug("postNodeCompletion: " + parent + " " + node); +// } + if (node instanceof RiakOperation) { + RiakOperation op = (RiakOperation) node; + try { + Object o = op.call(); + if (null != o && !o.equals(results)) { + results.add(o); + } + return o; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } else if (null == parent && node instanceof RiakMapReduceOperation) { + RiakMapReduceOperation oper = (RiakMapReduceOperation) node; + try { + Object o = oper.call(); + if (null != o) { + results.add(o); + } + return o; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + return super.postNodeCompletion(parent, node); + } + + protected RiakMapReduceOperation createMapReduceJob() { + AsyncRiakMapReduceJob job = new AsyncRiakMapReduceJob(riak); + if (null != defaultBucketName) { + List keys = new ArrayList(); + keys.add(defaultBucketName); + job.addInputs(keys); + } + return new RiakMapReduceOperation(riak, job); + } + + private class QueryPhase { + + AsyncRiakMapReduceJob job; + String phase; + String language = "javascript"; + String source = null; + String bucket = null; + String key = null; + String module = null; + String func = null; + Boolean keep = null; + Object arg = null; + + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakMapReduceOperation.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakMapReduceOperation.java new file mode 100644 index 000000000..6690e94b1 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakMapReduceOperation.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.groovy; + +import groovy.lang.Closure; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.keyvalue.riak.core.AsyncKeyValueStoreOperation; +import org.springframework.data.keyvalue.riak.core.AsyncRiakTemplate; +import org.springframework.data.keyvalue.riak.core.KeyValueStoreMetaData; +import org.springframework.data.keyvalue.riak.mapreduce.AsyncRiakMapReduceJob; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * @author J. Brisbin + */ +public class RiakMapReduceOperation implements Callable { + + protected final Log log = LogFactory.getLog(getClass()); + + protected AsyncRiakTemplate riak; + protected AsyncRiakMapReduceJob job; + protected Long timeout = -1L; + protected Closure completed = null; + protected Closure failed = null; + + public RiakMapReduceOperation(AsyncRiakTemplate riak, AsyncRiakMapReduceJob job) { + this.riak = riak; + this.job = job; + } + + public AsyncRiakMapReduceJob getJob() { + return job; + } + + public void setJob(AsyncRiakMapReduceJob job) { + this.job = job; + } + + public Long getTimeout() { + return timeout; + } + + public void setTimeout(Long timeout) { + this.timeout = timeout; + } + + public Closure getCompleted() { + return completed; + } + + public void setCompleted(Closure completed) { + this.completed = completed; + } + + public Closure getFailed() { + return failed; + } + + public void setFailed(Closure failed) { + this.failed = failed; + } + + public Object call() throws Exception { + Future f = riak.execute(job, new AsyncKeyValueStoreOperation, Object>() { + public Object completed(KeyValueStoreMetaData meta, List result) { + if (null != completed) { + if (completed.getParameterTypes().length == 2) { + return completed.call(new Object[]{result, meta}); + } else { + return completed.call(result); + } + } else { + return new Object[]{result, meta}; + } + } + + public Object failed(Throwable error) { + if (null != failed) { + return failed.call(error); + } else { + throw new RuntimeException(error); + } + } + }); + + if (timeout == 0) { + return f; + } else if (timeout < 0) { + return f.get(); + } else { + return f.get(timeout, TimeUnit.MILLISECONDS); + } + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakOperation.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakOperation.java new file mode 100644 index 000000000..0ce166410 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/RiakOperation.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.groovy; + +import groovy.lang.Closure; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.keyvalue.riak.DataStoreOperationException; +import org.springframework.data.keyvalue.riak.core.AsyncKeyValueStoreOperation; +import org.springframework.data.keyvalue.riak.core.AsyncRiakTemplate; +import org.springframework.data.keyvalue.riak.core.KeyValueStoreMetaData; +import org.springframework.data.keyvalue.riak.core.QosParameters; + +import java.util.*; +import java.util.concurrent.*; + +/** + * @author J. Brisbin + */ +public class RiakOperation implements Callable { + + static enum Type { + SET, SETASBYTES, PUT, GET, GETASBYTES, GETASTYPE, CONTAINSKEY, DELETE, FOREACH + } + + static String COMPLETED = "completed"; + static String FAILED = "failed"; + + protected final Log log = LogFactory.getLog(getClass()); + + protected AsyncRiakTemplate riak; + protected Type type; + protected String bucket; + protected String key; + protected T value; + protected Class requiredType = null; + protected long timeout = -1L; + protected QosParameters qosParameters; + protected Map> callbacks = new LinkedHashMap>(); + protected ClosureInvokingCallback callbackInvoker = new ClosureInvokingCallback(); + + public RiakOperation(AsyncRiakTemplate riak, Type type) { + this.riak = riak; + this.type = type; + } + + public Type getType() { + return type; + } + + public Map> getCallbacks() { + return callbacks; + } + + public QosParameters getQosParameters() { + return qosParameters; + } + + public void setQosParameters(QosParameters qosParameters) { + this.qosParameters = qosParameters; + } + + public String getBucket() { + return bucket; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + public Class getRequiredType() { + return requiredType; + } + + public void setRequiredType(Class requiredType) { + this.requiredType = requiredType; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public void addHandler(String type, Closure handler, Closure guard) { + List guardedClosures = callbacks.get(type); + if (null == guardedClosures) { + guardedClosures = new ArrayList(); + callbacks.put(type, guardedClosures); + } + guardedClosures.add(new GuardedClosure(handler, guard)); + } + + @SuppressWarnings({"unchecked"}) + public Object call() throws Exception { + Future f = null; + switch (type) { + case GET: + f = riak.get(bucket, key, callbackInvoker); + break; + case GETASBYTES: + f = riak.getAsBytes(bucket, key, callbackInvoker); + break; + case GETASTYPE: + f = riak.getAsType(bucket, key, requiredType, callbackInvoker); + break; + case PUT: + f = riak.put(bucket, value, callbackInvoker); + break; + case SET: + f = riak.set(bucket, key, value, callbackInvoker); + break; + case SETASBYTES: + byte[] bytes; + if (value instanceof byte[]) { + bytes = (byte[]) value; + } else { + bytes = riak.getConversionService().convert(value, byte[].class); + } + f = riak.setAsBytes(bucket, key, bytes, callbackInvoker); + break; + case CONTAINSKEY: + f = riak.containsKey(bucket, key, callbackInvoker); + break; + case DELETE: + f = riak.delete(bucket, key, callbackInvoker); + break; + case FOREACH: + f = riak.getBucketSchema(bucket, + null, + new AsyncKeyValueStoreOperation, Object>() { + public Object completed(KeyValueStoreMetaData meta, Map result) { + List results = new LinkedList(); + List keys = (List) result.get("keys"); + for (String key : keys) { + try { + Future getFut = riak.get(bucket, key, callbackInvoker); + if (timeout > 0) { + Object o = getFut.get(timeout, TimeUnit.MILLISECONDS); + if (null != o) { + results.add(o); + } + } else if (timeout < 0) { + Object o = getFut.get(); + if (null != o) { + results.add(o); + } + } else { + results.add(getFut); + } + } catch (InterruptedException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } catch (ExecutionException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } catch (TimeoutException e) { + throw new DataStoreOperationException(e.getMessage(), e); + } + } + return (results.size() > 0 ? results : null); + } + + public Object failed(Throwable error) { + throw new RuntimeException(error); + } + }); + break; + } + + if (null != f) { + if (timeout > 0) { + // Block until finished or timeout + return f.get(timeout, TimeUnit.MILLISECONDS); + } else if (timeout < 0) { + // Block indefinitely + return f.get(); + } + } + + return f; + } + + class GuardedClosure { + + private Closure delegate; + private Closure guard; + + GuardedClosure(Closure delegate, Closure guard) { + this.delegate = delegate; + this.guard = guard; + } + + public Closure getDelegate() { + return delegate; + } + + public Closure getGuard() { + return guard; + } + + } + + class ClosureInvokingCallback implements AsyncKeyValueStoreOperation { + + public Object completed(KeyValueStoreMetaData meta, Object result) { + if (!callbacks.containsKey(COMPLETED)) { + return new Object[]{result, meta}; + } + for (GuardedClosure cl : callbacks.get(COMPLETED)) { + boolean execute = true; + + Closure guardExpr = cl.getGuard(); + if (null != guardExpr) { + int noOfParams = guardExpr.getParameterTypes().length; + Object guardResult; + if (noOfParams == 2) { + guardResult = guardExpr.call(new Object[]{result, meta}); + } else { + guardResult = guardExpr.call(result); + } + if (null != guardResult) { + if (guardResult instanceof Boolean) { + execute = (Boolean) guardResult; + } else { + execute = true; + } + } + } + + if (execute) { + Closure callback = cl.getDelegate(); + if (callback.getParameterTypes().length == 2) { + return callback.call(new Object[]{result, meta}); + } else { + return callback.call(result); + } + } + } + return null; + } + + public Object failed(Throwable error) { + if (!callbacks.containsKey(FAILED)) { + throw new RuntimeException(error); + } + for (GuardedClosure cl : callbacks.get(FAILED)) { + boolean execute = true; + Closure guardExpr = cl.getGuard(); + if (null != guardExpr) { + Object guardResult = guardExpr.call(error); + if (null != guardResult) { + if (guardResult instanceof Boolean) { + execute = (Boolean) guardResult; + } else { + execute = true; + } + } + } + + if (execute) { + Closure callback = cl.getDelegate(); + return callback.call(error); + } + } + return null; + } + + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/overview.html b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/overview.html new file mode 100644 index 000000000..d7856cbe9 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/groovy/overview.html @@ -0,0 +1,8 @@ + + +

+ Utilities for making Riak data access easier in Groovy. The RiakBuilder + provides a Groovy DSL for interacting with Riak. +

+ + \ No newline at end of file diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AbstractRiakMapReduceJob.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AbstractRiakMapReduceJob.java new file mode 100644 index 000000000..9ce6b664a --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AbstractRiakMapReduceJob.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.map.ObjectMapper; +import org.springframework.data.keyvalue.riak.core.BucketKeyPair; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * An implementation of {@link MapReduceJob} for the Riak data store. + * + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public abstract class AbstractRiakMapReduceJob implements MapReduceJob { + + protected final Log log = LogFactory.getLog(getClass()); + protected List inputs = new LinkedList(); + protected List phases = new ArrayList(); + + public List getInputs() { + return this.inputs; + } + + public MapReduceJob addInputs(List keys) { + inputs.addAll(keys); + return this; + } + + public MapReduceJob addPhase(MapReducePhase phase) { + phases.add(phase); + return this; + } + + public List getPhases() { + return this.phases; + } + + public String toJson() { + StringWriter out = new StringWriter(); + try { + JsonGenerator json = new JsonFactory().createJsonGenerator(out); + json.setCodec(new ObjectMapper()); + json.writeStartObject(); + + // Inputs + json.writeFieldName("inputs"); + if (1 == inputs.size() && !(inputs.get(0) instanceof List)) { + json.writeString(inputs.get(0).toString()); + } else if (inputs.size() > 0) { + json.writeStartArray(); + for (Object obj : inputs) { + List pair = (List) obj; + json.writeStartArray(); + json.writeString(pair.get(0).toString()); + json.writeString(pair.get(1).toString()); + json.writeEndArray(); + } + json.writeEndArray(); + } + + // Query + json.writeFieldName("query"); + json.writeStartArray(); + for (MapReducePhase phase : phases) { + json.writeStartObject(); + switch (phase.getPhase()) { + case MAP: + json.writeFieldName("map"); + break; + case REDUCE: + json.writeFieldName("reduce"); + break; + case LINK: + json.writeFieldName("link"); + } + + json.writeStartObject(); + json.writeStringField("language", phase.getLanguage()); + Object repr = phase.getOperation().getRepresentation(); + if (repr instanceof String) { + // Using source + json.writeStringField("source", + String.format("%s", phase.getOperation().getRepresentation())); + } else if (repr instanceof BucketKeyPair) { + BucketKeyPair pair = (BucketKeyPair) repr; + json.writeStringField("bucket", + String.format("%s", pair.getBucket())); + json.writeStringField("key", String.format("%s", pair.getKey())); + } else if (repr instanceof Map) { + for (Map.Entry entry : ((Map) repr).entrySet()) { + json.writeStringField(entry.getKey().toString(), + entry.getValue().toString()); + } + } + if (phase.getKeepResults()) { + json.writeBooleanField("keep", true); + } + // Arg + if (null != phase.getArg()) { + json.writeObjectField("arg", phase.getArg()); + } + + json.writeEndObject(); + json.writeEndObject(); + } + json.writeEndArray(); + + json.writeEndObject(); + json.flush(); + + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return out.toString(); + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AsyncMapReduceOperations.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AsyncMapReduceOperations.java new file mode 100644 index 000000000..fffc52c14 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AsyncMapReduceOperations.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import org.springframework.data.keyvalue.riak.core.AsyncKeyValueStoreOperation; + +import java.util.List; +import java.util.concurrent.Future; + +/** + * Generic interface to Map/Reduce in data stores that support it. + * + * @author J. Brisbin + */ +public interface AsyncMapReduceOperations { + + /** + * Execute a {@link MapReduceJob} synchronously. + * + * @param job + * @return + */ + Future execute(MapReduceJob job, AsyncKeyValueStoreOperation, R> callback); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AsyncRiakMapReduceJob.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AsyncRiakMapReduceJob.java new file mode 100644 index 000000000..0679c6222 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/AsyncRiakMapReduceJob.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import org.springframework.data.keyvalue.riak.core.AsyncRiakTemplate; + +/** + * An implementation of {@link MapReduceJob} for the Riak data store. + * + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class AsyncRiakMapReduceJob extends AbstractRiakMapReduceJob { + + protected AsyncRiakTemplate riakTemplate; + + public AsyncRiakMapReduceJob(AsyncRiakTemplate riakTemplate) { + this.riakTemplate = riakTemplate; + } + + public AsyncRiakTemplate getAsyncRiakTemplate() { + return riakTemplate; + } + + public void setAsyncRiakTemplate(AsyncRiakTemplate riakTemplate) { + this.riakTemplate = riakTemplate; + } + + public Object call() throws Exception { + return riakTemplate.execute(this, null); + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/ErlangMapReduceOperation.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/ErlangMapReduceOperation.java new file mode 100644 index 000000000..072bad831 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/ErlangMapReduceOperation.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An implementation of {@link org.springframework.data.keyvalue.riak.mapreduce.MapReduceOperation} + * to represent an Erlang M/R function, which must be already defined inside the + * Riak server. + * + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class ErlangMapReduceOperation implements MapReduceOperation { + + protected String language = "erlang"; + protected Map moduleFunction = new LinkedHashMap(); + + public ErlangMapReduceOperation() { + } + + public ErlangMapReduceOperation(String module, String function) { + setModule(module); + setFunction(function); + } + + /** + * Set the Erlang module this function is defined in. + * + * @param module + */ + public void setModule(String module) { + moduleFunction.put("module", module); + } + + /** + * Set the name of this Erlang function. + * + * @param function + */ + public void setFunction(String function) { + moduleFunction.put("function", function); + } + + public Object getRepresentation() { + return moduleFunction; + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/JavascriptMapReduceOperation.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/JavascriptMapReduceOperation.java new file mode 100644 index 000000000..b941e4a80 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/JavascriptMapReduceOperation.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import org.springframework.data.keyvalue.riak.core.BucketKeyPair; + +/** + * An implementation of {@link org.springframework.data.keyvalue.riak.mapreduce.MapReduceOperation} + * to describe a Javascript language M/R function. + * + * @author J. Brisbin + */ +public class JavascriptMapReduceOperation implements MapReduceOperation { + + protected String source; + protected BucketKeyPair bucketKeyPair; + + public JavascriptMapReduceOperation(String source) { + this.source = source; + } + + public JavascriptMapReduceOperation(BucketKeyPair bucketKeyPair) { + this.bucketKeyPair = bucketKeyPair; + } + + public String getSource() { + return source; + } + + /** + * Set the anonymous source to use for the M/R function. + * + * @param source + */ + public void setSource(String source) { + this.source = source; + } + + public BucketKeyPair getBucketKeyPair() { + return bucketKeyPair; + } + + /** + * Set the {@link org.springframework.data.keyvalue.riak.core.BucketKeyPair} to + * point to for the Javascript to use in this M/R function. + * + * @param bucketKeyPair + */ + public void setBucketKeyPair(BucketKeyPair bucketKeyPair) { + this.bucketKeyPair = bucketKeyPair; + } + + public Object getRepresentation() { + return (null != bucketKeyPair ? bucketKeyPair : source); + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceJob.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceJob.java new file mode 100644 index 000000000..19ceebd6f --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceJob.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import java.util.List; +import java.util.concurrent.Callable; + +/** + * A generic interface to representing a Map/Reduce job to a data store that supports that + * operation. + * + * @author J. Brisbin + */ +public interface MapReduceJob extends Callable { + + /** + * Get the list of inputs for this job. + * + * @return + */ + List getInputs(); + + /** + * Set the list of inputs for this job. + * + * @param keys + * @param + * @return + */ + MapReduceJob addInputs(List keys); + + /** + * Add a phase to this operation. + * + * @param phase + * @return + */ + MapReduceJob addPhase(MapReducePhase phase); + + /** + * Get the list of phases for this job. + * + * @return + */ + List getPhases(); + + /** + * Convert this job into the appropriate JSON to send to the server. + * + * @return + */ + String toJson(); +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceLinkOperation.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceLinkOperation.java new file mode 100644 index 000000000..844628d63 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceLinkOperation.java @@ -0,0 +1,26 @@ +package org.springframework.data.keyvalue.riak.mapreduce; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author J. Brisbin + */ +public class MapReduceLinkOperation implements MapReduceOperation { + + protected String bucket = null; + protected String key; + + public MapReduceLinkOperation(String bucket, String key) { + this.bucket = bucket; + this.key = key; + } + + public Object getRepresentation() { + Map repr = new LinkedHashMap(); + repr.put("bucket", (null != bucket ? bucket : "_")); + repr.put("key", (null != key ? key : "_")); + return repr; + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceOperation.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceOperation.java new file mode 100644 index 000000000..feacc40f4 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceOperation.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +/** + * A generic interface to a Map/Reduce operation. + * + * @author J. Brisbin + */ +public interface MapReduceOperation { + + /** + * Get the implementation-specific representation of a Map/Reduce operation. + * + * @return + */ + Object getRepresentation(); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceOperations.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceOperations.java new file mode 100644 index 000000000..12b32e02b --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReduceOperations.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import java.util.List; +import java.util.concurrent.Future; + +/** + * Generic interface to Map/Reduce in data stores that support it. + * + * @author J. Brisbin + */ +public interface MapReduceOperations { + + /** + * Execute a {@link org.springframework.data.keyvalue.riak.mapreduce.MapReduceJob} + * synchronously. + * + * @param job + * @return + */ + Object execute(MapReduceJob job); + + /** + * Execute a MapReduceJob synchronously, converting the result into the given + * type. + * + * @param job + * @param targetType + * @return The converted value. + */ + T execute(MapReduceJob job, Class targetType); + + /** + * Submit the job to run asynchronously. + * + * @param job + * @return The Future representing the submitted job. + */ + Future> submit(MapReduceJob job); + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReducePhase.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReducePhase.java new file mode 100644 index 000000000..0eadb4b0f --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/MapReducePhase.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +/** + * A generic interface to the phases of Map/Reduce jobs. + * + * @author J. Brisbin + */ +public interface MapReducePhase { + + public static enum Phase { + MAP, REDUCE, LINK + } + + /** + * The bucket pattern to match on link phases. + * + * @return + */ + String getBucket(); + + /** + * Set the bucket pattern to match on link phases. + * + * @param bucket + */ + void setBucket(String bucket); + + Phase getPhase(); + + /** + * The language this phase is described in. + * + * @return + */ + String getLanguage(); + + /** + * Whether or not to keep the result of this phase. + * + * @return + */ + boolean getKeepResults(); + + /** + * Get the operation this phase will execute. + * + * @return + */ + MapReduceOperation getOperation(); + + /** + * Set the static argument for this job. + * + * @param arg + */ + void setArg(Object arg); + + /** + * Get the static argument for this phase. + * + * @return + */ + Object getArg(); +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/RiakMapReduceJob.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/RiakMapReduceJob.java new file mode 100644 index 000000000..424a4dad6 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/RiakMapReduceJob.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +import org.springframework.data.keyvalue.riak.core.RiakTemplate; + +/** + * An implementation of {@link org.springframework.data.keyvalue.riak.mapreduce.MapReduceJob} + * for the Riak data store. + * + * @author J. Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class RiakMapReduceJob extends AbstractRiakMapReduceJob { + + protected RiakTemplate riakTemplate; + + public RiakMapReduceJob(RiakTemplate riakTemplate) { + this.riakTemplate = riakTemplate; + } + + public RiakTemplate getRiakTemplate() { + return riakTemplate; + } + + public void setRiakTemplate(RiakTemplate riakTemplate) { + this.riakTemplate = riakTemplate; + } + + public Object call() throws Exception { + return riakTemplate.execute(this); + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/RiakMapReducePhase.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/RiakMapReducePhase.java new file mode 100644 index 000000000..3e5ca4b87 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/RiakMapReducePhase.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.mapreduce; + +/** + * An implementation of {@link org.springframework.data.keyvalue.riak.mapreduce.MapReducePhase} + * for the Riak data store. + * + * @author J. Brisbin + */ +public class RiakMapReducePhase implements MapReducePhase { + + protected Phase phase; + protected String bucket; + protected String language; + protected MapReduceOperation operation; + protected boolean keepResults = false; + protected Object arg; + + public RiakMapReducePhase(String phase, String language, MapReduceOperation oper) { + this.phase = Phase.valueOf(phase.toUpperCase()); + this.language = language; + this.operation = oper; + } + + public RiakMapReducePhase(Phase phase, String language, MapReduceOperation oper) { + this.phase = phase; + this.language = language; + this.operation = oper; + } + + public String getBucket() { + return this.bucket; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + public Phase getPhase() { + return phase; + } + + public String getLanguage() { + return language; + } + + public MapReduceOperation getOperation() { + return this.operation; + } + + public boolean getKeepResults() { + return this.keepResults; + } + + public void setKeepResults(boolean keepResults) { + this.keepResults = keepResults; + } + + public void setOperation(MapReduceOperation oper) { + + this.operation = oper; + } + + public Object getArg() { + return arg; + } + + public void setArg(Object arg) { + this.arg = arg; + } +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/overview.html b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/overview.html new file mode 100644 index 000000000..e9362f9de --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/mapreduce/overview.html @@ -0,0 +1,7 @@ + + +

+ Root package for +

+ + \ No newline at end of file diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/overview.html b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/overview.html new file mode 100644 index 000000000..4d1677397 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/overview.html @@ -0,0 +1,8 @@ + + +

+ Root package for integrating Riak with Spring + concepts. +

+ + \ No newline at end of file diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/Ignore404sErrorHandler.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/Ignore404sErrorHandler.java new file mode 100644 index 000000000..8e7bf5aac --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/Ignore404sErrorHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2011 by J. Brisbin + * Portions (c) 2011 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.util; + +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.DefaultResponseErrorHandler; + +import java.io.IOException; + +/** + * @author J. Brisbin + */ +public class Ignore404sErrorHandler extends DefaultResponseErrorHandler { + + @Override + protected boolean hasError(HttpStatus statusCode) { + if (statusCode != HttpStatus.NOT_FOUND) { + return super.hasError(statusCode); + } else { + return false; + } + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + // Ignore 404s entirely + if (response.getStatusCode() != HttpStatus.NOT_FOUND) { + super.handleError(response); + } + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/RiakClassFileLoader.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/RiakClassFileLoader.java new file mode 100644 index 000000000..42ee23072 --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/RiakClassFileLoader.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2011 by J. Brisbin + * Portions (c) 2011 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.util; + +import org.apache.commons.cli.*; +import org.springframework.data.keyvalue.riak.core.RiakTemplate; + +import java.io.*; +import java.net.URLEncoder; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * @author J. Brisbin + */ +public class RiakClassFileLoader { + + static Options opts = new Options(); + + static { + opts.addOption("v", false, "Verbose output"); + opts.addOption("u", + true, + "URL to Riak (defaults to: 'http://localhost:8098/riak/{bucket}/{key}')"); + opts.addOption("b", true, "Bucket to load class files into"); + opts.addOption("k", true, "Key under which to store an individual class file"); + opts.addOption("j", true, "JAR file to load into Riak"); + opts.addOption("c", true, "Class file to load into Riak"); + opts.addOption("d", true, "Directory from which to load all JAR files into Riak"); + } + + public static void main(String[] args) { + Parser p = new BasicParser(); + CommandLine cl = null; + try { + cl = p.parse(opts, args); + } catch (ParseException e) { + System.err.println("Error parsing command line: " + e.getMessage()); + } + + if (null != cl) { + boolean verbose = cl.hasOption('v'); + RiakTemplate riak = new RiakTemplate(); + riak.getRestTemplate().setErrorHandler(new Ignore404sErrorHandler()); + if (cl.hasOption('u')) { + riak.setDefaultUri(cl.getOptionValue('u')); + } + try { + riak.afterPropertiesSet(); + } catch (Exception e) { + System.err.println("Error creating RiakTemplate: " + e.getMessage()); + } + String[] files = cl.getOptionValues('j'); + if (null != files) { + for (String file : files) { + if (verbose) { + System.out.println(String.format("Loading JAR file %s into Riak...", file)); + } + try { + File zfile = new File(file); + ZipInputStream zin = new ZipInputStream(new FileInputStream(zfile)); + ZipEntry entry; + while (null != (entry = zin.getNextEntry())) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + byte[] buff = new byte[16384]; + for (int bytesRead = zin.read(buff); bytesRead > 0; bytesRead = zin.read(buff)) { + bout.write(buff, 0, bytesRead); + } + + if (entry.getName().endsWith(".class")) { + String name = entry.getName().replaceAll("/", "."); + name = URLEncoder.encode(name.substring(0, name.length() - 6), "UTF-8"); + String bucket; + if (cl.hasOption('b')) { + bucket = cl.getOptionValue('b'); + } else { + bucket = URLEncoder.encode(zfile.getCanonicalFile().getName(), "UTF-8"); + } + if (verbose) { + System.out.println(String.format("Uploading to %s/%s", bucket, name)); + } + + // Load these bytes into Riak + riak.setAsBytes(bucket, name, bout.toByteArray()); + } + } + } catch (FileNotFoundException e) { + System.err.println("Error reading JAR file: " + e.getMessage()); + } catch (IOException e) { + System.err.println("Error reading JAR file: " + e.getMessage()); + } + } + } + + String[] classFiles = cl.getOptionValues('c'); + if (null != classFiles) { + for (String classFile : classFiles) { + try { + FileInputStream fin = new FileInputStream(classFile); + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + byte[] buff = new byte[16384]; + for (int bytesRead = fin.read(buff); bytesRead > 0; bytesRead = fin.read(buff)) { + bout.write(buff, 0, bytesRead); + } + + String name; + if (cl.hasOption('k')) { + name = cl.getOptionValue('k'); + } else { + throw new IllegalStateException( + "Must specify a Riak key in which to store the data if loading individual class files."); + } + String bucket; + if (cl.hasOption('b')) { + bucket = cl.getOptionValue('b'); + } else { + throw new IllegalStateException( + "Must specify a Riak bucket in which to store the data if loading individual class files."); + } + if (verbose) { + System.out.println(String.format("Uploading to %s/%s", bucket, name)); + } + + // Load these bytes into Riak + riak.setAsBytes(bucket, name, bout.toByteArray()); + + } catch (FileNotFoundException e) { + System.err.println("Error reading class file: " + e.getMessage()); + } catch (IOException e) { + System.err.println("Error reading class file: " + e.getMessage()); + } + } + } + } + } + +} diff --git a/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/RiakClassLoader.java b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/RiakClassLoader.java new file mode 100644 index 000000000..bc1f8a82f --- /dev/null +++ b/spring-data-riak/src/main/java/org/springframework/data/keyvalue/riak/util/RiakClassLoader.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2011 by J. Brisbin + * Portions (c) 2011 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.keyvalue.riak.core.RiakTemplate; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * @author J. Brisbin + */ +public class RiakClassLoader extends ClassLoader { + + protected final Log log = LogFactory.getLog(getClass()); + protected Set buckets = new LinkedHashSet(); + protected RiakTemplate riakTemplate; + protected String defaultBucket = null; + + public RiakClassLoader(ClassLoader classLoader, RiakTemplate riakTemplate) { + super(classLoader); + init(riakTemplate); + loadBucketsFromClassPath(); + } + + public RiakClassLoader(RiakTemplate riakTemplate) { + init(riakTemplate); + loadBucketsFromClassPath(); + } + + public Set getBuckets() { + return buckets; + } + + public void setBuckets(Set buckets) { + this.buckets = buckets; + } + + public RiakTemplate getRiakTemplate() { + return riakTemplate; + } + + public void setRiakTemplate(RiakTemplate riakTemplate) { + this.riakTemplate = riakTemplate; + } + + public String getDefaultBucket() { + return defaultBucket; + } + + public void setDefaultBucket(String defaultBucket) { + this.defaultBucket = defaultBucket; + } + + @Override + protected Class findClass(String s) throws ClassNotFoundException { + Class c; + try { + c = super.findClass(s); + if (log.isDebugEnabled()) { + log.debug(String.format("Found class '%s' locally defined.", s)); + } + } catch (Throwable t) { + // Class not defined in this ClassLoader yet + } + + Set buckets = new LinkedHashSet(this.buckets); + if (null != defaultBucket) { + buckets.add(defaultBucket); + } + for (String bucket : buckets) { + if (bucket.indexOf("/") < 0) { + try { + if (log.isDebugEnabled()) { + log.debug(String.format("Class '%s' not locally defined, trying Riak.", s)); + } + byte[] buff = riakTemplate.getAsBytes(URLEncoder.encode(bucket, "UTF-8"), s); + c = defineClass(s, buff, 0, buff.length); + if (null != c) { + return c; + } + } catch (ClassFormatError ignored) { + } catch (UnsupportedEncodingException e) { + log.error(e.getMessage(), e); + } + } + } + + // Nothing found + throw new ClassNotFoundException("Class not found: " + s); + } + + + protected void loadBucketsFromClassPath() { + String classPath = System.getProperty("java.class.path"); + String pathSep = System.getProperty("path.separator", ":"); + if (null != classPath) { + String[] paths = classPath.split(pathSep); + for (String p : paths) { + buckets.add(p); + } + } + } + + protected void init(RiakTemplate riakTemplate) { + this.riakTemplate = riakTemplate; + RestTemplate tmpl = this.riakTemplate.getRestTemplate(); + tmpl.getMessageConverters().add(0, new JavaSerializationMessageHandler()); + tmpl.setErrorHandler(new Ignore404sErrorHandler()); + } + + private class JavaSerializationMessageHandler implements HttpMessageConverter { + + public boolean canRead(Class clazz, MediaType mediaType) { + return MediaType.APPLICATION_OCTET_STREAM.equals(mediaType); + } + + public boolean canWrite(Class clazz, MediaType mediaType) { + return null != clazz; + } + + public List getSupportedMediaTypes() { + List types = new ArrayList(1); + types.add(MediaType.APPLICATION_OCTET_STREAM); + return types; + } + + public Object read(java.lang.Class clazz, HttpInputMessage inputMessage) throws + IOException, + HttpMessageNotReadableException { + ObjectInputStream oin = new ObjectInputStream(inputMessage.getBody()); + try { + Class c = (Class) oin.readObject(); + if (log.isDebugEnabled()) { + log.debug("Loaded class: " + c); + } + return c; + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws + IOException, + HttpMessageNotWritableException { + outputMessage.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + ObjectOutputStream oout = new ObjectOutputStream(outputMessage.getBody()); + oout.writeObject(o); + oout.flush(); + } + + } + +} diff --git a/spring-data-riak/src/main/resources/META-INF/spring/app-context.xml b/spring-data-riak/src/main/resources/META-INF/spring/app-context.xml new file mode 100644 index 000000000..7210563d2 --- /dev/null +++ b/spring-data-riak/src/main/resources/META-INF/spring/app-context.xml @@ -0,0 +1,8 @@ + + + + Example configuration to get you started. + + diff --git a/spring-data-riak/src/test/classes/org/springframework/data/keyvalue/riak/core/ClassLoaderTest.class b/spring-data-riak/src/test/classes/org/springframework/data/keyvalue/riak/core/ClassLoaderTest.class new file mode 100644 index 000000000..5a576518a Binary files /dev/null and b/spring-data-riak/src/test/classes/org/springframework/data/keyvalue/riak/core/ClassLoaderTest.class differ diff --git a/spring-data-riak/src/test/classes/org/springframework/data/keyvalue/riak/core/ClassLoaderTest.java b/spring-data-riak/src/test/classes/org/springframework/data/keyvalue/riak/core/ClassLoaderTest.java new file mode 100644 index 000000000..b8cd0896d --- /dev/null +++ b/spring-data-riak/src/test/classes/org/springframework/data/keyvalue/riak/core/ClassLoaderTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2011 by J. Brisbin + * Portions (c) 2011 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * @author J. Brisbin + */ +public class ClassLoaderTest { + + String name = "ClassLoaderTest"; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/AsyncRiakTemplateSpec.groovy b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/AsyncRiakTemplateSpec.groovy new file mode 100644 index 000000000..4d24c5eb9 --- /dev/null +++ b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/AsyncRiakTemplateSpec.groovy @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core + +import java.util.concurrent.Future +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author J. Brisbin + */ +@ContextConfiguration(locations = "/org/springframework/data/AsyncRiakTemplateTests.xml") +class AsyncRiakTemplateSpec extends Specification { + + @Autowired + ApplicationContext appCtx + @Autowired + AsyncRiakTemplate riak + + def "Test async setWithMetaData"() { + + given: + def obj = [test: "value", integer: 12] + def success = false + def failure = false + def testValue = "bad value" + def callback = [ + completed: { v -> + success = true + testValue = v.get().test + }, + failed: { e -> + failure = true + } + ] as AsyncKeyValueStoreOperation + + when: + Future future = riak.setWithMetaData("test", "test", obj, null, null, callback) + println "Waiting for result: ${future.get()}" + + then: + success && !failure + "value" == testValue + + } + + def "Test async getWithMetaData"() { + + given: + def result = null + def callback = [ + completed: { meta, v -> + println "got value: $meta $v" + result = v + }, + failed: { e -> + println "got error: $e" + } + ] as AsyncKeyValueStoreOperation + + when: + riak.getWithMetaData("test", "test", Map, callback).get() + + then: + null != result + + } + +} diff --git a/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakBuilderSpec.groovy b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakBuilderSpec.groovy new file mode 100644 index 000000000..f7efc4f8a --- /dev/null +++ b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakBuilderSpec.groovy @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core + +import java.util.concurrent.Future +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.data.keyvalue.riak.groovy.RiakBuilder +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author J. Brisbin + */ +@ContextConfiguration(locations = "/org/springframework/data/AsyncRiakTemplateTests.xml") +class RiakBuilderSpec extends Specification { + + @Autowired + ApplicationContext appCtx + @Autowired + AsyncRiakTemplate riakTemplate + + def "Test builder set"() { + + given: + def obj = [test: "value", integer: 12] + def riak = new RiakBuilder(riakTemplate) + def result = null + + when: + riak { + set(bucket: "test", key: "test", qos: [dw: "all"], value: obj) { + completed(when: { it.integer == 12 }) { result = it.test } + completed { result = "otherwise" } + failed { it.printStackTrace() } + } + } + + then: + "value" == result + + } + + def "Test builder get"() { + + given: + def riak = new RiakBuilder(riakTemplate) + def result = null + + when: + riak.get(bucket: "test", key: "test") { + completed(when: { it.integer == 12 }) { result = it.test } + completed { result = "otherwise" } + failed { it.printStackTrace() } + } + + then: + null != result + "value" == result + + } + + def "Test builder async get"() { + + given: + def riak = new RiakBuilder(riakTemplate) + def result = null + + when: + def f = riak.get(bucket: "test", key: "test", wait: 0) { + completed(when: { it.integer == 12 }) { result = it.test } + completed { result = "otherwise" } + failed { it.printStackTrace() } + } + + then: + f instanceof Future + null != f.get() + + } + + def "Test builder getAsType"() { + + given: + def riak = new RiakBuilder(riakTemplate) + def result = null + + when: + riak.getAsType(bucket: "test", key: "test", type: Map) { + completed(when: { it instanceof Map }) { result = it.test } + completed { result = "otherwise" } + failed { it.printStackTrace() } + } + + then: + "value" == result + + } + + def "Test builder setAsBytes"() { + + given: + def obj = "test bytes".bytes + def riak = new RiakBuilder(riakTemplate) + def result = null + + when: + riak.setAsBytes(bucket: "test", key: "test", value: obj, qos: [dw: "all"]) { + completed { result = "success" } + failed { it.printStackTrace() } + } + + then: + null != result + "success" == result + + } + + def "Test builder get with bytes"() { + + given: + def riak = new RiakBuilder(riakTemplate) + + when: + def result = riak.get(bucket: "test", key: "test") { + failed { it.printStackTrace() } + } + + then: + null != result + "test bytes".bytes == result[0] + + } + + def "Test builder put"() { + + given: + def obj = [test: "value", integer: 12] + def riak = new RiakBuilder(riakTemplate) + + when: + def id = riak.put(bucket: "test", qos: [dw: "all"], value: obj) { + completed { v, meta -> meta.key } + failed { it.printStackTrace() } + } + + then: + null != id + + } + + def "Test builder foreach"() { + + given: + def riak = new RiakBuilder(riakTemplate) + def idCnt = 0 + + when: + riak.foreach(bucket: "test") { + completed { idCnt++ } + failed { it.printStackTrace() } + } + + then: + idCnt > 0 + + } + + def "Test builder batch operations"() { + + given: + def riak = new RiakBuilder(riakTemplate) + def ids = [] + + when: + riak { + test { + put(value: [test: "value 1"]) + put(value: [test: "value 2"]) + put(value: [test: "value 3"]) + + foreach { + completed { v, meta -> ids << meta.key } + failed { it.printStackTrace() } + } + } + } + + then: + null != ids + 3 <= ids.size() + + } + + def "Test builder bucket as node"() { + + given: + def riak = new RiakBuilder(riakTemplate) + def ids = [] + + when: + riak { + "test" { + put(value: [test: "value 1"]) + put(value: [test: "value 2"]) + put(value: [test: "value 3"]) + + foreach { + completed { v, meta -> ids << meta.key } + failed { it.printStackTrace() } + } + } + } + + then: + null != ids + 3 <= ids.size() + + } + + def "Test builder Map/Reduce"() { + + given: + def riak = new RiakBuilder(riakTemplate) + + when: + riak { + mapreduce { + inputs "test" + query { + map(arg: [test: "arg", alist: [1, 2, 3, 4]]) { + source "function(v, keyInfo, arg){ return [1]; }" + } + reduce { + source "function(v){ return Riak.reduceSum(v); }" + } + } + completed { it } + failed { it.printStackTrace() } + } + } + + then: + null != riak.results + 1 <= riak.results.size() + + } + + def "Test builder delete"() { + + given: + def riak = new RiakBuilder(riakTemplate) + + when: + riak { + "test" { + foreach { + completed { v, meta -> delete(key: meta.key) } + failed { deleted = false } + } + } + } + + then: + !riak.results.find { !it } + + } + +} diff --git a/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakClassLoaderSpec.groovy b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakClassLoaderSpec.groovy new file mode 100644 index 000000000..43a410793 --- /dev/null +++ b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakClassLoaderSpec.groovy @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2011 by J. Brisbin + * Portions (c) 2011 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core + +import org.springframework.data.keyvalue.riak.util.RiakClassFileLoader +import org.springframework.data.keyvalue.riak.util.RiakClassLoader +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author J. Brisbin + */ +class RiakClassLoaderSpec extends Specification { + + @Shared RiakTemplate riakTemplate = new RiakTemplate() + + def setupSpec() { + RiakQosParameters qos = new RiakQosParameters() + qos.durableWriteThreshold = "all" + riakTemplate.defaultQosParameters = qos + riakTemplate.ignoreNotFound = true + riakTemplate.afterPropertiesSet() + } + + def "Test load class file into Riak"() { + + when: + def args = [ + "-c", "src/test/classes/org/springframework/data/keyvalue/riak/core/ClassLoaderTest.class", + "-b", "test", + "-k", "org.springframework.data.keyvalue.riak.core.ClassLoaderTest" + ].toArray(new String[6]) + RiakClassFileLoader.main(args) + + then: + true + + } + + def "Test find class previously loaded into Riak"() { + + given: + RiakClassLoader classLoader = new RiakClassLoader(riakTemplate) + classLoader.defaultBucket = "test" + + when: + def clazz = Class.forName("org.springframework.data.keyvalue.riak.core.ClassLoaderTest", false, classLoader) + def inst = clazz?.newInstance() + + then: + null != clazz + null != inst + inst.name == "ClassLoaderTest" + + } + +} diff --git a/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakKeyValueTemplateSpec.groovy b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakKeyValueTemplateSpec.groovy new file mode 100644 index 000000000..25fcb48d6 --- /dev/null +++ b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakKeyValueTemplateSpec.groovy @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.riak.core + +import org.springframework.data.keyvalue.riak.mapreduce.JavascriptMapReduceOperation +import org.springframework.data.keyvalue.riak.mapreduce.MapReduceJob +import org.springframework.data.keyvalue.riak.mapreduce.RiakMapReducePhase +import org.springframework.data.keyvalue.riak.util.Ignore404sErrorHandler +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author J. Brisbin + */ +class RiakKeyValueTemplateSpec extends Specification { + + @Shared RiakKeyValueTemplate riak = new RiakKeyValueTemplate() + int run = 1 + @Shared def riakBin = System.properties["bamboo.RIAK_BIN"] ?: "/usr/sbin/riak" + @Shared def p + + def setupSpec() { + RiakQosParameters qos = new RiakQosParameters() + qos.setDurableWriteThreshold("all") + riak.setDefaultQosParameters(qos) + riak.getRestTemplate().setErrorHandler(new Ignore404sErrorHandler()) + + if (!riak.get("status", "")) { + p = "$riakBin start".execute() + p.waitFor() + shutdown = true + Thread.sleep(2000) + } + + riak.getBucketSchema("test", true).keys.each { + riak.delete("test", it) + } + riak.getBucketSchema(TestObject.name, true).keys.each { + riak.delete("test", it) + } + } + + def cleanupSpec() { + if (shutdown) { + p = "$riakBin stop".execute() + p.waitFor() + } + } + + def "Test Map object"() { + + given: + def val = "value" + def objIn = [test: val, integer: 12] + riak.set("test:test", objIn) + + when: + def objOut = riak.get("test:test") + + then: + objOut.test == val + + } + + def "Test custom object"() { + + given: + TestObject objIn = new TestObject() + riak.set("${TestObject.name}:test", objIn) + + when: + TestObject objOut = riak.get("${TestObject.name}:test") + + then: + objOut.test == "value" + + } + + def "Test getting bucket schema"() { + + when: + def schema = riak.getBucketSchema("test", true) + + then: + "test" == schema.props.name + + } + + def "Test updating bucket schema"() { + + when: + def schema = riak.updateBucketSchema("test", [n_val: 2]).getBucketSchema("test") + + then: + 2 == schema.props.n_val + + } + + def "Test get with metadata"() { + + when: + def val = riak.getWithMetaData([bucket: "test", key: "test"], LinkedHashMap) + + then: + val.metaData.properties["Server"].contains("WebMachine") + + } + + def "Test setting QosParameters"() { + + given: + def obj = riak.get("test:test") + + when: + def qos = new RiakQosParameters() + qos.durableWriteThreshold = "all" + riak.set("test:test", obj, qos) + + then: + true + + } + + def "Test containsKey"() { + + when: + def containsKey = riak.containsKey([bucket: "test", key: "test"]) + + then: + true == containsKey + + } + + def "Test linking"() { + + given: + riak.link("${TestObject.name}:test", "test:test", "test") + + when: + def val = riak.getWithMetaData("test:test", Map) + def result = val.metaData.properties["Link"].find { it.contains("riaktag=\"test\"") } + + then: + null != result + + } + + def "Test link walking"() { + + when: + def val = riak.linkWalk("test:test", "test") + + then: + null != val + 1 == val.size() + val.get(0) instanceof TestObject + + } + + def "Test multiple get"() { + + when: + def objs = riak.getValues([ + new SimpleBucketKeyPair("test", "test"), + new SimpleBucketKeyPair(TestObject.name, "test") + ]) + + then: + 2 == objs.size() + + } + + def "Test getAndSet with Map"() { + + given: + def i = run++ + def newObj = [test: "value $i", integer: 12] + + when: + def oldObj = riak.getAndSet("test:test", newObj) + + then: + "value" == oldObj.test + + } + + def "Test setMultipleIfKeysNonExistent with Map"() { + + given: + def testKey = new SimpleBucketKeyPair("test", "test") + def testKey2 = new SimpleBucketKeyPair(TestObject.name, "test") + def newObj = [:] + newObj[testKey] = [test: "value", integer: 12] + newObj[testKey2] = [test: "value", integer: 12] + + when: + def secondObj = riak.setMultipleIfKeysNonExistent(newObj).get(testKey2) + secondObj.test = "newValue" + def updObj = [:] + updObj[testKey2] = secondObj + def thirdObj = riak.setMultipleIfKeysNonExistent(updObj).get(testKey2) + + then: + "value" == thirdObj.test + + } + + def "Test Map/Reduce returning Integer"() { + + given: + MapReduceJob job = riak.createMapReduceJob() + def mapJs = new JavascriptMapReduceOperation("function(v){ var o=Riak.mapValuesJson(v); return [1]; }") + def mapPhase = new RiakMapReducePhase("map", "javascript", mapJs) + + def reduceJs = new JavascriptMapReduceOperation("function(v){ var s=Riak.reduceSum(v); return s; }") + def reducePhase = new RiakMapReducePhase("reduce", "javascript", reduceJs) + + job.addInputs(["test"]). + addPhase(mapPhase). + addPhase(reducePhase) + println job.toJson() + + when: + def result = riak.execute(job, Integer) + + then: + 1 == result + + } + + def "Test Map/Reduce returning List"() { + + given: + MapReduceJob job = riak.createMapReduceJob() + def mapJs = new JavascriptMapReduceOperation("function(v){ ejsLog('/tmp/mapred.log', 'map v: '+JSON.stringify(v)); var o=Riak.mapValuesJson(v); return [1]; }") + def mapPhase = new RiakMapReducePhase("map", "javascript", mapJs) + + def reduceJs = new JavascriptMapReduceOperation("function(v){ ejsLog('/tmp/mapred.log', 'red v: '+JSON.stringify(v)); var s=Riak.reduceSum(v); return s; }") + def reducePhase = new RiakMapReducePhase("reduce", "javascript", reduceJs) + + job.addInputs(["test"]). + addPhase(mapPhase). + addPhase(reducePhase) + + when: + def result = riak.execute(job) + + then: + 1 == result.size() + 1 == result[0] + + } + + def "Test deleteKeys"() { + + given: + def testKey = new SimpleBucketKeyPair("test", "test") + def testKey2 = new SimpleBucketKeyPair(TestObject.name, "test") + + when: + def deleted = riak.deleteKeys(testKey, testKey2) + + then: + true == deleted + + } + +} \ No newline at end of file diff --git a/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakTemplateSpec.groovy b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakTemplateSpec.groovy new file mode 100644 index 000000000..e6988c764 --- /dev/null +++ b/spring-data-riak/src/test/groovy/org/springframework/data/keyvalue/riak/core/RiakTemplateSpec.groovy @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * Portions (c) 2010 by NPC International, Inc. or the + * original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.keyvalue.riak.core + +import org.springframework.data.keyvalue.riak.core.io.RiakFile +import org.springframework.data.keyvalue.riak.mapreduce.JavascriptMapReduceOperation +import org.springframework.data.keyvalue.riak.mapreduce.MapReduceJob +import org.springframework.data.keyvalue.riak.mapreduce.RiakMapReduceJob +import org.springframework.data.keyvalue.riak.mapreduce.RiakMapReducePhase +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author J. Brisbin + */ +class RiakTemplateSpec extends Specification { + + @Shared RiakTemplate riak = new RiakTemplate() + int run = 1 + @Shared def riakBin = System.properties["bamboo.RIAK_BIN"] ?: "/usr/sbin/riak" + @Shared def p + @Shared def id + @Shared boolean shutdown = false + + def setupSpec() { + RiakQosParameters qos = new RiakQosParameters() + qos.setDurableWriteThreshold("all") + riak.setDefaultQosParameters(qos) + riak.ignoreNotFound = true + + if (!riak.get("status", "")) { + p = "$riakBin start".execute() + p.waitFor() + shutdown = true + Thread.sleep(2000) + } + + riak.getBucketSchema("test", true).keys.each { + riak.delete("test", it) + } + riak.getBucketSchema(TestObject.name, true).keys.each { + riak.delete("test", it) + } + } + + def cleanupSpec() { + if (shutdown) { + p = "$riakBin stop".execute() + p.waitFor() + } + } + + def "Test Map object"() { + + given: + def val = "value" + def objIn = [test: val, integer: 12] + riak.set("test", "test", objIn) + + when: + def objOut = riak.get("test", "test") + + then: + objOut.test == val + + } + + def "Test generating ID for object"() { + + given: + def val = "value" + def objIn = [test: val, integer: 12] + + when: + id = riak.put("test", objIn, null) + + then: + null != id + + } + + def "Test custom object"() { + + given: + TestObject objIn = new TestObject() + riak.set(TestObject.name, "test", objIn) + + when: + TestObject objOut = riak.getAsType(TestObject.name, "test", TestObject) + + then: + objOut.test == "value" + + } + + def "Test convert custom object from bytes"() { + + given: + def qos = new RiakQosParameters() + qos.durableWriteThreshold = "all" + riak.setAsBytes(TestObject.name, "test", "{\"test\":\"string data\",\"integer\":1}".bytes, qos) + + when: + def objOut = riak.getAsType(TestObject.name, "test", TestObject) + //riak.delete(TestObject.name, "test") + + then: + objOut instanceof TestObject + objOut.test == "string data" + + } + + def "Test getting bucket schema"() { + + when: + def schema = riak.getBucketSchema("test", true) + + then: + "test" == schema.props.name + + } + + def "Test updating bucket schema"() { + + when: + def schema = riak.updateBucketSchema("test", [n_val: 2]).getBucketSchema("test") + + then: + 2 == schema.props.n_val + + } + + def "Test get with metadata"() { + + when: + def val = riak.getWithMetaData("test", "test", LinkedHashMap) + + then: + val.metaData.properties["Server"].contains("WebMachine") + + } + + def "Test setting QosParameters"() { + + given: + def obj = riak.get("test", "test") + + when: + def qos = new RiakQosParameters() + qos.durableWriteThreshold = "all" + riak.set("test", "test", obj, qos) + + then: + true + + } + + def "Test containsKey"() { + + when: + def containsKey = riak.containsKey("test", "test") + + then: + true == containsKey + + } + + def "Test linking"() { + + given: + def qos = new RiakQosParameters() + qos.durableWriteThreshold = "all" + riak.set(TestObject.name, "test", new TestObject(), qos) + riak.link(TestObject.name, "test", "test", "test", "test") + + when: + def val = riak.getWithMetaData("test", "test", Map) + def result = val.metaData.properties["Link"].find { it.contains("riaktag=\"test\"") } + + then: + null != result + + } + + def "Test link walking"() { + + when: + def val = riak.linkWalk("test", "test", "test") + + then: + null != val + 1 == val.size() + val.get(0) instanceof TestObject + + } + + def "Test link walking as type"() { + + when: + def val = riak.linkWalkAsType("test", "test", "test", Map) + + then: + null != val + 1 == val.size() + val.get(0) instanceof Map + + } + + def "Test getAndSet with Map"() { + + given: + def i = run++ + def newObj = [test: "value $i".toString(), integer: 12] + + when: + def oldObj = riak.getAndSet("test", "test", newObj) + + then: + "value" == oldObj.test + + } + + def "Test Map/Reduce returning Integer"() { + + given: + MapReduceJob job = new RiakMapReduceJob(riak) + def uuid = UUID.randomUUID().toString() + def mapJs = new JavascriptMapReduceOperation("function(v){ var uuid='$uuid'; ejsLog('/tmp/mapred.log', 'map input: '+JSON.stringify(v)); var o=Riak.mapValuesJson(v); return [1]; }") + def mapPhase = new RiakMapReducePhase("map", "javascript", mapJs) + + def reduceJs = new JavascriptMapReduceOperation("function(v){ var uuid='$uuid'; ejsLog('/tmp/mapred.log', 'reduce input: '+JSON.stringify(v)); var s=Riak.reduceSum(v); ejsLog('/tmp/mapred.log', 'reduce output: '+JSON.stringify(s)); return s; }") + def reducePhase = new RiakMapReducePhase("reduce", "javascript", reduceJs) + + job.addInputs(["test"]). + addPhase(mapPhase). + addPhase(reducePhase) + println job.toJson() + + when: + def result = riak.execute(job, Integer) + + then: + 2 == result + + } + + def "Test Map/Reduce returning List"() { + + given: + MapReduceJob job = new RiakMapReduceJob(riak) + def uuid = UUID.randomUUID().toString() + def mapJs = new JavascriptMapReduceOperation("function(v){ var uuid='$uuid'; ejsLog('/tmp/mapred.log', 'map input: '+JSON.stringify(v)); var o=Riak.mapValuesJson(v); return [1]; }") + def mapPhase = new RiakMapReducePhase("map", "javascript", mapJs) + + def reduceJs = new JavascriptMapReduceOperation("function(v){ var uuid='$uuid'; ejsLog('/tmp/mapred.log', 'reduce input: '+JSON.stringify(v)); var s=Riak.reduceSum(v); ejsLog('/tmp/mapred.log', 'reduce output: '+JSON.stringify(s)); return s; }") + def reducePhase = new RiakMapReducePhase("reduce", "javascript", reduceJs) + + job.addInputs(["test"]). + addPhase(mapPhase). + addPhase(reducePhase) + + when: + def result = riak.execute(job) + + then: + 1 == result.size() + 2 == result[0] + + } + + def "Test RiakFile"() { + + given: + def file = new RiakFile(riak, "test", "test") + + when: + def exists = file.exists() + + then: + exists + + when: + def content = file.toURI().toURL().openConnection().getContent() + + then: + null != content + + } + + def "Test delete key"() { + + when: + def deleted = riak.deleteKeys("test:test", "${TestObject.name}:test", "test:$id") + + then: + true == deleted + + } + +} \ No newline at end of file diff --git a/spring-data-riak/src/test/java/org/springframework/data/keyvalue/riak/core/TestObject.java b/spring-data-riak/src/test/java/org/springframework/data/keyvalue/riak/core/TestObject.java new file mode 100644 index 000000000..ee9d5eb04 --- /dev/null +++ b/spring-data-riak/src/test/java/org/springframework/data/keyvalue/riak/core/TestObject.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010 by J. Brisbin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.keyvalue.riak.core; + +/** + * @author J. Brisbin + */ +public class TestObject { + String test = "value"; + Integer integer = 12; + + public String getTest() { + return test; + } + + public void setTest(String test) { + this.test = test; + } + + public Integer getInteger() { + return integer; + } + + public void setInteger(Integer integer) { + this.integer = integer; + } +} diff --git a/spring-data-riak/src/test/resources/log4j.properties b/spring-data-riak/src/test/resources/log4j.properties new file mode 100644 index 000000000..002fb5bcf --- /dev/null +++ b/spring-data-riak/src/test/resources/log4j.properties @@ -0,0 +1,14 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n + +log4j.category.org.apache.activemq=ERROR +log4j.category.org.springframework.batch=DEBUG +log4j.category.org.springframework.transaction=INFO +log4j.category.org.springframework.data=DEBUG + +log4j.category.org.hibernate.SQL=DEBUG +# for debugging datasource initialization +# log4j.category.test.jdbc=DEBUG diff --git a/spring-data-riak/src/test/resources/org/springframework/data/AsyncRiakTemplateTests.xml b/spring-data-riak/src/test/resources/org/springframework/data/AsyncRiakTemplateTests.xml new file mode 100644 index 000000000..7fab17ab0 --- /dev/null +++ b/spring-data-riak/src/test/resources/org/springframework/data/AsyncRiakTemplateTests.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/spring-data-riak/template.mf b/spring-data-riak/template.mf new file mode 100644 index 000000000..a7eae64c5 --- /dev/null +++ b/spring-data-riak/template.mf @@ -0,0 +1,31 @@ +Bundle-SymbolicName: org.springframework.data.keyvalue.riak +Bundle-Name: Spring Data Riak Support +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Package: + sun.reflect;version="0";resolution:=optional +Import-Template: + org.springframework.beans.*;version="[3.0.0, 4.0.0)", + org.springframework.core.*;version="[3.0.0, 4.0.0)", + org.springframework.dao.*;version="[3.0.0, 4.0.0)", + org.springframework.http.*;version="[3.0.0, 4.0.0)", + org.springframework.http.client.*;version="[3.0.0, 4.0.0)", + org.springframework.web.*;version="[3.0.0, 4.0.0)", + org.springframework.web.client.*;version="[3.0.0, 4.0.0)", + org.springframework.util.*;version="[3.0.0, 4.0.0)", + org.springframework.data.core.*;version="[1.0.0, 2.0.0)", + org.springframework.data.core.*;version="[1.0.0, 2.0.0)", + org.springframework.data.*;version="[1.0.0, 2.0.0)", + org.springframework.data.persistence.*;version="[1.0.0, 2.0.0)", + org.springframework.data.document.*;version="[1.0.0, 2.0.0)", + org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.w3c.dom.*;version="0", + org.codehaus.jackson.*;version="[1.5.6, 1.5.6)", + org.codehaus.jackson.map.*;version="[1.5.6, 1.5.6)", + org.codehaus.groovy.runtime.*;version="[1.7.5, 2.0.0)", + groovy.lang.*;version="[1.7.5, 2.0.0)", + groovy.util.*;version="[1.7.5, 2.0.0)", + javax.activation.*;version="[1.1, 2.0)", + javax.mail.*;version="[1.4.0, 2.0.0)", + org.apache.commons.cli.*;version="[1.2, 2.0)", \ No newline at end of file