diff --git a/.travis.yml b/.travis.yml
index f321772014..f207b66572 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,8 @@
language: java
jdk:
- - oraclejdk7
- oraclejdk8
env:
matrix:
- - PROFILE=ci
- - PROFILE=spring41-next
- PROFILE=spring42
- PROFILE=spring42-next
- PROFILE=spring43-next
diff --git a/pom.xml b/pom.xml
index fdfcb2ad02..c8de41dbc0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-redis
- 1.7.0.BUILD-SNAPSHOT
+ 1.7.0.DATAREDIS-425-SNAPSHOTSpring Data Redis
@@ -17,7 +17,7 @@
DATAREDIS
- 1.12.0.BUILD-SNAPSHOT
+ 1.1.0.BUILD-SNAPSHOT1.11.9.21.4.8
@@ -53,8 +53,8 @@
org.springframework.data
- spring-data-commons
- ${springdata.commons}
+ spring-data-keyvalue
+ ${springdata.keyvalue}
@@ -152,6 +152,36 @@
true
+
+
+ javax.enterprise
+ cdi-api
+ ${cdi}
+ provided
+ true
+
+
+
+ javax.el
+ el-api
+ ${cdi}
+ test
+
+
+
+ org.apache.openwebbeans.test
+ cditest-owb
+ ${webbeans}
+ test
+
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ test
+
+
diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc
index 53ebd3bbd0..4fd3f8ac0b 100644
--- a/src/main/asciidoc/index.adoc
+++ b/src/main/asciidoc/index.adoc
@@ -34,6 +34,7 @@ include::introduction/getting-started.adoc[]
include::reference/introduction.adoc[]
include::reference/redis.adoc[]
include::reference/redis-cluster.adoc[]
+include::reference/redis-repositories.adoc[]
:leveloffset: -1
[[appendixes]]
diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc
new file mode 100644
index 0000000000..f0fc1c1280
--- /dev/null
+++ b/src/main/asciidoc/reference/redis-repositories.adoc
@@ -0,0 +1,604 @@
+[[redis.repositories]]
+= Redis Repositories
+
+Working with Redis Repositories allows to seamlessly convert and store domain objects in Redis Hashes, apply custom mapping strategies and make use of secondary indexes.
+
+WARNING: Redis Repositories requires at least Redis Server version 2.8.0.
+
+[[redis.repositories.usage]]
+== Usage
+
+To access domain entities stored in a Redis you can leverage repository support that eases implementing those quite significantly.
+
+.Sample Person Entity
+====
+[source,java]
+----
+@RedisHash("persons")
+public class Person {
+
+ @Id String id;
+ String firstname;
+ String lastname;
+ Address address;
+}
+----
+====
+
+We have a pretty simple domain object here. Note that it has a property named `id` annotated with `org.springframework.data.annotation.Id` and a `@RedisHash` annotation on its type.
+Those two are responsible for creating the actual key used to persist the hash.
+
+NOTE: Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties. Those with the annotation are favored over others.
+
+To now actually have a component responsible for storage and retrieval we need to define a repository interface.
+
+.Basic Repository Interface To Persist Person Entities
+====
+[source,java]
+----
+public interface PersonRepository extends CrudRepository {
+
+}
+----
+====
+
+As our repository extends `CrudRepository` it provides basic CRUD and finder operations. The thing we need in between to glue things together is the according Spring configuration.
+
+.JavaConfig for Redis Repositories
+====
+[source,java]
+----
+@Configuration
+@EnableRedisRepositories
+public class ApplicationConfig {
+
+ @Bean
+ public RedisConnectionFactory connectionFactory() {
+ return new JedisConnectionFactory();
+ }
+
+ @Bean
+ public RedisTemplate, ?> redisTemplate() {
+
+ RedisTemplate template = new RedisTemplate();
+ return template;
+ }
+}
+----
+====
+
+Given the setup above we can go on and inject `PersonRepository` into our components.
+
+.Access to Person Entities
+====
+[source,java]
+----
+@Autowired PersonRepository repo;
+
+public void basicCrudOperations() {
+
+ Person rand = new Person("rand", "al'thor");
+ rand.setAddress(new Address("emond's field", "andor"));
+
+ repo.save(rand); <1>
+
+ repo.findOne(rand.getId()); <2>
+
+ repo.count(); <3>
+
+ repo.delete(rand); <4>
+}
+----
+<1> Generates a new id if current value is `null` or reuses an already set id value and stores properties of type `Person` inside the Redis Hash with key with pattern `keyspace:id` in this case eg. `persons:5d67b7e1-8640-4475-beeb-c666fab4c0e5`.
+<2> Uses the provided id to retrieve the object stored at `keyspace:id`.
+<3> Counts the total number of entities available within the keyspace _persons_ defined by `@RedisHash` on `Person`.
+<4> Removes the key for the given object from Redis.
+====
+
+[[redis.repositories.mapping]]
+== Object to Hash Mapping
+The Redis Repository support persists Objects in Hashes. This requires an Object to Hash conversion which is done by a `RedisConverter`. The default implementation uses `Converter` for mapping property values to and from Redis native `byte[]`.
+
+Given the `Person` type from the previous sections the default mapping looks like the following:
+
+====
+[source,text]
+----
+_class = org.example.Person <1>
+id = e2c7dcee-b8cd-4424-883e-736ce564363e
+firstname = rand <2>
+lastname = al’thor
+address.city = emond's field <3>
+address.country = andor
+----
+<1> The `_class` attribute is included on root level as well as on any nested interface or abstract types.
+<2> Simple property values are mapped by path.
+<3> Properties of complex types are mapped by their dot path.
+====
+
+[cols="1,2,3", options="header"]
+.Default Mapping Rules
+|===
+| Type
+| Sample
+| Mapped Value
+
+| Simple Type +
+(eg. String)
+| String firstname = "rand";
+| firstname = "rand"
+
+| Complex Type +
+(eg. Address)
+| Address adress = new Address("emond's field");
+| address.city = "emond's field"
+
+| List +
+of Simple Type
+| List nicknames = asList("dragon reborn", "lews therin");
+| nicknames.[0] = "dragon reborn", +
+nicknames.[1] = "lews therin"
+
+| Map +
+of Simple Type
+| Map atts = asMap({"eye-color", "grey"}, {"...
+| atts.[eye-color] = "grey", +
+atts.[hair-color] = "...
+
+| List +
+of Complex Type
+| List addresses = asList(new Address("em...
+| addresses.[0].city = "emond's field", +
+addresses.[1].city = "...
+
+| Map +
+of Complex Type
+| Map addresses = asMap({"home", new Address("em...
+| addresses.[home].city = "emond's field", +
+addresses.[work].city = "...
+|===
+
+Mapping behavior can be customized by registering the according `Converter` in `CustomConversions`. Those converters can take care of converting from/to a single `byte[]` as well as `Map` whereas the first one is suitable for eg. converting one complex type to eg. a binary JSON representation that still uses the default mappings hash structure. The second option offers full control over the resulting hash. Writing objects to a Redis hash will delete the content from the hash and re-create the whole hash, so not mapped data will be lost.
+
+.Sample byte[] Converters
+====
+[source,java]
+----
+@WritingConverter
+public class AddressToBytesConverter implements Converter {
+
+ private final Jackson2JsonRedisSerializer serializer;
+
+ public AddressToBytesConverter() {
+
+ serializer = new Jackson2JsonRedisSerializer(Address.class);
+ serializer.setObjectMapper(new ObjectMapper());
+ }
+
+ @Override
+ public byte[] convert(Address value) {
+ return serializer.serialize(value);
+ }
+}
+
+@ReadingConverter
+public class BytesToAddressConverter implements Converter {
+
+ private final Jackson2JsonRedisSerializer serializer;
+
+ public BytesToAddressConverter() {
+
+ serializer = new Jackson2JsonRedisSerializer(Address.class);
+ serializer.setObjectMapper(new ObjectMapper());
+ }
+
+ @Override
+ public Address convert(byte[] value) {
+ return serializer.deserialize(value);
+ }
+}
+----
+====
+
+Using the above byte[] `Converter` produces eg.
+====
+[source,text]
+----
+_class = org.example.Person
+id = e2c7dcee-b8cd-4424-883e-736ce564363e
+firstname = rand
+lastname = al’thor
+address = { city : "emond's field", country : "andor" }
+----
+====
+
+
+.Sample Map Converters
+====
+[source,java]
+----
+@WritingConverter
+public class AddressToMapConverter implements Converter> {
+
+ @Override
+ public Map convert(Address source) {
+ return singletonMap("ciudad", source.getCity().getBytes());
+ }
+}
+
+@ReadingConverter
+public class MapToAddressConverter implements Converter> {
+
+ @Override
+ public Address convert(Map source) {
+ return new Address(new String(source.get("ciudad")));
+ }
+}
+----
+====
+
+Using the above Map `Converter` produces eg.
+
+====
+[source,text]
+----
+_class = org.example.Person
+id = e2c7dcee-b8cd-4424-883e-736ce564363e
+firstname = rand
+lastname = al’thor
+ciudad = "emond's field"
+----
+====
+
+NOTE: Custom conversions have no effect on index resolution. <> will still be created even for custom converted types.
+
+[[redis.repositories.keyspaces]]
+== Keyspaces
+Keyspaces define prefixes used to create the actual _key_ for the Redis Hash.
+By default the prefix is set to `getClass().getName()`. This default can be altered via `@RedisHash` on aggregate root level or by setting up a programmatic configuration. However, the annotated keyspace supersedes any other configuration.
+
+.Keyspace Setup via @EnableRedisRepositories
+====
+[source,java]
+----
+@Configuration
+@EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class)
+public class ApplicationConfig {
+
+ //... RedisConnectionFactory and RedisTemplate Bean definitions omitted
+
+ public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
+
+ @Override
+ protected Iterable initialConfiguration() {
+ return Collections.singleton(new KeyspaceSettings(Person.class, "persons"));
+ }
+ }
+}
+----
+====
+
+.Programmatic Keyspace setup
+====
+[source,java]
+----
+@Configuration
+@EnableRedisRepositories
+public class ApplicationConfig {
+
+ //... RedisConnectionFactory and RedisTemplate Bean definitions omitted
+
+ @Bean
+ public RedisMappingContext keyValueMappingContext() {
+ return new RedisMappingContext(
+ new MappingConfiguration(
+ new MyKeyspaceConfiguration(), new IndexConfiguration()));
+ }
+
+ public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
+
+ @Override
+ protected Iterable initialConfiguration() {
+ return Collections.singleton(new KeyspaceSettings(Person.class, "persons"));
+ }
+ }
+}
+----
+====
+
+[[redis.repositories.indexes]]
+== Secondary Indexes
+http://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native Redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or <>.
+
+Given the sample `Person` entity we can create an index for _firstname_ by annotating the property with `@Indexed`.
+
+.Annotation driven indexing
+====
+[source,java]
+----
+@RedisHash("persons")
+public class Person {
+
+ @Id String id;
+ @Indexed String firstname;
+ String lastname;
+ Address address;
+}
+----
+====
+
+Indexes are built up for actual property values. Saving two Persons eg. "rand" and "aviendha" results in setting up indexes like below.
+
+====
+[source,text]
+----
+SADD persons:firstname:rand e2c7dcee-b8cd-4424-883e-736ce564363e
+SADD persons:firstname:aviendha a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56
+----
+====
+
+It is also possible to have indexes on nested elements. Assume `Address` has a _city_ property that is annotated with `@Indexed`. In that case, once `person.address.city` is not `null`, we have Sets for each city.
+
+====
+[source,text]
+----
+SADD persons:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e
+----
+====
+
+Further more the programmatic setup allows to define indexes on map keys and list properties.
+
+====
+[source,java]
+----
+@RedisHash("persons")
+public class Person {
+
+ // ... other properties omitted
+
+ Map attributes; <1>
+ Map relatives; <2>
+ List addresses; <3>
+}
+----
+<1> `SADD persons:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e`
+<2> `SADD persons:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e`
+<3> `SADD persons:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e`
+====
+
+WARNING: Indexes will not be resolved on <>.
+
+Same as with _keyspaces_ it is possible to configure indexes without the need of annotating the actual domain type.
+
+.Index Setup via @EnableRedisRepositories
+====
+[source,java]
+----
+@Configuration
+@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class)
+public class ApplicationConfig {
+
+ //... RedisConnectionFactory and RedisTemplate Bean definitions omitted
+
+ public static class MyIndexConfiguration extends IndexConfiguration {
+
+ @Override
+ protected Iterable initialConfiguration() {
+ return Collections.singleton(new RedisIndexSetting("persons", "firstname"));
+ }
+ }
+}
+----
+====
+
+.Programmatic Index setup
+====
+[source,java]
+----
+@Configuration
+@EnableRedisRepositories
+public class ApplicationConfig {
+
+ //... RedisConnectionFactory and RedisTemplate Bean definitions omitted
+
+ @Bean
+ public RedisMappingContext keyValueMappingContext() {
+ return new RedisMappingContext(
+ new MappingConfiguration(
+ new KeyspaceConfiguration(), new MyIndexConfiguration()));
+ }
+
+ public static class MyIndexConfiguration extends IndexConfiguration {
+
+ @Override
+ protected Iterable initialConfiguration() {
+ return Collections.singleton(new RedisIndexSetting("persons", "firstname"));
+ }
+ }
+}
+----
+====
+
+
+[[redis.repositories.expirations]]
+== Time To Live
+Objects stored in Redis may only be valid for a certain amount of time. This is especially useful for persisting short lived objects in Redis without having to remove them manually when they reached their end of life.
+The expiration time in seconds can be set via `@RedisHash(timeToLive=...)` as well as via `KeyspaceSettings` (see <>).
+
+More flexible expiration times can be set by using the `@TimeToLive` annotation on either a numeric property or method. However do not apply `@TimeToLive` on both a method and a property within the same class.
+
+.Expirations
+====
+[source,java]
+----
+public class TimeToLiveOnProperty {
+
+ @Id
+ private String id;
+
+ @TimeToLive
+ private Long expiration;
+}
+
+public class TimeToLiveOnMethod {
+
+ @Id
+ private String id;
+
+ @TimeToLive
+ public long getTimeToLive() {
+ return new Random().nextLong();
+ }
+}
+----
+====
+
+
+The repository implementation ensures subscription to http://redis.io/topics/notifications[Redis keyspace notifications] via `RedisMessageListenerContainer`.
+
+When the expiration is set to a positive value the according `EXPIRE` command is executed.
+Additionally to persisting the original, a _phantom_ copy is persisted in Redis and set to expire 5 minutes after the original one. This is done to enable the Repository support to publish `RedisKeyExpiredEvent` holding the expired value via Springs `ApplicationEventPublisher` whenever a key expires even though the original values have already been gone. Expiry events
+will be received on all connected applications using Spring Data Redis repositories.
+
+The `RedisKeyExpiredEvent` will hold a copy of the actually expired domain object as well as the key.
+
+NOTE: The keyspace notification message listener will alter `notify-keyspace-events` settings in Redis if those are not already set. Existing settings will not be overridden, so it is left to the user to set those up correctly when not leaving them empty.
+
+NOTE: Redis Pub/Sub messages are not persistent. If a key expires while the application is down the expiry event will not be processed which may lead to secondary indexes containing still references to the expired object.
+
+[[redis.repositories.references]]
+== Persisting References
+Marking properties with `@Reference` allows storing a simple key reference instead of copying values into the hash itself.
+On loading from Redis, references are resolved automatically and mapped back into the object.
+
+.Sample Property Reference
+====
+[source,text]
+----
+_class = org.example.Person
+id = e2c7dcee-b8cd-4424-883e-736ce564363e
+firstname = rand
+lastname = al’thor
+mother = persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 <1>
+----
+<1> Reference stores the whole key (`keyspace:id`) of the referenced object.
+====
+
+WARNING: Referenced Objects are not subject of persisting changes when saving the referencing object. Please make sure to persist changes on referenced objects separately, since only the reference will be stored.
+Indexes set on properties of referenced types will not be resolved.
+
+[[redis.repositories.queries]]
+== Queries and Query Methods
+Query methods allow automatic derivation of simple finder queries from the method name.
+
+.Sample Repository finder Method
+====
+[source,java]
+----
+public interface PersonRepository extends CrudRepository {
+
+ List findByFirstname(String firstname);
+}
+----
+====
+
+
+NOTE: Please make sure properties used in finder methods are set up for indexing.
+
+NOTE: Query methods for Redis repositories support only queries for entities and collections of entities with paging.
+
+Using derived query methods might not always be sufficient to model the queries to execute. `RedisCallback` offers more control over the actual matching of index structures or even custom added ones. All it takes is providing a `RedisCallback` that returns a single or `Iterable` set of _id_ values.
+
+.Sample finder using RedisCallback
+====
+[source,java]
+----
+String user = //...
+
+List sessionsByUser = template.find(new RedisCallback>() {
+
+ public Set doInRedis(RedisConnection connection) throws DataAccessException {
+ return connection
+ .sMembers("sessions:securityContext.authentication.principal.username:" + user);
+ }}, RedisSession.class);
+----
+====
+
+Here's an overview of the keywords supported for Redis and what a method containing that keyword essentially translates to.
+====
+
+.Supported keywords inside method names
+[options = "header, autowidth"]
+|===============
+|Keyword|Sample|Redis snippet
+|`And`|`findByLastnameAndFirstname`|`SINTER …:firstname:rand …:lastname:al’thor`
+|`Or`|`findByLastnameOrFirstname`|`SUNION …:firstname:rand …:lastname:al’thor`
+|`Is,Equals`|`findByFirstname`,`findByFirstnameIs`,`findByFirstnameEquals`|`SINTER …:firstname:rand`
+|===============
+====
+
+[[redis.misc.cdi-integration]]
+== CDI integration
+
+Instances of the repository interfaces are usually created by a container, which Spring is the most natural choice when working with Spring Data. There's sophisticated support to easily set up Spring to create bean instances. Spring Data Redis ships with a custom CDI extension that allows using the repository abstraction in CDI environments. The extension is part of the JAR so all you need to do to activate it is dropping the Spring Data Redis JAR into your classpath.
+
+You can now set up the infrastructure by implementing a CDI Producer for the `RedisConnectionFactory` and `RedisOperations`:
+
+[source, java]
+----
+class RedisOperationsProducer {
+
+
+ @Produces
+ RedisConnectionFactory redisConnectionFactory() {
+
+ JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
+ jedisConnectionFactory.setHostName("localhost");
+ jedisConnectionFactory.setPort(6379);
+ jedisConnectionFactory.afterPropertiesSet();
+
+ return jedisConnectionFactory;
+ }
+
+ void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception {
+
+ if (redisConnectionFactory instanceof DisposableBean) {
+ ((DisposableBean) redisConnectionFactory).destroy();
+ }
+ }
+
+ @Produces
+ @ApplicationScoped
+ RedisOperations redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) {
+
+ RedisTemplate template = new RedisTemplate();
+ template.setConnectionFactory(redisConnectionFactory);
+ template.afterPropertiesSet();
+
+ return template;
+ }
+
+}
+----
+
+The necessary setup can vary depending on the JavaEE environment you run in.
+
+The Spring Data Redis CDI extension will pick up all Repositories available as CDI beans and create a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. Thus obtaining an instance of a Spring Data repository is a matter of declaring an `@Injected` property:
+
+[source, java]
+----
+class RepositoryClient {
+
+ @Inject
+ PersonRepository repository;
+
+ public void businessMethod() {
+ List people = repository.findAll();
+ }
+}
+----
+
+A Redis Repository requires `RedisKeyValueAdapter` and `RedisKeyValueTemplate` instances. These beans are created and managed by the Spring Data CDI extension if no provided beans are found. You can however supply your own beans to configure the specific properties of `RedisKeyValueAdapter` and `RedisKeyValueTemplate`.
+
+
+
diff --git a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java
index 47866c7ceb..f6cf551dce 100644
--- a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java
+++ b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java
@@ -35,14 +35,13 @@
import org.springframework.data.redis.connection.RedisClusterNode.SlotRange;
import org.springframework.data.redis.connection.RedisNode.NodeType;
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
-import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
/**
* Common type converters
- *
+ *
* @author Jennifer Hickey
* @author Thomas Darimont
* @author Mark Paluch
@@ -85,7 +84,7 @@ public RedisClusterNode convert(String source) {
Set flags = parseFlags(args);
String portPart = hostAndPort[1];
- if(portPart.contains("@")){
+ if (portPart.contains("@")) {
portPart = portPart.substring(0, portPart.indexOf('@'));
}
@@ -195,7 +194,7 @@ public static byte[] toBit(Boolean source) {
/**
* Converts the result of a single line of {@code CLUSTER NODES} into a {@link RedisClusterNode}.
- *
+ *
* @param clusterNodesLine
* @return
* @since 1.7
@@ -206,7 +205,7 @@ protected static RedisClusterNode toClusterNode(String clusterNodesLine) {
/**
* Converts lines from the result of {@code CLUSTER NODES} into {@link RedisClusterNode}s.
- *
+ *
* @param clusterNodes
* @return
* @since 1.7
@@ -228,6 +227,7 @@ public static Set toSetOfRedisClusterNodes(Collection
/**
* Converts the result of {@code CLUSTER NODES} into {@link RedisClusterNode}s.
+ *
* @param clusterNodes
* @return
* @since 1.7
@@ -259,24 +259,8 @@ public static List