diff --git a/.travis.yml b/.travis.yml
index 62282cafb..f207b6657 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,5 @@
language: java
jdk:
- - oraclejdk7
- oraclejdk8
env:
matrix:
diff --git a/pom.xml b/pom.xml
index 89593f87d..c7e69871c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -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/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc
index d125dbe2a..f0fc1c128 100644
--- a/src/main/asciidoc/reference/redis-repositories.adoc
+++ b/src/main/asciidoc/reference/redis-repositories.adoc
@@ -14,7 +14,7 @@ To access domain entities stored in a Redis you can leverage repository support
====
[source,java]
----
-@RedisHash("persons");
+@RedisHash("persons")
public class Person {
@Id String id;
@@ -158,7 +158,7 @@ of Complex Type
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, whereas the second options offers full control over the resulting hash.
+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
====
@@ -308,7 +308,7 @@ public class ApplicationConfig {
[[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 <>.
+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`.
@@ -316,7 +316,7 @@ Given the sample `Person` entity we can create an index for _firstname_ by annot
====
[source,java]
----
-@RedisHash("persons");
+@RedisHash("persons")
public class Person {
@Id String id;
@@ -351,7 +351,7 @@ Further more the programmatic setup allows to define indexes on map keys and lis
====
[source,java]
----
-@RedisHash("persons");
+@RedisHash("persons")
public class Person {
// ... other properties omitted
@@ -457,16 +457,19 @@ public class TimeToLiveOnMethod {
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 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.
+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: 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 to store a simple key reference instead of copying the all values into the hash itself.
-On loading from Redis references are resolved automatically and mapped back into the object.
+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
====
@@ -499,9 +502,12 @@ public interface PersonRepository extends CrudRepository {
----
====
+
NOTE: Please make sure properties used in finder methods are set up for indexing.
-Using derived finder 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 customly added ones. All it takes is providing a `RedisCallback` that returns a single or `Iterable` set of _id_ values.
+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
====
@@ -518,6 +524,81 @@ List sessionsByUser = template.find(new RedisCallback>
----
====
+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/core/RedisKeyExpiredEvent.java b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java
index 4e629c700..054860c29 100644
--- a/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java
+++ b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java
@@ -29,6 +29,11 @@ import org.springframework.data.redis.util.ByteUtils;
*/
public class RedisKeyExpiredEvent extends RedisKeyspaceEvent {
+ /**
+ * Use {@literal UTF-8} as default charset.
+ */
+ public static final Charset CHARSET = Charset.forName("UTF-8");
+
private final byte[][] args;
private final Object value;
@@ -62,7 +67,7 @@ public class RedisKeyExpiredEvent extends RedisKeyspaceEvent {
public String getKeyspace() {
if (args.length >= 2) {
- return new String(args[0], Charset.forName("UTF-8"));
+ return new String(args[0], CHARSET);
}
return null;
diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java
index 43c2339f6..18a2504e6 100644
--- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java
+++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java
@@ -160,6 +160,12 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
initKeyExpirationListener();
}
+ /**
+ * Default constructor.
+ */
+ protected RedisKeyValueAdapter() {
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#put(java.io.Serializable, java.lang.Object, java.io.Serializable)
diff --git a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java
index f97aeeefa..1b322e718 100644
--- a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java
+++ b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java
@@ -66,6 +66,7 @@ class RedisQueryEngine extends QueryEngine Collection execute(final RedisOperationChain criteria, final Comparator> sort, final int offset,
final int rows, final Serializable keyspace, Class type) {
diff --git a/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java b/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java
index 54a59dde7..6ab15f28c 100644
--- a/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java
+++ b/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java
@@ -45,7 +45,7 @@ final class BinaryConverters {
* @author Christoph Strobl
* @since 1.7
*/
- private static class StringBasedConverter {
+ static class StringBasedConverter {
byte[] fromString(String source) {
@@ -166,7 +166,7 @@ final class BinaryConverters {
if (value == null || value.length() == 0) {
return null;
}
- return (T) Enum.valueOf(this.enumType, value.trim());
+ return Enum.valueOf(this.enumType, value.trim());
}
}
}
@@ -183,8 +183,8 @@ final class BinaryConverters {
return new BytesToNumberConverter(targetType);
}
- private static final class BytesToNumberConverter extends StringBasedConverter implements
- Converter {
+ private static final class BytesToNumberConverter extends StringBasedConverter
+ implements Converter {
private final Class targetType;
diff --git a/src/main/java/org/springframework/data/redis/core/convert/Bucket.java b/src/main/java/org/springframework/data/redis/core/convert/Bucket.java
index e79cf3391..7a928fdd1 100644
--- a/src/main/java/org/springframework/data/redis/core/convert/Bucket.java
+++ b/src/main/java/org/springframework/data/redis/core/convert/Bucket.java
@@ -98,6 +98,13 @@ public class Bucket {
return data.isEmpty();
}
+ /**
+ * @return the number of key-value mappings of the {@link Bucket}.
+ */
+ public int size() {
+ return data.size();
+ }
+
/**
* @return never {@literal null}.
*/
@@ -121,6 +128,12 @@ public class Bucket {
return Collections.unmodifiableMap(this.data);
}
+ /**
+ * Extracts a bucket containing key/value pairs with the {@code prefix}.
+ *
+ * @param prefix
+ * @return
+ */
public Bucket extract(String prefix) {
Bucket partial = new Bucket();
diff --git a/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java b/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java
index 129fea5cc..d485ee6d9 100644
--- a/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java
+++ b/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java
@@ -103,6 +103,8 @@ public class CustomConversions {
toRegister.add(new BinaryConverters.DateToBytesConverter());
toRegister.add(new BinaryConverters.BytesToDateConverter());
+ toRegister.addAll(Jsr310Converters.getConvertersToRegister());
+
for (Object c : toRegister) {
registerConversion(c);
}
@@ -161,7 +163,8 @@ public class CustomConversions {
}
if (!added) {
- throw new IllegalArgumentException("Given set contains element that is neither Converter nor ConverterFactory!");
+ throw new IllegalArgumentException(
+ "Given set contains element that is neither Converter nor ConverterFactory!");
}
}
}
diff --git a/src/main/java/org/springframework/data/redis/core/convert/Jsr310Converters.java b/src/main/java/org/springframework/data/redis/core/convert/Jsr310Converters.java
new file mode 100644
index 000000000..dcebe1953
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/core/convert/Jsr310Converters.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2016 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.redis.core.convert;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.ReadingConverter;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.data.redis.core.convert.BinaryConverters.StringBasedConverter;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Helper class to register JSR-310 specific {@link Converter} implementations in case the we're running on Java 8.
+ *
+ * @author Mark Paluch
+ */
+public abstract class Jsr310Converters {
+
+ private static final boolean JAVA_8_IS_PRESENT = ClassUtils.isPresent("java.time.LocalDateTime",
+ Jsr310Converters.class.getClassLoader());
+
+ /**
+ * Returns the converters to be registered. Will only return converters in case we're running on Java 8.
+ *
+ * @return
+ */
+ public static Collection> getConvertersToRegister() {
+
+ if (!JAVA_8_IS_PRESENT) {
+ return Collections.emptySet();
+ }
+
+ List> converters = new ArrayList>();
+ converters.add(new LocalDateTimeToBytesConverter());
+ converters.add(new BytesToLocalDateTimeConverter());
+ converters.add(new LocalDateToBytesConverter());
+ converters.add(new BytesToLocalDateConverter());
+ converters.add(new LocalTimeToBytesConverter());
+ converters.add(new BytesToLocalTimeConverter());
+ converters.add(new ZonedDateTimeToBytesConverter());
+ converters.add(new BytesToZonedDateTimeConverter());
+ converters.add(new InstantToBytesConverter());
+ converters.add(new BytesToInstantConverter());
+ converters.add(new ZoneIdToBytesConverter());
+ converters.add(new BytesToZoneIdConverter());
+ converters.add(new PeriodToBytesConverter());
+ converters.add(new BytesToPeriodConverter());
+ converters.add(new DurationToBytesConverter());
+ converters.add(new BytesToDurationConverter());
+
+ return converters;
+ }
+
+ public static boolean supports(Class> type) {
+
+ if (!JAVA_8_IS_PRESENT) {
+ return false;
+ }
+
+ return Arrays.> asList(LocalDateTime.class, LocalDate.class, LocalTime.class, Instant.class,
+ ZonedDateTime.class, ZoneId.class, Period.class, Duration.class).contains(type);
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class LocalDateTimeToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(LocalDateTime source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToLocalDateTimeConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public LocalDateTime convert(byte[] source) {
+ return LocalDateTime.parse(toString(source));
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class LocalDateToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(LocalDate source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToLocalDateConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public LocalDate convert(byte[] source) {
+ return LocalDate.parse(toString(source));
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class LocalTimeToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(LocalTime source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToLocalTimeConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public LocalTime convert(byte[] source) {
+ return LocalTime.parse(toString(source));
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class ZonedDateTimeToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(ZonedDateTime source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToZonedDateTimeConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public ZonedDateTime convert(byte[] source) {
+ return ZonedDateTime.parse(toString(source));
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class InstantToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(Instant source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToInstantConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public Instant convert(byte[] source) {
+ return Instant.parse(toString(source));
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class ZoneIdToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(ZoneId source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToZoneIdConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public ZoneId convert(byte[] source) {
+ return ZoneId.of(toString(source));
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class PeriodToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(Period source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToPeriodConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public Period convert(byte[] source) {
+ return Period.parse(toString(source));
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @WritingConverter
+ static class DurationToBytesConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public byte[] convert(Duration source) {
+ return fromString(source.toString());
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ * @since 1.7
+ */
+ @ReadingConverter
+ static class BytesToDurationConverter extends StringBasedConverter implements Converter {
+
+ @Override
+ public Duration convert(byte[] source) {
+ return Duration.parse(toString(source));
+ }
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java
index 5c3921bb3..002c7f30b 100644
--- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java
+++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java
@@ -144,6 +144,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
* (non-Javadoc)
* @see org.springframework.data.convert.EntityReader#read(java.lang.Class, java.lang.Object)
*/
+ @Override
public R read(Class type, final RedisData source) {
return readInternal("", type, source);
}
@@ -282,11 +283,11 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
if (association.getInverse().isCollectionLike()) {
- Collection