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.data spring-data-redis - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAREDIS-425-SNAPSHOT Spring Data Redis @@ -17,7 +17,7 @@ DATAREDIS - 1.12.0.BUILD-SNAPSHOT + 1.1.0.BUILD-SNAPSHOT 1.1 1.9.2 1.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 toObjects(Set tuples) { * @return */ public static Long toTimeMillis(String seconds, String microseconds) { - return NumberUtils.parseNumber(seconds, Long.class) * 1000L + NumberUtils.parseNumber(microseconds, Long.class) - / 1000L; + return NumberUtils.parseNumber(seconds, Long.class) * 1000L + + NumberUtils.parseNumber(microseconds, Long.class) / 1000L; } - /** - * Merge multiple {@code byte} arrays into one array - * @param firstArray must not be {@literal null} - * @param additionalArrays must not be {@literal null} - * @return - */ - public static byte[][] mergeArrays(byte[] firstArray, byte[]... additionalArrays){ - Assert.notNull(firstArray, "first array must not be null"); - Assert.notNull(additionalArrays, "additional arrays must not be null"); - - byte[][] result = new byte[additionalArrays.length + 1][]; - result[0] = firstArray; - System.arraycopy(additionalArrays, 0, result, 1, additionalArrays.length); - - return result; - } } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java index cea3470056..b1c3aaa577 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java @@ -65,6 +65,7 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.util.ByteUtils; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -79,7 +80,7 @@ * {@link RedisClusterConnection} implementation on top of {@link JedisCluster}.
* Uses the native {@link JedisCluster} api where possible and falls back to direct node communication using * {@link Jedis} where needed. - * + * * @author Christoph Strobl * @author Mark Paluch * @since 1.7 @@ -126,7 +127,7 @@ public JedisClusterConnection(JedisCluster cluster) { /** * Create new {@link JedisClusterConnection} utilizing native connections via {@link JedisCluster} running commands * across the cluster via given {@link ClusterCommandExecutor}. - * + * * @param cluster must not be {@literal null}. * @param executor must not be {@literal null}. */ @@ -894,7 +895,7 @@ public Long bitCount(byte[] key, long begin, long end) { @Override public Long bitOp(BitOperation op, byte[] destination, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destination, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destination, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1332,7 +1333,7 @@ public Set doInCluster(Jedis client, byte[] key) { @Override public Long sInterStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1392,7 +1393,7 @@ public Set doInCluster(Jedis client, byte[] key) { @Override public Long sUnionStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1455,7 +1456,7 @@ public Set doInCluster(Jedis client, byte[] key) { @Override public Long sDiffStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -2083,7 +2084,7 @@ public Long zRemRangeByScore(byte[] key, double min, double max) { @Override public Long zUnionStore(byte[] destKey, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -2104,7 +2105,7 @@ public Long zUnionStore(byte[] destKey, byte[]... sets) { @Override public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -2123,7 +2124,7 @@ public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte @Override public Long zInterStore(byte[] destKey, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -2140,7 +2141,7 @@ public Long zInterStore(byte[] destKey, byte[]... sets) { @Override public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -3298,7 +3299,7 @@ public Long pfCount(byte[]... keys) { @Override public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { - byte[][] allKeys = Converters.mergeArrays(destinationKey, sourceKeys); + byte[][] allKeys = ByteUtils.mergeArrays(destinationKey, sourceKeys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -3592,6 +3593,7 @@ public List doInCluster(Jedis client) { * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetMasterSlaveMap() */ + @Override public Map> clusterGetMasterSlaveMap() { List>> nodeResults = clusterCommandExecutor @@ -3753,7 +3755,7 @@ public RedisSentinelConnection getSentinelConnection() { /** * {@link Jedis} specific {@link ClusterCommandCallback}. - * + * * @author Christoph Strobl * @param * @since 1.7 @@ -3771,7 +3773,7 @@ protected interface JedisMultiKeyClusterCommandCallback extends MultiKeyClust /** * Jedis specific implementation of {@link ClusterNodeResourceProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ @@ -3781,7 +3783,7 @@ static class JedisClusterNodeResourceProvider implements ClusterNodeResourceProv /** * Creates new {@link JedisClusterNodeResourceProvider}. - * + * * @param cluster must not be {@literal null}. */ public JedisClusterNodeResourceProvider(JedisCluster cluster) { @@ -3829,20 +3831,20 @@ public void returnResourceForSpecificNode(RedisClusterNode node, Object client) /** * Jedis specific implementation of {@link ClusterTopologyProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ static class JedisClusterTopologyProvider implements ClusterTopologyProvider { - private Object lock = new Object(); + private final Object lock = new Object(); private final JedisCluster cluster; private long time = 0; private ClusterTopology cached; /** * Create new {@link JedisClusterTopologyProvider}.s - * + * * @param cluster */ public JedisClusterTopologyProvider(JedisCluster cluster) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java index be02fdc418..c84461524d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java @@ -51,6 +51,7 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.util.ByteUtils; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -102,7 +103,7 @@ public LettuceClusterConnection(RedisClusterClient clusterClient) { /** * Creates new {@link LettuceClusterConnection} using {@link RedisClusterClient} running commands across the cluster * via given {@link ClusterCommandExecutor}. - * + * * @param clusterClient must not be {@literal null}. * @param executor must not be {@literal null}. */ @@ -131,6 +132,7 @@ public Cursor scan(long cursorId, ScanOptions options) { * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#keys(byte[]) */ + @Override public Set keys(final byte[] pattern) { Assert.notNull(pattern, "Pattern must not be null!"); @@ -156,6 +158,7 @@ public List doInCluster(RedisClusterConnection connectio * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#flushAll() */ + @Override public void flushAll() { clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { @@ -1109,7 +1112,7 @@ public Set doInCluster(RedisClusterConnection client, by @Override public Long sInterStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { return super.sInterStore(destKey, keys); @@ -1161,7 +1164,7 @@ public Set doInCluster(RedisClusterConnection client, by @Override public Long sUnionStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { return super.sUnionStore(destKey, keys); @@ -1216,7 +1219,7 @@ public Set doInCluster(RedisClusterConnection client, by @Override public Long sDiffStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { return super.sDiffStore(destKey, keys); @@ -1276,7 +1279,7 @@ public Long pfCount(byte[]... keys) { @Override public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { - byte[][] allKeys = Converters.mergeArrays(destinationKey, sourceKeys); + byte[][] allKeys = ByteUtils.mergeArrays(destinationKey, sourceKeys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1318,7 +1321,7 @@ public void multi() { } /* - * + * * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#getConfig(java.lang.String) */ @@ -1538,7 +1541,7 @@ public Set doInCluster(RedisClusterConnection /** * Lettuce specific implementation of {@link ClusterCommandCallback}. - * + * * @author Christoph Strobl * @param * @since 1.7 @@ -1548,7 +1551,7 @@ protected interface LettuceClusterCommandCallback /** * Lettuce specific implementation of {@link MultiKeyClusterCommandCallback}. - * + * * @author Christoph Strobl * @param * @since 1.7 @@ -1560,7 +1563,7 @@ protected interface LettuceMultiKeyClusterCommandCallback /** * Lettuce specific implementation of {@link ClusterNodeResourceProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ @@ -1605,7 +1608,7 @@ public void returnResourceForSpecificNode(RedisClusterNode node, Object resource /** * Lettuce specific implementation of {@link ClusterTopologyProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 675568dad4..9d57b17c2b 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -150,7 +150,7 @@ public List convert(KeyValue source) { public Map convert(final List source) { if (CollectionUtils.isEmpty(source)) { - Collections.emptyMap(); + return Collections.emptyMap(); } Map target = new LinkedHashMap(); diff --git a/src/main/java/org/springframework/data/redis/core/IndexWriter.java b/src/main/java/org/springframework/data/redis/core/IndexWriter.java new file mode 100644 index 0000000000..b179180fd3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/IndexWriter.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015-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; + +import java.util.Set; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.convert.IndexedData; +import org.springframework.data.redis.core.convert.RedisConverter; +import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue; +import org.springframework.data.redis.util.ByteUtils; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@link IndexWriter} takes care of writing secondary index structures to + * Redis. Depending on the type of {@link IndexedData} it uses eg. Sets with specific names to add actually referenced + * keys to. While doing so {@link IndexWriter} also keeps track of all indexes associated with the root types key, which + * allows to remove the root key from all indexes in case of deletion. + * + * @author Christoph Strobl + * @author Rob Winch + * @since 1.7 + */ +class IndexWriter { + + private final RedisConnection connection; + private final RedisConverter converter; + + /** + * Creates new {@link IndexWriter}. + * + * @param keyspace The key space to write index values to. Must not be {@literal null}. + * @param connection must not be {@literal null}. + * @param converter must not be {@literal null}. + */ + public IndexWriter(RedisConnection connection, RedisConverter converter) { + + Assert.notNull(connection, "RedisConnection cannot be null!"); + Assert.notNull(converter, "RedisConverter cannot be null!"); + + this.connection = connection; + this.converter = converter; + } + + /** + * Updates indexes by first removing key from existing one and then persisting new index data. + * + * @param key must not be {@literal null}. + * @param indexValues can be {@literal null}. + */ + public void updateIndexes(Object key, Iterable indexValues) { + + Assert.notNull(key, "Key must not be null!"); + if (indexValues == null) { + return; + } + + byte[] binKey = toBytes(key); + + removeKeyFromExistingIndexes(binKey, indexValues); + addKeyToIndexes(binKey, indexValues); + } + + /** + * Removes a key from all available indexes. + * + * @param key must not be {@literal null}. + */ + public void removeKeyFromIndexes(String keyspace, Object key) { + + Assert.notNull(key, "Key must not be null!"); + + byte[] binKey = toBytes(key); + byte[] indexHelperKey = ByteUtils.concatAll(toBytes(keyspace + ":"), binKey, toBytes(":idx")); + + for (byte[] indexKey : connection.sMembers(indexHelperKey)) { + connection.sRem(indexKey, binKey); + } + + connection.del(indexHelperKey); + } + + /** + * Removes all indexes. + */ + public void removeAllIndexes(String keyspace) { + + Set potentialIndex = connection.keys(toBytes(keyspace + ":*")); + + if (!potentialIndex.isEmpty()) { + connection.del(potentialIndex.toArray(new byte[potentialIndex.size()][])); + } + } + + private void removeKeyFromExistingIndexes(byte[] key, Iterable indexValues) { + + for (IndexedData indexData : indexValues) { + removeKeyFromExistingIndexes(key, indexData); + } + } + + /** + * Remove given key from all indexes matching {@link IndexedData#getIndexName()}: + * + * @param key + * @param indexedData + */ + protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData) { + + Assert.notNull(indexedData, "IndexedData must not be null!"); + Set existingKeys = connection.keys(toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + + ":*")); + + if (!CollectionUtils.isEmpty(existingKeys)) { + for (byte[] existingKey : existingKeys) { + connection.sRem(existingKey, key); + } + } + } + + private void addKeyToIndexes(byte[] key, Iterable indexValues) { + + for (IndexedData indexData : indexValues) { + addKeyToIndex(key, indexData); + } + } + + /** + * Adds a given key to the index for {@link IndexedData#getIndexName()}. + * + * @param key must not be {@literal null}. + * @param indexedData must not be {@literal null}. + */ + protected void addKeyToIndex(byte[] key, IndexedData indexedData) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(indexedData, "IndexedData must not be null!"); + + if (indexedData instanceof SimpleIndexedPropertyValue) { + + Object value = ((SimpleIndexedPropertyValue) indexedData).getValue(); + + if (value == null) { + return; + } + + byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + ":"); + indexKey = ByteUtils.concat(indexKey, toBytes(value)); + connection.sAdd(indexKey, key); + + // keep track of indexes used for the object + connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey); + } else { + throw new IllegalArgumentException(String.format("Cannot write index data for unknown index type %s", + indexedData.getClass())); + } + } + + private byte[] toBytes(Object source) { + + if (source == null) { + return new byte[] {}; + } + + if (source instanceof byte[]) { + return (byte[]) source; + } + + if (converter.getConversionService().canConvert(source.getClass(), byte[].class)) { + return converter.getConversionService().convert(source, byte[].class); + } + + throw new InvalidDataAccessApiUsageException( + String + .format( + "Cannot convert %s to binary representation for index key generation. Are you missing a Converter? Did you register a non PathBasedRedisIndexDefinition that might apply to a complex type?", + source.getClass())); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/RedisHash.java b/src/main/java/org/springframework/data/redis/core/RedisHash.java new file mode 100644 index 0000000000..2bf460065b --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/RedisHash.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.annotation.Persistent; +import org.springframework.data.keyvalue.annotation.KeySpace; + +/** + * {@link RedisHash} marks Objects as aggregate roots to be stored in a Redis hash. + * + * @author Christoph Strobl + * @since 1.7 + */ +@Persistent +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.TYPE }) +@KeySpace +public @interface RedisHash { + + /** + * The prefix to distinguish between domain types. + * + * @return + * @see KeySpace + */ + @AliasFor(annotation = KeySpace.class, attribute = "value") + String value() default ""; + + /** + * Time before expire in seconds. Superseded by {@link TimeToLive}. + * + * @return positive number when expiration should be applied. + */ + long timeToLive() default -1L; + +} diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java new file mode 100644 index 0000000000..054860c296 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015 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; + +import java.nio.charset.Charset; + +import org.springframework.context.ApplicationEvent; +import org.springframework.data.redis.util.ByteUtils; + +/** + * {@link RedisKeyExpiredEvent} is Redis specific {@link ApplicationEvent} published when a specific key in Redis + * expires. It might but must not hold the expired value itself next to the key. + * + * @author Christoph Strobl + * @since 1.7 + */ +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; + + /** + * Creates new {@link RedisKeyExpiredEvent}. + * + * @param key + */ + public RedisKeyExpiredEvent(byte[] key) { + this(key, null); + } + + /** + * Creates new {@link RedisKeyExpiredEvent} + * + * @param key + * @param value + */ + public RedisKeyExpiredEvent(byte[] key, Object value) { + super(key); + + args = ByteUtils.split(key, ':'); + this.value = value; + } + + /** + * Gets the keyspace in which the expiration occured. + * + * @return {@literal null} if it could not be determined. + */ + public String getKeyspace() { + + if (args.length >= 2) { + return new String(args[0], CHARSET); + } + + return null; + } + + /** + * Get the expired objects id; + * + * @return + */ + public byte[] getId() { + return args.length == 2 ? args[1] : args[0]; + } + + /** + * Get the expired Object + * + * @return {@literal null} if not present. + */ + public Object getValue() { + return value; + } + + /* + * (non-Javadoc) + * @see java.util.EventObject#toString() + */ + @Override + public String toString() { + return "RedisKeyExpiredEvent [keyspace=" + getKeyspace() + ", id=" + getId() + "]"; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java new file mode 100644 index 0000000000..18a2504e60 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -0,0 +1,589 @@ +/* + * Copyright 2015-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; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.dao.DataAccessException; +import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; +import org.springframework.data.keyvalue.core.KeyValueAdapter; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.convert.CustomConversions; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.MappingRedisConverter; +import org.springframework.data.redis.core.convert.PathIndexResolver; +import org.springframework.data.redis.core.convert.RedisConverter; +import org.springframework.data.redis.core.convert.RedisData; +import org.springframework.data.redis.core.convert.ReferenceResolverImpl; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.util.ByteUtils; +import org.springframework.data.util.CloseableIterator; +import org.springframework.util.Assert; + +/** + * Redis specific {@link KeyValueAdapter} implementation. Uses binary codec to read/write data from/to Redis. Objects + * are stored in a Redis Hash using the value of {@link RedisHash}, the {@link KeyspaceConfiguration} or just + * {@link Class#getName()} as a prefix.
+ * Example + * + *
+ * 
+ * @RedisHash("persons")
+ * class Person {
+ *   @Id String id;
+ *   String name;
+ * }
+ * 
+ * 
+ *         prefix              ID
+ *           |                 |
+ *           V                 V
+ * hgetall persons:5d67b7e1-8640-4475-beeb-c666fab4c0e5
+ * 1) id
+ * 2) 5d67b7e1-8640-4475-beeb-c666fab4c0e5
+ * 3) name
+ * 4) Rand al'Thor
+ * 
+ * 
+ * + *
+ * The {@link KeyValueAdapter} is not intended to store simple types such as {@link String} values. + * Please use {@link RedisTemplate} for this purpose. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisKeyValueAdapter extends AbstractKeyValueAdapter + implements ApplicationContextAware, ApplicationListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RedisKeyValueAdapter.class); + + private RedisOperations redisOps; + private RedisConverter converter; + private RedisMessageListenerContainer messageListenerContainer; + private KeyExpirationEventMessageListener expirationListener; + + /** + * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default + * {@link CustomConversions}. + * + * @param redisOps must not be {@literal null}. + */ + public RedisKeyValueAdapter(RedisOperations redisOps) { + this(redisOps, new RedisMappingContext()); + } + + /** + * Creates new {@link RedisKeyValueAdapter} with default {@link CustomConversions}. + * + * @param redisOps must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public RedisKeyValueAdapter(RedisOperations redisOps, RedisMappingContext mappingContext) { + this(redisOps, mappingContext, new CustomConversions()); + } + + /** + * Creates new {@link RedisKeyValueAdapter}. + * + * @param redisOps must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @param customConversions can be {@literal null}. + */ + public RedisKeyValueAdapter(RedisOperations redisOps, RedisMappingContext mappingContext, + CustomConversions customConversions) { + + super(new RedisQueryEngine()); + + Assert.notNull(redisOps, "RedisOperations must not be null!"); + Assert.notNull(mappingContext, "RedisMappingContext must not be null!"); + + MappingRedisConverter mappingConverter = new MappingRedisConverter(mappingContext, + new PathIndexResolver(mappingContext), new ReferenceResolverImpl(redisOps)); + mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions); + mappingConverter.afterPropertiesSet(); + + converter = mappingConverter; + this.redisOps = redisOps; + + initKeyExpirationListener(); + } + + /** + * Creates new {@link RedisKeyValueAdapter} with specific {@link RedisConverter}. + * + * @param redisOps must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public RedisKeyValueAdapter(RedisOperations redisOps, RedisConverter redisConverter) { + + super(new RedisQueryEngine()); + + Assert.notNull(redisOps, "RedisOperations must not be null!"); + + converter = redisConverter; + this.redisOps = redisOps; + + initKeyExpirationListener(); + } + + /** + * Default constructor. + */ + protected RedisKeyValueAdapter() { + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#put(java.io.Serializable, java.lang.Object, java.io.Serializable) + */ + public Object put(final Serializable id, final Object item, final Serializable keyspace) { + + final RedisData rdo = item instanceof RedisData ? (RedisData) item : new RedisData(); + if (!(item instanceof RedisData)) { + converter.write(item, rdo); + } + + if (rdo.getId() == null) { + + rdo.setId(converter.getConversionService().convert(id, String.class)); + + if (!(item instanceof RedisData)) { + KeyValuePersistentProperty idProperty = converter.getMappingContext().getPersistentEntity(item.getClass()) + .getIdProperty(); + converter.getMappingContext().getPersistentEntity(item.getClass()).getPropertyAccessor(item) + .setProperty(idProperty, id); + } + } + + redisOps.execute(new RedisCallback() { + + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + + byte[] key = toBytes(rdo.getId()); + byte[] objectKey = createKey(rdo.getKeyspace(), rdo.getId()); + + connection.del(objectKey); + + connection.hMSet(objectKey, rdo.getBucket().rawMap()); + + if (rdo.getTimeToLive() != null && rdo.getTimeToLive().longValue() > 0) { + + connection.expire(objectKey, rdo.getTimeToLive().longValue()); + + // add phantom key so values can be restored + byte[] phantomKey = ByteUtils.concat(objectKey, toBytes(":phantom")); + connection.del(phantomKey); + connection.hMSet(phantomKey, rdo.getBucket().rawMap()); + connection.expire(phantomKey, rdo.getTimeToLive().longValue() + 300); + } + + connection.sAdd(toBytes(rdo.getKeyspace()), key); + + new IndexWriter(connection, converter).updateIndexes(key, rdo.getIndexedData()); + return null; + } + }); + + return item; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#contains(java.io.Serializable, java.io.Serializable) + */ + public boolean contains(final Serializable id, final Serializable keyspace) { + + Boolean exists = redisOps.execute(new RedisCallback() { + + @Override + public Boolean doInRedis(RedisConnection connection) throws DataAccessException { + return connection.sIsMember(toBytes(keyspace), toBytes(id)); + } + }); + + return exists != null ? exists.booleanValue() : false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#get(java.io.Serializable, java.io.Serializable) + */ + public Object get(Serializable id, Serializable keyspace) { + return get(id, keyspace, Object.class); + } + + /** + * @param id + * @param keyspace + * @param type + * @return + */ + public T get(Serializable id, Serializable keyspace, Class type) { + + String stringId = asString(id); + String stringKeyspace = asString(keyspace); + + final byte[] binId = createKey(stringKeyspace, stringId); + + Map raw = redisOps.execute(new RedisCallback>() { + + @Override + public Map doInRedis(RedisConnection connection) throws DataAccessException { + return connection.hGetAll(binId); + } + }); + + RedisData data = new RedisData(raw); + data.setId(stringId); + data.setKeyspace(stringKeyspace); + + return converter.read(type, data); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#delete(java.io.Serializable, java.io.Serializable) + */ + public Object delete(final Serializable id, final Serializable keyspace) { + return delete(id, keyspace, Object.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.AbstractKeyValueAdapter#delete(java.io.Serializable, java.io.Serializable, java.lang.Class) + */ + public T delete(final Serializable id, final Serializable keyspace, final Class type) { + + final byte[] binId = toBytes(id); + final byte[] binKeyspace = toBytes(keyspace); + + T o = get(id, keyspace, type); + + if (o != null) { + + final byte[] keyToDelete = createKey(asString(keyspace), asString(id)); + + redisOps.execute(new RedisCallback() { + + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + + connection.del(keyToDelete); + connection.sRem(binKeyspace, binId); + + new IndexWriter(connection, converter).removeKeyFromIndexes(asString(keyspace), binId); + return null; + } + }); + + } + return o; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#getAllOf(java.io.Serializable) + */ + public List getAllOf(final Serializable keyspace) { + + final byte[] binKeyspace = toBytes(keyspace); + + List> raw = redisOps.execute(new RedisCallback>>() { + + @Override + public List> doInRedis(RedisConnection connection) throws DataAccessException { + + final List> rawData = new ArrayList>(); + + Set members = connection.sMembers(binKeyspace); + + for (byte[] id : members) { + rawData.add(connection + .hGetAll(createKey(asString(keyspace), getConverter().getConversionService().convert(id, String.class)))); + } + + return rawData; + } + }); + + List result = new ArrayList(raw.size()); + for (Map rawData : raw) { + result.add(converter.read(Object.class, new RedisData(rawData))); + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#deleteAllOf(java.io.Serializable) + */ + public void deleteAllOf(final Serializable keyspace) { + + redisOps.execute(new RedisCallback() { + + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + + connection.del(toBytes(keyspace)); + new IndexWriter(connection, converter).removeAllIndexes(asString(keyspace)); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#entries(java.io.Serializable) + */ + public CloseableIterator> entries(Serializable keyspace) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueAdapter#count(java.io.Serializable) + */ + public long count(final Serializable keyspace) { + + Long count = redisOps.execute(new RedisCallback() { + + @Override + public Long doInRedis(RedisConnection connection) throws DataAccessException { + return connection.sCard(toBytes(keyspace)); + } + }); + + return count != null ? count.longValue() : 0; + } + + /** + * Execute {@link RedisCallback} via underlying {@link RedisOperations}. + * + * @param callback must not be {@literal null}. + * @see RedisOperations#execute(RedisCallback) + * @return + */ + public T execute(RedisCallback callback) { + return redisOps.execute(callback); + } + + /** + * Get the {@link RedisConverter} in use. + * + * @return never {@literal null}. + */ + public RedisConverter getConverter() { + return this.converter; + } + + public void clear() { + // nothing to do + } + + private String asString(Serializable value) { + return value instanceof String ? (String) value + : getConverter().getConversionService().convert(value, String.class); + } + + public byte[] createKey(String keyspace, String id) { + return toBytes(keyspace + ":" + id); + } + + /** + * Convert given source to binary representation using the underlying {@link ConversionService}. + * + * @param source + * @return + * @throws ConverterNotFoundException + */ + public byte[] toBytes(Object source) { + + if (source instanceof byte[]) { + return (byte[]) source; + } + + return converter.getConversionService().convert(source, byte[].class); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + public void destroy() throws Exception { + + if (redisOps instanceof RedisTemplate) { + RedisConnectionFactory connectionFactory = ((RedisTemplate) redisOps).getConnectionFactory(); + if (connectionFactory instanceof DisposableBean) { + ((DisposableBean) connectionFactory).destroy(); + } + } + + this.expirationListener.destroy(); + this.messageListenerContainer.destroy(); + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + @Override + public void onApplicationEvent(RedisKeyspaceEvent event) { + + LOGGER.debug("Received %s .", event); + + if (event instanceof RedisKeyExpiredEvent) { + + final RedisKeyExpiredEvent expiredEvent = (RedisKeyExpiredEvent) event; + + redisOps.execute(new RedisCallback() { + + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + + LOGGER.debug("Cleaning up expired key '%s' data structures in keyspace '%s'.", expiredEvent.getSource(), + expiredEvent.getKeyspace()); + + connection.sRem(toBytes(expiredEvent.getKeyspace()), expiredEvent.getId()); + new IndexWriter(connection, converter).removeKeyFromIndexes(expiredEvent.getKeyspace(), expiredEvent.getId()); + return null; + } + }); + + } + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.expirationListener.setApplicationEventPublisher(applicationContext); + } + + private void initKeyExpirationListener() { + + this.messageListenerContainer = new RedisMessageListenerContainer(); + messageListenerContainer.setConnectionFactory(((RedisTemplate) redisOps).getConnectionFactory()); + messageListenerContainer.afterPropertiesSet(); + messageListenerContainer.start(); + + this.expirationListener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps, + this.converter); + this.expirationListener.init(); + } + + /** + * {@link MessageListener} implementation used to capture Redis keypspace notifications. Tries to read a previously + * created phantom key {@code keyspace:id:phantom} to provide the expired object as part of the published + * {@link RedisKeyExpiredEvent}. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class MappingExpirationListener extends KeyExpirationEventMessageListener { + + private final RedisOperations ops; + private final RedisConverter converter; + + /** + * Creates new {@link MappingExpirationListener}. + * + * @param listenerContainer + * @param ops + * @param converter + */ + public MappingExpirationListener(RedisMessageListenerContainer listenerContainer, RedisOperations ops, + RedisConverter converter) { + + super(listenerContainer); + this.ops = ops; + this.converter = converter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#onMessage(org.springframework.data.redis.connection.Message, byte[]) + */ + @Override + public void onMessage(Message message, byte[] pattern) { + + if (!isKeyExpirationMessage(message)) { + return; + } + + byte[] key = message.getBody(); + + final byte[] phantomKey = ByteUtils.concat(key, + converter.getConversionService().convert(":phantom", byte[].class)); + + Map hash = ops.execute(new RedisCallback>() { + + @Override + public Map doInRedis(RedisConnection connection) throws DataAccessException { + + Map hash = connection.hGetAll(phantomKey); + + if (!org.springframework.util.CollectionUtils.isEmpty(hash)) { + connection.del(phantomKey); + } + return hash; + } + }); + + Object value = converter.read(Object.class, new RedisData(hash)); + publishEvent(new RedisKeyExpiredEvent(key, value)); + } + + private boolean isKeyExpirationMessage(Message message) { + + if (message == null || message.getChannel() == null || message.getBody() == null) { + return false; + } + + byte[][] args = ByteUtils.split(message.getBody(), ':'); + if (args.length != 2) { + return false; + } + + return true; + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java new file mode 100644 index 0000000000..2f1d1f057c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java @@ -0,0 +1,134 @@ +/* + * Copyright 2015 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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.keyvalue.core.KeyValueAdapter; +import org.springframework.data.keyvalue.core.KeyValueCallback; +import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Redis specific implementation of {@link KeyValueTemplate}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisKeyValueTemplate extends KeyValueTemplate { + + /** + * Create new {@link RedisKeyValueTemplate}. + * + * @param adapter must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public RedisKeyValueTemplate(RedisKeyValueAdapter adapter, RedisMappingContext mappingContext) { + super(adapter, mappingContext); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeyValueTemplate#getMappingContext() + */ + @Override + public RedisMappingContext getMappingContext() { + return (RedisMappingContext) super.getMappingContext(); + } + + /** + * Retrieve entities by resolving their {@literal id}s and converting them into required type.
+ * The callback provides either a single {@literal id} or an {@link Iterable} of {@literal id}s, used for retrieving + * the actual domain types and shortcuts manual retrieval and conversion of {@literal id}s via {@link RedisTemplate}. + * + *
+	 * 
+	 * List<RedisSession> sessions = template.find(new RedisCallback<Set<byte[]>>() {
+	 *   public Set<byte[]< doInRedis(RedisConnection connection) throws DataAccessException {
+	 *     return connection
+	 *       .sMembers("spring:session:sessions:securityContext.authentication.principal.username:user"
+	 *         .getBytes());
+	 *   }
+	 * }, RedisSession.class);
+	 * 
+	 * 
+	 * 
+	 * @param callback provides the to retrieve entity ids. Must not be {@literal null}.
+	 * @param type must not be {@literal null}.
+	 * @return empty list if not elements found.
+	 */
+	public  List find(final RedisCallback callback, final Class type) {
+
+		Assert.notNull(callback, "Callback must not be null.");
+
+		return execute(new RedisKeyValueCallback>() {
+
+			@Override
+			public List doInRedis(RedisKeyValueAdapter adapter) {
+
+				Object callbackResult = adapter.execute(callback);
+
+				if (callbackResult == null) {
+					return Collections.emptyList();
+				}
+
+				Iterable ids = ClassUtils.isAssignable(Iterable.class, callbackResult.getClass()) ? (Iterable) callbackResult
+						: Collections.singleton(callbackResult);
+
+				List result = new ArrayList();
+				for (Object id : ids) {
+
+					String idToUse = adapter.getConverter().getConversionService().canConvert(id.getClass(), String.class) ? adapter
+							.getConverter().getConversionService().convert(id, String.class)
+							: id.toString();
+
+					T candidate = findById(idToUse, type);
+					if (candidate != null) {
+						result.add(candidate);
+					}
+				}
+
+				return result;
+			}
+		});
+	}
+
+	/**
+	 * Redis specific {@link KeyValueCallback}.
+	 * 
+	 * @author Christoph Strobl
+	 * @param 
+	 * @since 1.7
+	 */
+	public static abstract class RedisKeyValueCallback implements KeyValueCallback {
+
+		/*
+		 * (non-Javadoc)
+		 * @see org.springframework.data.keyvalue.core.KeyValueCallback#doInKeyValue(org.springframework.data.keyvalue.core.KeyValueAdapter)
+		 */
+		@Override
+		public T doInKeyValue(KeyValueAdapter adapter) {
+			return doInRedis((RedisKeyValueAdapter) adapter);
+		}
+
+		public abstract T doInRedis(RedisKeyValueAdapter adapter);
+	}
+
+}
diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyspaceEvent.java b/src/main/java/org/springframework/data/redis/core/RedisKeyspaceEvent.java
new file mode 100644
index 0000000000..503825903a
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/core/RedisKeyspaceEvent.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 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;
+
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * Redis specific {@link ApplicationEvent} published when a key expires in Redis.
+ * 
+ * @author Christoph Strobl
+ * @since 1.7
+ */
+public class RedisKeyspaceEvent extends ApplicationEvent {
+
+	/**
+	 * Creates new {@link RedisKeyspaceEvent}.
+	 * 
+	 * @param key The key that expired. Must not be {@literal null}.
+	 */
+	public RedisKeyspaceEvent(byte[] key) {
+		super(key);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see java.util.EventObject#getSource()
+	 */
+	public byte[] getSource() {
+		return (byte[]) super.getSource();
+	}
+
+}
diff --git a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java
new file mode 100644
index 0000000000..1b322e718f
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2015 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;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.keyvalue.core.CriteriaAccessor;
+import org.springframework.data.keyvalue.core.QueryEngine;
+import org.springframework.data.keyvalue.core.SortAccessor;
+import org.springframework.data.keyvalue.core.query.KeyValueQuery;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.core.convert.RedisData;
+import org.springframework.data.redis.repository.query.RedisOperationChain;
+import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue;
+import org.springframework.data.redis.util.ByteUtils;
+
+/**
+ * Redis specific {@link QueryEngine} implementation.
+ * 
+ * @author Christoph Strobl
+ * @since 1.7
+ */
+class RedisQueryEngine extends QueryEngine> {
+
+	/**
+	 * Creates new {@link RedisQueryEngine} with defaults.
+	 */
+	public RedisQueryEngine() {
+		this(new RedisCriteriaAccessor(), null);
+	}
+
+	/**
+	 * Creates new {@link RedisQueryEngine}.
+	 * 
+	 * @param criteriaAccessor
+	 * @param sortAccessor
+	 * @see QueryEngine#QueryEngine(CriteriaAccessor, SortAccessor)
+	 */
+	public RedisQueryEngine(CriteriaAccessor criteriaAccessor,
+			SortAccessor> sortAccessor) {
+		super(criteriaAccessor, sortAccessor);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see org.springframework.data.keyvalue.core.QueryEngine#execute(java.lang.Object, java.lang.Object, int, int, java.io.Serializable, java.lang.Class)
+	 */
+	@Override
+	public  Collection execute(final RedisOperationChain criteria, final Comparator sort, final int offset,
+			final int rows, final Serializable keyspace, Class type) {
+
+		RedisCallback>> callback = new RedisCallback>>() {
+
+			@Override
+			public Map> doInRedis(RedisConnection connection) throws DataAccessException {
+
+				String key = keyspace + ":";
+				byte[][] keys = new byte[criteria.getSismember().size()][];
+				int i = 0;
+				for (Object o : criteria.getSismember()) {
+					keys[i] = getAdapter().getConverter().getConversionService().convert(key + o, byte[].class);
+					i++;
+				}
+
+				List allKeys = new ArrayList();
+				if (!criteria.getSismember().isEmpty()) {
+					allKeys.addAll(connection.sInter(keys(keyspace + ":", criteria.getSismember())));
+				}
+				if (!criteria.getOrSismember().isEmpty()) {
+					allKeys.addAll(connection.sUnion(keys(keyspace + ":", criteria.getOrSismember())));
+				}
+
+				byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class);
+
+				final Map> rawData = new LinkedHashMap>();
+
+				if (allKeys.size() == 0 || allKeys.size() < offset) {
+					return Collections.emptyMap();
+				}
+
+				if (offset >= 0 && rows > 0) {
+					allKeys = allKeys.subList(Math.max(0, offset), Math.min(offset + rows, allKeys.size()));
+				}
+				for (byte[] id : allKeys) {
+
+					byte[] singleKey = ByteUtils.concat(keyspaceBin, id);
+					rawData.put(id, connection.hGetAll(singleKey));
+				}
+
+				return rawData;
+
+			}
+		};
+
+		Map> raw = this.getAdapter().execute(callback);
+
+		List result = new ArrayList(raw.size());
+		for (Map.Entry> entry : raw.entrySet()) {
+
+			RedisData data = new RedisData(entry.getValue());
+			data.setId(getAdapter().getConverter().getConversionService().convert(entry.getKey(), String.class));
+			data.setKeyspace(keyspace.toString());
+
+			T converted = this.getAdapter().getConverter().read(type, data);
+
+			if (converted != null) {
+				result.add(converted);
+			}
+		}
+		return result;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see org.springframework.data.keyvalue.core.QueryEngine#execute(java.lang.Object, java.lang.Object, int, int, java.io.Serializable)
+	 */
+	@Override
+	public Collection execute(final RedisOperationChain criteria, Comparator sort, int offset, int rows,
+			final Serializable keyspace) {
+		return execute(criteria, sort, offset, rows, keyspace, Object.class);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see org.springframework.data.keyvalue.core.QueryEngine#count(java.lang.Object, java.io.Serializable)
+	 */
+	@Override
+	public long count(final RedisOperationChain criteria, final Serializable keyspace) {
+
+		return this.getAdapter().execute(new RedisCallback() {
+
+			@Override
+			public Long doInRedis(RedisConnection connection) throws DataAccessException {
+
+				String key = keyspace + ":";
+				byte[][] keys = new byte[criteria.getSismember().size()][];
+				int i = 0;
+				for (Object o : criteria.getSismember()) {
+					keys[i] = getAdapter().getConverter().getConversionService().convert(key + o, byte[].class);
+				}
+
+				return (long) connection.sInter(keys).size();
+			}
+		});
+	}
+
+	private byte[][] keys(String prefix, Collection source) {
+
+		byte[][] keys = new byte[source.size()][];
+		int i = 0;
+		for (PathAndValue pathAndValue : source) {
+
+			byte[] convertedValue = getAdapter().getConverter().getConversionService()
+					.convert(pathAndValue.getFirstValue(), byte[].class);
+			byte[] fullPath = getAdapter().getConverter().getConversionService()
+					.convert(prefix + pathAndValue.getPath() + ":", byte[].class);
+
+			keys[i] = ByteUtils.concat(fullPath, convertedValue);
+			i++;
+		}
+		return keys;
+	}
+
+	/**
+	 * @author Christoph Strobl
+	 * @since 1.7
+	 */
+	static class RedisCriteriaAccessor implements CriteriaAccessor {
+
+		@Override
+		public RedisOperationChain resolve(KeyValueQuery query) {
+			return (RedisOperationChain) query.getCritieria();
+		}
+	}
+
+}
diff --git a/src/main/java/org/springframework/data/redis/core/TimeToLive.java b/src/main/java/org/springframework/data/redis/core/TimeToLive.java
new file mode 100644
index 0000000000..d8cc8f0e89
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/core/TimeToLive.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2015 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;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.annotation.ReadOnlyProperty;
+
+/**
+ * {@link TimeToLive} marks a single numeric property on aggregate root to be used for setting expirations in Redis. The
+ * annotated property supersedes any other timeout configuration.
+ * 
+ * 
+ * 
+ * @RedisHash
+ * class Person {
+ *   @Id String id;
+ *   String name;
+ *   @TimeToLive Long timeout;
+ * }
+ * 
+ * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Target(value = { ElementType.FIELD, ElementType.METHOD }) +@ReadOnlyProperty +public @interface TimeToLive { + + /** + * {@link TimeUnit} unit to use. + * + * @return {@link TimeUnit#SECONDS} by default. + */ + TimeUnit unit() default TimeUnit.SECONDS; +} diff --git a/src/main/java/org/springframework/data/redis/core/TimeToLiveAccessor.java b/src/main/java/org/springframework/data/redis/core/TimeToLiveAccessor.java new file mode 100644 index 0000000000..b8ea4091e3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/TimeToLiveAccessor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 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; + +/** + * {@link TimeToLiveAccessor} extracts the objects time to live used for {@code EXPIRE}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface TimeToLiveAccessor { + + /** + * @param source must not be {@literal null}. + * @return {@literal null} if not configured. + */ + Long getTimeToLive(Object source); +} 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 new file mode 100644 index 0000000000..6ab15f28ce --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java @@ -0,0 +1,295 @@ +/* + * Copyright 2015 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.nio.charset.Charset; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.util.NumberUtils; + +/** + * Set of {@link ReadingConverter} and {@link WritingConverter} used to convert Objects into binary format. + * + * @author Christoph Strobl + * @since 1.7 + */ +final class BinaryConverters { + + /** + * Use {@literal UTF-8} as default charset. + */ + public static final Charset CHARSET = Charset.forName("UTF-8"); + + private BinaryConverters() {} + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static class StringBasedConverter { + + byte[] fromString(String source) { + + if (source == null) { + return new byte[] {}; + } + + return source.getBytes(CHARSET); + } + + String toString(byte[] source) { + return new String(source, CHARSET); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @WritingConverter + static class StringToBytesConverter extends StringBasedConverter implements Converter { + + @Override + public byte[] convert(String source) { + return fromString(source); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @ReadingConverter + static class BytesToStringConverter extends StringBasedConverter implements Converter { + + @Override + public String convert(byte[] source) { + return toString(source); + } + + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @WritingConverter + static class NumberToBytesConverter extends StringBasedConverter implements Converter { + + @Override + public byte[] convert(Number source) { + + if (source == null) { + return new byte[] {}; + } + + return fromString(source.toString()); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @WritingConverter + static class EnumToBytesConverter extends StringBasedConverter implements Converter, byte[]> { + + @Override + public byte[] convert(Enum source) { + + if (source == null) { + return new byte[] {}; + } + + return fromString(source.toString()); + } + + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @ReadingConverter + static final class BytesToEnumConverterFactory implements ConverterFactory> { + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public > Converter getConverter(Class targetType) { + + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + if (enumType == null) { + throw new IllegalArgumentException("The target type " + targetType.getName() + " does not refer to an enum"); + } + return new BytesToEnum(enumType); + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + private class BytesToEnum> extends StringBasedConverter implements Converter { + + private final Class enumType; + + public BytesToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(byte[] source) { + + String value = toString(source); + + if (value == null || value.length() == 0) { + return null; + } + return Enum.valueOf(this.enumType, value.trim()); + } + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @ReadingConverter + static class BytesToNumberConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new BytesToNumberConverter(targetType); + } + + private static final class BytesToNumberConverter extends StringBasedConverter + implements Converter { + + private final Class targetType; + + public BytesToNumberConverter(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(byte[] source) { + + if (source == null || source.length == 0) { + return null; + } + + return NumberUtils.parseNumber(toString(source), targetType); + } + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @WritingConverter + static class BooleanToBytesConverter extends StringBasedConverter implements Converter { + + final byte[] _true = fromString("1"); + final byte[] _false = fromString("0"); + + @Override + public byte[] convert(Boolean source) { + + if (source == null) { + return new byte[] {}; + } + + return source.booleanValue() ? _true : _false; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @ReadingConverter + static class BytesToBooleanConverter extends StringBasedConverter implements Converter { + + @Override + public Boolean convert(byte[] source) { + + if (source == null || source.length == 0) { + return null; + } + + String value = toString(source); + return ("1".equals(value) || "true".equalsIgnoreCase(value)) ? Boolean.TRUE : Boolean.FALSE; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @WritingConverter + static class DateToBytesConverter extends StringBasedConverter implements Converter { + + @Override + public byte[] convert(Date source) { + + if (source == null) { + return new byte[] {}; + } + + return fromString(Long.toString(source.getTime())); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @ReadingConverter + static class BytesToDateConverter extends StringBasedConverter implements Converter { + + @Override + public Date convert(byte[] source) { + + if (source == null || source.length == 0) { + return null; + } + + String value = toString(source); + try { + return new Date(NumberUtils.parseNumber(value, Long.class)); + } catch (NumberFormatException nfe) { + // ignore + } + + try { + return DateFormat.getInstance().parse(value); + } catch (ParseException e) { + // ignore + } + + throw new IllegalArgumentException("Cannot parse date out of " + source); + } + } +} 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 new file mode 100644 index 0000000000..7a928fdd19 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/Bucket.java @@ -0,0 +1,263 @@ +/* + * Copyright 2015 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.nio.charset.Charset; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Bucket is the data bag for Redis hash structures to be used with {@link RedisData}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class Bucket { + + /** + * Encoding used for converting {@link Byte} to and from {@link String}. + */ + public static final Charset CHARSET = Charset.forName("UTF-8"); + + private final Map data; + + /** + * Creates new empty bucket + */ + public Bucket() { + data = new LinkedHashMap(); + } + + Bucket(Map data) { + + Assert.notNull(data, "Inital data must not be null!"); + this.data = new LinkedHashMap(data.size()); + this.data.putAll(data); + } + + /** + * Add {@link String} representation of property dot path with given value. + * + * @param path must not be {@literal null} or {@link String#isEmpty()}. + * @param value can be {@literal null}. + */ + public void put(String path, byte[] value) { + + Assert.hasText(path, "Path to property must not be null or empty."); + data.put(path, value); + } + + /** + * Get value assigned with path. + * + * @param path path must not be {@literal null} or {@link String#isEmpty()}. + * @return {@literal null} if not set. + */ + public byte[] get(String path) { + + Assert.hasText(path, "Path to property must not be null or empty."); + return data.get(path); + } + + /** + * A set view of the mappings contained in this bucket. + * + * @return never {@literal null}. + */ + public Set> entrySet() { + return data.entrySet(); + } + + /** + * @return {@literal true} when no data present in {@link Bucket}. + */ + public boolean isEmpty() { + return data.isEmpty(); + } + + /** + * @return the number of key-value mappings of the {@link Bucket}. + */ + public int size() { + return data.size(); + } + + /** + * @return never {@literal null}. + */ + public Collection values() { + return data.values(); + } + + /** + * @return never {@literal null}. + */ + public Set keySet() { + return data.keySet(); + } + + /** + * Key/value pairs contained in the {@link Bucket}. + * + * @return never {@literal null}. + */ + public Map asMap() { + 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(); + for (Map.Entry entry : data.entrySet()) { + if (entry.getKey().startsWith(prefix)) { + partial.put(entry.getKey(), entry.getValue()); + } + } + + return partial; + } + + /** + * Get all the keys matching a given path. + * + * @param path the path to look for. Can be {@literal null}. + * @return all keys if path is {@null} or empty. + */ + public Set extractAllKeysFor(String path) { + + if (!StringUtils.hasText(path)) { + return keySet(); + } + + Pattern pattern = Pattern.compile("(" + Pattern.quote(path) + ")\\.\\[.*?\\]"); + + Set keys = new LinkedHashSet(); + for (Map.Entry entry : data.entrySet()) { + + Matcher matcher = pattern.matcher(entry.getKey()); + if (matcher.find()) { + keys.add(matcher.group()); + } + } + + return keys; + } + + /** + * Get keys and values in binary format. + * + * @return never {@literal null}. + */ + public Map rawMap() { + + Map raw = new LinkedHashMap(data.size()); + for (Map.Entry entry : data.entrySet()) { + if (entry.getValue() != null) { + raw.put(entry.getKey().getBytes(CHARSET), entry.getValue()); + } + } + return raw; + } + + /** + * Creates a new Bucket from a given raw map. + * + * @param source can be {@literal null}. + * @return never {@literal null}. + */ + public static Bucket newBucketFromRawMap(Map source) { + + Bucket bucket = new Bucket(); + if (source == null) { + return bucket; + } + + for (Map.Entry entry : source.entrySet()) { + bucket.put(new String(entry.getKey(), CHARSET), entry.getValue()); + } + return bucket; + } + + /** + * Creates a new Bucket from a given {@link String} map. + * + * @param source can be {@literal null}. + * @return never {@literal null}. + */ + public static Bucket newBucketFromStringMap(Map source) { + + Bucket bucket = new Bucket(); + if (source == null) { + return bucket; + } + + for (Map.Entry entry : source.entrySet()) { + bucket.put(entry.getKey(), StringUtils.hasText(entry.getValue()) ? entry.getValue().getBytes(CHARSET) + : new byte[] {}); + } + return bucket; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Bucket [data=" + safeToString() + "]"; + } + + private String safeToString() { + + Map serialized = new LinkedHashMap(); + for (Map.Entry entry : data.entrySet()) { + if (entry.getValue() != null) { + serialized.put(entry.getKey(), toUtf8String(entry.getValue())); + } else { + serialized.put(entry.getKey(), null); + } + } + return serialized.toString(); + + } + + private String toUtf8String(byte[] raw) { + + try { + return new String(raw, CHARSET); + } catch (Exception e) { + // Ignore this one + } + return null; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java new file mode 100644 index 0000000000..2ebe21b3f7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java @@ -0,0 +1,75 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Composite {@link IndexResolver} implementation that iterates over a given collection of delegate + * {@link IndexResolver} instances.
+ *
+ * NOTE {@link IndexedData} created by an {@link IndexResolver} can be overwritten by subsequent + * {@link IndexResolver}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class CompositeIndexResolver implements IndexResolver { + + private final List resolvers; + + /** + * Create new {@link CompositeIndexResolver}. + * + * @param resolvers must not be {@literal null}. + */ + public CompositeIndexResolver(Collection resolvers) { + + Assert.notNull(resolvers, "Resolvers must not be null!"); + if (CollectionUtils.contains(resolvers.iterator(), null)) { + throw new IllegalArgumentException("Resolvers must no contain null values"); + } + this.resolvers = new ArrayList(resolvers); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(org.springframework.data.util.TypeInformation, java.lang.Object) + */ + @Override + public Set resolveIndexesFor(TypeInformation typeInformation, Object value) { + + if (resolvers.isEmpty()) { + return Collections.emptySet(); + } + + Set data = new LinkedHashSet(); + for (IndexResolver resolver : resolvers) { + data.addAll(resolver.resolveIndexesFor(typeInformation, value)); + } + return data; + } + +} 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 new file mode 100644 index 0000000000..d485ee6d96 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java @@ -0,0 +1,488 @@ +/* + * Copyright 2011-2015 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.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.CacheValue; +import org.springframework.util.Assert; + +/** + * Value object to capture custom conversion. That is essentially a {@link List} of converters and some additional logic + * around them. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Christoph Strobl + * @since 1.7 + */ +public class CustomConversions { + + private static final Logger LOG = LoggerFactory.getLogger(CustomConversions.class); + private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a Redis supported type! You might wanna check you annotation setup at the converter implementation."; + private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a Redis supported type! You might wanna check you annotation setup at the converter implementation."; + + private final Set readingPairs; + private final Set writingPairs; + private final Set> customSimpleTypes; + private final SimpleTypeHolder simpleTypeHolder; + + private final List converters; + + private final Map>> customReadTargetTypes; + private final Map>> customWriteTargetTypes; + private final Map, CacheValue>> rawWriteTargetTypes; + + /** + * Creates an empty {@link CustomConversions} object. + */ + public CustomConversions() { + this(new ArrayList()); + } + + /** + * Creates a new {@link CustomConversions} instance registering the given converters. + * + * @param converters + */ + public CustomConversions(List converters) { + + Assert.notNull(converters); + + this.readingPairs = new LinkedHashSet(); + this.writingPairs = new LinkedHashSet(); + this.customSimpleTypes = new HashSet>(); + this.customReadTargetTypes = new ConcurrentHashMap>>(); + this.customWriteTargetTypes = new ConcurrentHashMap>>(); + this.rawWriteTargetTypes = new ConcurrentHashMap, CacheValue>>(); + + List toRegister = new ArrayList(); + + // Add user provided converters to make sure they can override the defaults + toRegister.addAll(converters); + toRegister.add(new BinaryConverters.StringToBytesConverter()); + toRegister.add(new BinaryConverters.BytesToStringConverter()); + toRegister.add(new BinaryConverters.NumberToBytesConverter()); + toRegister.add(new BinaryConverters.BytesToNumberConverterFactory()); + toRegister.add(new BinaryConverters.EnumToBytesConverter()); + toRegister.add(new BinaryConverters.BytesToEnumConverterFactory()); + toRegister.add(new BinaryConverters.BooleanToBytesConverter()); + toRegister.add(new BinaryConverters.BytesToBooleanConverter()); + toRegister.add(new BinaryConverters.DateToBytesConverter()); + toRegister.add(new BinaryConverters.BytesToDateConverter()); + + toRegister.addAll(Jsr310Converters.getConvertersToRegister()); + + for (Object c : toRegister) { + registerConversion(c); + } + + Collections.reverse(toRegister); + + this.converters = Collections.unmodifiableList(toRegister); + this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, true); + } + + /** + * Returns the underlying {@link SimpleTypeHolder}. + * + * @return + */ + public SimpleTypeHolder getSimpleTypeHolder() { + return simpleTypeHolder; + } + + /** + * Returns whether the given type is considered to be simple. That means it's either a general simple type or we have + * a writing {@link Converter} registered for a particular type. + * + * @see SimpleTypeHolder#isSimpleType(Class) + * @param type + * @return + */ + public boolean isSimpleType(Class type) { + return simpleTypeHolder.isSimpleType(type); + } + + /** + * Populates the given {@link GenericConversionService} with the convertes registered. + * + * @param conversionService + */ + public void registerConvertersIn(GenericConversionService conversionService) { + + for (Object converter : converters) { + + boolean added = false; + + if (converter instanceof Converter) { + conversionService.addConverter((Converter) converter); + added = true; + } + + if (converter instanceof ConverterFactory) { + conversionService.addConverterFactory((ConverterFactory) converter); + added = true; + } + + if (converter instanceof GenericConverter) { + conversionService.addConverter((GenericConverter) converter); + added = true; + } + + if (!added) { + throw new IllegalArgumentException( + "Given set contains element that is neither Converter nor ConverterFactory!"); + } + } + } + + /** + * Registers a conversion for the given converter. Inspects either generics or the {@link ConvertiblePair}s returned + * by a {@link GenericConverter}. + * + * @param converter + */ + private void registerConversion(Object converter) { + + Class type = converter.getClass(); + boolean isWriting = type.isAnnotationPresent(WritingConverter.class); + boolean isReading = type.isAnnotationPresent(ReadingConverter.class); + + if (converter instanceof GenericConverter) { + GenericConverter genericConverter = (GenericConverter) converter; + for (ConvertiblePair pair : genericConverter.getConvertibleTypes()) { + register(new ConverterRegistration(pair, isReading, isWriting)); + } + } else if (converter instanceof Converter) { + Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class); + register(new ConverterRegistration(new ConvertiblePair(arguments[0], arguments[1]), isReading, isWriting)); + } else if (converter instanceof ConverterFactory) { + + Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), ConverterFactory.class); + register(new ConverterRegistration(new ConvertiblePair(arguments[0], arguments[1]), isReading, isWriting)); + } + + else { + throw new IllegalArgumentException("Unsupported Converter type!"); + } + } + + /** + * Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic + * Redis types. + * + * @param pair + */ + private void register(ConverterRegistration converterRegistration) { + + ConvertiblePair pair = converterRegistration.getConvertiblePair(); + + if (converterRegistration.isReading()) { + + readingPairs.add(pair); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleSourceType()) { + LOG.warn(String.format(READ_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + + if (converterRegistration.isWriting()) { + + writingPairs.add(pair); + customSimpleTypes.add(pair.getSourceType()); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleTargetType()) { + LOG.warn(String.format(WRITE_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + } + + /** + * Returns the target type to convert to in case we have a custom conversion registered to convert the given source + * type into a Redis native one. + * + * @param sourceType must not be {@literal null} + * @return + */ + public Class getCustomWriteTarget(final Class sourceType) { + + return getOrCreateAndCache(sourceType, rawWriteTargetTypes, new Producer() { + + @Override + public Class get() { + return getCustomTarget(sourceType, null, writingPairs); + } + }); + } + + /** + * Returns the target type we can readTargetWriteLocl an inject of the given source type to. The returned type might + * be a subclass of the given expected type though. If {@code expectedTargetType} is {@literal null} we will simply + * return the first target type matching or {@literal null} if no conversion can be found. + * + * @param sourceType must not be {@literal null} + * @param requestedTargetType + * @return + */ + public Class getCustomWriteTarget(final Class sourceType, final Class requestedTargetType) { + + if (requestedTargetType == null) { + return getCustomWriteTarget(sourceType); + } + + return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customWriteTargetTypes, + new Producer() { + + @Override + public Class get() { + return getCustomTarget(sourceType, requestedTargetType, writingPairs); + } + }); + } + + /** + * Returns whether we have a custom conversion registered to readTargetWriteLocl into a Redis native type. The + * returned type might be a subclass of the given expected type though. + * + * @param sourceType must not be {@literal null} + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType) { + return hasCustomWriteTarget(sourceType, null); + } + + /** + * Returns whether we have a custom conversion registered to readTargetWriteLocl an object of the given source type + * into an object of the given Redis native target type. + * + * @param sourceType must not be {@literal null}. + * @param requestedTargetType + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType, Class requestedTargetType) { + return getCustomWriteTarget(sourceType, requestedTargetType) != null; + } + + /** + * Returns whether we have a custom conversion registered to readTargetReadLock the given source into the given target + * type. + * + * @param sourceType must not be {@literal null} + * @param requestedTargetType must not be {@literal null} + * @return + */ + public boolean hasCustomReadTarget(Class sourceType, Class requestedTargetType) { + return getCustomReadTarget(sourceType, requestedTargetType) != null; + } + + /** + * Returns the actual target type for the given {@code sourceType} and {@code requestedTargetType}. Note that the + * returned {@link Class} could be an assignable type to the given {@code requestedTargetType}. + * + * @param sourceType must not be {@literal null}. + * @param requestedTargetType can be {@literal null}. + * @return + */ + private Class getCustomReadTarget(final Class sourceType, final Class requestedTargetType) { + + if (requestedTargetType == null) { + return null; + } + + return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customReadTargetTypes, + new Producer() { + + @Override + public Class get() { + return getCustomTarget(sourceType, requestedTargetType, readingPairs); + } + }); + } + + /** + * Inspects the given {@link ConvertiblePair}s for ones that have a source compatible type as source. Additionally + * checks assignability of the target type if one is given. + * + * @param sourceType must not be {@literal null}. + * @param requestedTargetType can be {@literal null}. + * @param pairs must not be {@literal null}. + * @return + */ + private static Class getCustomTarget(Class sourceType, Class requestedTargetType, + Collection pairs) { + + Assert.notNull(sourceType); + Assert.notNull(pairs); + + if (requestedTargetType != null && pairs.contains(new ConvertiblePair(sourceType, requestedTargetType))) { + return requestedTargetType; + } + + for (ConvertiblePair typePair : pairs) { + if (typePair.getSourceType().isAssignableFrom(sourceType)) { + Class targetType = typePair.getTargetType(); + if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) { + return targetType; + } + } + } + + return null; + } + + /** + * Will try to find a value for the given key in the given cache or produce one using the given {@link Producer} and + * store it in the cache. + * + * @param key the key to lookup a potentially existing value, must not be {@literal null}. + * @param cache the cache to find the value in, must not be {@literal null}. + * @param producer the {@link Producer} to create values to cache, must not be {@literal null}. + * @return + */ + private static Class getOrCreateAndCache(T key, Map>> cache, Producer producer) { + + CacheValue> cacheValue = cache.get(key); + + if (cacheValue != null) { + return cacheValue.getValue(); + } + + Class type = producer.get(); + cache.put(key, CacheValue.> ofNullable(type)); + + return type; + } + + private interface Producer { + + Class get(); + } + + /** + * Conversion registration information. + * + * @author Oliver Gierke + */ + static class ConverterRegistration { + + private final ConvertiblePair convertiblePair; + private final boolean reading; + private final boolean writing; + + /** + * Creates a new {@link ConverterRegistration}. + * + * @param convertiblePair must not be {@literal null}. + * @param isReading whether to force to consider the converter for reading. + * @param isWritingwhether to force to consider the converter for reading. + */ + public ConverterRegistration(ConvertiblePair convertiblePair, boolean isReading, boolean isWriting) { + + Assert.notNull(convertiblePair); + + this.convertiblePair = convertiblePair; + this.reading = isReading; + this.writing = isWriting; + } + + /** + * Creates a new {@link ConverterRegistration} from the given source and target type and read/write flags. + * + * @param source the source type to be converted from, must not be {@literal null}. + * @param target the target type to be converted to, must not be {@literal null}. + * @param isReading whether to force to consider the converter for reading. + * @param isWriting whether to force to consider the converter for writing. + */ + public ConverterRegistration(Class source, Class target, boolean isReading, boolean isWriting) { + this(new ConvertiblePair(source, target), isReading, isWriting); + } + + /** + * Returns whether the converter shall be used for writing. + * + * @return + */ + public boolean isWriting() { + return writing == true || (!reading && isSimpleTargetType()); + } + + /** + * Returns whether the converter shall be used for reading. + * + * @return + */ + public boolean isReading() { + return reading == true || (!writing && isSimpleSourceType()); + } + + /** + * Returns the actual conversion pair. + * + * @return + */ + public ConvertiblePair getConvertiblePair() { + return convertiblePair; + } + + /** + * Returns whether the source type is a Redis simple one. + * + * @return + */ + public boolean isSimpleSourceType() { + return isRedisBasicType(convertiblePair.getSourceType()); + } + + /** + * Returns whether the target type is a Redis simple one. + * + * @return + */ + public boolean isSimpleTargetType() { + return isRedisBasicType(convertiblePair.getTargetType()); + } + + /** + * Returns whether the given type is a type that Redis can handle basically. + * + * @param type + * @return + */ + private static boolean isRedisBasicType(Class type) { + return (byte[].class.equals(type) || Map.class.equals(type)); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/IndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/IndexResolver.java new file mode 100644 index 0000000000..a90e376f7a --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/IndexResolver.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 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.util.Set; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.util.TypeInformation; + +/** + * {@link IndexResolver} extracts secondary index structures to be applied on a given path, {@link PersistentProperty} + * and value. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexResolver { + + /** + * Resolves all indexes for given type information / value combination. + * + * @param typeInformation must not be {@literal null}. + * @param value the actual value. Can be {@literal null}. + * @return never {@literal null}. + */ + Set resolveIndexesFor(TypeInformation typeInformation, Object value); + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java b/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java new file mode 100644 index 0000000000..37584a9d0a --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015-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; + +/** + * {@link IndexedData} represents a secondary index for a property path in a given keyspace. + * + * @author Christoph Strobl + * @author Rob Winch + * @since 1.7 + */ +public interface IndexedData { + + /** + * Get the {@link String} representation of the index name. + * + * @return never {@literal null}. + */ + String getIndexName(); + + /** + * Get the associated keyspace the index resides in. + * + * @return + */ + String getKeyspace(); + +} 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 0000000000..dcebe19536 --- /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/KeyspaceConfiguration.java b/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java new file mode 100644 index 0000000000..f0f8b3bfa7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java @@ -0,0 +1,186 @@ +/* + * Copyright 2015 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.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link KeyspaceConfiguration} allows programmatic setup of keyspaces and time to live options for certain types. This + * is suitable for cases where there is no option to use the equivalent {@link RedisHash} or {@link TimeToLive} + * annotations. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class KeyspaceConfiguration { + + private Map, KeyspaceSettings> settingsMap; + + public KeyspaceConfiguration() { + + this.settingsMap = new ConcurrentHashMap, KeyspaceSettings>(); + for (KeyspaceSettings initial : initialConfiguration()) { + settingsMap.put(initial.type, initial); + } + } + + /** + * Check if specific {@link KeyspaceSettings} are available for given type. + * + * @param type must not be {@literal null}. + * @return true if settings exist. + */ + public boolean hasSettingsFor(Class type) { + + Assert.notNull(type, "Type to lookup must not be null!"); + + if (settingsMap.containsKey(type)) { + + if (settingsMap.get(type) instanceof DefaultKeyspaceSetting) { + return false; + } + + return true; + } + + for (KeyspaceSettings assignment : settingsMap.values()) { + if (assignment.inherit) { + if (ClassUtils.isAssignable(assignment.type, type)) { + settingsMap.put(type, assignment.cloneFor(type)); + return true; + } + } + } + + settingsMap.put(type, new DefaultKeyspaceSetting(type)); + return false; + } + + /** + * Get the {@link KeyspaceSettings} for given type. + * + * @param type must not be {@literal null} + * @return {@literal null} if no settings configured. + */ + public KeyspaceSettings getKeyspaceSettings(Class type) { + + if (!hasSettingsFor(type)) { + return null; + } + + KeyspaceSettings settings = settingsMap.get(type); + if (settings == null || settings instanceof DefaultKeyspaceSetting) { + return null; + } + + return settings; + } + + /** + * Customization hook. + * + * @return must not return {@literal null}. + */ + protected Iterable initialConfiguration() { + return Collections.emptySet(); + } + + /** + * Add {@link KeyspaceSettings} for type. + * + * @param keyspaceSettings must not be {@literal null}. + */ + public void addKeyspaceSettings(KeyspaceSettings keyspaceSettings) { + + Assert.notNull(keyspaceSettings); + this.settingsMap.put(keyspaceSettings.getType(), keyspaceSettings); + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static class KeyspaceSettings { + + private final String keyspace; + private final Class type; + private final boolean inherit; + private Long timeToLive; + private String timeToLivePropertyName; + + public KeyspaceSettings(Class type, String keyspace) { + this(type, keyspace, true); + } + + public KeyspaceSettings(Class type, String keyspace, boolean inherit) { + + this.type = type; + this.keyspace = keyspace; + this.inherit = inherit; + } + + KeyspaceSettings cloneFor(Class type) { + return new KeyspaceSettings(type, this.keyspace, false); + } + + public String getKeyspace() { + return keyspace; + } + + public Class getType() { + return type; + } + + public void setTimeToLive(Long timeToLive) { + this.timeToLive = timeToLive; + } + + public Long getTimeToLive() { + return timeToLive; + } + + public void setTimeToLivePropertyName(String propertyName) { + timeToLivePropertyName = propertyName; + } + + public String getTimeToLivePropertyName() { + return timeToLivePropertyName; + } + + } + + /** + * Marker class indicating no settings defined. + * + * @author Christoph Strobl + * @since 1.7 + */ + private static class DefaultKeyspaceSetting extends KeyspaceSettings { + + public DefaultKeyspaceSetting(Class type) { + super(type, "#default#", false); + } + + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java b/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java new file mode 100644 index 0000000000..908f96a0bc --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-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 org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider; + +/** + * {@link MappingConfiguration} is used for programmatic configuration of secondary indexes, key prefixes, expirations + * and the such. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class MappingConfiguration { + + private final ConfigurableIndexDefinitionProvider indexConfiguration; + private final KeyspaceConfiguration keyspaceConfiguration; + + /** + * Creates new {@link MappingConfiguration}. + * + * @param indexConfiguration must not be {@literal null}. + * @param keyspaceConfiguration must not be {@literal null}. + */ + public MappingConfiguration(ConfigurableIndexDefinitionProvider indexConfiguration, + KeyspaceConfiguration keyspaceConfiguration) { + + this.indexConfiguration = indexConfiguration; + this.keyspaceConfiguration = keyspaceConfiguration; + } + + /** + * @return never {@literal null}. + */ + public ConfigurableIndexDefinitionProvider getIndexConfiguration() { + return indexConfiguration; + } + + /** + * @return never {@literal null}. + */ + public KeyspaceConfiguration getKeyspaceConfiguration() { + return keyspaceConfiguration; + } +} 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 new file mode 100644 index 0000000000..ddb345e7ce --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -0,0 +1,911 @@ +/* + * Copyright 2015-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.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.DefaultTypeMapper; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.convert.TypeAliasAccessor; +import org.springframework.data.convert.TypeMapper; +import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; +import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.redis.core.index.Indexed; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.comparator.NullSafeComparator; + +/** + * {@link RedisConverter} implementation creating flat binary map structure out of a given domain type. Considers + * {@link Indexed} annotation for enabling helper structures for finder operations.
+ *
+ * NOTE {@link MappingRedisConverter} is an {@link InitializingBean} and requires + * {@link MappingRedisConverter#afterPropertiesSet()} to be called. + * + *
+ * 
+ * @RedisHash("persons")
+ * class Person {
+ * 
+ *   @Id String id;
+ *   String firstname;
+ * 
+ *   List nicknames;
+ *   List coworkers;
+ * 
+ *   Address address;
+ *   @Reference Country nationality;
+ * }
+ * 
+ * 
+ * + * The above is represented as: + * + *
+ * 
+ * _class=org.example.Person
+ * id=1
+ * firstname=rand
+ * lastname=al'thor
+ * coworkers.[0].firstname=mat
+ * coworkers.[0].nicknames.[0]=prince of the ravens
+ * coworkers.[1].firstname=perrin
+ * coworkers.[1].address.city=two rivers
+ * nationality=nationality:andora
+ * 
+ * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ +public class MappingRedisConverter implements RedisConverter, InitializingBean { + + private final RedisMappingContext mappingContext; + private final GenericConversionService conversionService; + private final EntityInstantiators entityInstantiators; + private final TypeMapper typeMapper; + private final Comparator listKeyComparator = new NullSafeComparator( + NaturalOrderingKeyComparator.INSTANCE, true); + + private ReferenceResolver referenceResolver; + private IndexResolver indexResolver; + private CustomConversions customConversions; + + /** + * Creates new {@link MappingRedisConverter}. + * + * @param context can be {@literal null}. + */ + MappingRedisConverter(RedisMappingContext context) { + this(context, null, null); + } + + /** + * Creates new {@link MappingRedisConverter} and defaults {@link RedisMappingContext} when {@literal null}. + * + * @param mappingContext can be {@literal null}. + * @param indexResolver can be {@literal null}. + * @param referenceResolver must not be {@literal null}. + */ + public MappingRedisConverter(RedisMappingContext mappingContext, IndexResolver indexResolver, + ReferenceResolver referenceResolver) { + + this.mappingContext = mappingContext != null ? mappingContext : new RedisMappingContext(); + + entityInstantiators = new EntityInstantiators(); + + this.conversionService = new DefaultConversionService(); + this.customConversions = new CustomConversions(); + + typeMapper = new DefaultTypeMapper(new RedisTypeAliasAccessor(this.conversionService)); + + this.referenceResolver = referenceResolver; + + this.indexResolver = indexResolver != null ? indexResolver : new PathIndexResolver(this.mappingContext); + } + + /* + * (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); + } + + @SuppressWarnings("unchecked") + private R readInternal(final String path, Class type, final RedisData source) { + + if (source.getBucket() == null || source.getBucket().isEmpty()) { + return null; + } + + TypeInformation readType = typeMapper.readType(source); + TypeInformation typeToUse = readType != null ? readType : ClassTypeInformation.from(type); + final RedisPersistentEntity entity = mappingContext.getPersistentEntity(typeToUse); + + if (customConversions.hasCustomReadTarget(Map.class, typeToUse.getType())) { + + Map partial = new HashMap(); + + if (!path.isEmpty()) { + + for (Entry entry : source.getBucket().extract(path + ".").entrySet()) { + partial.put(entry.getKey().substring(path.length() + 1), entry.getValue()); + } + + } else { + partial.putAll(source.getBucket().asMap()); + } + R instance = (R) conversionService.convert(partial, typeToUse.getType()); + + if (entity.hasIdProperty()) { + entity.getPropertyAccessor(instance).setProperty(entity.getIdProperty(), source.getId()); + } + return instance; + } + + if (conversionService.canConvert(byte[].class, typeToUse.getType())) { + return (R) conversionService.convert(source.getBucket().get(StringUtils.hasText(path) ? path : "_raw"), + typeToUse.getType()); + } + + EntityInstantiator instantiator = entityInstantiators.getInstantiatorFor(entity); + + Object instance = instantiator.createInstance((RedisPersistentEntity) entity, + new PersistentEntityParameterValueProvider(entity, + new ConverterAwareParameterValueProvider(source, conversionService), null)); + + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) { + + String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName(); + + PreferredConstructor constructor = entity.getPersistenceConstructor(); + + if (constructor.isConstructorParameter(persistentProperty)) { + return; + } + + if (persistentProperty.isMap()) { + + if (conversionService.canConvert(byte[].class, persistentProperty.getMapValueType())) { + accessor.setProperty(persistentProperty, readMapOfSimpleTypes(currentPath, persistentProperty.getType(), + persistentProperty.getComponentType(), persistentProperty.getMapValueType(), source)); + } else { + accessor.setProperty(persistentProperty, readMapOfComplexTypes(currentPath, persistentProperty.getType(), + persistentProperty.getComponentType(), persistentProperty.getMapValueType(), source)); + } + } + + else if (persistentProperty.isCollectionLike()) { + + if (conversionService.canConvert(byte[].class, persistentProperty.getComponentType())) { + accessor.setProperty(persistentProperty, + readCollectionOfSimpleTypes(currentPath, persistentProperty.getType(), + persistentProperty.getTypeInformation().getComponentType().getActualType().getType(), source)); + } else { + accessor.setProperty(persistentProperty, + readCollectionOfComplexTypes(currentPath, persistentProperty.getType(), + persistentProperty.getTypeInformation().getComponentType().getActualType().getType(), + source.getBucket())); + } + + } else if (persistentProperty.isEntity() && !conversionService.canConvert(byte[].class, + persistentProperty.getTypeInformation().getActualType().getType())) { + + Class targetType = persistentProperty.getTypeInformation().getActualType().getType(); + + Bucket bucket = source.getBucket().extract(currentPath + "."); + + RedisData source = new RedisData(bucket); + + byte[] type = bucket.get(currentPath + "._class"); + if (type != null && type.length > 0) { + source.getBucket().put("_class", type); + } + + accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, source)); + } else { + + if (persistentProperty.isIdProperty() && StringUtils.isEmpty(path.isEmpty())) { + + if (source.getBucket().get(currentPath) == null) { + accessor.setProperty(persistentProperty, + fromBytes(source.getBucket().get(currentPath), persistentProperty.getActualType())); + } else { + accessor.setProperty(persistentProperty, source.getId()); + } + } + + accessor.setProperty(persistentProperty, + fromBytes(source.getBucket().get(currentPath), persistentProperty.getActualType())); + } + } + + }); + + readAssociation(path, source, entity, accessor); + + return (R) instance; + } + + private void readAssociation(final String path, final RedisData source, final KeyValuePersistentEntity entity, + final PersistentPropertyAccessor accessor) { + + entity.doWithAssociations(new AssociationHandler() { + + @Override + public void doWithAssociation(Association association) { + + String currentPath = !path.isEmpty() ? path + "." + association.getInverse().getName() + : association.getInverse().getName(); + + if (association.getInverse().isCollectionLike()) { + + Bucket bucket = source.getBucket().extract(currentPath + ".["); + + Collection target = CollectionFactory.createCollection(association.getInverse().getType(), + association.getInverse().getComponentType(), bucket.size()); + + for (Entry entry : bucket.entrySet()) { + + String referenceKey = fromBytes(entry.getValue(), String.class); + String[] args = referenceKey.split(":"); + + Map rawHash = referenceResolver.resolveReference(args[1], args[0]); + + if (!CollectionUtils.isEmpty(rawHash)) { + target.add(read(association.getInverse().getActualType(), new RedisData(rawHash))); + } + } + + accessor.setProperty(association.getInverse(), target); + + } else { + + byte[] binKey = source.getBucket().get(currentPath); + if (binKey == null || binKey.length == 0) { + return; + } + + String key = fromBytes(binKey, String.class); + + String[] args = key.split(":"); + + Map rawHash = referenceResolver.resolveReference(args[1], args[0]); + + if (!CollectionUtils.isEmpty(rawHash)) { + accessor.setProperty(association.getInverse(), + read(association.getInverse().getActualType(), new RedisData(rawHash))); + } + } + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.EntityWriter#write(java.lang.Object, java.lang.Object) + */ + @Override + @SuppressWarnings({ "rawtypes" }) + public void write(Object source, final RedisData sink) { + + final RedisPersistentEntity entity = mappingContext.getPersistentEntity(source.getClass()); + + if (!customConversions.hasCustomWriteTarget(source.getClass())) { + typeMapper.writeType(ClassUtils.getUserClass(source), sink); + } + sink.setKeyspace(entity.getKeySpace()); + + writeInternal(entity.getKeySpace(), "", source, entity.getTypeInformation(), sink); + sink.setId(getConversionService().convert(entity.getIdentifierAccessor(source).getIdentifier(), String.class)); + + Long ttl = entity.getTimeToLiveAccessor().getTimeToLive(source); + if (ttl != null && ttl > 0) { + sink.setTimeToLive(ttl); + } + + for (IndexedData indexeData : indexResolver.resolveIndexesFor(entity.getTypeInformation(), source)) { + sink.addIndexedData(indexeData); + } + + } + + /** + * @param keyspace + * @param path + * @param value + * @param typeHint + * @param sink + */ + private void writeInternal(final String keyspace, final String path, final Object value, TypeInformation typeHint, + final RedisData sink) { + + if (value == null) { + return; + } + + if (customConversions.hasCustomWriteTarget(value.getClass())) { + + if (customConversions.getCustomWriteTarget(value.getClass()).equals(byte[].class)) { + sink.getBucket().put(StringUtils.hasText(path) ? path : "_raw", conversionService.convert(value, byte[].class)); + } else { + writeToBucket(path, value, sink); + } + return; + } + + if (value.getClass() != typeHint.getType()) { + sink.getBucket().put((!path.isEmpty() ? path + "._class" : "_class"), toBytes(value.getClass().getName())); + } + + final KeyValuePersistentEntity entity = mappingContext.getPersistentEntity(value.getClass()); + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) { + + String propertyStringPath = (!path.isEmpty() ? path + "." : "") + persistentProperty.getName(); + + if (persistentProperty.isIdProperty()) { + + sink.getBucket().put(propertyStringPath, toBytes(accessor.getProperty(persistentProperty))); + return; + } + + if (persistentProperty.isMap()) { + writeMap(keyspace, propertyStringPath, persistentProperty.getMapValueType(), + (Map) accessor.getProperty(persistentProperty), sink); + } else if (persistentProperty.isCollectionLike()) { + writeCollection(keyspace, propertyStringPath, (Collection) accessor.getProperty(persistentProperty), + persistentProperty.getTypeInformation().getComponentType(), sink); + } else if (persistentProperty.isEntity()) { + writeInternal(keyspace, propertyStringPath, accessor.getProperty(persistentProperty), + persistentProperty.getTypeInformation().getActualType(), sink); + } else { + + Object propertyValue = accessor.getProperty(persistentProperty); + sink.getBucket().put(propertyStringPath, toBytes(propertyValue)); + } + } + }); + + writeAssiciation(keyspace, path, entity, value, sink); + } + + private void writeAssiciation(final String keyspace, final String path, final KeyValuePersistentEntity entity, + final Object value, final RedisData sink) { + + if (value == null) { + return; + } + + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); + + entity.doWithAssociations(new AssociationHandler() { + + @Override + public void doWithAssociation(Association association) { + + Object refObject = accessor.getProperty(association.getInverse()); + if (refObject == null) { + return; + } + + if (association.getInverse().isCollectionLike()) { + + KeyValuePersistentEntity ref = mappingContext + .getPersistentEntity(association.getInverse().getTypeInformation().getComponentType().getActualType()); + + String keyspace = ref.getKeySpace(); + String propertyStringPath = (!path.isEmpty() ? path + "." : "") + association.getInverse().getName(); + + int i = 0; + for (Object o : (Collection) refObject) { + + Object refId = ref.getPropertyAccessor(o).getProperty(ref.getIdProperty()); + sink.getBucket().put(propertyStringPath + ".[" + i + "]", toBytes(keyspace + ":" + refId)); + i++; + } + + } else { + + KeyValuePersistentEntity ref = mappingContext + .getPersistentEntity(association.getInverse().getTypeInformation()); + String keyspace = ref.getKeySpace(); + + Object refId = ref.getPropertyAccessor(refObject).getProperty(ref.getIdProperty()); + + String propertyStringPath = (!path.isEmpty() ? path + "." : "") + association.getInverse().getName(); + sink.getBucket().put(propertyStringPath, toBytes(keyspace + ":" + refId)); + } + } + }); + } + + /** + * @param keyspace + * @param path + * @param values + * @param typeHint + * @param sink + */ + private void writeCollection(String keyspace, String path, Collection values, TypeInformation typeHint, + RedisData sink) { + + if (values == null) { + return; + } + + int i = 0; + for (Object value : values) { + + String currentPath = path + ".[" + i + "]"; + + if (customConversions.hasCustomWriteTarget(value.getClass())) { + writeToBucket(currentPath, value, sink); + } else { + writeInternal(keyspace, currentPath, value, typeHint, sink); + } + i++; + } + } + + private void writeToBucket(String path, Object value, RedisData sink) { + + if (value == null) { + return; + } + + if (customConversions.hasCustomWriteTarget(value.getClass())) { + Class targetType = customConversions.getCustomWriteTarget(value.getClass()); + + if (ClassUtils.isAssignable(Map.class, targetType)) { + + Map map = (Map) conversionService.convert(value, targetType); + for (Map.Entry entry : map.entrySet()) { + sink.getBucket().put(path + (StringUtils.hasText(path) ? "." : "") + entry.getKey(), + toBytes(entry.getValue())); + } + } else if (ClassUtils.isAssignable(byte[].class, targetType)) { + sink.getBucket().put(path, toBytes(value)); + } else { + throw new IllegalArgumentException("converter must not fool me!!"); + } + + } + + } + + /** + * @param path + * @param collectionType + * @param valueType + * @param source + * @return + */ + private Collection readCollectionOfSimpleTypes(String path, Class collectionType, Class valueType, + RedisData source) { + + Bucket partial = source.getBucket().extract(path + ".["); + + List keys = new ArrayList(partial.keySet()); + keys.sort(listKeyComparator); + + Collection target = CollectionFactory.createCollection(collectionType, valueType, partial.size()); + + for (String key : keys) { + target.add(fromBytes(partial.get(key), valueType)); + } + + return target; + } + + /** + * @param path + * @param collectionType + * @param valueType + * @param source + * @return + */ + private Collection readCollectionOfComplexTypes(String path, Class collectionType, Class valueType, + Bucket source) { + + List keys = new ArrayList(source.extractAllKeysFor(path)); + keys.sort(listKeyComparator); + + Collection target = CollectionFactory.createCollection(collectionType, valueType, keys.size()); + + for (String key : keys) { + + Bucket elementData = source.extract(key); + + byte[] typeInfo = elementData.get(key + "._class"); + if (typeInfo != null && typeInfo.length > 0) { + elementData.put("_class", typeInfo); + } + + Object o = readInternal(key, valueType, new RedisData(elementData)); + target.add(o); + } + + return target; + } + + /** + * @param keyspace + * @param path + * @param mapValueType + * @param source + * @param sink + */ + private void writeMap(String keyspace, String path, Class mapValueType, Map source, RedisData sink) { + + if (CollectionUtils.isEmpty(source)) { + return; + } + + for (Map.Entry entry : source.entrySet()) { + + if (entry.getValue() == null || entry.getKey() == null) { + continue; + } + + String currentPath = path + ".[" + entry.getKey() + "]"; + + if (customConversions.hasCustomWriteTarget(entry.getValue().getClass())) { + writeToBucket(currentPath, entry.getValue(), sink); + } else { + writeInternal(keyspace, currentPath, entry.getValue(), ClassTypeInformation.from(mapValueType), sink); + } + } + } + + /** + * @param path + * @param mapType + * @param keyType + * @param valueType + * @param source + * @return + */ + private Map readMapOfSimpleTypes(String path, Class mapType, Class keyType, Class valueType, + RedisData source) { + + Bucket partial = source.getBucket().extract(path + ".["); + + Map target = CollectionFactory.createMap(mapType, partial.size()); + + for (Entry entry : partial.entrySet()) { + + String regex = "^(" + Pattern.quote(path) + "\\.\\[)(.*?)(\\])"; + Pattern pattern = Pattern.compile(regex); + + Matcher matcher = pattern.matcher(entry.getKey()); + if (!matcher.find()) { + throw new RuntimeException("baähhh"); + } + String key = matcher.group(2); + target.put(key, fromBytes(entry.getValue(), valueType)); + } + + return target; + } + + /** + * @param path + * @param mapType + * @param keyType + * @param valueType + * @param source + * @return + */ + private Map readMapOfComplexTypes(String path, Class mapType, Class keyType, Class valueType, + RedisData source) { + + Set keys = source.getBucket().extractAllKeysFor(path); + + Map target = CollectionFactory.createMap(mapType, keys.size()); + + for (String key : keys) { + + String regex = "^(" + Pattern.quote(path) + "\\.\\[)(.*?)(\\])"; + Pattern pattern = Pattern.compile(regex); + + Matcher matcher = pattern.matcher(key); + if (!matcher.find()) { + throw new RuntimeException("baähhh"); + } + String mapKey = matcher.group(2); + + Bucket partial = source.getBucket().extract(key); + + byte[] typeInfo = partial.get(key + "._class"); + if (typeInfo != null && typeInfo.length > 0) { + partial.put("_class", typeInfo); + } + + Object o = readInternal(key, valueType, new RedisData(partial)); + target.put(mapKey, o); + } + + return target; + } + + /** + * Convert given source to binary representation using the underlying {@link ConversionService}. + * + * @param source + * @return + * @throws ConverterNotFoundException + */ + public byte[] toBytes(Object source) { + + if (source instanceof byte[]) { + return (byte[]) source; + } + + return conversionService.convert(source, byte[].class); + } + + /** + * Convert given binary representation to desired target type using the underlying {@link ConversionService}. + * + * @param source + * @param type + * @return + * @throws ConverterNotFoundException + */ + public T fromBytes(byte[] source, Class type) { + return conversionService.convert(source, type); + } + + /** + * Set {@link CustomConversions} to be applied. + * + * @param customConversions + */ + public void setCustomConversions(CustomConversions customConversions) { + this.customConversions = customConversions != null ? customConversions : new CustomConversions(); + } + + public void setReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + } + + public void setIndexResolver(IndexResolver indexResolver) { + this.indexResolver = indexResolver; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.EntityConverter#getMappingContext() + */ + @Override + public RedisMappingContext getMappingContext() { + return this.mappingContext; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.EntityConverter#getConversionService() + */ + @Override + public ConversionService getConversionService() { + return this.conversionService; + } + + @Override + public void afterPropertiesSet() { + this.initializeConverters(); + } + + private void initializeConverters() { + customConversions.registerConvertersIn(conversionService); + } + + /** + * @author Christoph Strobl + */ + private static class ConverterAwareParameterValueProvider + implements PropertyValueProvider { + + private final RedisData source; + private final ConversionService conversionService; + + public ConverterAwareParameterValueProvider(RedisData source, ConversionService conversionService) { + this.source = source; + this.conversionService = conversionService; + } + + @Override + @SuppressWarnings("unchecked") + public T getPropertyValue(KeyValuePersistentProperty property) { + return (T) conversionService.convert(source.getBucket().get(property.getName()), property.getActualType()); + } + } + + /** + * @author Christoph Strobl + */ + private static class RedisTypeAliasAccessor implements TypeAliasAccessor { + + private final String typeKey; + + private final ConversionService conversionService; + + RedisTypeAliasAccessor(ConversionService conversionService) { + this(conversionService, "_class"); + } + + RedisTypeAliasAccessor(ConversionService conversionService, String typeKey) { + + this.conversionService = conversionService; + this.typeKey = typeKey; + } + + @Override + public Object readAliasFrom(RedisData source) { + return conversionService.convert(source.getBucket().get(typeKey), String.class); + } + + @Override + public void writeTypeTo(RedisData sink, Object alias) { + sink.getBucket().put(typeKey, conversionService.convert(alias, byte[].class)); + } + } + + enum ClassNameKeySpaceResolver implements KeySpaceResolver { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeySpaceResolver#resolveKeySpace(java.lang.Class) + */ + @Override + public String resolveKeySpace(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return ClassUtils.getUserClass(type).getName(); + } + } + + private enum NaturalOrderingKeyComparator implements Comparator { + + INSTANCE; + + /* + * (non-Javadoc) + * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) + */ + public int compare(String s1, String s2) { + + int s1offset = 0; + int s2offset = 0; + + while (s1offset < s1.length() && s2offset < s2.length()) { + + Part thisPart = extractPart(s1, s1offset); + Part thatPart = extractPart(s2, s2offset); + + int result = thisPart.compareTo(thatPart); + + if (result != 0) { + return result; + } + + s1offset += thisPart.length(); + s2offset += thatPart.length(); + } + + return 0; + } + + private Part extractPart(String source, int offset) { + + StringBuilder builder = new StringBuilder(); + + char c = source.charAt(offset); + builder.append(c); + + boolean isDigit = Character.isDigit(c); + for (int i = offset + 1; i < source.length(); i++) { + + c = source.charAt(i); + if ((isDigit && !Character.isDigit(c)) || (!isDigit && Character.isDigit(c))) { + break; + } + builder.append(c); + } + + return new Part(builder.toString(), isDigit); + } + + private static class Part implements Comparable { + + private final String rawValue; + private final Long longValue; + + Part(String value, boolean isDigit) { + + this.rawValue = value; + this.longValue = isDigit ? Long.valueOf(value) : null; + } + + boolean isNumeric() { + return longValue != null; + } + + int length() { + return rawValue.length(); + } + + /* + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(Part that) { + + if (this.isNumeric() && that.isNumeric()) { + return this.longValue.compareTo(that.longValue); + } + + return this.rawValue.compareTo(that.rawValue); + } + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java new file mode 100644 index 0000000000..94c5334215 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java @@ -0,0 +1,224 @@ +/* + * Copyright 2015-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.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.IndexDefinition; +import org.springframework.data.redis.core.index.IndexDefinition.Condition; +import org.springframework.data.redis.core.index.IndexDefinition.IndexingContext; +import org.springframework.data.redis.core.index.Indexed; +import org.springframework.data.redis.core.index.SimpleIndexDefinition; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +/** + * {@link IndexResolver} implementation considering properties annotated with {@link Indexed} or paths set up in + * {@link IndexConfiguration}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class PathIndexResolver implements IndexResolver { + + private ConfigurableIndexDefinitionProvider indexConfiguration; + private RedisMappingContext mappingContext; + + /** + * Creates new {@link PathIndexResolver} with empty {@link IndexConfiguration}. + */ + public PathIndexResolver() { + this(new RedisMappingContext()); + } + + /** + * Creates new {@link PathIndexResolver} with given {@link IndexConfiguration}. + * + * @param mapppingContext must not be {@literal null}. + */ + public PathIndexResolver(RedisMappingContext mappingContext) { + + Assert.notNull(mappingContext, "MappingContext must not be null!"); + this.mappingContext = mappingContext; + this.indexConfiguration = mappingContext.getMappingConfiguration().getIndexConfiguration(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(org.springframework.data.util.TypeInformation, java.lang.Object) + */ + public Set resolveIndexesFor(TypeInformation typeInformation, Object value) { + return doResolveIndexesFor(mappingContext.getPersistentEntity(typeInformation).getKeySpace(), "", typeInformation, + null, value); + } + + private Set doResolveIndexesFor(final String keyspace, final String path, + TypeInformation typeInformation, PersistentProperty fallback, Object value) { + + RedisPersistentEntity entity = mappingContext.getPersistentEntity(typeInformation); + + if (entity == null) { + return resolveIndex(keyspace, path, fallback, value); + } + + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); + final Set indexes = new LinkedHashSet(); + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) { + + String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName(); + + Object propertyValue = accessor.getProperty(persistentProperty); + + if (propertyValue != null) { + + TypeInformation typeHint = persistentProperty.isMap() + ? persistentProperty.getTypeInformation().getMapValueType() + : persistentProperty.getTypeInformation().getActualType(); + + if (persistentProperty.isMap()) { + + for (Entry entry : ((Map) propertyValue).entrySet()) { + + TypeInformation typeToUse = updateTypeHintForActualValue(typeHint, entry.getValue()); + indexes.addAll(doResolveIndexesFor(keyspace, currentPath + "." + entry.getKey(), + typeToUse.getActualType(), persistentProperty, entry.getValue())); + } + + } else if (persistentProperty.isCollectionLike()) { + + for (Object listValue : (Iterable) propertyValue) { + + TypeInformation typeToUse = updateTypeHintForActualValue(typeHint, listValue); + indexes.addAll( + doResolveIndexesFor(keyspace, currentPath, typeToUse.getActualType(), persistentProperty, listValue)); + } + } + + else if (persistentProperty.isEntity() + || persistentProperty.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)) { + + typeHint = updateTypeHintForActualValue(typeHint, propertyValue); + indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeHint.getActualType(), persistentProperty, + propertyValue)); + } else { + indexes.addAll(resolveIndex(keyspace, currentPath, persistentProperty, propertyValue)); + } + } + + } + + private TypeInformation updateTypeHintForActualValue(TypeInformation typeHint, Object propertyValue) { + + if (typeHint.equals(ClassTypeInformation.OBJECT) || typeHint.getClass().isInterface()) { + try { + typeHint = mappingContext.getPersistentEntity(propertyValue.getClass()).getTypeInformation(); + } catch (Exception e) { + // ignore for cases where property value cannot be resolved as an entity, in that case the provided type + // hint has to be sufficient + } + } + return typeHint; + } + + }); + + return indexes; + } + + protected Set resolveIndex(String keyspace, String propertyPath, PersistentProperty property, + Object value) { + + if (value == null) { + return Collections.emptySet(); + } + + String path = normalizeIndexPath(propertyPath, property); + + Set data = new LinkedHashSet(); + + if (indexConfiguration.hasIndexFor(keyspace, path)) { + + IndexingContext context = new IndexingContext(keyspace, path, + property != null ? property.getTypeInformation() : ClassTypeInformation.OBJECT); + + for (IndexDefinition indexDefinition : indexConfiguration.getIndexDefinitionsFor(keyspace, path)) { + + if (!verifyConditions(indexDefinition.getConditions(), value, context)) { + continue; + } + + data.add(new SimpleIndexedPropertyValue(keyspace, indexDefinition.getIndexName(), + indexDefinition.valueTransformer().convert(value))); + } + } + + else if (property != null && property.isAnnotationPresent(Indexed.class)) { + + SimpleIndexDefinition indexDefinition = new SimpleIndexDefinition(keyspace, path); + indexConfiguration.addIndexDefinition(indexDefinition); + + data.add(new SimpleIndexedPropertyValue(keyspace, path, indexDefinition.valueTransformer().convert(value))); + } + return data; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private boolean verifyConditions(Iterable> conditions, Object value, IndexingContext context) { + + for (Condition condition : conditions) { + + // TODO: generics lookup + if (!condition.matches(value, context)) { + return false; + } + } + + return true; + } + + private String normalizeIndexPath(String path, PersistentProperty property) { + + if (property == null) { + return path; + } + + if (property.isMap()) { + return path.replaceAll("\\[", "").replaceAll("\\]", ""); + } + if (property.isCollectionLike()) { + return path.replaceAll("\\[(\\p{Digit})*\\]", "").replaceAll("\\.\\.", "."); + } + + return path; + } +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java new file mode 100644 index 0000000000..5754da8a87 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 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 org.springframework.data.convert.EntityConverter; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; + +/** + * Redis specific {@link EntityConverter}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface RedisConverter extends + EntityConverter, KeyValuePersistentProperty, Object, RedisData> { + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/RedisData.java b/src/main/java/org/springframework/data/redis/core/convert/RedisData.java new file mode 100644 index 0000000000..0bfd78b6c8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisData.java @@ -0,0 +1,165 @@ +/* + * Copyright 2015 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.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.springframework.util.Assert; + +/** + * Data object holding {@link Bucket} representing the domain object to be stored in a Redis hash. Index information + * points to additional structures holding the objects is for searching. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisData { + + private String keyspace; + private String id; + + private Bucket bucket; + private Set indexedData; + + private Long timeToLive; + + /** + * Creates new {@link RedisData} with empty {@link Bucket}. + */ + public RedisData() { + this(Collections. emptyMap()); + } + + /** + * Creates new {@link RedisData} with {@link Bucket} holding provided values. + * + * @param raw should not be {@literal null}. + */ + public RedisData(Map raw) { + this(Bucket.newBucketFromRawMap(raw)); + } + + /** + * Creates new {@link RedisData} with {@link Bucket} + * + * @param bucket must not be {@literal null}. + */ + public RedisData(Bucket bucket) { + + Assert.notNull(bucket, "Bucket must not be null!"); + this.bucket = bucket; + this.indexedData = new HashSet(); + } + + /** + * Set the id to be used as part of the key. + * + * @param id + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return + */ + public String getId() { + return this.id; + } + + /** + * Get the time before expiration in seconds. + * + * @return {@literal null} if not set. + */ + public Long getTimeToLive() { + return timeToLive; + } + + /** + * @param index + */ + public void addIndexedData(IndexedData index) { + + Assert.notNull(index, "IndexedData to add must not be null!"); + this.indexedData.add(index); + } + + /** + * @return never {@literal null}. + */ + public Set getIndexedData() { + return Collections.unmodifiableSet(this.indexedData); + } + + /** + * @return + */ + public String getKeyspace() { + return keyspace; + } + + /** + * @param keyspace + */ + public void setKeyspace(String keyspace) { + this.keyspace = keyspace; + } + + /** + * @return + */ + public Bucket getBucket() { + return bucket; + } + + /** + * Set the time before expiration in {@link TimeUnit#SECONDS}. + * + * @param timeToLive can be {@literal null}. + */ + public void setTimeToLive(Long timeToLive) { + this.timeToLive = timeToLive; + } + + /** + * Set the time before expiration converting the given arguments to {@link TimeUnit#SECONDS}. + * + * @param timeToLive must not be {@literal null} + * @param timeUnit must not be {@literal null} + */ + public void setTimeToLive(Long timeToLive, TimeUnit timeUnit) { + + Assert.notNull(timeToLive, "TimeToLive must not be null when used with TimeUnit!"); + Assert.notNull(timeToLive, "TimeUnit must not be null!"); + + setTimeToLive(TimeUnit.SECONDS.convert(timeToLive, timeUnit)); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "RedisDataObject [key=" + keyspace + ":" + id + ", hash=" + bucket + "]"; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java new file mode 100644 index 0000000000..1e29e08b32 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 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.io.Serializable; +import java.util.Map; + +import org.springframework.data.annotation.Reference; + +/** + * {@link ReferenceResolver} retrieves Objects marked with {@link Reference} from Redis. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface ReferenceResolver { + + /** + * @param id must not be {@literal null}. + * @param keyspace must not be {@literal null}. + * @return {@literal null} if referenced object does not exist. + */ + Map resolveReference(Serializable id, String keyspace); +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java new file mode 100644 index 0000000000..e4240de70a --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java @@ -0,0 +1,68 @@ +/* + * 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.io.Serializable; +import java.util.Map; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.convert.BinaryConverters.StringToBytesConverter; +import org.springframework.util.Assert; + +/** + * {@link ReferenceResolver} using {@link RedisKeyValueAdapter} to read raw data. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ReferenceResolverImpl implements ReferenceResolver { + + private final RedisOperations redisOps; + private final StringToBytesConverter converter; + + /** + * @param redisOperations must not be {@literal null}. + */ + public ReferenceResolverImpl(RedisOperations redisOperations) { + + Assert.notNull(redisOperations); + + this.redisOps = redisOperations; + this.converter = new StringToBytesConverter(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.convert.ReferenceResolver#resolveReference(java.io.Serializable, java.io.Serializable, java.lang.Class) + */ + @Override + public Map resolveReference(Serializable id, String keyspace) { + + final byte[] key = converter.convert(keyspace + ":" + id); + + return redisOps.execute(new RedisCallback>() { + + @Override + public Map doInRedis(RedisConnection connection) throws DataAccessException { + return connection.hGetAll(key); + } + }); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java b/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java new file mode 100644 index 0000000000..0c936c1cc2 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java @@ -0,0 +1,124 @@ +/* + * Copyright 2015-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 org.springframework.util.ObjectUtils; + +/** + * {@link IndexedData} implementation indicating storage of data within a Redis Set. + * + * @author Christoph Strobl + * @author Rob Winch + * @since 1.7 + */ +public class SimpleIndexedPropertyValue implements IndexedData { + + private final String keyspace; + private final String indexName; + private final Object value; + + /** + * Creates new {@link SimpleIndexedPropertyValue}. + * + * @param keyspace must not be {@literal null}. + * @param indexName must not be {@literal null}. + * @param value can be {@literal null}. + */ + public SimpleIndexedPropertyValue(String keyspace, String indexName, Object value) { + + this.keyspace = keyspace; + this.indexName = indexName; + this.value = value; + } + + /** + * Get the value to index. + * + * @return can be {@literal null}. + */ + public Object getValue() { + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.convert.IndexedData#getIndexName() + */ + @Override + public String getIndexName() { + return indexName; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.convert.IndexedData#getKeySpace() + */ + @Override + public String getKeyspace() { + return this.keyspace; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 1; + result += ObjectUtils.nullSafeHashCode(keyspace); + result += ObjectUtils.nullSafeHashCode(indexName); + result += ObjectUtils.nullSafeHashCode(value); + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof SimpleIndexedPropertyValue)) { + return false; + } + + SimpleIndexedPropertyValue that = (SimpleIndexedPropertyValue) obj; + + if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(this.indexName, that.indexName)) { + return false; + } + return ObjectUtils.nullSafeEquals(this.value, that.value); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "SimpleIndexedPropertyValue [keyspace=" + keyspace + ", indexName=" + indexName + ", value=" + value + "]"; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java new file mode 100644 index 0000000000..b4203f243e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java @@ -0,0 +1,142 @@ +/* + * 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.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider; +import org.springframework.data.redis.core.index.IndexDefinition; +import org.springframework.data.redis.core.index.SpelIndexDefinition; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; + +/** + * An {@link IndexResolver} that resolves {@link IndexedData} using a {@link SpelExpressionParser}. + * + * @author Rob Winch + * @author Christoph Strobl + * @since 1.7 + */ +public class SpelIndexResolver implements IndexResolver { + + private final ConfigurableIndexDefinitionProvider settings; + private final SpelExpressionParser parser; + private final RedisMappingContext mappingContext; + + private BeanResolver beanResolver; + + private Map expressionCache; + + /** + * Creates a new instance using a default {@link SpelExpressionParser}. + * + * @param mappingContext the {@link RedisMappingContext} to use. Cannot be null. + */ + public SpelIndexResolver(RedisMappingContext mappingContext) { + this(mappingContext, new SpelExpressionParser()); + } + + /** + * Creates a new instance + * + * @param mappingContext the {@link RedisMappingContext} to use. Cannot be null. + * @param parser the {@link SpelExpressionParser} to use. Cannot be null. + */ + public SpelIndexResolver(RedisMappingContext mappingContext, SpelExpressionParser parser) { + + Assert.notNull(mappingContext, "RedisMappingContext must not be null!"); + Assert.notNull(parser, "SpelExpressionParser must not be null!"); + this.mappingContext = mappingContext; + this.settings = mappingContext.getMappingConfiguration().getIndexConfiguration(); + this.expressionCache = new HashMap(); + this.parser = parser; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(org.springframework.data.util.TypeInformation, java.lang.Object) + */ + public Set resolveIndexesFor(TypeInformation typeInformation, Object value) { + + if (value == null) { + return Collections.emptySet(); + } + + KeyValuePersistentEntity entity = mappingContext.getPersistentEntity(typeInformation); + + if (entity == null) { + return Collections.emptySet(); + } + + String keyspace = entity.getKeySpace(); + + Set indexes = new HashSet(); + + for (IndexDefinition setting : settings.getIndexDefinitionsFor(keyspace)) { + + if (setting instanceof SpelIndexDefinition) { + + Expression expression = getAndCacheIfAbsent((SpelIndexDefinition) setting); + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(value); + context.setVariable("this", value); + + if (beanResolver != null) { + context.setBeanResolver(beanResolver); + } + + Object index = expression.getValue(context); + if (index != null) { + indexes.add(new SimpleIndexedPropertyValue(keyspace, setting.getIndexName(), index)); + } + } + } + + return indexes; + } + + private Expression getAndCacheIfAbsent(SpelIndexDefinition indexDefinition) { + + if (expressionCache.containsKey(indexDefinition)) { + return expressionCache.get(indexDefinition); + } + + Expression expression = parser.parseExpression(indexDefinition.getExpression()); + expressionCache.put(indexDefinition, expression); + return expression; + } + + /** + * Allows setting the BeanResolver + * + * @param beanResolver can be {@literal null}. + * @see BeanFactoryResolver + */ + public void setBeanResolver(BeanResolver beanResolver) { + this.beanResolver = beanResolver; + } +} diff --git a/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java b/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java new file mode 100644 index 0000000000..f1f478329c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java @@ -0,0 +1,27 @@ +/* + * 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.index; + +/** + * {@link IndexDefinitionProvider} that allows registering new {@link IndexDefinition} via + * {@link IndexDefinitionRegistry}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface ConfigurableIndexDefinitionProvider extends IndexDefinitionProvider, IndexDefinitionRegistry { + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java new file mode 100644 index 0000000000..614f996aa4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015-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.index; + +import java.io.Serializable; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * {@link IndexConfiguration} allows programmatic setup of indexes. This is suitable for cases where there is no option + * to use the equivalent {@link Indexed} annotation. + * + * @author Christoph Strobl + * @author Rob Winch + * @since 1.7 + */ +public class IndexConfiguration implements ConfigurableIndexDefinitionProvider { + + private final Set definitions; + + /** + * Creates new empty {@link IndexConfiguration}. + */ + public IndexConfiguration() { + + this.definitions = new CopyOnWriteArraySet(); + for (IndexDefinition initial : initialConfiguration()) { + addIndexDefinition(initial); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#hasIndexFor(java.io.Serializable) + */ + @Override + public boolean hasIndexFor(Serializable keyspace) { + return !getIndexDefinitionsFor(keyspace).isEmpty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#hasIndexFor(java.io.Serializable, java.lang.String) + */ + public boolean hasIndexFor(Serializable keyspace, String path) { + return !getIndexDefinitionsFor(keyspace, path).isEmpty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#getIndexDefinitionsFor(java.io.Serializable, java.lang.String) + */ + public Set getIndexDefinitionsFor(Serializable keyspace, String path) { + return getIndexDefinitions(keyspace, path, Object.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#getIndexDefinitionsFor(java.io.Serializable) + */ + public Set getIndexDefinitionsFor(Serializable keyspace) { + + Set indexDefinitions = new LinkedHashSet(); + + for (IndexDefinition indexDef : definitions) { + if (indexDef.getKeyspace().equals(keyspace)) { + indexDefinitions.add(indexDef); + } + } + + return indexDefinitions; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionRegistry#addIndexDefinition(org.springframework.data.redis.core.index.IndexDefinition) + */ + public void addIndexDefinition(IndexDefinition indexDefinition) { + + Assert.notNull(indexDefinition, "RedisIndexDefinition must not be null in order to be added."); + this.definitions.add(indexDefinition); + } + + private Set getIndexDefinitions(Serializable keyspace, String path, Class type) { + + Set def = new LinkedHashSet(); + for (IndexDefinition indexDef : definitions) { + if (ClassUtils.isAssignable(type, indexDef.getClass()) && indexDef.getKeyspace().equals(keyspace)) { + + if (indexDef instanceof PathBasedRedisIndexDefinition) { + if (ObjectUtils.nullSafeEquals(((PathBasedRedisIndexDefinition) indexDef).getPath(), path)) { + def.add(indexDef); + } + } else { + def.add(indexDef); + } + } + } + + return def; + } + + /** + * Customization hook. + * + * @return must not return {@literal null}. + */ + protected Iterable initialConfiguration() { + return Collections.emptySet(); + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java new file mode 100644 index 0000000000..a80587a8f7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java @@ -0,0 +1,94 @@ +/* + * 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.index; + +import java.util.Collection; + +import org.springframework.data.util.TypeInformation; + +/** + * {@link IndexDefinition} allow to set up a blueprint for creating secondary index structures in Redis. Setting up + * conditions allows to define {@link Condition} that have to be passed in order to add a value to the index. This + * allows to fine grained tune the index structure. {@link IndexValueTransformer} gets applied to the raw value for + * creating the actual index entry. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexDefinition { + + /** + * @return never {@literal null}. + */ + String getKeyspace(); + + /** + * @return never {@literal null}. + */ + Collection> getConditions(); + + /** + * @return never {@literal null}. + */ + IndexValueTransformer valueTransformer(); + + /** + * @return never {@literal null}. + */ + String getIndexName(); + + /** + * @author Christoph Strobl + * @since 1.7 + * @param + */ + public static interface Condition { + boolean matches(T value, IndexingContext context); + } + + /** + * Context in which a particular value is about to get indexed. + * + * @author Christoph Strobl + * @since 1.7 + */ + public class IndexingContext { + + private final String keyspace; + private final String path; + private final TypeInformation typeInformation; + + public IndexingContext(String keyspace, String path, TypeInformation typeInformation) { + + this.keyspace = keyspace; + this.path = path; + this.typeInformation = typeInformation; + } + + public String getKeyspace() { + return keyspace; + } + + public String getPath() { + return path; + } + + public TypeInformation getTypeInformation() { + return typeInformation; + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java new file mode 100644 index 0000000000..366386bebc --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java @@ -0,0 +1,62 @@ +/* + * 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.index; + +import java.io.Serializable; +import java.util.Set; + +/** + * {@link IndexDefinitionProvider} give access to {@link IndexDefinition}s for creating secondary index structures. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexDefinitionProvider { + + /** + * Gets all of the {@link RedisIndexSetting} for a given keyspace. + * + * @param keyspace the keyspace to get + * @return never {@literal null} + */ + boolean hasIndexFor(Serializable keyspace); + + /** + * Checks if an index is defined for a given keyspace and property path. + * + * @param keyspace + * @param path + * @return true if index is defined. + */ + boolean hasIndexFor(Serializable keyspace, String path); + + /** + * Get the list of {@link IndexDefinition} for a given keyspace. + * + * @param keyspace + * @return never {@literal null}. + */ + Set getIndexDefinitionsFor(Serializable keyspace); + + /** + * Get the list of {@link IndexDefinition} for a given keyspace and property path. + * + * @param keyspace + * @param path + * @return never {@literal null}. + */ + Set getIndexDefinitionsFor(Serializable keyspace, String path); +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java new file mode 100644 index 0000000000..6eba2274a7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java @@ -0,0 +1,32 @@ +/* + * 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.index; + +/** + * Registry that allows adding {@link IndexDefinition}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexDefinitionRegistry { + + /** + * Add given {@link RedisIndexSetting}. + * + * @param indexDefinition must not be {@literal null}. + */ + void addIndexDefinition(IndexDefinition indexDefinition); +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java b/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java new file mode 100644 index 0000000000..748e72ff3b --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java @@ -0,0 +1,28 @@ +/* + * 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.index; + +import org.springframework.core.convert.converter.Converter; + +/** + * {@link Converter} implementation that is used to transform values for usage in a particular secondary index. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexValueTransformer extends Converter { + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/Indexed.java b/src/main/java/org/springframework/data/redis/core/index/Indexed.java new file mode 100644 index 0000000000..563f21bf62 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/Indexed.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 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.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark properties value to be included in a secondary index.
+ * Uses Redos {@literal SET} for storage.
+ * The value will be part of the key built for the index. + * + * @author Christoph Strobl + * @since 1.7 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface Indexed { + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java new file mode 100644 index 0000000000..1411a9e342 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java @@ -0,0 +1,31 @@ +/* + * 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.index; + +/** + * {@link IndexDefinition} that is based on a property paths. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface PathBasedRedisIndexDefinition extends IndexDefinition { + + /** + * @return can be {@literal null}. + */ + String getPath(); + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java new file mode 100644 index 0000000000..96a628f4c5 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java @@ -0,0 +1,263 @@ +/* + * 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.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base {@link IndexDefinition} implementation. + * + * @author Christoph Strobl + * @since 1.7 + */ +public abstract class RedisIndexDefinition implements IndexDefinition { + + private final String keyspace; + private final String indexName; + private final String path; + private List> conditions; + private IndexValueTransformer valueTransformer; + + /** + * Creates new {@link RedisIndexDefinition}. + * + * @param keyspace + * @param path + * @param indexName + */ + protected RedisIndexDefinition(String keyspace, String path, String indexName) { + + this.keyspace = keyspace; + this.indexName = indexName; + this.path = path; + this.conditions = new ArrayList>(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#getKeyspace() + */ + @Override + public String getKeyspace() { + return keyspace; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#getConditions() + */ + @Override + public Collection> getConditions() { + return Collections.unmodifiableCollection(conditions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#valueTransformer() + */ + @Override + public IndexValueTransformer valueTransformer() { + return valueTransformer != null ? valueTransformer : NoOpValueTransformer.INSTANCE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#getIndexName() + */ + @Override + public String getIndexName() { + return indexName; + } + + public String getPath() { + return this.path; + } + + protected void addCondition(Condition condition) { + + Assert.notNull(condition, "Condition must not be null!"); + this.conditions.add(condition); + } + + public void setValueTransformer(IndexValueTransformer valueTransformer) { + this.valueTransformer = valueTransformer; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(indexName); + return result + ObjectUtils.nullSafeHashCode(keyspace); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof RedisIndexDefinition)) { + return false; + } + RedisIndexDefinition that = (RedisIndexDefinition) obj; + + if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) { + return false; + } + + return ObjectUtils.nullSafeEquals(this.indexName, that.indexName); + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static enum NoOpValueTransformer implements IndexValueTransformer { + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + return source; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static enum LowercaseIndexValueTransformer implements IndexValueTransformer { + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (!(source instanceof String)) { + return source; + } + + return ((String) source).toLowerCase(); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static class CompositeValueTransformer implements IndexValueTransformer { + + private final List transformers = new ArrayList(); + + public CompositeValueTransformer(Collection transformers) { + this.transformers.addAll(transformers); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + Object tmp = source; + for (IndexValueTransformer transformer : transformers) { + tmp = transformer.convert(tmp); + } + return tmp; + } + + } + + /** + * @author Christoph Strobl + * @since 1.7 + * @param + */ + public static class OrCondition implements Condition { + + private final List> conditions = new ArrayList>(); + + public OrCondition(Collection> conditions) { + this.conditions.addAll(conditions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition.Condition#matches(java.lang.Object, org.springframework.data.redis.core.index.IndexDefinition.IndexingContext) + */ + @Override + public boolean matches(T value, IndexingContext context) { + + for (Condition condition : conditions) { + if (condition.matches(value, context)) { + return true; + } + } + return false; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static class PathCondition implements Condition { + + private final String path; + + public PathCondition(String path) { + this.path = path; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition.Condition#matches(java.lang.Object, org.springframework.data.redis.core.index.IndexDefinition.IndexingContext) + */ + @Override + public boolean matches(Object value, IndexingContext context) { + + if (!StringUtils.hasText(path)) { + return true; + } + + return ObjectUtils.nullSafeEquals(context.getPath(), path); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java new file mode 100644 index 0000000000..31c3f7be4f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java @@ -0,0 +1,49 @@ +/* + * 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.index; + +/** + * {@link PathBasedRedisIndexDefinition} for including property values in a secondary index.
+ * Uses Redis {@literal SET} for storage.
+ * + * @author Christoph Strobl + * @since 1.7 + */ +public class SimpleIndexDefinition extends RedisIndexDefinition implements PathBasedRedisIndexDefinition { + + /** + * Creates new {@link SimpleIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param path + */ + public SimpleIndexDefinition(String keyspace, String path) { + this(keyspace, path, path); + } + + /** + * Creates new {@link SimpleIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param path + * @param name must not be {@literal null}. + */ + public SimpleIndexDefinition(String keyspace, String path, String name) { + super(keyspace, path, name); + addCondition(new PathCondition(path)); + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java new file mode 100644 index 0000000000..8eaadeff61 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java @@ -0,0 +1,84 @@ +/* + * 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.index; + +import org.springframework.data.redis.core.convert.SpelIndexResolver; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.util.ObjectUtils; + +/** + * {@link SpelIndexDefinition} defines index that is evaluated based on a {@link SpelExpression} requires the + * {@link SpelIndexResolver} to be evaluated. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class SpelIndexDefinition extends RedisIndexDefinition { + + private final String expression; + + /** + * Creates new {@link SpelIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param expression must not be {@literal null}. + * @param indexName must not be {@literal null}. + */ + public SpelIndexDefinition(String keyspace, String expression, String indexName) { + super(keyspace, null, indexName); + this.expression = expression; + } + + /** + * Get the raw expression. + * + * @return + */ + public String getExpression() { + return expression; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.RedisIndexDefinition#hashCode() + */ + @Override + public int hashCode() { + int result = super.hashCode(); + result += ObjectUtils.nullSafeHashCode(expression); + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.RedisIndexDefinition#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof SpelIndexDefinition)) { + return false; + } + SpelIndexDefinition that = (SpelIndexDefinition) obj; + return ObjectUtils.nullSafeEquals(this.expression, that.expression); + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java new file mode 100644 index 0000000000..b2579f32b3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015 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.mapping; + +import org.springframework.data.annotation.Id; +import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.TimeToLiveAccessor; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +/** + * {@link RedisPersistentEntity} implementation. + * + * @author Christoph Strobl + * @param + */ +public class BasicRedisPersistentEntity extends BasicKeyValuePersistentEntity + implements RedisPersistentEntity { + + private TimeToLiveAccessor timeToLiveAccessor; + + /** + * Creates new {@link BasicRedisPersistentEntity}. + * + * @param information must not be {@literal null}. + * @param fallbackKeySpaceResolver can be {@literal null}. + * @param timeToLiveResolver can be {@literal null}. + */ + public BasicRedisPersistentEntity(TypeInformation information, KeySpaceResolver fallbackKeySpaceResolver, + TimeToLiveAccessor timeToLiveAccessor) { + super(information, fallbackKeySpaceResolver); + + Assert.notNull(timeToLiveAccessor, "TimeToLiveAccessor must not be null"); + this.timeToLiveAccessor = timeToLiveAccessor; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.mapping.RedisPersistentEntity#getTimeToLiveAccessor() + */ + @Override + public TimeToLiveAccessor getTimeToLiveAccessor() { + return this.timeToLiveAccessor; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.BasicPersistentEntity#returnPropertyIfBetterIdPropertyCandidateOrNull(org.springframework.data.mapping.PersistentProperty) + */ + @Override + protected KeyValuePersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull( + KeyValuePersistentProperty property) { + + Assert.notNull(property); + + if (!property.isIdProperty()) { + return null; + } + + KeyValuePersistentProperty currentIdProperty = getIdProperty(); + boolean currentIdPropertyIsSet = currentIdProperty != null; + + if (!currentIdPropertyIsSet) { + return property; + } + + boolean currentIdPropertyIsExplicit = currentIdProperty.isAnnotationPresent(Id.class); + boolean newIdPropertyIsExplicit = property.isAnnotationPresent(Id.class); + + if (currentIdPropertyIsExplicit && newIdPropertyIsExplicit) { + throw new MappingException(String.format( + "Attempt to add explicit id property %s but already have an property %s registered " + + "as explicit id. Check your mapping configuration!", + property.getField(), currentIdProperty.getField())); + } + + if (!currentIdPropertyIsExplicit && !newIdPropertyIsExplicit) { + throw new MappingException( + String.format("Attempt to add id property %s but already have an property %s registered " + + "as id. Check your mapping configuration!", property.getField(), currentIdProperty.getField())); + } + + if (newIdPropertyIsExplicit) { + return property; + } + + return null; + } +} diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java new file mode 100644 index 0000000000..3017bda5fe --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java @@ -0,0 +1,371 @@ +/* + * Copyright 2015 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.mapping; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.keyvalue.annotation.KeySpace; +import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.TimeToLiveAccessor; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; +import org.springframework.data.redis.core.convert.MappingConfiguration; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodCallback; +import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.util.StringUtils; + +/** + * Redis specific {@link MappingContext}. + * + * @author Christoph Strobl + * @author Oliver Gierke + * @since 1.7 + */ +public class RedisMappingContext extends KeyValueMappingContext { + + private final MappingConfiguration mappingConfiguration; + private final TimeToLiveAccessor timeToLiveAccessor; + + private KeySpaceResolver fallbackKeySpaceResolver; + + /** + * Creates new {@link RedisMappingContext} with empty {@link MappingConfiguration}. + */ + public RedisMappingContext() { + this(new MappingConfiguration(new IndexConfiguration(), new KeyspaceConfiguration())); + } + + /** + * Creates new {@link RedisMappingContext}. + * + * @param mappingConfiguration can be {@literal null}. + */ + public RedisMappingContext(MappingConfiguration mappingConfiguration) { + + this.mappingConfiguration = mappingConfiguration != null ? mappingConfiguration : new MappingConfiguration( + new IndexConfiguration(), new KeyspaceConfiguration()); + + setFallbackKeySpaceResolver(new ConfigAwareKeySpaceResolver(this.mappingConfiguration.getKeyspaceConfiguration())); + this.timeToLiveAccessor = new ConfigAwareTimeToLiveAccessor(this.mappingConfiguration.getKeyspaceConfiguration(), + this); + } + + /** + * Configures the {@link KeySpaceResolver} to be used if not explicit key space is annotated to the domain type. + * + * @param fallbackKeySpaceResolver can be {@literal null}. + */ + public void setFallbackKeySpaceResolver(KeySpaceResolver fallbackKeySpaceResolver) { + this.fallbackKeySpaceResolver = fallbackKeySpaceResolver; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) + */ + @Override + protected RedisPersistentEntity createPersistentEntity(TypeInformation typeInformation) { + return new BasicRedisPersistentEntity(typeInformation, fallbackKeySpaceResolver, timeToLiveAccessor); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(java.lang.Class) + */ + @Override + public RedisPersistentEntity getPersistentEntity(Class type) { + return (RedisPersistentEntity) super.getPersistentEntity(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public RedisPersistentEntity getPersistentEntity(KeyValuePersistentProperty persistentProperty) { + return (RedisPersistentEntity) super.getPersistentEntity(persistentProperty); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation) + */ + @Override + public RedisPersistentEntity getPersistentEntity(TypeInformation type) { + return (RedisPersistentEntity) super.getPersistentEntity(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder) + */ + @Override + protected KeyValuePersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, + KeyValuePersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + return new RedisPersistentProperty(field, descriptor, owner, simpleTypeHolder); + } + + /** + * Get the {@link MappingConfiguration} used. + * + * @return never {@literal null}. + */ + public MappingConfiguration getMappingConfiguration() { + return mappingConfiguration; + } + + /** + * {@link KeySpaceResolver} implementation considering {@link KeySpace} and {@link KeyspaceConfiguration}. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class ConfigAwareKeySpaceResolver implements KeySpaceResolver { + + private final KeyspaceConfiguration keyspaceConfig; + + public ConfigAwareKeySpaceResolver(KeyspaceConfiguration keyspaceConfig) { + + this.keyspaceConfig = keyspaceConfig; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.mapping.KeySpaceResolver#resolveKeySpace(java.lang.Class) + */ + @Override + public String resolveKeySpace(Class type) { + + Assert.notNull(type, "Type must not be null!"); + if (keyspaceConfig.hasSettingsFor(type)) { + + String value = keyspaceConfig.getKeyspaceSettings(type).getKeyspace(); + if (StringUtils.hasText(value)) { + return value; + } + } + + return ClassNameKeySpaceResolver.INSTANCE.resolveKeySpace(type); + } + } + + /** + * {@link KeySpaceResolver} implementation considering {@link KeySpace}. + * + * @author Christoph Strobl + * @since 1.7 + */ + enum ClassNameKeySpaceResolver implements KeySpaceResolver { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.core.KeySpaceResolver#resolveKeySpace(java.lang.Class) + */ + @Override + public String resolveKeySpace(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return ClassUtils.getUserClass(type).getName(); + } + } + + /** + * {@link TimeToLiveAccessor} implementation considering {@link KeyspaceConfiguration}. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class ConfigAwareTimeToLiveAccessor implements TimeToLiveAccessor { + + private final Map, Long> defaultTimeouts; + private final Map, PersistentProperty> timeoutProperties; + private final Map, Method> timeoutMethods; + private final KeyspaceConfiguration keyspaceConfig; + private final RedisMappingContext mappingContext; + + /** + * Creates new {@link ConfigAwareTimeToLiveAccessor} + * + * @param keyspaceConfig must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + ConfigAwareTimeToLiveAccessor(KeyspaceConfiguration keyspaceConfig, RedisMappingContext mappingContext) { + + Assert.notNull(keyspaceConfig, "KeyspaceConfiguration must not be null!"); + Assert.notNull(mappingContext, "MappingContext must not be null!"); + + this.defaultTimeouts = new HashMap, Long>(); + this.timeoutProperties = new HashMap, PersistentProperty>(); + this.timeoutMethods = new HashMap, Method>(); + this.keyspaceConfig = keyspaceConfig; + this.mappingContext = mappingContext; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.TimeToLiveResolver#resolveTimeToLive(java.lang.Object) + */ + @Override + @SuppressWarnings({ "rawtypes" }) + public Long getTimeToLive(final Object source) { + + Assert.notNull(source, "Source must not be null!"); + Class type = source instanceof Class ? (Class) source : source.getClass(); + + Long defaultTimeout = resolveDefaultTimeOut(type); + TimeUnit unit = TimeUnit.SECONDS; + + PersistentProperty ttlProperty = resolveTtlProperty(type); + + if (ttlProperty != null) { + + if (ttlProperty.findAnnotation(TimeToLive.class) != null) { + unit = ttlProperty.findAnnotation(TimeToLive.class).unit(); + } + + RedisPersistentEntity entity = mappingContext.getPersistentEntity(type); + Long timeout = (Long) entity.getPropertyAccessor(source).getProperty(ttlProperty); + if (timeout != null) { + return TimeUnit.SECONDS.convert(timeout, unit); + } + } else { + + Method timeoutMethod = resolveTimeMethod(type); + if (timeoutMethod != null) { + + TimeToLive ttl = AnnotationUtils.findAnnotation(timeoutMethod, TimeToLive.class); + try { + Number timeout = (Number) timeoutMethod.invoke(source); + if (timeout != null) { + return TimeUnit.SECONDS.convert(timeout.longValue(), ttl.unit()); + } + } catch (IllegalAccessException e) { + throw new IllegalStateException("Not allowed to access method '" + timeoutMethod.getName() + "': " + + e.getMessage(), e); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Cannot invoke method '" + timeoutMethod.getName() + + " without arguments': " + e.getMessage(), e); + } catch (InvocationTargetException e) { + throw new IllegalStateException( + "Cannot access method '" + timeoutMethod.getName() + "': " + e.getMessage(), e); + } + } + } + + return defaultTimeout; + } + + private Long resolveDefaultTimeOut(Class type) { + + if (this.defaultTimeouts.containsKey(type)) { + return defaultTimeouts.get(type); + } + + Long defaultTimeout = null; + + if (keyspaceConfig.hasSettingsFor(type)) { + defaultTimeout = keyspaceConfig.getKeyspaceSettings(type).getTimeToLive(); + } + + RedisHash hash = mappingContext.getPersistentEntity(type).findAnnotation(RedisHash.class); + if (hash != null && hash.timeToLive() > 0) { + defaultTimeout = hash.timeToLive(); + } + + defaultTimeouts.put(type, defaultTimeout); + return defaultTimeout; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private PersistentProperty resolveTtlProperty(Class type) { + + if (timeoutProperties.containsKey(type)) { + return timeoutProperties.get(type); + } + + RedisPersistentEntity entity = mappingContext.getPersistentEntity(type); + PersistentProperty ttlProperty = entity.getPersistentProperty(TimeToLive.class); + + if (ttlProperty != null) { + + timeoutProperties.put(type, ttlProperty); + return ttlProperty; + } + + if (keyspaceConfig.hasSettingsFor(type)) { + + KeyspaceSettings settings = keyspaceConfig.getKeyspaceSettings(type); + if (StringUtils.hasText(settings.getTimeToLivePropertyName())) { + + ttlProperty = entity.getPersistentProperty(settings.getTimeToLivePropertyName()); + timeoutProperties.put(type, ttlProperty); + return ttlProperty; + } + } + + timeoutProperties.put(type, null); + return null; + } + + private Method resolveTimeMethod(final Class type) { + + if (timeoutMethods.containsKey(type)) { + return timeoutMethods.get(type); + } + + timeoutMethods.put(type, null); + ReflectionUtils.doWithMethods(type, new MethodCallback() { + + @Override + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + timeoutMethods.put(type, method); + } + }, new MethodFilter() { + + @Override + public boolean matches(Method method) { + return ClassUtils.isAssignable(Number.class, method.getReturnType()) + && AnnotationUtils.findAnnotation(method, TimeToLive.class) != null; + } + }); + + return timeoutMethods.get(type); + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java new file mode 100644 index 0000000000..14c6470a4d --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 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.mapping; + +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.redis.core.TimeToLiveAccessor; + +/** + * Redis specific {@link PersistentEntity}. + * + * @author Christoph Strobl + * @param + * @since 1.7 + */ +public interface RedisPersistentEntity extends KeyValuePersistentEntity { + + /** + * Get the {@link TimeToLiveAccessor} associated with the entity. + * + * @return never {@literal null}. + */ + TimeToLiveAccessor getTimeToLiveAccessor(); + +} diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java new file mode 100644 index 0000000000..f856053ca5 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-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.mapping; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Redis specific {@link PersistentProperty} implementation. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisPersistentProperty extends KeyValuePersistentProperty { + + private static final Set SUPPORTED_ID_PROPERTY_NAMES = new HashSet(); + + static { + SUPPORTED_ID_PROPERTY_NAMES.add("id"); + } + + /** + * Creates new {@link RedisPersistentProperty}. + * + * @param field + * @param propertyDescriptor + * @param owner + * @param simpleTypeHolder + */ + public RedisPersistentProperty(Field field, PropertyDescriptor propertyDescriptor, + PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + super(field, propertyDescriptor, owner, simpleTypeHolder); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.AnnotationBasedPersistentProperty#isIdProperty() + */ + @Override + public boolean isIdProperty() { + + if (super.isIdProperty()) { + return true; + } + + return SUPPORTED_ID_PROPERTY_NAMES.contains(getName()); + } +} diff --git a/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java b/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java new file mode 100644 index 0000000000..3c4b63066c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 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.listener; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisKeyExpiredEvent; + +/** + * {@link MessageListener} publishing {@link RedisKeyExpiredEvent}s via {@link ApplicationEventPublisher} by listening + * to Redis keyspace notifications for key expirations. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements + ApplicationEventPublisherAware { + + private ApplicationEventPublisher publisher; + private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired"); + + /** + * Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages. + * + * @param listenerContainer must not be {@literal null}. + */ + public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#doRegister(org.springframework.data.redis.listener.RedisMessageListenerContainer) + */ + @Override + protected void doRegister(RedisMessageListenerContainer listenerContainer) { + listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#doHandleMessage(org.springframework.data.redis.connection.Message) + */ + @Override + protected void doHandleMessage(Message message) { + publishEvent(new RedisKeyExpiredEvent(message.getBody())); + } + + /** + * Publish the event in case an {@link ApplicationEventPublisher} is set. + * + * @param event can be {@literal null}. + */ + protected void publishEvent(RedisKeyExpiredEvent event) { + + if (publisher != null && event != null) { + this.publisher.publishEvent(event); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) + */ + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } +} diff --git a/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java b/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java new file mode 100644 index 0000000000..ff73b0148f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015 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.listener; + +import java.util.List; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Base {@link MessageListener} implementation for listening to Redis keyspace notifications. + * + * @author Christoph Strobl + * @since 1.7 + */ +public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean { + + private final RedisMessageListenerContainer listenerContainer; + private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*"); + + /** + * Creates new {@link KeyspaceEventMessageListener}. + * + * @param listenerContainer must not be {@literal null}. + */ + public KeyspaceEventMessageListener(RedisMessageListenerContainer listenerContainer) { + + Assert.notNull(listenerContainer, "RedisMessageListenerContainer to run in must not be null!"); + this.listenerContainer = listenerContainer; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.MessageListener#onMessage(org.springframework.data.redis.connection.Message, byte[]) + */ + @Override + public void onMessage(Message message, byte[] pattern) { + + if (message == null || message.getChannel() == null || message.getBody() == null) { + return; + } + + doHandleMessage(message); + } + + /** + * Handle the actual message + * + * @param message never {@literal null}. + */ + protected abstract void doHandleMessage(Message message); + + /** + * Initialize the message listener by writing requried redis config for {@literal notify-keyspace-events} and + * registering the listener within the container. + */ + public void init() { + + RedisConnection connection = listenerContainer.getConnectionFactory().getConnection(); + List config = connection.getConfig("notify-keyspace-events"); + + if (config.size() == 2) { + + if (!StringUtils.hasText(config.get(1))) { + + // TODO more fine grained reaction on event configuration + connection.setConfig("notify-keyspace-events", "KEA"); + } + } + connection.close(); + + doRegister(listenerContainer); + } + + /** + * Register instance within the container. + * + * @param container never {@literal null}. + */ + protected void doRegister(RedisMessageListenerContainer container) { + listenerContainer.addMessageListener(this, TOPIC_ALL_KEYEVENTS); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + @Override + public void destroy() throws Exception { + listenerContainer.removeMessageListener(this); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() throws Exception { + init(); + } +} diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/CdiBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/CdiBean.java new file mode 100644 index 0000000000..6edb0fbe97 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/cdi/CdiBean.java @@ -0,0 +1,258 @@ +/* + * 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.repository.cdi; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.Stereotype; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.PassivationCapable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Base class for {@link Bean} wrappers. + * + * @author Mark Paluch + */ +public abstract class CdiBean implements Bean, PassivationCapable { + + private static final Logger LOGGER = LoggerFactory.getLogger(CdiBean.class); + + protected final BeanManager beanManager; + + private final Set qualifiers; + private final Set types; + private final Class beanClass; + private final String passivationId; + + /** + * Creates a new {@link CdiBean}. + * + * @param qualifiers must not be {@literal null}. + * @param beanClass has to be an interface must not be {@literal null}. + * @param beanManager the CDI {@link BeanManager}, must not be {@literal null}. + */ + public CdiBean(Set qualifiers, Class beanClass, BeanManager beanManager) { + this(qualifiers, Collections. emptySet(), beanClass, beanManager); + } + + /** + * Creates a new {@link CdiBean}. + * + * @param qualifiers must not be {@literal null}. + * @param types additional bean types, must not be {@literal null}. + * @param beanClass must not be {@literal null}. + * @param beanManager the CDI {@link BeanManager}, must not be {@literal null}. + */ + public CdiBean(Set qualifiers, Set types, Class beanClass, BeanManager beanManager) { + + Assert.notNull(qualifiers); + Assert.notNull(beanManager); + Assert.notNull(types); + Assert.notNull(beanClass); + + this.qualifiers = qualifiers; + this.types = types; + this.beanClass = beanClass; + this.beanManager = beanManager; + this.passivationId = createPassivationId(qualifiers, beanClass); + } + + /** + * Creates a unique identifier for the given repository type and the given annotations. + * + * @param qualifiers must not be {@literal null} or contain {@literal null} values. + * @param repositoryType must not be {@literal null}. + * @return + */ + private final String createPassivationId(Set qualifiers, Class repositoryType) { + + List qualifierNames = new ArrayList(qualifiers.size()); + + for (Annotation qualifier : qualifiers) { + qualifierNames.add(qualifier.annotationType().getName()); + } + + Collections.sort(qualifierNames); + + StringBuilder builder = new StringBuilder(StringUtils.collectionToDelimitedString(qualifierNames, ":")); + builder.append(":").append(repositoryType.getName()); + + return builder.toString(); + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#getTypes() + */ + public Set getTypes() { + + Set types = new HashSet(); + types.add(beanClass); + types.addAll(Arrays.asList(beanClass.getInterfaces())); + types.addAll(this.types); + + return types; + } + + /** + * Returns an instance of the given {@link Bean} from the container. + * + * @param the actual class type of the {@link Bean}. + * @param bean the {@link Bean} defining the instance to create. + * @param type the expected component type of the instance created from the {@link Bean}. + * @return an instance of the given {@link Bean}. + * @see javax.enterprise.inject.spi.BeanManager#getReference(Bean, Type, CreationalContext) + * @see javax.enterprise.inject.spi.Bean + * @see java.lang.reflect.Type + */ + @SuppressWarnings("unchecked") + protected S getDependencyInstance(Bean bean, Type type) { + return (S) beanManager.getReference(bean, type, beanManager.createCreationalContext(bean)); + } + + /** + * Forces the initialization of bean target. + */ + public final void initialize() { + create(beanManager.createCreationalContext(this)); + } + + /* + * (non-Javadoc) + * @see javax.enterprise.context.spi.Contextual#destroy(java.lang.Object, javax.enterprise.context.spi.CreationalContext) + */ + public void destroy(T instance, CreationalContext creationalContext) { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("Destroying bean instance %s for repository type '%s'.", instance.toString(), + beanClass.getName())); + } + + creationalContext.release(); + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#getQualifiers() + */ + public Set getQualifiers() { + return qualifiers; + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#getName() + */ + public String getName() { + + return getQualifiers().contains(Default.class) ? beanClass.getName() + : beanClass.getName() + "-" + getQualifiers().toString(); + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#getStereotypes() + */ + public Set> getStereotypes() { + + Set> stereotypes = new HashSet>(); + + for (Annotation annotation : beanClass.getAnnotations()) { + Class annotationType = annotation.annotationType(); + if (annotationType.isAnnotationPresent(Stereotype.class)) { + stereotypes.add(annotationType); + } + } + + return stereotypes; + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#getBeanClass() + */ + public Class getBeanClass() { + return beanClass; + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#isAlternative() + */ + public boolean isAlternative() { + return beanClass.isAnnotationPresent(Alternative.class); + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#isNullable() + */ + public boolean isNullable() { + return false; + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#getInjectionPoints() + */ + public Set getInjectionPoints() { + return Collections.emptySet(); + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.Bean#getScope() + */ + public Class getScope() { + return ApplicationScoped.class; + } + + /* + * (non-Javadoc) + * @see javax.enterprise.inject.spi.PassivationCapable#getId() + */ + public String getId() { + return passivationId; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("CdiBean: type='%s', qualifiers=%s", beanClass.getName(), qualifiers.toString()); + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueAdapterBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueAdapterBean.java new file mode 100644 index 0000000000..75b761cbe6 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueAdapterBean.java @@ -0,0 +1,106 @@ +/* + * 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.repository.cdi; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Set; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisKeyValueTemplate; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.util.Assert; + +/** + * {@link CdiBean} to create {@link RedisKeyValueAdapter} instances. + * + * @author Mark Paluch + */ +public class RedisKeyValueAdapterBean extends CdiBean { + + private final Bean> redisOperations; + + /** + * Creates a new {@link RedisKeyValueAdapterBean}. + * + * @param redisOperations must not be {@literal null}. + * @param qualifiers must not be {@literal null}. + * @param repositoryType must not be {@literal null}. + * @param beanManager must not be {@literal null}. + */ + public RedisKeyValueAdapterBean(Bean> redisOperations, Set qualifiers, + BeanManager beanManager) { + + super(qualifiers, RedisKeyValueAdapter.class, beanManager); + Assert.notNull(redisOperations); + this.redisOperations = redisOperations; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.cdi.CdiRepositoryBean#create(javax.enterprise.context.spi.CreationalContext, java.lang.Class) + */ + @Override + public RedisKeyValueAdapter create(CreationalContext creationalContext) { + + Type beanType = getBeanType(); + + RedisOperations redisOperations = getDependencyInstance(this.redisOperations, beanType); + RedisKeyValueAdapter redisKeyValueAdapter = new RedisKeyValueAdapter(redisOperations); + + return redisKeyValueAdapter; + } + + private Type getBeanType() { + + for (Type type : this.redisOperations.getTypes()) { + if (type instanceof Class && RedisOperations.class.isAssignableFrom((Class) type)) { + return type; + } + + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + if (parameterizedType.getRawType() instanceof Class + && RedisOperations.class.isAssignableFrom((Class) parameterizedType.getRawType())) { + return type; + } + } + } + throw new IllegalStateException("Cannot resolve bean type for class " + RedisOperations.class.getName()); + } + + @Override + public void destroy(RedisKeyValueAdapter instance, CreationalContext creationalContext) { + + if (instance instanceof DisposableBean) { + try { + instance.destroy(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + super.destroy(instance, creationalContext); + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueTemplateBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueTemplateBean.java new file mode 100644 index 0000000000..ac81599858 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueTemplateBean.java @@ -0,0 +1,88 @@ +/* + * 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.repository.cdi; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisKeyValueTemplate; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.util.Assert; + +/** + * {@link CdiBean} to create {@link RedisKeyValueTemplate} instances. + * + * @author Mark Paluch + */ +public class RedisKeyValueTemplateBean extends CdiBean { + + private final Bean keyValueAdapter; + + /** + * Creates a new {@link RedisKeyValueTemplateBean}. + * + * @param keyValueAdapter must not be {@literal null}. + * @param qualifiers must not be {@literal null}. + * @param beanManager must not be {@literal null}. + */ + public RedisKeyValueTemplateBean(Bean keyValueAdapter, Set qualifiers, + BeanManager beanManager) { + + super(qualifiers, KeyValueOperations.class, beanManager); + Assert.notNull(keyValueAdapter); + this.keyValueAdapter = keyValueAdapter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.cdi.CdiRepositoryBean#create(javax.enterprise.context.spi.CreationalContext, java.lang.Class) + */ + @Override + public KeyValueOperations create(CreationalContext creationalContext) { + + RedisKeyValueAdapter keyValueAdapter = getDependencyInstance(this.keyValueAdapter, RedisKeyValueAdapter.class); + + RedisMappingContext redisMappingContext = new RedisMappingContext(); + redisMappingContext.afterPropertiesSet(); + + RedisKeyValueTemplate redisKeyValueTemplate = new RedisKeyValueTemplate(keyValueAdapter, redisMappingContext); + return redisKeyValueTemplate; + } + + @Override + public void destroy(KeyValueOperations instance, CreationalContext creationalContext) { + + if (instance.getMappingContext() instanceof DisposableBean) { + try { + ((DisposableBean) instance.getMappingContext()).destroy(); + instance.destroy(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + super.destroy(instance, creationalContext); + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryBean.java new file mode 100644 index 0000000000..34ce963595 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryBean.java @@ -0,0 +1,73 @@ +/* + * 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.repository.cdi; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; + +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.redis.repository.query.RedisQueryCreator; +import org.springframework.data.redis.repository.support.RedisRepositoryFactory; +import org.springframework.data.repository.cdi.CdiRepositoryBean; +import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; +import org.springframework.util.Assert; + +/** + * {@link CdiRepositoryBean} to create Redis repository instances. + * + * @author Mark Paluch + */ +public class RedisRepositoryBean extends CdiRepositoryBean { + + private final Bean keyValueTemplate; + + /** + * Creates a new {@link CdiRepositoryBean}. + * + * @param keyValueTemplate must not be {@literal null}. + * @param qualifiers must not be {@literal null}. + * @param repositoryType must not be {@literal null}. + * @param beanManager must not be {@literal null}. + * @param detector detector for the custom {@link org.springframework.data.repository.Repository} implementations + * {@link CustomRepositoryImplementationDetector}, can be {@literal null}. + */ + public RedisRepositoryBean(Bean keyValueTemplate, Set qualifiers, + Class repositoryType, BeanManager beanManager, CustomRepositoryImplementationDetector detector) { + + super(qualifiers, repositoryType, beanManager, detector); + Assert.notNull(keyValueTemplate); + this.keyValueTemplate = keyValueTemplate; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.cdi.CdiRepositoryBean#create(javax.enterprise.context.spi.CreationalContext, java.lang.Class) + */ + @Override + protected T create(CreationalContext creationalContext, Class repositoryType, Object customImplementation) { + + KeyValueOperations keyValueTemplate = getDependencyInstance(this.keyValueTemplate, KeyValueOperations.class); + RedisRepositoryFactory factory = new RedisRepositoryFactory(keyValueTemplate, RedisQueryCreator.class); + + return factory.getRepository(repositoryType, customImplementation); + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryExtension.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryExtension.java new file mode 100644 index 0000000000..b3bd09c408 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryExtension.java @@ -0,0 +1,242 @@ +/* + * 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.repository.cdi; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.UnsatisfiedResolutionException; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.ProcessBean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisKeyValueTemplate; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.repository.cdi.CdiRepositoryBean; +import org.springframework.data.repository.cdi.CdiRepositoryExtensionSupport; + +/** + * CDI extension to export Redis repositories. This extension enables Redis + * {@link org.springframework.data.repository.Repository} support. It requires either a {@link RedisKeyValueTemplate} or a + * {@link RedisOperations} bean. If no {@link RedisKeyValueTemplate} or {@link RedisKeyValueAdapter} are provided by the + * user, the extension creates own managed beans. + * + * @author Mark Paluch + */ +public class RedisRepositoryExtension extends CdiRepositoryExtensionSupport { + + private static final Logger LOG = LoggerFactory.getLogger(RedisRepositoryExtension.class); + + private final Map, Bean> redisKeyValueAdapters = new HashMap, Bean>(); + private final Map, Bean> redisKeyValueTemplates = new HashMap, Bean>(); + private final Map, Bean>> redisOperations = new HashMap, Bean>>(); + + public RedisRepositoryExtension() { + LOG.info("Activating CDI extension for Spring Data Redis repositories."); + } + + /** + * Pick up existing bean definitions that are required for a Repository to work. + * + * @param processBean + * @param + */ + @SuppressWarnings("unchecked") + void processBean(@Observes ProcessBean processBean) { + + Bean bean = processBean.getBean(); + + for (Type type : bean.getTypes()) { + Type beanType = type; + + if (beanType instanceof ParameterizedType) { + beanType = ((ParameterizedType) beanType).getRawType(); + } + + if (beanType instanceof Class && RedisKeyValueTemplate.class.isAssignableFrom((Class) beanType)) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Discovered %s with qualifiers %s.", RedisKeyValueTemplate.class.getName(), + bean.getQualifiers())); + } + + // Store the Key-Value Templates bean using its qualifiers. + redisKeyValueTemplates.put(new HashSet(bean.getQualifiers()), (Bean) bean); + } + + if (beanType instanceof Class && RedisKeyValueAdapter.class.isAssignableFrom((Class) beanType)) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Discovered %s with qualifiers %s.", RedisKeyValueAdapter.class.getName(), + bean.getQualifiers())); + } + + // Store the RedisKeyValueAdapter bean using its qualifiers. + redisKeyValueAdapters.put(new HashSet(bean.getQualifiers()), (Bean) bean); + } + + if (beanType instanceof Class && RedisOperations.class.isAssignableFrom((Class) beanType)) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Discovered %s with qualifiers %s.", RedisOperations.class.getName(), + bean.getQualifiers())); + } + + // Store the RedisOperations bean using its qualifiers. + redisOperations.put(new HashSet(bean.getQualifiers()), (Bean>) bean); + } + } + } + + void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager) { + + registerDependenciesIfNecessary(afterBeanDiscovery, beanManager); + + for (Entry, Set> entry : getRepositoryTypes()) { + + Class repositoryType = entry.getKey(); + Set qualifiers = entry.getValue(); + + // Create the bean representing the repository. + CdiRepositoryBean repositoryBean = createRepositoryBean(repositoryType, qualifiers, beanManager); + + if (LOG.isInfoEnabled()) { + LOG.info(String.format("Registering bean for %s with qualifiers %s.", repositoryType.getName(), qualifiers)); + } + + // Register the bean to the container. + registerBean(repositoryBean); + afterBeanDiscovery.addBean(repositoryBean); + } + } + + /** + * Register {@link RedisKeyValueAdapter} and {@link RedisKeyValueTemplate} if these beans are not provided by the CDI + * application. + * + * @param afterBeanDiscovery + * @param beanManager + */ + private void registerDependenciesIfNecessary(@Observes AfterBeanDiscovery afterBeanDiscovery, + BeanManager beanManager) { + + for (Entry, Set> entry : getRepositoryTypes()) { + + Set qualifiers = entry.getValue(); + + if (!redisKeyValueAdapters.containsKey(qualifiers)) { + if (LOG.isInfoEnabled()) { + LOG.info(String.format("Registering bean for %s with qualifiers %s.", RedisKeyValueAdapter.class.getName(), + qualifiers)); + } + RedisKeyValueAdapterBean redisKeyValueAdapterBean = createRedisKeyValueAdapterBean(qualifiers, beanManager); + redisKeyValueAdapters.put(qualifiers, redisKeyValueAdapterBean); + afterBeanDiscovery.addBean(redisKeyValueAdapterBean); + } + + if (!redisKeyValueTemplates.containsKey(qualifiers)) { + if (LOG.isInfoEnabled()) { + LOG.info(String.format("Registering bean for %s with qualifiers %s.", RedisKeyValueTemplate.class.getName(), + qualifiers)); + } + + RedisKeyValueTemplateBean redisKeyValueTemplateBean = createRedisKeyValueTemplateBean(qualifiers, beanManager); + redisKeyValueTemplates.put(qualifiers, redisKeyValueTemplateBean); + afterBeanDiscovery.addBean(redisKeyValueTemplateBean); + } + } + } + + /** + * Creates a {@link CdiRepositoryBean} for the repository of the given type, requires a {@link KeyValueOperations} + * bean with the same qualifiers. + * + * @param the type of the repository. + * @param repositoryType the class representing the repository. + * @param qualifiers the qualifiers to be applied to the bean. + * @param beanManager the BeanManager instance. + * @return + */ + private CdiRepositoryBean createRepositoryBean(Class repositoryType, Set qualifiers, + BeanManager beanManager) { + + // Determine the MongoOperations bean which matches the qualifiers of the repository. + Bean redisKeyValueTemplate = this.redisKeyValueTemplates.get(qualifiers); + + if (redisKeyValueTemplate == null) { + throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.", + RedisKeyValueTemplate.class.getName(), qualifiers)); + } + + // Construct and return the repository bean. + return new RedisRepositoryBean(redisKeyValueTemplate, qualifiers, repositoryType, beanManager, + getCustomImplementationDetector()); + } + + /** + * Creates a {@link RedisKeyValueAdapterBean}, requires a {@link RedisOperations} bean with the same qualifiers. + * + * @param qualifiers the qualifiers to be applied to the bean. + * @param beanManager the BeanManager instance. + * @return + */ + private RedisKeyValueAdapterBean createRedisKeyValueAdapterBean(Set qualifiers, BeanManager beanManager) { + + // Determine the MongoOperations bean which matches the qualifiers of the repository. + Bean> redisOperationsBean = this.redisOperations.get(qualifiers); + + if (redisOperationsBean == null) { + throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.", + RedisOperations.class.getName(), qualifiers)); + } + + // Construct and return the repository bean. + return new RedisKeyValueAdapterBean(redisOperationsBean, qualifiers, beanManager); + } + + /** + * Creates a {@link RedisKeyValueTemplateBean}, requires a {@link RedisKeyValueAdapter} bean with the same qualifiers. + * + * @param qualifiers the qualifiers to be applied to the bean. + * @param beanManager the BeanManager instance. + * @return + */ + private RedisKeyValueTemplateBean createRedisKeyValueTemplateBean(Set qualifiers, + BeanManager beanManager) { + + // Determine the MongoOperations bean which matches the qualifiers of the repository. + Bean redisKeyValueAdapterBean = this.redisKeyValueAdapters.get(qualifiers); + + if (redisKeyValueAdapterBean == null) { + throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.", + RedisKeyValueAdapter.class.getName(), qualifiers)); + } + + // Construct and return the repository bean. + return new RedisKeyValueTemplateBean(redisKeyValueAdapterBean, qualifiers, beanManager); + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/package-info.java b/src/main/java/org/springframework/data/redis/repository/cdi/package-info.java new file mode 100644 index 0000000000..85bc35bf6c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/cdi/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * CDI support for Redis specific repository implementation. + */ +package org.springframework.data.redis.repository.cdi; diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java new file mode 100644 index 0000000000..329a3d6520 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java @@ -0,0 +1,157 @@ +/* + * Copyright 2015 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.repository.configuration; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.repository.config.QueryCreatorType; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.repository.query.RedisQueryCreator; +import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean; +import org.springframework.data.repository.config.DefaultRepositoryBaseClass; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; + +/** + * Annotation to activate Redis repositories. If no base package is configured through either {@link #value()}, + * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class. + * + * @author Christoph Strobl + * @since 1.7 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(RedisRepositoriesRegistrar.class) +@QueryCreatorType(RedisQueryCreator.class) +public @interface EnableRedisRepositories { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: + * {@code @EnableRedisRepositories("org.my.pkg")} instead of {@code @EnableRedisRepositories(basePackages="org.my.pkg")}. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this + * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The + * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in + * each package that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + + /** + * Specifies which types are not eligible for component scanning. + */ + Filter[] excludeFilters() default {}; + + /** + * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from + * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. + */ + Filter[] includeFilters() default {}; + + /** + * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So + * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning + * for {@code PersonRepositoryImpl}. + * + * @return + */ + String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures the location of where to find the Spring Data named queries properties file. + * + * @return + */ + String namedQueriesLocation() default ""; + + /** + * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to + * {@link Key#CREATE_IF_NOT_FOUND}. + * + * @return + */ + Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; + + /** + * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to + * {@link RedisRepositoryFactoryBean}. + * + * @return + */ + Class repositoryFactoryBeanClass() default RedisRepositoryFactoryBean.class; + + /** + * Configure the repository base class to be used to create repository proxies for this particular configuration. + * + * @return + */ + Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; + + /** + * Configures the name of the {@link KeyValueOperations} bean to be used with the repositories detected. + * + * @return + */ + String keyValueTemplateRef() default "redisKeyValueTemplate"; + + /** + * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the + * repositories infrastructure. + */ + boolean considerNestedRepositories() default false; + + /** + * Configures the bean name of the {@link RedisOperations} to be used. Defaulted to {@literal redisTemplate}. + * + * @return + */ + String redisTemplateRef() default "redisTemplate"; + + /** + * Set up index patterns using simple configuration class. + * + * @return + */ + Class indexConfiguration() default IndexConfiguration.class; + + /** + * Set up keyspaces for specific types. + * + * @return + */ + Class keyspaceConfiguration() default KeyspaceConfiguration.class; + +} diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrar.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrar.java new file mode 100644 index 0000000000..701c034651 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrar.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 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.repository.configuration; + +import java.lang.annotation.Annotation; + +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * Redis specific {@link ImportBeanDefinitionRegistrar}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableRedisRepositories.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension() + */ + @Override + protected RepositoryConfigurationExtension getExtension() { + return new RedisRepositoryConfigurationExtension(); + } +} diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java new file mode 100644 index 0000000000..fd34a64f14 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java @@ -0,0 +1,224 @@ +/* + * Copyright 2015 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.repository.configuration; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisKeyValueTemplate; +import org.springframework.data.redis.core.convert.CustomConversions; +import org.springframework.data.redis.core.convert.MappingConfiguration; +import org.springframework.data.redis.core.convert.MappingRedisConverter; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.util.StringUtils; + +/** + * {@link RepositoryConfigurationExtension} for Redis. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension { + + private static final String REDIS_CONVERTER_BEAN_NAME = "redisConverter"; + private static final String REDIS_REFERENCE_RESOLVER_BEAN_NAME = "redisReferenceResolver"; + private static final String REDIS_ADAPTER_BEAN_NAME = "redisKeyValueAdapter"; + private static final String REDIS_CUSTOM_CONVERSIONS_BEAN_NAME = "redisCustomConversions"; + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getModuleName() + */ + @Override + public String getModuleName() { + return "Redis"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getModulePrefix() + */ + @Override + protected String getModulePrefix() { + return "redis"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getDefaultKeyValueTemplateRef() + */ + @Override + protected String getDefaultKeyValueTemplateRef() { + return "redisKeyValueTemplate"; + } + + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) { + + String redisTemplateRef = configurationSource.getAttribute("redisTemplateRef"); + + RootBeanDefinition mappingContextDefinition = createRedisMappingContext(configurationSource); + mappingContextDefinition.setSource(configurationSource.getSource()); + + registerIfNotAlreadyRegistered(mappingContextDefinition, registry, MAPPING_CONTEXT_BEAN_NAME, configurationSource); + + // register coustom conversions + RootBeanDefinition customConversions = new RootBeanDefinition(CustomConversions.class); + registerIfNotAlreadyRegistered(customConversions, registry, REDIS_CUSTOM_CONVERSIONS_BEAN_NAME, + configurationSource); + + // Register referenceResolver + RootBeanDefinition redisReferenceResolver = createRedisReferenceResolverDefinition(redisTemplateRef); + redisReferenceResolver.setSource(configurationSource.getSource()); + registerIfNotAlreadyRegistered(redisReferenceResolver, registry, REDIS_REFERENCE_RESOLVER_BEAN_NAME, + configurationSource); + + // Register converter + RootBeanDefinition redisConverterDefinition = createRedisConverterDefinition(); + redisConverterDefinition.setSource(configurationSource.getSource()); + + registerIfNotAlreadyRegistered(redisConverterDefinition, registry, REDIS_CONVERTER_BEAN_NAME, configurationSource); + + // register Adapter + RootBeanDefinition redisKeyValueAdapterDefinition = new RootBeanDefinition(RedisKeyValueAdapter.class); + + ConstructorArgumentValues constructorArgumentValuesForRedisKeyValueAdapter = new ConstructorArgumentValues(); + if (StringUtils.hasText(redisTemplateRef)) { + + constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(0, + new RuntimeBeanReference(redisTemplateRef)); + } + + constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(1, + new RuntimeBeanReference(REDIS_CONVERTER_BEAN_NAME)); + + redisKeyValueAdapterDefinition.setConstructorArgumentValues(constructorArgumentValuesForRedisKeyValueAdapter); + registerIfNotAlreadyRegistered(redisKeyValueAdapterDefinition, registry, REDIS_ADAPTER_BEAN_NAME, + configurationSource); + + super.registerBeansForRoot(registry, configurationSource); + } + + private RootBeanDefinition createRedisReferenceResolverDefinition(String redisTemplateRef) { + + RootBeanDefinition beanDef = new RootBeanDefinition(); + beanDef.setBeanClassName("org.springframework.data.redis.core.convert.ReferenceResolverImpl"); + + ConstructorArgumentValues constructorArgs = new ConstructorArgumentValues(); + constructorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference(redisTemplateRef)); + + beanDef.setConstructorArgumentValues(constructorArgs); + + return beanDef; + } + + private RootBeanDefinition createRedisMappingContext(RepositoryConfigurationSource configurationSource) { + + ConstructorArgumentValues mappingContextArgs = new ConstructorArgumentValues(); + mappingContextArgs.addIndexedArgumentValue(0, createMappingConfigBeanDef(configurationSource)); + + RootBeanDefinition mappingContextBeanDef = new RootBeanDefinition(RedisMappingContext.class); + mappingContextBeanDef.setConstructorArgumentValues(mappingContextArgs); + + return mappingContextBeanDef; + } + + private BeanDefinition createMappingConfigBeanDef(RepositoryConfigurationSource configurationSource) { + + DirectFieldAccessor dfa = new DirectFieldAccessor(configurationSource); + AnnotationAttributes aa = (AnnotationAttributes) dfa.getPropertyValue("attributes"); + + GenericBeanDefinition indexConfiguration = new GenericBeanDefinition(); + indexConfiguration.setBeanClass(aa.getClass("indexConfiguration")); + + GenericBeanDefinition keyspaceConfig = new GenericBeanDefinition(); + keyspaceConfig.setBeanClass(aa.getClass("keyspaceConfiguration")); + + ConstructorArgumentValues mappingConfigArgs = new ConstructorArgumentValues(); + mappingConfigArgs.addIndexedArgumentValue(0, indexConfiguration); + mappingConfigArgs.addIndexedArgumentValue(1, keyspaceConfig); + + GenericBeanDefinition mappingConfigBeanDef = new GenericBeanDefinition(); + mappingConfigBeanDef.setBeanClass(MappingConfiguration.class); + mappingConfigBeanDef.setConstructorArgumentValues(mappingConfigArgs); + + return mappingConfigBeanDef; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getDefaultKeyValueTemplateBeanDefinition(org.springframework.data.repository.config.RepositoryConfigurationSource) + */ + @Override + protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( + RepositoryConfigurationSource configurationSource) { + + RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition(RedisKeyValueTemplate.class); + + ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues(); + constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(0, + new RuntimeBeanReference(REDIS_ADAPTER_BEAN_NAME)); + constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(1, + new RuntimeBeanReference(MAPPING_CONTEXT_BEAN_NAME)); + + keyValueTemplateDefinition.setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate); + + return keyValueTemplateDefinition; + } + + private RootBeanDefinition createRedisConverterDefinition() { + + RootBeanDefinition beanDef = new RootBeanDefinition(); + beanDef.setBeanClass(MappingRedisConverter.class); + + ConstructorArgumentValues args = new ConstructorArgumentValues(); + args.addIndexedArgumentValue(0, new RuntimeBeanReference(MAPPING_CONTEXT_BEAN_NAME)); + beanDef.setConstructorArgumentValues(args); + + MutablePropertyValues props = new MutablePropertyValues(); + props.add("referenceResolver", new RuntimeBeanReference(REDIS_REFERENCE_RESOLVER_BEAN_NAME)); + props.add("customConversions", new RuntimeBeanReference(REDIS_CUSTOM_CONVERSIONS_BEAN_NAME)); + beanDef.setPropertyValues(props); + + return beanDef; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingAnnotations() + */ + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.> singleton(RedisHash.class); + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java b/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java new file mode 100644 index 0000000000..74f538e672 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java @@ -0,0 +1,53 @@ +/* + * 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.repository.core; + +import java.io.Serializable; + +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.repository.core.support.PersistentEntityInformation; + +/** + * {@link RedisEntityInformation} implementation using a {@link MongoPersistentEntity} instance to lookup the necessary + * information. Can be configured with a custom collection to be returned which will trump the one returned by the + * {@link MongoPersistentEntity} if given. + * + * @author Christoph Strobl + * @param + * @param + */ +public class MappingRedisEntityInformation + extends PersistentEntityInformation implements RedisEntityInformation { + + private final RedisPersistentEntity entityMetadata; + + /** + * @param entity + */ + public MappingRedisEntityInformation(RedisPersistentEntity entity) { + super(entity); + + this.entityMetadata = entity; + + if (!entityMetadata.hasIdProperty()) { + + throw new MappingException( + String.format("Entity %s requires to have an explicit id field. Did you forget to provide one using @Id?", + entity.getName())); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java b/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java new file mode 100644 index 0000000000..804e1d4c9e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java @@ -0,0 +1,29 @@ +/* + * 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.repository.core; + +import java.io.Serializable; + +import org.springframework.data.repository.core.EntityInformation; + +/** + * @author Christoph Strobl + * @param + * @param + */ +public interface RedisEntityInformation extends EntityInformation { + +} diff --git a/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java b/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java new file mode 100644 index 0000000000..f272052bc5 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015 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.repository.query; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.util.ObjectUtils; + +/** + * Simple set of operations requried to run queries against Redis. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisOperationChain { + + private Set sismember = new LinkedHashSet(); + private Set orSismember = new LinkedHashSet(); + + public void sismember(String path, Object value) { + sismember(new PathAndValue(path, value)); + } + + public void sismember(PathAndValue pathAndValue) { + sismember.add(pathAndValue); + } + + public Set getSismember() { + return sismember; + } + + public void orSismember(String path, Object value) { + orSismember(new PathAndValue(path, value)); + } + + public void orSismember(PathAndValue pathAndValue) { + orSismember.add(pathAndValue); + } + + public void orSismember(Collection next) { + orSismember.addAll(next); + } + + public Set getOrSismember() { + return orSismember; + } + + public static class PathAndValue { + + private final String path; + private final Collection values; + + public PathAndValue(String path, Object singleValue) { + + this.path = path; + this.values = Collections.singleton(singleValue); + } + + public PathAndValue(String path, Collection values) { + + this.path = path; + this.values = values != null ? values : Collections.emptySet(); + } + + public boolean isSingleValue() { + return values.size() == 1; + } + + public String getPath() { + return path; + } + + public Collection values() { + return values; + } + + public Object getFirstValue() { + return values.isEmpty() ? null : values.iterator().next(); + } + + @Override + public String toString() { + return path + ":" + (isSingleValue() ? getFirstValue() : values); + } + + @Override + public int hashCode() { + + int result = ObjectUtils.nullSafeHashCode(path); + result += ObjectUtils.nullSafeHashCode(values); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof PathAndValue)) { + return false; + } + PathAndValue that = (PathAndValue) obj; + if (!ObjectUtils.nullSafeEquals(this.path, that.path)) { + return false; + } + + return ObjectUtils.nullSafeEquals(this.values, that.values); + } + + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java b/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java new file mode 100644 index 0000000000..b051dc3160 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015 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.repository.query; + +import java.util.Iterator; + +import org.springframework.data.domain.Sort; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * Redis specific query creator. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisQueryCreator extends AbstractQueryCreator, RedisOperationChain> { + + public RedisQueryCreator(PartTree tree, ParameterAccessor parameters) { + super(tree, parameters); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator) + */ + @Override + protected RedisOperationChain create(Part part, Iterator iterator) { + return from(part, iterator, new RedisOperationChain()); + } + + private RedisOperationChain from(Part part, Iterator iterator, RedisOperationChain sink) { + + switch (part.getType()) { + case SIMPLE_PROPERTY: + sink.sismember(part.getProperty().toDotPath(), iterator.next()); + break; + default: + throw new IllegalArgumentException(part.getType() + "is not supported for redis query derivation"); + } + + return sink; + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator) + */ + @Override + protected RedisOperationChain and(Part part, RedisOperationChain base, Iterator iterator) { + return from(part, iterator, base); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object) + */ + @Override + protected RedisOperationChain or(RedisOperationChain base, RedisOperationChain criteria) { + base.orSismember(criteria.getSismember()); + return base; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort) + */ + @Override + protected KeyValueQuery complete(final RedisOperationChain criteria, Sort sort) { + + KeyValueQuery query = new KeyValueQuery(criteria); + + if (query.getCritieria().getSismember().size() == 1 && query.getCritieria().getOrSismember().size() == 1) { + + query.getCritieria().getOrSismember().add(query.getCritieria().getSismember().iterator().next()); + query.getCritieria().getSismember().clear(); + } + + if (sort != null) { + query.setSort(sort); + } + + return query; + } + +} diff --git a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java new file mode 100644 index 0000000000..0fa0dc1911 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright 2015-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.repository.support; + +import java.io.Serializable; + +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; +import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.redis.repository.core.MappingRedisEntityInformation; +import org.springframework.data.redis.repository.query.RedisQueryCreator; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; + +/** + * {@link RepositoryFactorySupport} specific of handing Redis + * {@link org.springframework.data.keyvalue.repository.KeyValueRepository}. + * + * @author Christoph Strobl + * @author Oliver Gierke + * @since 1.7 + */ +public class RedisRepositoryFactory extends KeyValueRepositoryFactory { + + private final KeyValueOperations operations; + + /** + * @param keyValueOperations + * @see KeyValueRepositoryFactory#KeyValueRepositoryFactory(KeyValueOperations) + */ + public RedisRepositoryFactory(KeyValueOperations keyValueOperations) { + this(keyValueOperations, RedisQueryCreator.class); + } + + /** + * @param keyValueOperations + * @param queryCreator + * @see KeyValueRepositoryFactory#KeyValueRepositoryFactory(KeyValueOperations, Class) + */ + public RedisRepositoryFactory(KeyValueOperations keyValueOperations, + Class> queryCreator) { + this(keyValueOperations, queryCreator, KeyValuePartTreeQuery.class); + } + + /** + * @param keyValueOperations + * @param queryCreator + * @param repositoryQueryType + * @see KeyValueRepositoryFactory#KeyValueRepositoryFactory(KeyValueOperations, Class, Class) + */ + public RedisRepositoryFactory(KeyValueOperations keyValueOperations, + Class> queryCreator, Class repositoryQueryType) { + super(keyValueOperations, queryCreator, repositoryQueryType); + + this.operations = keyValueOperations; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory#getEntityInformation(java.lang.Class) + */ + @Override + @SuppressWarnings("unchecked") + public EntityInformation getEntityInformation(Class domainClass) { + + RedisPersistentEntity entity = (RedisPersistentEntity) operations.getMappingContext() + .getPersistentEntity(domainClass); + EntityInformation entityInformation = (EntityInformation) new MappingRedisEntityInformation( + entity); + + return entityInformation; + } +} diff --git a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java new file mode 100644 index 0000000000..3dcc8fb7b3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-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.repository.support; + +import java.io.Serializable; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; + +/** + * Adapter for Springs {@link FactoryBean} interface to allow easy setup of {@link RedisRepositoryFactory} via Spring + * configuration. + * + * @author Christoph Strobl + * @param The repository type. + * @param The repository domain type. + * @param The repository id type. + * @since 1.7 + */ +public class RedisRepositoryFactoryBean, S, ID extends Serializable> + extends KeyValueRepositoryFactoryBean { + + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean#createRepositoryFactory(org.springframework.data.keyvalue.core.KeyValueOperations, java.lang.Class, java.lang.Class) + */ + @Override + protected RedisRepositoryFactory createRepositoryFactory(KeyValueOperations operations, + Class> queryCreator, Class repositoryQueryType) { + return new RedisRepositoryFactory(operations, queryCreator, repositoryQueryType); + } +} diff --git a/src/main/java/org/springframework/data/redis/util/ByteUtils.java b/src/main/java/org/springframework/data/redis/util/ByteUtils.java new file mode 100644 index 0000000000..1c9235dc29 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/util/ByteUtils.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-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.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Some handy methods for dealing with byte arrays. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 1.7 + */ +public final class ByteUtils { + + private ByteUtils() {} + + public static byte[] concat(byte[] arg1, byte[] arg2) { + + byte[] result = Arrays.copyOf(arg1, arg1.length + arg2.length); + System.arraycopy(arg2, 0, result, arg1.length, arg2.length); + + return result; + } + + public static byte[] concatAll(byte[]... args) { + + if (args.length == 0) { + return new byte[] {}; + } + if (args.length == 1) { + return args[0]; + } + + byte[] cur = concat(args[0], args[1]); + for (int i = 2; i < args.length; i++) { + cur = concat(cur, args[i]); + } + return cur; + } + + public static byte[][] split(byte[] source, int c) { + + if (source == null || source.length == 0) { + return new byte[][] {}; + } + + List bytes = new ArrayList(); + int offset = 0; + for (int i = 0; i <= source.length; i++) { + + if (i == source.length) { + + bytes.add(Arrays.copyOfRange(source, offset, i)); + break; + } + + if (source[i] == c) { + bytes.add(Arrays.copyOfRange(source, offset, i)); + offset = i + 1; + } + } + return bytes.toArray(new byte[bytes.size()][]); + } + + /** + * Merge multiple {@code byte} arrays into one array + * + * @param firstArray must not be {@literal null} + * @param additionalArrays must not be {@literal null} + * @return + */ + public static byte[][] mergeArrays(byte[] firstArray, byte[]... additionalArrays) { + + Assert.notNull(firstArray, "first array must not be null"); + Assert.notNull(additionalArrays, "additional arrays must not be null"); + + byte[][] result = new byte[additionalArrays.length + 1][]; + result[0] = firstArray; + System.arraycopy(additionalArrays, 0, result, 1, additionalArrays.length); + + return result; + } +} diff --git a/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 0000000000..40107ebc18 --- /dev/null +++ b/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +org.springframework.data.redis.repository.cdi.RedisRepositoryExtension \ No newline at end of file diff --git a/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java b/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java new file mode 100644 index 0000000000..ba512f9a18 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2015-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; + +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.convert.IndexedData; +import org.springframework.data.redis.core.convert.MappingRedisConverter; +import org.springframework.data.redis.core.convert.PathIndexResolver; +import org.springframework.data.redis.core.convert.ReferenceResolver; +import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + * @auhtor Rob Winch + */ +@RunWith(MockitoJUnitRunner.class) +public class IndexWriterUnitTests { + + private static final Charset CHARSET = Charset.forName("UTF-8"); + private static final String KEYSPACE = "persons"; + private static final String KEY = "key-1"; + private static final byte[] KEY_BIN = KEY.getBytes(CHARSET); + IndexWriter writer; + MappingRedisConverter converter; + + @Mock RedisConnection connectionMock; + @Mock ReferenceResolver referenceResolverMock; + + @Before + public void setUp() { + + converter = new MappingRedisConverter(new RedisMappingContext(), new PathIndexResolver(), referenceResolverMock); + converter.afterPropertiesSet(); + + writer = new IndexWriter(connectionMock, converter); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void addKeyToIndexShouldInvokeSaddCorrectly() { + + writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", "Rand")); + + verify(connectionMock).sAdd(eq("persons:firstname:Rand".getBytes(CHARSET)), eq(KEY_BIN)); + verify(connectionMock).sAdd(eq("persons:key-1:idx".getBytes(CHARSET)), + eq("persons:firstname:Rand".getBytes(CHARSET))); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void addKeyToIndexShouldThrowErrorWhenIndexedDataIsNull() { + writer.addKeyToIndex(KEY_BIN, null); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void removeKeyFromExistingIndexesShouldCheckForExistingIndexesForPath() { + + writer.removeKeyFromExistingIndexes(KEY_BIN, new StubIndxedData()); + + verify(connectionMock).keys(eq(("persons:address.city:*").getBytes(CHARSET))); + verifyNoMoreInteractions(connectionMock); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void removeKeyFromExistingIndexesShouldRemoveKeyFromAllExistingIndexesForPath() { + + byte[] indexKey1 = "persons:firstname:rand".getBytes(CHARSET); + byte[] indexKey2 = "persons:firstname:mat".getBytes(CHARSET); + + when(connectionMock.keys(any(byte[].class))).thenReturn( + new LinkedHashSet(Arrays.asList(indexKey1, indexKey2))); + + writer.removeKeyFromExistingIndexes(KEY_BIN, new StubIndxedData()); + + verify(connectionMock).sRem(indexKey1, KEY_BIN); + verify(connectionMock).sRem(indexKey2, KEY_BIN); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void removeKeyFromExistingIndexesShouldThrowExecptionForNullIndexedData() { + writer.removeKeyFromExistingIndexes(KEY_BIN, null); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void removeAllIndexesShouldDeleteAllIndexKeys() { + + byte[] indexKey1 = "persons:firstname:rand".getBytes(CHARSET); + byte[] indexKey2 = "persons:firstname:mat".getBytes(CHARSET); + + when(connectionMock.keys(any(byte[].class))).thenReturn( + new LinkedHashSet(Arrays.asList(indexKey1, indexKey2))); + + writer.removeAllIndexes(KEYSPACE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class); + + verify(connectionMock, times(1)).del(captor.capture()); + assertThat(captor.getAllValues(), hasItems(indexKey1, indexKey2)); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void addToIndexShouldThrowDataAccessExceptionWhenAddingDataThatConnotBeConverted() { + writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", new DummyObject())); + + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void addToIndexShouldUseRegisteredConverterWhenAddingData() { + + DummyObject value = new DummyObject(); + final String identityHexString = ObjectUtils.getIdentityHexString(value); + + ((GenericConversionService) converter.getConversionService()).addConverter(new Converter() { + + @Override + public byte[] convert(DummyObject source) { + return identityHexString.getBytes(CHARSET); + } + }); + + writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", value)); + + verify(connectionMock).sAdd(eq(("persons:firstname:" + identityHexString).getBytes(CHARSET)), eq(KEY_BIN)); + } + + static class StubIndxedData implements IndexedData { + + @Override + public String getIndexName() { + return "address.city"; + } + + @Override + public String getKeyspace() { + return KEYSPACE; + } + } + + static class DummyObject { + + } +} diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java new file mode 100644 index 0000000000..4e39be81af --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java @@ -0,0 +1,318 @@ +/* + * Copyright 2015 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; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.hamcrest.core.IsNot.*; +import static org.junit.Assert.*; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Reference; +import org.springframework.data.keyvalue.annotation.KeySpace; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.convert.Bucket; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.MappingConfiguration; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.Indexed; +import org.springframework.data.redis.core.mapping.RedisMappingContext; + +/** + * @author Christoph Strobl + */ +public class RedisKeyValueAdapterTests { + + RedisKeyValueAdapter adapter; + StringRedisTemplate template; + RedisConnectionFactory connectionFactory; + + @Before + public void setUp() { + + JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(); + jedisConnectionFactory.afterPropertiesSet(); + connectionFactory = jedisConnectionFactory; + + template = new StringRedisTemplate(connectionFactory); + template.afterPropertiesSet(); + + RedisMappingContext mappingContext = new RedisMappingContext(new MappingConfiguration(new IndexConfiguration(), + new KeyspaceConfiguration())); + mappingContext.afterPropertiesSet(); + + adapter = new RedisKeyValueAdapter(template, mappingContext); + + template.execute(new RedisCallback() { + + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + connection.flushDb(); + return null; + } + }); + } + + @After + public void tearDown() { + + try { + adapter.destroy(); + } catch (Exception e) { + // ignore + } + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void putWritesDataCorrectly() { + + Person rand = new Person(); + rand.age = 24; + + adapter.put("1", rand, "persons"); + + assertThat(template.keys("persons*"), hasItems("persons", "persons:1")); + assertThat(template.opsForSet().size("persons"), is(1L)); + assertThat(template.opsForSet().members("persons"), hasItems("1")); + assertThat(template.opsForHash().entries("persons:1").size(), is(2)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void putWritesSimpleIndexDataCorrectly() { + + Person rand = new Person(); + rand.firstname = "rand"; + + adapter.put("1", rand, "persons"); + + assertThat(template.keys("persons*"), hasItem("persons:firstname:rand")); + assertThat(template.opsForSet().members("persons:firstname:rand"), hasItems("1")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void putWritesNestedDataCorrectly() { + + Person rand = new Person(); + rand.address = new Address(); + rand.address.city = "Emond's Field"; + + adapter.put("1", rand, "persons"); + + assertThat(template.keys("persons*"), hasItems("persons", "persons:1")); + assertThat(template.opsForHash().entries("persons:1").size(), is(2)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void putWritesSimpleNestedIndexValuesCorrectly() { + + Person rand = new Person(); + rand.address = new Address(); + rand.address.country = "Andor"; + + adapter.put("1", rand, "persons"); + + assertThat(template.keys("persons*"), hasItem("persons:address.country:Andor")); + assertThat(template.opsForSet().members("persons:address.country:Andor"), hasItems("1")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getShouldReadSimpleObjectCorrectly() { + + Map map = new LinkedHashMap(); + map.put("_class", Person.class.getName()); + map.put("age", "24"); + template.opsForHash().putAll("persons:load-1", map); + + Object loaded = adapter.get("load-1", "persons"); + + assertThat(loaded, instanceOf(Person.class)); + assertThat(((Person) loaded).age, is(24)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getShouldReadNestedObjectCorrectly() { + + Map map = new LinkedHashMap(); + map.put("_class", Person.class.getName()); + map.put("address.country", "Andor"); + template.opsForHash().putAll("persons:load-1", map); + + Object loaded = adapter.get("load-1", "persons"); + + assertThat(loaded, instanceOf(Person.class)); + assertThat(((Person) loaded).address.country, is("Andor")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void couldReadsKeyspaceSizeCorrectly() { + + Map map = new LinkedHashMap(); + map.put("_class", Person.class.getName()); + map.put("address.country", "Andor"); + template.opsForHash().putAll("persons:load-1", map); + + template.opsForSet().add("persons", "1", "2", "3"); + + assertThat(adapter.count("persons"), is(3L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void deleteRemovesEntriesCorrectly() { + + Map map = new LinkedHashMap(); + map.put("_class", Person.class.getName()); + map.put("address.country", "Andor"); + template.opsForHash().putAll("persons:1", map); + template.opsForSet().add("persons", "1"); + + adapter.delete("1", "persons"); + + assertThat(template.opsForSet().members("persons"), not(hasItem("1"))); + assertThat(template.hasKey("persons:1"), is(false)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void deleteCleansIndexedDataCorrectly() { + + Map map = new LinkedHashMap(); + map.put("_class", Person.class.getName()); + map.put("firstname", "rand"); + map.put("address.country", "Andor"); + template.opsForHash().putAll("persons:1", map); + template.opsForSet().add("persons", "1"); + template.opsForSet().add("persons:1:idx", "persons:firstname:rand"); + template.opsForSet().add("persons:firstname:rand", "1"); + + adapter.delete("1", "persons"); + + assertThat(template.opsForSet().members("persons:firstname:rand"), not(hasItem("1"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void keyExpiredEventShouldRemoveHelperStructures() { + + Map map = new LinkedHashMap(); + map.put("_class", Person.class.getName()); + map.put("firstname", "rand"); + map.put("address.country", "Andor"); + + template.opsForSet().add("persons", "1"); + template.opsForSet().add("persons:firstname:rand", "1"); + template.opsForSet().add("persons:1:idx", "persons:firstname:rand"); + + adapter.onApplicationEvent(new RedisKeyExpiredEvent("persons:1".getBytes(Bucket.CHARSET))); + + assertThat(template.hasKey("persons:firstname:rand"), is(false)); + assertThat(template.hasKey("persons:1:idx"), is(false)); + assertThat(template.opsForSet().members("persons"), not(hasItem("1"))); + } + + @KeySpace("persons") + static class Person { + + @Id String id; + @Indexed String firstname; + Gender gender; + + List nicknames; + List coworkers; + Integer age; + Boolean alive; + Date birthdate; + + Address address; + + Map physicalAttributes; + Map relatives; + + @Reference Location location; + @Reference List visited; + } + + static class Address { + + String city; + @Indexed String country; + } + + static class AddressWithId extends Address { + + @Id String id; + } + + static enum Gender { + MALE, FEMALE + } + + static class AddressWithPostcode extends Address { + + String postcode; + } + + static class TaVeren extends Person { + + } + + @KeySpace("locations") + static class Location { + + @Id String id; + String name; + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java new file mode 100644 index 0000000000..88d69c5670 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java @@ -0,0 +1,235 @@ +/* + * Copyright 2015 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; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collections; +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.dao.DataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +@RunWith(Parameterized.class) +public class RedisKeyValueTemplateTests { + + RedisConnectionFactory connectionFactory; + RedisKeyValueTemplate template; + RedisTemplate nativeTemplate; + + public RedisKeyValueTemplateTests(RedisConnectionFactory connectionFactory) { + + this.connectionFactory = connectionFactory; + ConnectionFactoryTracker.add(connectionFactory); + } + + @Parameters + public static List params() { + + JedisConnectionFactory jedis = new JedisConnectionFactory(); + jedis.afterPropertiesSet(); + + return Collections. singletonList(jedis); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + @Before + public void setUp() { + + nativeTemplate = new RedisTemplate(); + nativeTemplate.setConnectionFactory(connectionFactory); + nativeTemplate.afterPropertiesSet(); + + RedisMappingContext context = new RedisMappingContext(); + + RedisKeyValueAdapter adapter = new RedisKeyValueAdapter(nativeTemplate, context); + template = new RedisKeyValueTemplate(adapter, context); + } + + @After + public void tearDown() { + + nativeTemplate.execute(new RedisCallback() { + + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + + connection.flushDb(); + return null; + } + }); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void savesObjectCorrectly() { + + final Person rand = new Person(); + rand.firstname = "rand"; + + template.insert(rand); + + nativeTemplate.execute(new RedisCallback() { + + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + + assertThat(connection.exists(("template-test-person:" + rand.id).getBytes()), is(true)); + return null; + } + }); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findProcessesCallbackReturningSingleIdCorrectly() { + + Person rand = new Person(); + rand.firstname = "rand"; + + final Person mat = new Person(); + mat.firstname = "mat"; + + template.insert(rand); + template.insert(mat); + + List result = template.find(new RedisCallback() { + + @Override + public byte[] doInRedis(RedisConnection connection) throws DataAccessException { + return mat.id.getBytes(); + } + }, Person.class); + + assertThat(result.size(), is(1)); + assertThat(result, hasItems(mat)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findProcessesCallbackReturningMultipleIdsCorrectly() { + + final Person rand = new Person(); + rand.firstname = "rand"; + + final Person mat = new Person(); + mat.firstname = "mat"; + + template.insert(rand); + template.insert(mat); + + List result = template.find(new RedisCallback>() { + + @Override + public List doInRedis(RedisConnection connection) throws DataAccessException { + return Arrays.asList(rand.id.getBytes(), mat.id.getBytes()); + } + }, Person.class); + + assertThat(result.size(), is(2)); + assertThat(result, hasItems(rand, mat)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findProcessesCallbackReturningNullCorrectly() { + + Person rand = new Person(); + rand.firstname = "rand"; + + Person mat = new Person(); + mat.firstname = "mat"; + + template.insert(rand); + template.insert(mat); + + List result = template.find(new RedisCallback>() { + + @Override + public List doInRedis(RedisConnection connection) throws DataAccessException { + return null; + } + }, Person.class); + + assertThat(result.size(), is(0)); + } + + @RedisHash("template-test-person") + static class Person { + + @Id String id; + String firstname; + + @Override + public int hashCode() { + + int result = ObjectUtils.nullSafeHashCode(firstname); + return result + ObjectUtils.nullSafeHashCode(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Person)) { + return false; + } + Person that = (Person) obj; + + if (!ObjectUtils.nullSafeEquals(this.firstname, that.firstname)) { + return false; + } + + return ObjectUtils.nullSafeEquals(this.id, that.id); + } + + } +} diff --git a/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java new file mode 100644 index 0000000000..acea3e774a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java @@ -0,0 +1,73 @@ +/* + * 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 static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class CompositeIndexResolverUnitTests { + + @Mock IndexResolver resolver1; + @Mock IndexResolver resolver2; + @Mock TypeInformation typeInfoMock; + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectNull() { + new CompositeIndexResolver(null); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectCollectionWithNullValues() { + new CompositeIndexResolver(Arrays.asList(resolver1, null, resolver2)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldCollectionIndexesFromResolvers() { + + when(resolver1.resolveIndexesFor(any(TypeInformation.class), anyObject())).thenReturn( + Collections. singleton(new SimpleIndexedPropertyValue("spring", "data", "redis"))); + when(resolver2.resolveIndexesFor(any(TypeInformation.class), anyObject())).thenReturn( + Collections. singleton(new SimpleIndexedPropertyValue("redis", "data", "spring"))); + + CompositeIndexResolver resolver = new CompositeIndexResolver(Arrays.asList(resolver1, resolver2)); + + assertThat(resolver.resolveIndexesFor(typeInfoMock, "o.O").size(), equalTo(2)); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java b/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java new file mode 100644 index 0000000000..ccdf91c036 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java @@ -0,0 +1,163 @@ +/* + * Copyright 2015 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.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Reference; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.index.Indexed; + +import lombok.EqualsAndHashCode; + +/** + * @author Christoph Strobl + */ +public class ConversionTestEntities { + + static final String KEYSPACE_PERSON = "persons"; + static final String KEYSPACE_TWOT = "twot"; + static final String KEYSPACE_LOCATION = "locations"; + + @RedisHash(KEYSPACE_PERSON) + public static class Person { + + @Id String id; + String firstname; + Gender gender; + + List nicknames; + List coworkers; + Integer age; + Boolean alive; + Date birthdate; + + LocalDate localDate; + LocalDateTime localDateTime; + LocalTime localTime; + Instant instant; + ZonedDateTime zonedDateTime; + ZoneId zoneId; + Duration duration; + Period period; + + Address address; + + Map physicalAttributes; + Map relatives; + + @Reference Location location; + @Reference List visited; + + Species species; + } + + public static class PersonWithAddressReference extends Person { + + @Reference AddressWithId addressRef; + } + + public static class Address { + + String city; + @Indexed String country; + } + + public static class AddressWithId extends Address { + + @Id String id; + } + + public static enum Gender { + MALE, FEMALE + } + + public static class AddressWithPostcode extends Address { + + String postcode; + } + + public static class TaVeren extends Person { + + Object feature; + Map characteristics; + List items; + } + + @EqualsAndHashCode + @RedisHash(KEYSPACE_LOCATION) + public static class Location { + + @Id String id; + String name; + Address address; + + } + + @RedisHash(timeToLive = 5) + public static class ExpiringPerson { + + @Id String id; + String name; + } + + public static class ExipringPersonWithExplicitProperty extends ExpiringPerson { + + @TimeToLive(unit = TimeUnit.MINUTES) Long ttl; + } + + public static class Species { + + String name; + List alsoKnownAs; + } + + @RedisHash(KEYSPACE_TWOT) + public static class TheWheelOfTime { + + List mainCharacters; + List species; + Map places; + } + + public static class Item { + + @Indexed String type; + String description; + Size size; + } + + public static class Size { + + int width; + int height; + int length; + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java new file mode 100644 index 0000000000..3b655dc6b7 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java @@ -0,0 +1,1424 @@ +/* + * Copyright 2015-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 static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.redis.core.convert.ConversionTestEntities.*; +import static org.springframework.data.redis.test.util.IsBucketMatcher.*; + +import java.io.Serializable; +import java.nio.charset.Charset; +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.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +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.ConversionTestEntities.Address; +import org.springframework.data.redis.core.convert.ConversionTestEntities.AddressWithId; +import org.springframework.data.redis.core.convert.ConversionTestEntities.AddressWithPostcode; +import org.springframework.data.redis.core.convert.ConversionTestEntities.ExipringPersonWithExplicitProperty; +import org.springframework.data.redis.core.convert.ConversionTestEntities.ExpiringPerson; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Gender; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Location; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Person; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Species; +import org.springframework.data.redis.core.convert.ConversionTestEntities.TaVeren; +import org.springframework.data.redis.core.convert.ConversionTestEntities.TheWheelOfTime; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class MappingRedisConverterUnitTests { + + @Mock ReferenceResolver resolverMock; + MappingRedisConverter converter; + Person rand; + + @Before + public void setUp() { + + converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); + converter.afterPropertiesSet(); + + rand = new Person(); + + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsTypeHintForRootCorrectly() { + assertThat(write(rand).getBucket(), isBucket().containingTypeHint("_class", Person.class)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsKeyCorrectly() { + + rand.id = "1"; + + assertThat(write(rand).getId(), is((Serializable) "1")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsKeyCorrectlyWhenThereIsAnAdditionalIdFieldInNestedElement() { + + AddressWithId address = new AddressWithId(); + address.id = "tear"; + address.city = "Tear"; + + rand.id = "1"; + rand.address = address; + + RedisData data = write(rand); + + assertThat(data.getId(), is((Serializable) "1")); + assertThat(data.getBucket(), isBucket().containingUtf8String("address.id", "tear")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeDoesNotAppendPropertiesWithNullValues() { + + rand.firstname = "rand"; + + assertThat(write(rand).getBucket(), isBucket().without("lastname")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeDoesNotAppendPropertiesWithEmtpyCollections() { + + rand.firstname = "rand"; + + assertThat(write(rand).getBucket(), isBucket().without("nicknames")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsSimpleRootPropertyCorrectly() { + + rand.firstname = "nynaeve"; + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("firstname", "nynaeve")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsListOfSimplePropertiesCorrectly() { + + rand.nicknames = Arrays.asList("dragon reborn", "lews therin"); + + RedisData target = write(rand); + + assertThat(target.getBucket(), isBucket().containingUtf8String("nicknames.[0]", "dragon reborn") + .containingUtf8String("nicknames.[1]", "lews therin")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsComplexObjectCorrectly() { + + Address address = new Address(); + address.city = "two rivers"; + address.country = "andora"; + rand.address = address; + + RedisData target = write(rand); + + assertThat(target.getBucket(), isBucket().containingUtf8String("address.city", "two rivers") + .containingUtf8String("address.country", "andora")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsListOfComplexObjectsCorrectly() { + + Person mat = new Person(); + mat.firstname = "mat"; + mat.nicknames = Arrays.asList("prince of the ravens"); + + Person perrin = new Person(); + perrin.firstname = "perrin"; + perrin.address = new Address(); + perrin.address.city = "two rivers"; + + rand.coworkers = Arrays.asList(mat, perrin); + rand.id = UUID.randomUUID().toString(); + rand.firstname = "rand"; + + RedisData target = write(rand); + + assertThat(target.getBucket(), + isBucket().containingUtf8String("coworkers.[0].firstname", "mat") // + .containingUtf8String("coworkers.[0].nicknames.[0]", "prince of the ravens") // + .containingUtf8String("coworkers.[1].firstname", "perrin") // + .containingUtf8String("coworkers.[1].address.city", "two rivers")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeDoesNotAddClassTypeInformationCorrectlyForMatchingTypes() { + + Address address = new Address(); + address.city = "two rivers"; + + rand.address = address; + + RedisData target = write(rand); + + assertThat(target.getBucket(), isBucket().without("address._class")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAddsClassTypeInformationCorrectlyForNonMatchingTypes() { + + AddressWithPostcode address = new AddressWithPostcode(); + address.city = "two rivers"; + address.postcode = "1234"; + + rand.address = address; + + RedisData target = write(rand); + + assertThat(target.getBucket(), isBucket().containingTypeHint("address._class", AddressWithPostcode.class)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readConsidersClassTypeInformationCorrectlyForNonMatchingTypes() { + + Map map = new HashMap(); + map.put("address._class", AddressWithPostcode.class.getName()); + map.put("address.postcode", "1234"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.address, instanceOf(AddressWithPostcode.class)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAddsClassTypeInformationCorrectlyForNonMatchingTypesInCollections() { + + Person mat = new TaVeren(); + mat.firstname = "mat"; + + rand.coworkers = Arrays.asList(mat); + + RedisData target = write(rand); + + assertThat(target.getBucket(), isBucket().containingTypeHint("coworkers.[0]._class", TaVeren.class)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readConvertsSimplePropertiesCorrectly() { + + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("firstname", "rand"))); + + assertThat(converter.read(Person.class, rdo).firstname, is("rand")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readConvertsListOfSimplePropertiesCorrectly() { + + Map map = new LinkedHashMap(); + map.put("nicknames.[0]", "dragon reborn"); + map.put("nicknames.[1]", "lews therin"); + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map)); + + assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "lews therin")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readConvertsUnorderedListOfSimplePropertiesCorrectly() { + + Map map = new LinkedHashMap(); + map.put("nicknames.[9]", "car'a'carn"); + map.put("nicknames.[10]", "lews therin"); + map.put("nicknames.[1]", "dragon reborn"); + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map)); + + assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "car'a'carn", "lews therin")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readComplexPropertyCorrectly() { + + Map map = new LinkedHashMap(); + map.put("address.city", "two rivers"); + map.put("address.country", "andor"); + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map)); + + Person target = converter.read(Person.class, rdo); + + assertThat(target.address, notNullValue()); + assertThat(target.address.city, is("two rivers")); + assertThat(target.address.country, is("andor")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readListComplexPropertyCorrectly() { + + Map map = new LinkedHashMap(); + map.put("coworkers.[0].firstname", "mat"); + map.put("coworkers.[0].nicknames.[0]", "prince of the ravens"); + map.put("coworkers.[0].nicknames.[1]", "gambler"); + map.put("coworkers.[1].firstname", "perrin"); + map.put("coworkers.[1].address.city", "two rivers"); + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map)); + + Person target = converter.read(Person.class, rdo); + + assertThat(target.coworkers, notNullValue()); + assertThat(target.coworkers.get(0).firstname, is("mat")); + assertThat(target.coworkers.get(0).nicknames, notNullValue()); + assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens")); + assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler")); + + assertThat(target.coworkers.get(1).firstname, is("perrin")); + assertThat(target.coworkers.get(1).address.city, is("two rivers")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readUnorderedListOfComplexPropertyCorrectly() { + + Map map = new LinkedHashMap(); + map.put("coworkers.[10].firstname", "perrin"); + map.put("coworkers.[10].address.city", "two rivers"); + map.put("coworkers.[1].firstname", "mat"); + map.put("coworkers.[1].nicknames.[1]", "gambler"); + map.put("coworkers.[1].nicknames.[0]", "prince of the ravens"); + + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map)); + + Person target = converter.read(Person.class, rdo); + + assertThat(target.coworkers, notNullValue()); + assertThat(target.coworkers.get(0).firstname, is("mat")); + assertThat(target.coworkers.get(0).nicknames, notNullValue()); + assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens")); + assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler")); + + assertThat(target.coworkers.get(1).firstname, is("perrin")); + assertThat(target.coworkers.get(1).address.city, is("two rivers")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readListComplexPropertyCorrectlyAndConsidersClassTypeInformation() { + + Map map = new LinkedHashMap(); + map.put("coworkers.[0]._class", TaVeren.class.getName()); + map.put("coworkers.[0].firstname", "mat"); + + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map)); + + Person target = converter.read(Person.class, rdo); + + assertThat(target.coworkers, notNullValue()); + assertThat(target.coworkers.get(0), instanceOf(TaVeren.class)); + assertThat(target.coworkers.get(0).firstname, is("mat")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsMapWithSimpleKeyCorrectly() { + + Map map = new LinkedHashMap(); + map.put("hair-color", "red"); + map.put("eye-color", "grey"); + + rand.physicalAttributes = map; + + RedisData target = write(rand); + + assertThat(target.getBucket(), isBucket().containingUtf8String("physicalAttributes.[hair-color]", "red") // + .containingUtf8String("physicalAttributes.[eye-color]", "grey")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsMapWithSimpleKeyOnNestedObjectCorrectly() { + + Map map = new LinkedHashMap(); + map.put("hair-color", "red"); + map.put("eye-color", "grey"); + + rand.coworkers = new ArrayList(); + rand.coworkers.add(new Person()); + rand.coworkers.get(0).physicalAttributes = map; + + RedisData target = write(rand); + + assertThat(target.getBucket(), + isBucket().containingUtf8String("coworkers.[0].physicalAttributes.[hair-color]", "red") // + .containingUtf8String("coworkers.[0].physicalAttributes.[eye-color]", "grey")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readSimpleMapValuesCorrectly() { + + Map map = new LinkedHashMap(); + map.put("physicalAttributes.[hair-color]", "red"); + map.put("physicalAttributes.[eye-color]", "grey"); + + RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map)); + + Person target = converter.read(Person.class, rdo); + + assertThat(target.physicalAttributes, notNullValue()); + assertThat(target.physicalAttributes.get("hair-color"), is("red")); + assertThat(target.physicalAttributes.get("eye-color"), is("grey")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsMapWithComplexObjectsCorrectly() { + + Map map = new LinkedHashMap(); + Person janduin = new Person(); + janduin.firstname = "janduin"; + map.put("father", janduin); + Person tam = new Person(); + tam.firstname = "tam"; + map.put("step-father", tam); + + rand.relatives = map; + + RedisData target = write(rand); + + assertThat(target.getBucket(), isBucket().containingUtf8String("relatives.[father].firstname", "janduin") // + .containingUtf8String("relatives.[step-father].firstname", "tam")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readMapWithComplexObjectsCorrectly() { + + Map map = new LinkedHashMap(); + map.put("relatives.[father].firstname", "janduin"); + map.put("relatives.[step-father].firstname", "tam"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.relatives, notNullValue()); + assertThat(target.relatives.get("father"), notNullValue()); + assertThat(target.relatives.get("father").firstname, is("janduin")); + assertThat(target.relatives.get("step-father"), notNullValue()); + assertThat(target.relatives.get("step-father").firstname, is("tam")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeAppendsClassTypeInformationCorrectlyForMapWithComplexObjects() { + + Map map = new LinkedHashMap(); + Person lews = new TaVeren(); + lews.firstname = "lews"; + map.put("previous-incarnation", lews); + + rand.relatives = map; + + RedisData target = write(rand); + + assertThat(target.getBucket(), + isBucket().containingTypeHint("relatives.[previous-incarnation]._class", TaVeren.class)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readConsidersClassTypeInformationCorrectlyForMapWithComplexObjects() { + + Map map = new LinkedHashMap(); + map.put("relatives.[previous-incarnation]._class", TaVeren.class.getName()); + map.put("relatives.[previous-incarnation].firstname", "lews"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.relatives.get("previous-incarnation"), notNullValue()); + assertThat(target.relatives.get("previous-incarnation"), instanceOf(TaVeren.class)); + assertThat(target.relatives.get("previous-incarnation").firstname, is("lews")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesIntegerValuesCorrectly() { + + rand.age = 20; + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("age", "20")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesLocalDateTimeValuesCorrectly() { + + rand.localDateTime = LocalDateTime.parse("2016-02-19T10:18:01"); + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("localDateTime", "2016-02-19T10:18:01")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsLocalDateTimeValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("localDateTime", "2016-02-19T10:18:01")))); + + assertThat(target.localDateTime, is(LocalDateTime.parse("2016-02-19T10:18:01"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesLocalDateValuesCorrectly() { + + rand.localDate = LocalDate.parse("2016-02-19"); + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("localDate", "2016-02-19")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsLocalDateValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("localDate", "2016-02-19")))); + + assertThat(target.localDate, is(LocalDate.parse("2016-02-19"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesLocalTimeValuesCorrectly() { + + rand.localTime = LocalTime.parse("11:12:13"); + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("localTime", "11:12:13")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsLocalTimeValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("localTime", "11:12")))); + + assertThat(target.localTime, is(LocalTime.parse("11:12:00"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesZonedDateTimeValuesCorrectly() { + + rand.zonedDateTime = ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"); + + assertThat(write(rand).getBucket(), + isBucket().containingUtf8String("zonedDateTime", "2007-12-03T10:15:30+01:00[Europe/Paris]")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsZonedDateTimeValuesCorrectly() { + + Person target = converter.read(Person.class, new RedisData(Bucket + .newBucketFromStringMap(Collections.singletonMap("zonedDateTime", "2007-12-03T10:15:30+01:00[Europe/Paris]")))); + + assertThat(target.zonedDateTime, is(ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesInstantValuesCorrectly() { + + rand.instant = Instant.parse("2007-12-03T10:15:30.01Z"); + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("instant", "2007-12-03T10:15:30.010Z")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsInstantValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("instant", "2007-12-03T10:15:30.01Z")))); + + assertThat(target.instant, is(Instant.parse("2007-12-03T10:15:30.01Z"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesZoneIdValuesCorrectly() { + + rand.zoneId = ZoneId.of("Europe/Paris"); + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("zoneId", "Europe/Paris")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsZoneIdValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("zoneId", "Europe/Paris")))); + + assertThat(target.zoneId, is(ZoneId.of("Europe/Paris"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesDurationValuesCorrectly() { + + rand.duration = Duration.parse("P2DT3H4M"); + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("duration", "PT51H4M")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsDurationValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("duration", "PT51H4M")))); + + assertThat(target.duration, is(Duration.parse("P2DT3H4M"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesPeriodValuesCorrectly() { + + rand.period = Period.parse("P1Y2M25D"); + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("period", "P1Y2M25D")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsPeriodValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("period", "P1Y2M25D")))); + + assertThat(target.period, is(Period.parse("P1Y2M25D"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesEnumValuesCorrectly() { + + rand.gender = Gender.MALE; + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("gender", "MALE")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsEnumValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("gender", "MALE")))); + + assertThat(target.gender, is(Gender.MALE)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesBooleanValuesCorrectly() { + + rand.alive = Boolean.TRUE; + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("alive", "1")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsBooleanValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("alive", "1")))); + + assertThat(target.alive, is(Boolean.TRUE)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsStringBooleanValuesCorrectly() { + + Person target = converter.read(Person.class, + new RedisData(Bucket.newBucketFromStringMap(Collections.singletonMap("alive", "true")))); + + assertThat(target.alive, is(Boolean.TRUE)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writesDateValuesCorrectly() { + + Calendar cal = Calendar.getInstance(); + cal.set(1978, 10, 25); + + rand.birthdate = cal.getTime(); + + assertThat(write(rand).getBucket(), isBucket().containingDateAsMsec("birthdate", rand.birthdate)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readsDateValuesCorrectly() { + + Calendar cal = Calendar.getInstance(); + cal.set(1978, 10, 25); + + Date date = cal.getTime(); + + Person target = converter.read(Person.class, new RedisData( + Bucket.newBucketFromStringMap(Collections.singletonMap("birthdate", Long.valueOf(date.getTime()).toString())))); + + assertThat(target.birthdate, is(date)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeSingleReferenceOnRootCorrectly() { + + Location location = new Location(); + location.id = "1"; + location.name = "tar valon"; + + rand.location = location; + + RedisData target = write(rand); + + assertThat(target.getBucket(), + isBucket().containingUtf8String("location", "locations:1") // + .without("location.id") // + .without("location.name")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readLoadsReferenceDataOnRootCorrectly() { + + Location location = new Location(); + location.id = "1"; + location.name = "tar valon"; + + Map locationMap = new LinkedHashMap(); + locationMap.put("id", location.id); + locationMap.put("name", location.name); + + when(resolverMock.resolveReference(eq("1"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(locationMap).rawMap()); + + Map map = new LinkedHashMap(); + map.put("location", "locations:1"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.location, is(location)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeSingleReferenceOnNestedElementCorrectly() { + + Location location = new Location(); + location.id = "1"; + location.name = "tar valon"; + + Person egwene = new Person(); + egwene.location = location; + + rand.coworkers = Collections.singletonList(egwene); + + RedisData target = write(rand); + + assertThat(target.getBucket(), + isBucket().containingUtf8String("coworkers.[0].location", "locations:1") // + .without("coworkers.[0].location.id") // + .without("coworkers.[0].location.name")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readLoadsReferenceDataOnNestedElementCorrectly() { + + Location location = new Location(); + location.id = "1"; + location.name = "tar valon"; + + Map locationMap = new LinkedHashMap(); + locationMap.put("id", location.id); + locationMap.put("name", location.name); + + when(resolverMock.resolveReference(eq("1"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(locationMap).rawMap()); + + Map map = new LinkedHashMap(); + map.put("coworkers.[0].location", "locations:1"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.coworkers.get(0).location, is(location)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeListOfReferencesOnRootCorrectly() { + + Location tarValon = new Location(); + tarValon.id = "1"; + tarValon.name = "tar valon"; + + Location falme = new Location(); + falme.id = "2"; + falme.name = "falme"; + + Location tear = new Location(); + tear.id = "3"; + tear.name = "city of tear"; + + rand.visited = Arrays.asList(tarValon, falme, tear); + + RedisData target = write(rand); + + assertThat(target.getBucket(), + isBucket().containingUtf8String("visited.[0]", "locations:1") // + .containingUtf8String("visited.[1]", "locations:2") // + .containingUtf8String("visited.[2]", "locations:3")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readLoadsListOfReferencesOnRootCorrectly() { + + Location tarValon = new Location(); + tarValon.id = "1"; + tarValon.name = "tar valon"; + + Location falme = new Location(); + falme.id = "2"; + falme.name = "falme"; + + Location tear = new Location(); + tear.id = "3"; + tear.name = "city of tear"; + + Map tarValonMap = new LinkedHashMap(); + tarValonMap.put("id", tarValon.id); + tarValonMap.put("name", tarValon.name); + + Map falmeMap = new LinkedHashMap(); + falmeMap.put("id", falme.id); + falmeMap.put("name", falme.name); + + Map tearMap = new LinkedHashMap(); + tearMap.put("id", tear.id); + tearMap.put("name", tear.name); + + Bucket.newBucketFromStringMap(tearMap).rawMap(); + + when(resolverMock.resolveReference(eq("1"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(tarValonMap).rawMap()); + when(resolverMock.resolveReference(eq("2"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(falmeMap).rawMap()); + when(resolverMock.resolveReference(eq("3"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(tearMap).rawMap()); + + Map map = new LinkedHashMap(); + map.put("visited.[0]", "locations:1"); + map.put("visited.[1]", "locations:2"); + map.put("visited.[2]", "locations:3"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.visited.get(0), is(tarValon)); + assertThat(target.visited.get(1), is(falme)); + assertThat(target.visited.get(2), is(tear)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeSetsAnnotatedTimeToLiveCorrectly() { + + ExpiringPerson birgitte = new ExpiringPerson(); + birgitte.id = "birgitte"; + birgitte.name = "Birgitte Silverbow"; + + assertThat(write(birgitte).getTimeToLive(), is(5L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeDoesNotTTLWhenNotPresent() { + + Location tear = new Location(); + tear.id = "tear"; + tear.name = "Tear"; + + assertThat(write(tear).getTimeToLive(), nullValue()); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldConsiderKeyspaceConfiguration() { + + this.converter.getMappingContext().getMappingConfiguration().getKeyspaceConfiguration() + .addKeyspaceSettings(new KeyspaceSettings(Address.class, "o_O")); + + Address address = new Address(); + address.city = "Tear"; + + assertThat(write(address).getKeyspace(), is("o_O")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldConsiderTimeToLiveConfiguration() { + + KeyspaceSettings assignment = new KeyspaceSettings(Address.class, "o_O"); + assignment.setTimeToLive(5L); + + this.converter.getMappingContext().getMappingConfiguration().getKeyspaceConfiguration() + .addKeyspaceSettings(assignment); + + Address address = new Address(); + address.city = "Tear"; + + assertThat(write(address).getTimeToLive(), is(5L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldHonorCustomConversionOnRootType() { + + this.converter = new MappingRedisConverter(null, null, resolverMock); + this.converter + .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + this.converter.afterPropertiesSet(); + + Address address = new Address(); + address.country = "Tel'aran'rhiod"; + address.city = "unknown"; + + assertThat(write(address).getBucket(), + isBucket().containingUtf8String("_raw", "{\"city\":\"unknown\",\"country\":\"Tel'aran'rhiod\"}")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldHonorCustomConversionOnNestedType() { + + this.converter = new MappingRedisConverter(null, null, resolverMock); + this.converter + .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + this.converter.afterPropertiesSet(); + + Address address = new Address(); + address.country = "Tel'aran'rhiod"; + address.city = "unknown"; + rand.address = address; + + assertThat(write(rand).getBucket(), + isBucket().containingUtf8String("address", "{\"city\":\"unknown\",\"country\":\"Tel'aran'rhiod\"}")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldHonorIndexOnCustomConversionForNestedType() { + + this.converter = new MappingRedisConverter(null, null, resolverMock); + this.converter + .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + this.converter.afterPropertiesSet(); + + Address address = new Address(); + address.country = "andor"; + rand.address = address; + + assertThat(write(rand).getIndexedData(), + hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "address.country", "andor"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldHonorIndexAnnotationsOnWhenCustomConversionOnNestedype() { + + this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); + this.converter + .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + this.converter.afterPropertiesSet(); + + Address address = new Address(); + address.country = "Tel'aran'rhiod"; + address.city = "unknown"; + rand.address = address; + + assertThat(write(rand).getIndexedData().isEmpty(), is(false)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readShouldHonorCustomConversionOnRootType() { + + this.converter = new MappingRedisConverter(null, null, resolverMock); + this.converter + .setCustomConversions(new CustomConversions(Collections.singletonList(new BytesToAddressConverter()))); + this.converter.afterPropertiesSet(); + + Map map = new LinkedHashMap(); + map.put("_raw", "{\"city\":\"unknown\",\"country\":\"Tel'aran'rhiod\"}"); + + Address target = converter.read(Address.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.city, is("unknown")); + assertThat(target.country, is("Tel'aran'rhiod")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readShouldHonorCustomConversionOnNestedType() { + + this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); + this.converter + .setCustomConversions(new CustomConversions(Collections.singletonList(new BytesToAddressConverter()))); + this.converter.afterPropertiesSet(); + + Map map = new LinkedHashMap(); + map.put("address", "{\"city\":\"unknown\",\"country\":\"Tel'aran'rhiod\"}"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target.address, notNullValue()); + assertThat(target.address.city, is("unknown")); + assertThat(target.address.country, is("Tel'aran'rhiod")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldPickUpTimeToLiveFromPropertyIfPresent() { + + ExipringPersonWithExplicitProperty aviendha = new ExipringPersonWithExplicitProperty(); + aviendha.id = "aviendha"; + aviendha.ttl = 2L; + + assertThat(write(aviendha).getTimeToLive(), is(120L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldUseDefaultTimeToLiveIfPropertyIsPresentButNull() { + + ExipringPersonWithExplicitProperty aviendha = new ExipringPersonWithExplicitProperty(); + aviendha.id = "aviendha"; + + assertThat(write(aviendha).getTimeToLive(), is(5L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldConsiderMapConvertersForRootType() { + + this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); + this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); + this.converter.afterPropertiesSet(); + + Species myrddraal = new Species(); + myrddraal.name = "myrddraal"; + myrddraal.alsoKnownAs = Arrays.asList("halfmen", "fades", "neverborn"); + + assertThat(write(myrddraal).getBucket(), isBucket().containingUtf8String("species-name", "myrddraal") + .containingUtf8String("species-nicknames", "halfmen,fades,neverborn")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldConsiderMapConvertersForNestedType() { + + this.converter = new MappingRedisConverter(null, null, resolverMock); + this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); + this.converter.afterPropertiesSet(); + + rand.species = new Species(); + rand.species.name = "human"; + + assertThat(write(rand).getBucket(), isBucket().containingUtf8String("species.species-name", "human")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readShouldConsiderMapConvertersForRootType() { + + this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); + this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); + this.converter.afterPropertiesSet(); + Map map = new LinkedHashMap(); + map.put("species-name", "trolloc"); + + Species target = converter.read(Species.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target, notNullValue()); + assertThat(target.name, is("trolloc")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readShouldConsiderMapConvertersForNestedType() { + + this.converter = new MappingRedisConverter(null, null, resolverMock); + this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); + this.converter.afterPropertiesSet(); + + Map map = new LinkedHashMap(); + map.put("species.species-name", "trolloc"); + + Person target = converter.read(Person.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target, notNullValue()); + assertThat(target.species.name, is("trolloc")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void writeShouldConsiderMapConvertersInsideLists() { + + this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); + this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); + this.converter.afterPropertiesSet(); + + TheWheelOfTime twot = new TheWheelOfTime(); + twot.species = new ArrayList(); + + Species myrddraal = new Species(); + myrddraal.name = "myrddraal"; + myrddraal.alsoKnownAs = Arrays.asList("halfmen", "fades", "neverborn"); + twot.species.add(myrddraal); + + assertThat(write(twot).getBucket(), isBucket().containingUtf8String("species.[0].species-name", "myrddraal") + .containingUtf8String("species.[0].species-nicknames", "halfmen,fades,neverborn")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void readShouldConsiderMapConvertersForValuesInList() { + + this.converter = new MappingRedisConverter(null, null, resolverMock); + this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); + this.converter.afterPropertiesSet(); + + Map map = new LinkedHashMap(); + map.put("species.[0].species-name", "trolloc"); + + TheWheelOfTime target = converter.read(TheWheelOfTime.class, new RedisData(Bucket.newBucketFromStringMap(map))); + + assertThat(target, notNullValue()); + assertThat(target.species, notNullValue()); + assertThat(target.species.get(0), notNullValue()); + assertThat(target.species.get(0).name, is("trolloc")); + } + + private RedisData write(Object source) { + + RedisData rdo = new RedisData(); + converter.write(source, rdo); + return rdo; + } + + @WritingConverter + static class AddressToBytesConverter implements Converter { + + private final ObjectMapper mapper; + private final Jackson2JsonRedisSerializer
serializer; + + AddressToBytesConverter() { + + mapper = new ObjectMapper(); + mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() + .withFieldVisibility(Visibility.ANY).withGetterVisibility(Visibility.NONE) + .withSetterVisibility(Visibility.NONE).withCreatorVisibility(Visibility.NONE)); + + serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer.setObjectMapper(mapper); + } + + @Override + public byte[] convert(Address value) { + return serializer.serialize(value); + } + } + + @WritingConverter + static class SpeciesToMapConverter implements Converter> { + + @Override + public Map convert(Species source) { + + if (source == null) { + return null; + } + + Map map = new LinkedHashMap(); + if (source.name != null) { + map.put("species-name", source.name.getBytes(Charset.forName("UTF-8"))); + } + map.put("species-nicknames", + StringUtils.collectionToCommaDelimitedString(source.alsoKnownAs).getBytes(Charset.forName("UTF-8"))); + return map; + } + } + + @ReadingConverter + static class MapToSpeciesConverter implements Converter, Species> { + + @Override + public Species convert(Map source) { + + if (source == null || source.isEmpty()) { + return null; + } + + Species species = new Species(); + + if (source.containsKey("species-name")) { + species.name = new String(source.get("species-name"), Charset.forName("UTF-8")); + } + if (source.containsKey("species-nicknames")) { + species.alsoKnownAs = Arrays.asList(StringUtils + .commaDelimitedListToStringArray(new String(source.get("species-nicknames"), Charset.forName("UTF-8")))); + } + return species; + } + } + + @ReadingConverter + static class BytesToAddressConverter implements Converter { + + private final ObjectMapper mapper; + private final Jackson2JsonRedisSerializer
serializer; + + BytesToAddressConverter() { + + mapper = new ObjectMapper(); + mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() + .withFieldVisibility(Visibility.ANY).withGetterVisibility(Visibility.NONE) + .withSetterVisibility(Visibility.NONE).withCreatorVisibility(Visibility.NONE)); + + serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer.setObjectMapper(mapper); + } + + @Override + public Address convert(byte[] value) { + return serializer.deserialize(value); + } + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java new file mode 100644 index 0000000000..40bb1a8265 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java @@ -0,0 +1,527 @@ +/* + * Copyright 2015-216 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 static org.hamcrest.collection.IsEmptyCollection.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.redis.core.convert.ConversionTestEntities.*; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hamcrest.core.IsCollectionContaining; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Address; +import org.springframework.data.redis.core.convert.ConversionTestEntities.AddressWithId; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Item; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Location; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Person; +import org.springframework.data.redis.core.convert.ConversionTestEntities.PersonWithAddressReference; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Size; +import org.springframework.data.redis.core.convert.ConversionTestEntities.TaVeren; +import org.springframework.data.redis.core.convert.ConversionTestEntities.TheWheelOfTime; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.Indexed; +import org.springframework.data.redis.core.index.SimpleIndexDefinition; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.util.ClassTypeInformation; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class PathIndexResolverUnitTests { + + IndexConfiguration indexConfig; + PathIndexResolver indexResolver; + + @Mock PersistentProperty propertyMock; + + @Before + public void setUp() { + + indexConfig = new IndexConfiguration(); + this.indexResolver = new PathIndexResolver( + new RedisMappingContext(new MappingConfiguration(indexConfig, new KeyspaceConfiguration()))); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionOnNullMappingContext() { + new PathIndexResolver(null); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldResolveAnnotatedIndexOnRootWhenValueIsNotNull() { + + Address address = new Address(); + address.country = "andor"; + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Address.class), address); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(Address.class.getName(), "country", "andor"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldNotResolveAnnotatedIndexOnRootWhenValueIsNull() { + + Address address = new Address(); + address.country = null; + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Address.class), address); + + assertThat(indexes.size(), is(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldResolveAnnotatedIndexOnNestedObjectWhenValueIsNotNull() { + + Person person = new Person(); + person.address = new Address(); + person.address.country = "andor"; + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), person); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "address.country", "andor"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldResolveMultipleAnnotatedIndexesInLists() { + + TheWheelOfTime twot = new TheWheelOfTime(); + twot.mainCharacters = new ArrayList(); + + Person rand = new Person(); + rand.address = new Address(); + rand.address.country = "andor"; + + Person zarine = new Person(); + zarine.address = new Address(); + zarine.address.country = "saldaea"; + + twot.mainCharacters.add(rand); + twot.mainCharacters.add(zarine); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TheWheelOfTime.class), twot); + + assertThat(indexes.size(), is(2)); + assertThat(indexes, + IsCollectionContaining. hasItems( + new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "mainCharacters.address.country", "andor"), + new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "mainCharacters.address.country", "saldaea"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldResolveAnnotatedIndexesInMap() { + + TheWheelOfTime twot = new TheWheelOfTime(); + twot.places = new LinkedHashMap(); + + Location stoneOfTear = new Location(); + stoneOfTear.name = "Stone of Tear"; + stoneOfTear.address = new Address(); + stoneOfTear.address.city = "tear"; + stoneOfTear.address.country = "illian"; + + twot.places.put("stone-of-tear", stoneOfTear); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TheWheelOfTime.class), twot); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, + hasItem(new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "places.stone-of-tear.address.country", "illian"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldResolveConfiguredIndexesInMapOfSimpleTypes() { + + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "physicalAttributes.eye-color")); + + Person rand = new Person(); + rand.physicalAttributes = new LinkedHashMap(); + rand.physicalAttributes.put("eye-color", "grey"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), rand); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, + hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "physicalAttributes.eye-color", "grey"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldResolveConfiguredIndexesInMapOfComplexTypes() { + + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "relatives.father.firstname")); + + Person rand = new Person(); + rand.relatives = new LinkedHashMap(); + + Person janduin = new Person(); + janduin.firstname = "janduin"; + + rand.relatives.put("father", janduin); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), rand); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, + hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "relatives.father.firstname", "janduin"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldIgnoreConfiguredIndexesInMapWhenValueIsNull() { + + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "physicalAttributes.eye-color")); + + Person rand = new Person(); + rand.physicalAttributes = new LinkedHashMap(); + rand.physicalAttributes.put("eye-color", null); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), rand); + + assertThat(indexes.size(), is(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldNotResolveIndexOnReferencedEntity() { + + PersonWithAddressReference rand = new PersonWithAddressReference(); + rand.addressRef = new AddressWithId(); + rand.addressRef.id = "emond_s_field"; + rand.addressRef.country = "andor"; + + Set indexes = indexResolver + .resolveIndexesFor(ClassTypeInformation.from(PersonWithAddressReference.class), rand); + + assertThat(indexes.size(), is(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldReturnNullWhenNoIndexConfigured() { + + when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(false); + assertThat(resolve("foo", "rand"), nullValue()); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldReturnDataWhenIndexConfigured() { + + when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(false); + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "foo")); + + assertThat(resolve("foo", "rand"), notNullValue()); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldReturnDataWhenNoIndexConfiguredButPropertyAnnotated() { + + when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); + + assertThat(resolve("foo", "rand"), notNullValue()); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldRemovePositionIndicatorForValuesInLists() { + + when(propertyMock.isCollectionLike()).thenReturn(true); + when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); + + IndexedData index = resolve("list.[0].name", "rand"); + + assertThat(index.getIndexName(), is("list.name")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldRemoveKeyIndicatorForValuesInMap() { + + when(propertyMock.isMap()).thenReturn(true); + when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); + + IndexedData index = resolve("map.[foo].name", "rand"); + + assertThat(index.getIndexName(), is("map.foo.name")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldKeepNumericalKeyForValuesInMap() { + + when(propertyMock.isMap()).thenReturn(true); + when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); + + IndexedData index = resolve("map.[0].name", "rand"); + + assertThat(index.getIndexName(), is("map.0.name")); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldInspectObjectTypeProperties() { + + Item hat = new Item(); + hat.type = "hat"; + + TaVeren mat = new TaVeren(); + mat.feature = hat; + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "feature.type", "hat"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldInspectObjectTypePropertiesButIgnoreNullValues() { + + Item hat = new Item(); + hat.description = "wide brimmed hat"; + + TaVeren mat = new TaVeren(); + mat.feature = hat; + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat); + + assertThat(indexes.size(), is(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldInspectObjectTypeValuesInMapProperties() { + + Item hat = new Item(); + hat.type = "hat"; + + TaVeren mat = new TaVeren(); + mat.characteristics = new LinkedHashMap(2); + mat.characteristics.put("clothing", hat); + mat.characteristics.put("gambling", "owns the dark one's luck"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, + hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "characteristics.clothing.type", "hat"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexShouldInspectObjectTypeValuesInListProperties() { + + Item hat = new Item(); + hat.type = "hat"; + + TaVeren mat = new TaVeren(); + mat.items = new ArrayList(2); + mat.items.add(hat); + mat.items.add("foxhead medallion"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "items.type", "hat"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexAllowCustomIndexName() { + + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "items.type", "itemsType")); + + Item hat = new Item(); + hat.type = "hat"; + + TaVeren mat = new TaVeren(); + mat.items = new ArrayList(2); + mat.items.add(hat); + mat.items.add("foxhead medallion"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "itemsType", "hat"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexForTypeThatHasNoIndexDefined() { + + Size size = new Size(); + size.height = 10; + size.length = 20; + size.width = 30; + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Size.class), size); + assertThat(indexes, is(empty())); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexOnMapField() { + + IndexedOnMapField source = new IndexedOnMapField(); + source.values = new LinkedHashMap(); + + source.values.put("jon", "snow"); + source.values.put("arya", "stark"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(IndexedOnMapField.class), + source); + + assertThat(indexes.size(), is(2)); + assertThat(indexes, + IsCollectionContaining. hasItems( + new SimpleIndexedPropertyValue(IndexedOnMapField.class.getName(), "values.jon", "snow"), + new SimpleIndexedPropertyValue(IndexedOnMapField.class.getName(), "values.arya", "stark"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexOnListField() { + + IndexedOnListField source = new IndexedOnListField(); + source.values = new ArrayList(); + + source.values.add("jon"); + source.values.add("arya"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(IndexedOnListField.class), + source); + + assertThat(indexes.size(), is(2)); + assertThat(indexes, + IsCollectionContaining. hasItems( + new SimpleIndexedPropertyValue(IndexedOnListField.class.getName(), "values", "jon"), + new SimpleIndexedPropertyValue(IndexedOnListField.class.getName(), "values", "arya"))); + } + + private IndexedData resolve(String path, Object value) { + + Set data = indexResolver.resolveIndex(KEYSPACE_PERSON, path, propertyMock, value); + + if (data.isEmpty()) { + return null; + } + + assertThat(data.size(), is(1)); + return data.iterator().next(); + } + + private Indexed createIndexedInstance() { + + return new Indexed() { + + @Override + public Class annotationType() { + return Indexed.class; + } + + }; + } + + static class IndexedOnListField { + + @Indexed List values; + } + + static class IndexedOnMapField { + + @Indexed Map values; + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java new file mode 100644 index 0000000000..5ce91e3858 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java @@ -0,0 +1,235 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.SpelIndexDefinition; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.SpelEvaluationException; + +/** + * @author Rob Winch + * @author Christoph Strobl + */ +public class SpelIndexResolverUnitTests { + + String keyspace; + + String indexName; + + String username; + + SpelIndexResolver resolver; + + Session session; + + ClassTypeInformation typeInformation; + + String securityContextAttrName; + + RedisMappingContext mappingContext; + + KeyValuePersistentEntity entity; + + @Before + public void setup() { + + username = "rob"; + keyspace = "spring:session:sessions"; + indexName = "principalName"; + securityContextAttrName = "SPRING_SECURITY_CONTEXT"; + + typeInformation = ClassTypeInformation.from(Session.class); + session = createSession(); + + resolver = createWithExpression("getAttribute('" + securityContextAttrName + "')?.authentication?.name"); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void constructorNullRedisMappingContext() { + + mappingContext = null; + new SpelIndexResolver(mappingContext); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void constructorNullSpelExpressionParser() { + new SpelIndexResolver(mappingContext, null); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void nullValue() { + + Set indexes = resolver.resolveIndexesFor(typeInformation, null); + + assertThat(indexes.size(), equalTo(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void wrongKeyspace() { + + typeInformation = ClassTypeInformation.from(String.class); + Set indexes = resolver.resolveIndexesFor(typeInformation, ""); + + assertThat(indexes.size(), equalTo(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void sessionAttributeNull() { + + session = new Session(); + Set indexes = resolver.resolveIndexesFor(typeInformation, session); + + assertThat(indexes.size(), equalTo(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolvePrincipalName() { + + Set indexes = resolver.resolveIndexesFor(typeInformation, session); + + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(keyspace, indexName, username))); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = SpelEvaluationException.class) + public void spelError() { + + session.setAttribute(securityContextAttrName, ""); + + resolver.resolveIndexesFor(typeInformation, session); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void withBeanAndThis() { + + this.resolver = createWithExpression("@bean.run(#this)"); + this.resolver.setBeanResolver(new BeanResolver() { + @Override + public Object resolve(EvaluationContext context, String beanName) throws AccessException { + return new Object() { + @SuppressWarnings("unused") + public Object run(Object arg) { + return arg; + } + }; + } + }); + + Set indexes = resolver.resolveIndexesFor(typeInformation, session); + + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(keyspace, indexName, session))); + } + + private SpelIndexResolver createWithExpression(String expression) { + + SpelIndexDefinition principalIndex = new SpelIndexDefinition(keyspace, expression, indexName); + IndexConfiguration configuration = new IndexConfiguration(); + configuration.addIndexDefinition(principalIndex); + + KeyspaceSettings keyspaceSettings = new KeyspaceSettings(Session.class, keyspace); + KeyspaceConfiguration keyspaceConfiguration = new KeyspaceConfiguration(); + keyspaceConfiguration.addKeyspaceSettings(keyspaceSettings); + + MappingConfiguration mapping = new MappingConfiguration(configuration, keyspaceConfiguration); + + mappingContext = new RedisMappingContext(mapping); + + return new SpelIndexResolver(mappingContext); + } + + private Session createSession() { + + Session session = new Session(); + session.setAttribute(securityContextAttrName, new SecurityContextImpl(new Authentication(username))); + return session; + } + + static class Session { + + private Map sessionAttrs = new HashMap(); + + public void setAttribute(String attrName, Object attrValue) { + this.sessionAttrs.put(attrName, attrValue); + } + + public Object getAttribute(String attributeName) { + return sessionAttrs.get(attributeName); + } + } + + static class SecurityContextImpl { + private final Authentication authentication; + + public SecurityContextImpl(Authentication authentication) { + this.authentication = authentication; + } + + public Authentication getAuthentication() { + return authentication; + } + } + + public static class Authentication { + private final String principalName; + + public Authentication(String principalName) { + this.principalName = principalName; + } + + public String getName() { + return principalName; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java new file mode 100644 index 0000000000..7c873e8a43 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-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.index; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * @author Rob Winch + * @author Christoph Strobl + */ +public class IndexConfigurationUnitTests { + + /** + * @see DATAREDIS-425 + */ + @Test + public void redisIndexSettingIndexNameDefaulted() { + + String path = "path"; + SimpleIndexDefinition setting = new SimpleIndexDefinition("keyspace", path); + assertThat(setting.getIndexName(), equalTo(path)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void redisIndexSettingIndexNameExplicit() { + + String indexName = "indexName"; + SimpleIndexDefinition setting = new SimpleIndexDefinition("keyspace", "index", indexName); + assertThat(setting.getIndexName(), equalTo(indexName)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void redisIndexSettingIndexNameUsedInEquals() { + + SimpleIndexDefinition setting1 = new SimpleIndexDefinition("keyspace", "path", "indexName1"); + SimpleIndexDefinition setting2 = new SimpleIndexDefinition(setting1.getKeyspace(), "path", setting1.getIndexName() + + "other"); + + assertThat(setting1, not(equalTo(setting2))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void redisIndexSettingIndexNameUsedInHashCode() { + + SimpleIndexDefinition setting1 = new SimpleIndexDefinition("keyspace", "path", "indexName1"); + SimpleIndexDefinition setting2 = new SimpleIndexDefinition(setting1.getKeyspace(), "path", setting1.getIndexName() + + "other"); + + assertThat(setting1.hashCode(), not(equalTo(setting2.hashCode()))); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java new file mode 100644 index 0000000000..5fa8fb6a67 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java @@ -0,0 +1,126 @@ +/* + * 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.mapping; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.TimeToLiveAccessor; +import org.springframework.data.redis.core.convert.ConversionTestEntities; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + * @param + * @param + */ +@RunWith(MockitoJUnitRunner.class) +public class BasicRedisPersistentEntityUnitTests { + + public @Rule ExpectedException expectedException = ExpectedException.none(); + + @Mock TypeInformation entityInformation; + @Mock KeySpaceResolver keySpaceResolver; + @Mock TimeToLiveAccessor ttlAccessor; + + BasicRedisPersistentEntity entity; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + + when(entityInformation.getType()).thenReturn((Class) ConversionTestEntities.Person.class); + entity = new BasicRedisPersistentEntity(entityInformation, keySpaceResolver, ttlAccessor); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void addingMultipleIdPropertiesWithoutAnExplicitOneThrowsException() { + + expectedException.expect(MappingException.class); + expectedException.expectMessage("Attempt to add id property"); + expectedException.expectMessage("but already have an property"); + + KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class); + when(property1.isIdProperty()).thenReturn(true); + + KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class); + when(property2.isIdProperty()).thenReturn(true); + + entity.addPersistentProperty(property1); + entity.addPersistentProperty(property2); + } + + /** + * @see DATAREDIS-425 + */ + @Test + @SuppressWarnings("unchecked") + public void addingMultipleExplicitIdPropertiesThrowsException() { + + expectedException.expect(MappingException.class); + expectedException.expectMessage("Attempt to add explicit id property"); + expectedException.expectMessage("but already have an property"); + + KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class); + when(property1.isIdProperty()).thenReturn(true); + when(property1.isAnnotationPresent(any(Class.class))).thenReturn(true); + + KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class); + when(property2.isIdProperty()).thenReturn(true); + when(property2.isAnnotationPresent(any(Class.class))).thenReturn(true); + + entity.addPersistentProperty(property1); + entity.addPersistentProperty(property2); + } + + /** + * @see DATAREDIS-425 + */ + @Test + @SuppressWarnings("unchecked") + public void explicitIdPropertiyShouldBeFavoredOverNonExplicit() { + + KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class); + when(property1.isIdProperty()).thenReturn(true); + + KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class); + when(property2.isIdProperty()).thenReturn(true); + when(property2.isAnnotationPresent(any(Class.class))).thenReturn(true); + + entity.addPersistentProperty(property1); + entity.addPersistentProperty(property2); + + assertThat(entity.getIdProperty(), is(equalTo(property2))); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareKeySpaceResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareKeySpaceResolverUnitTests.java new file mode 100644 index 0000000000..a0aab535a5 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareKeySpaceResolverUnitTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 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.mapping; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; +import org.springframework.data.redis.core.mapping.RedisMappingContext.ConfigAwareKeySpaceResolver; + +/** + * @author Christoph Strobl + */ +public class ConfigAwareKeySpaceResolverUnitTests { + + static final String CUSTOM_KEYSPACE = "car'a'carn"; + KeyspaceConfiguration config = new KeyspaceConfiguration(); + ConfigAwareKeySpaceResolver resolver; + + @Before + public void setUp() { + this.resolver = new ConfigAwareKeySpaceResolver(config); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void resolveShouldThrowExceptionWhenTypeIsNull() { + resolver.resolveKeySpace(null); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveShouldUseClassNameAsDefaultKeyspace() { + assertThat(resolver.resolveKeySpace(TypeWithoutAnySettings.class), is(TypeWithoutAnySettings.class.getName())); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveShouldFavorConfiguredNameOverClassName() { + + config.addKeyspaceSettings(new KeyspaceSettings(TypeWithoutAnySettings.class, "ji'e'toh")); + assertThat(resolver.resolveKeySpace(TypeWithoutAnySettings.class), is("ji'e'toh")); + } + + static class TypeWithoutAnySettings { + + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareTimeToLiveAccessorUnitTests.java b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareTimeToLiveAccessorUnitTests.java new file mode 100644 index 0000000000..8e85f85d41 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareTimeToLiveAccessorUnitTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2015 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.mapping; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; +import org.springframework.data.redis.core.mapping.RedisMappingContext.ConfigAwareTimeToLiveAccessor; + +/** + * @author Christoph Strobl + */ +public class ConfigAwareTimeToLiveAccessorUnitTests { + + ConfigAwareTimeToLiveAccessor accessor; + KeyspaceConfiguration config; + + @Before + public void setUp() { + + config = new KeyspaceConfiguration(); + accessor = new ConfigAwareTimeToLiveAccessor(config, new RedisMappingContext()); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void getTimeToLiveShouldThrowExceptionWhenSourceObjectIsNull() { + accessor.getTimeToLive(null); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnNullIfNothingConfiguredOrAnnotated() { + assertThat(accessor.getTimeToLive(new SimpleType()), nullValue()); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnConfiguredValueForSimpleType() { + + KeyspaceSettings setting = new KeyspaceSettings(SimpleType.class, null); + setting.setTimeToLive(10L); + config.addKeyspaceSettings(setting); + + assertThat(accessor.getTimeToLive(new SimpleType()), is(10L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnValueWhenTypeIsAnnotated() { + assertThat(accessor.getTimeToLive(new TypeWithRedisHashAnnotation()), is(5L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveConsidersAnnotationOverConfig() { + + KeyspaceSettings setting = new KeyspaceSettings(TypeWithRedisHashAnnotation.class, null); + setting.setTimeToLive(10L); + config.addKeyspaceSettings(setting); + + assertThat(accessor.getTimeToLive(new TypeWithRedisHashAnnotation()), is(5L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnValueWhenPropertyIsAnnotatedAndHasValue() { + assertThat(accessor.getTimeToLive(new TypeWithRedisHashAnnotationAndTTLProperty(20L)), is(20L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnValueFromTypeAnnotationWhenPropertyIsAnnotatedAndHasNullValue() { + assertThat(accessor.getTimeToLive(new TypeWithRedisHashAnnotationAndTTLProperty()), is(10L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnNullWhenPropertyIsAnnotatedAndHasNullValue() { + assertThat(accessor.getTimeToLive(new SimpleTypeWithTTLProperty()), nullValue()); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnConfiguredValueWhenPropertyIsAnnotatedAndHasNullValue() { + + KeyspaceSettings setting = new KeyspaceSettings(SimpleTypeWithTTLProperty.class, null); + setting.setTimeToLive(10L); + config.addKeyspaceSettings(setting); + + assertThat(accessor.getTimeToLive(new SimpleTypeWithTTLProperty()), is(10L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldFavorAnnotatedNotNullPropertyValueOverConfiguredOne() { + + KeyspaceSettings setting = new KeyspaceSettings(SimpleTypeWithTTLProperty.class, null); + setting.setTimeToLive(10L); + config.addKeyspaceSettings(setting); + + assertThat(accessor.getTimeToLive(new SimpleTypeWithTTLProperty(25L)), is(25L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnMethodLevelTimeToLiveIfPresent() { + assertThat(accessor.getTimeToLive(new TypeWithTtlOnMethod(10L)), is(10L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnConfiguredValueWhenMethodLevelTimeToLiveIfPresentButHasNullValue() { + + KeyspaceSettings setting = new KeyspaceSettings(TypeWithTtlOnMethod.class, null); + setting.setTimeToLive(10L); + config.addKeyspaceSettings(setting); + + assertThat(accessor.getTimeToLive(new TypeWithTtlOnMethod(null)), is(10L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void getTimeToLiveShouldReturnValueWhenMethodLevelTimeToLiveIfPresentAlthoughConfiguredValuePresent() { + + KeyspaceSettings setting = new KeyspaceSettings(TypeWithTtlOnMethod.class, null); + setting.setTimeToLive(10L); + config.addKeyspaceSettings(setting); + + assertThat(accessor.getTimeToLive(new TypeWithTtlOnMethod(100L)), is(100L)); + } + + static class SimpleType {} + + static class SimpleTypeWithTTLProperty { + + @TimeToLive Long ttl; + + SimpleTypeWithTTLProperty() {} + + SimpleTypeWithTTLProperty(Long ttl) { + this.ttl = ttl; + } + } + + @RedisHash(timeToLive = 5) + static class TypeWithRedisHashAnnotation {} + + @RedisHash(timeToLive = 10) + static class TypeWithRedisHashAnnotationAndTTLProperty { + + @TimeToLive Long ttl; + + TypeWithRedisHashAnnotationAndTTLProperty() {} + + TypeWithRedisHashAnnotationAndTTLProperty(Long ttl) { + this.ttl = ttl; + } + } + + static class TypeWithTtlOnMethod { + + Long value; + + public TypeWithTtlOnMethod(Long value) { + this.value = value; + } + + @TimeToLive + Long getTimeToLive() { + return value; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerTests.java b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerTests.java new file mode 100644 index 0000000000..fc3a2d1cf1 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2015 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.listener; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.UUID; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class KeyExpirationEventMessageListenerTests { + + RedisMessageListenerContainer container; + RedisConnectionFactory connectionFactory; + KeyExpirationEventMessageListener listener; + + @Mock ApplicationEventPublisher publisherMock; + + @Before + public void setUp() { + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); + connectionFactory.afterPropertiesSet(); + this.connectionFactory = connectionFactory; + + container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.afterPropertiesSet(); + container.start(); + + listener = new KeyExpirationEventMessageListener(container); + listener.setApplicationEventPublisher(publisherMock); + listener.init(); + } + + @After + public void tearDown() throws Exception { + + RedisConnection connection = connectionFactory.getConnection(); + try { + connection.flushAll(); + } finally { + connection.close(); + } + + listener.destroy(); + container.destroy(); + if (connectionFactory instanceof DisposableBean) { + ((DisposableBean) connectionFactory).destroy(); + } + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void listenerShouldPublishEventCorrectly() throws InterruptedException { + + byte[] key = ("to-expire:" + UUID.randomUUID().toString()).getBytes(); + + RedisConnection connection = connectionFactory.getConnection(); + try { + connection.setEx(key, 2, "foo".getBytes()); + + int iteration = 0; + while (connection.get(key) != null || iteration >= 3) { + + Thread.sleep(2000); + iteration++; + } + } finally { + connection.close(); + } + + Thread.sleep(2000); + ArgumentCaptor captor = ArgumentCaptor.forClass(ApplicationEvent.class); + + verify(publisherMock, times(1)).publishEvent(captor.capture()); + assertThat((byte[]) captor.getValue().getSource(), is(key)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void listenerShouldNotReactToDeleteEvents() throws InterruptedException { + + byte[] key = ("to-delete:" + UUID.randomUUID().toString()).getBytes(); + + RedisConnection connection = connectionFactory.getConnection(); + try { + + connection.setEx(key, 10, "foo".getBytes()); + Thread.sleep(2000); + connection.del(key); + Thread.sleep(2000); + } finally { + connection.close(); + } + + Thread.sleep(2000); + verifyZeroInteractions(publisherMock); + } +} diff --git a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerUnitTests.java b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerUnitTests.java new file mode 100644 index 0000000000..eb5604af57 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerUnitTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2015 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.listener; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.connection.DefaultMessage; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.core.RedisKeyExpiredEvent; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class KeyExpirationEventMessageListenerUnitTests { + + private static final String MESSAGE_CHANNEL = "channel"; + private static final String MESSAGE_BODY = "body"; + private static final Message MESSAGE = new DefaultMessage(MESSAGE_CHANNEL.getBytes(), MESSAGE_BODY.getBytes()); + + @Mock RedisMessageListenerContainer containerMock; + @Mock ApplicationEventPublisher publisherMock; + KeyExpirationEventMessageListener listener; + + @Before + public void setUp() { + + listener = new KeyExpirationEventMessageListener(containerMock); + listener.setApplicationEventPublisher(publisherMock); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void handleMessageShouldPublishKeyExpiredEvent() { + + listener.onMessage(MESSAGE, "*".getBytes()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ApplicationEvent.class); + + verify(publisherMock, times(1)).publishEvent(captor.capture()); + assertThat(captor.getValue(), instanceOf(RedisKeyExpiredEvent.class)); + assertThat((byte[]) captor.getValue().getSource(), is(MESSAGE_BODY.getBytes())); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void handleMessageShouldNotRespondToNullMessage() { + + listener.onMessage(null, "*".getBytes()); + + verifyZeroInteractions(publisherMock); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void handleMessageShouldNotRespondToEmptyMessage() { + + listener.onMessage(new DefaultMessage(null, null), "*".getBytes()); + + verifyZeroInteractions(publisherMock); + } +} diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java new file mode 100644 index 0000000000..866190dc3e --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java @@ -0,0 +1,399 @@ +/* + * Copyright 2015 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.repository; + +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.hamcrest.core.IsNull; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Reference; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.IndexDefinition; +import org.springframework.data.redis.core.index.Indexed; +import org.springframework.data.redis.core.index.SimpleIndexDefinition; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class RedisRepositoryIntegrationTests { + + @Configuration + @EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class, + keyspaceConfiguration = MyKeyspaceConfiguration.class, + includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") }) + static class Config { + + @Bean + RedisTemplate redisTemplate() { + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); + connectionFactory.afterPropertiesSet(); + + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(connectionFactory); + + return template; + } + + } + + @Autowired PersonRepository repo; + @Autowired KeyValueTemplate kvTemplate; + + @Before + public void setUp() { + + // flush keyspaces + kvTemplate.delete(Person.class); + kvTemplate.delete(City.class); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void simpleFindShouldReturnEntitiesCorrectly() { + + Person rand = new Person(); + rand.firstname = "rand"; + rand.lastname = "al'thor"; + + Person egwene = new Person(); + egwene.firstname = "egwene"; + + repo.save(Arrays.asList(rand, egwene)); + + assertThat(repo.count(), is(2L)); + + assertThat(repo.findOne(rand.id), is(rand)); + assertThat(repo.findOne(egwene.id), is(egwene)); + + assertThat(repo.findByFirstname("rand").size(), is(1)); + assertThat(repo.findByFirstname("rand"), hasItem(rand)); + + assertThat(repo.findByFirstname("egwene").size(), is(1)); + assertThat(repo.findByFirstname("egwene"), hasItem(egwene)); + + assertThat(repo.findByLastname("al'thor"), hasItem(rand)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void simpleFindByMultipleProperties() { + + Person egwene = new Person(); + egwene.firstname = "egwene"; + egwene.lastname = "al'vere"; + + Person marin = new Person(); + marin.firstname = "marin"; + marin.lastname = "al'vere"; + + repo.save(Arrays.asList(egwene, marin)); + + assertThat(repo.findByLastname("al'vere").size(), is(2)); + + assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").size(), is(1)); + assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").get(0), is(egwene)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findReturnsReferenceDataCorrectly() { + + // Prepare referenced data entry + City tarValon = new City(); + tarValon.id = "1"; + tarValon.name = "tar valon"; + + kvTemplate.insert(tarValon); + + // Prepare domain entity + Person moiraine = new Person(); + moiraine.firstname = "moiraine"; + moiraine.city = tarValon; // reference data + + // save domain entity + repo.save(moiraine); + + // find and assert current location set correctly + Person loaded = repo.findOne(moiraine.getId()); + assertThat(loaded.city, is(tarValon)); + + // remove reference location data + kvTemplate.delete("1", City.class); + + // find and assert the location is gone + Person reLoaded = repo.findOne(moiraine.getId()); + assertThat(reLoaded.city, IsNull.nullValue()); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findReturnsPageCorrectly() { + + Person eddard = new Person("eddard", "stark"); + Person robb = new Person("robb", "stark"); + Person sansa = new Person("sansa", "stark"); + Person arya = new Person("arya", "stark"); + Person bran = new Person("bran", "stark"); + Person rickon = new Person("rickon", "stark"); + + repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon)); + + Page page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5)); + + assertThat(page1.getNumberOfElements(), is(5)); + assertThat(page1.getTotalElements(), is(6L)); + + Page page2 = repo.findPersonByLastname("stark", page1.nextPageable()); + + assertThat(page2.getNumberOfElements(), is(1)); + assertThat(page2.getTotalElements(), is(6L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findUsingOrReturnsResultCorrectly() { + + Person eddard = new Person("eddard", "stark"); + Person robb = new Person("robb", "stark"); + Person jon = new Person("jon", "snow"); + + repo.save(Arrays.asList(eddard, robb, jon)); + + List eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow"); + + assertThat(eddardAndJon, hasSize(2)); + assertThat(eddardAndJon, containsInAnyOrder(eddard, jon)); + } + + public static interface PersonRepository extends CrudRepository { + + List findByFirstname(String firstname); + + List findByLastname(String lastname); + + Page findPersonByLastname(String lastname, Pageable page); + + List findByFirstnameAndLastname(String firstname, String lastname); + + List findByFirstnameOrLastname(String firstname, String lastname); + } + + /** + * Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}. + * + * @author Christoph Strobl + */ + static class MyIndexConfiguration extends IndexConfiguration { + + @Override + protected Iterable initialConfiguration() { + return Collections. singleton(new SimpleIndexDefinition("persons", "lastname")); + } + } + + /** + * Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}. + * + * @author Christoph Strobl + */ + static class MyKeyspaceConfiguration extends KeyspaceConfiguration { + + @Override + protected Iterable initialConfiguration() { + return Collections.singleton(new KeyspaceSettings(City.class, "cities")); + } + } + + @RedisHash("persons") + @SuppressWarnings("serial") + public static class Person implements Serializable { + + @Id String id; + @Indexed String firstname; + String lastname; + @Reference City city; + + public Person() {} + + public Person(String firstname, String lastname) { + + this.firstname = firstname; + this.lastname = lastname; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public String getLastname() { + return lastname; + } + + @Override + public String toString() { + return "Person [id=" + id + ", firstname=" + firstname + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((firstname == null) ? 0 : firstname.hashCode()); + result = prime * result + ((id == null) ? 0 : id.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 (firstname == null) { + if (other.firstname != null) { + return false; + } + } else if (!firstname.equals(other.firstname)) { + return false; + } + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + } + + public static class City { + @Id String id; + String name; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.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 (!(obj instanceof City)) { + return false; + } + City other = (City) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + } + +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/CdiExtensionIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/cdi/CdiExtensionIntegrationTests.java new file mode 100644 index 0000000000..0e7a2b1636 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/CdiExtensionIntegrationTests.java @@ -0,0 +1,114 @@ +/* + * 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.repository.cdi; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.List; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.spi.Bean; + +import org.apache.webbeans.cditest.CdiTestContainer; +import org.apache.webbeans.cditest.CdiTestContainerLoader; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Integration tests for Spring Data Redis CDI extension. + * + * @author Mark Paluch + */ +public class CdiExtensionIntegrationTests { + + private static Logger LOGGER = LoggerFactory.getLogger(CdiExtensionIntegrationTests.class); + + static CdiTestContainer container; + + @BeforeClass + public static void setUp() throws Exception { + + container = CdiTestContainerLoader.getCdiContainer(); + container.bootContainer(); + + LOGGER.debug("CDI container bootstrapped!"); + } + + /** + * @see DATAREDIS-425 + */ + @Test + @SuppressWarnings("rawtypes") + public void beanShouldBeRegistered() { + + Set> beans = container.getBeanManager().getBeans(PersonRepository.class); + + assertThat(beans, hasSize(1)); + assertThat(beans.iterator().next().getScope(), is(equalTo((Class) ApplicationScoped.class))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void saveAndFindUnqualified() { + + RepositoryConsumer repositoryConsumer = container.getInstance(RepositoryConsumer.class); + repositoryConsumer.deleteAll(); + + Person person = new Person(); + person.setName("foo"); + repositoryConsumer.getUnqualifiedRepo().save(person); + List result = repositoryConsumer.getUnqualifiedRepo().findByName("foo"); + + assertThat(result, contains(person)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void saveAndFindQualified() { + + RepositoryConsumer repositoryConsumer = container.getInstance(RepositoryConsumer.class); + repositoryConsumer.deleteAll(); + + Person person = new Person(); + person.setName("foo"); + repositoryConsumer.getUnqualifiedRepo().save(person); + List result = repositoryConsumer.getQualifiedRepo().findByName("foo"); + + assertThat(result, contains(person)); + } + + + /** + * @see DATAREDIS-425 + */ + @Test + public void callMethodOnCustomRepositoryShouldSuceed() { + + RepositoryConsumer repositoryConsumer = container.getInstance(RepositoryConsumer.class); + + int result = repositoryConsumer.getUnqualifiedRepo().returnOne(); + assertThat(result, is(1)); + } + +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/Person.java b/src/test/java/org/springframework/data/redis/repository/cdi/Person.java new file mode 100644 index 0000000000..324ae3cb5e --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/Person.java @@ -0,0 +1,68 @@ +/* + * 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.repository.cdi; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +/** + * @author Mark Paluch + */ +@RedisHash +class Person { + + @Id private String id; + + @Indexed private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Person)) + return false; + + Person person = (Person) o; + + if (id != null ? !id.equals(person.id) : person.id != null) + return false; + return name != null ? name.equals(person.name) : person.name == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/PersonDB.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonDB.java new file mode 100644 index 0000000000..34aee2c299 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonDB.java @@ -0,0 +1,33 @@ +/* + * 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.repository.cdi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * @author Mark Paluch + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) +@interface PersonDB { + +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepository.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepository.java new file mode 100644 index 0000000000..287d66c2a7 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepository.java @@ -0,0 +1,33 @@ +/* + * 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.repository.cdi; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.cdi.Eager; + +/** + * @author Mark Paluch + */ +@Eager +public interface PersonRepository extends CrudRepository, PersonRepositoryCustom { + + List findAll(); + + List findByName(String name); + +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepositoryCustom.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepositoryCustom.java new file mode 100644 index 0000000000..97424a8815 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepositoryCustom.java @@ -0,0 +1,25 @@ +/* + * 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.repository.cdi; + +/** + * @author Mark Paluch + */ +interface PersonRepositoryCustom { + + int returnOne(); +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepositoryImpl.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepositoryImpl.java new file mode 100644 index 0000000000..1d1cc6ab7b --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepositoryImpl.java @@ -0,0 +1,28 @@ +/* + * 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.repository.cdi; + +/** + * @author Mark Paluch + */ +public class PersonRepositoryImpl implements PersonRepositoryCustom { + + @Override + public int returnOne() { + return 1; + } +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/QualifiedPersonRepository.java b/src/test/java/org/springframework/data/redis/repository/cdi/QualifiedPersonRepository.java new file mode 100644 index 0000000000..824960e345 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/QualifiedPersonRepository.java @@ -0,0 +1,25 @@ +/* + * 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.repository.cdi; + +/** + * @author Mark Paluch + */ +@PersonDB +public interface QualifiedPersonRepository extends PersonRepository { + +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/RedisCdiDependenciesProducer.java b/src/test/java/org/springframework/data/redis/repository/cdi/RedisCdiDependenciesProducer.java new file mode 100644 index 0000000000..64f26ad8ba --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/RedisCdiDependenciesProducer.java @@ -0,0 +1,94 @@ +/* + * 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.repository.cdi; + +import javax.enterprise.inject.Disposes; +import javax.enterprise.inject.Produces; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.redis.SettingsUtils; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisKeyValueTemplate; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.mapping.RedisMappingContext; + +/** + * @author Mark Paluch + */ +public class RedisCdiDependenciesProducer { + + /** + * Provides a producer method for {@link RedisConnectionFactory}. + */ + @Produces + public RedisConnectionFactory redisConnectionFactory() { + + JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(); + jedisConnectionFactory.setHostName(SettingsUtils.getHost()); + jedisConnectionFactory.setPort(SettingsUtils.getPort()); + jedisConnectionFactory.afterPropertiesSet(); + return jedisConnectionFactory; + } + + public void closeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception { + + if (redisConnectionFactory instanceof DisposableBean) { + ((DisposableBean) redisConnectionFactory).destroy(); + } + } + + /** + * Provides a producer method for {@link RedisOperations}. + */ + @Produces + public RedisOperations redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) { + + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + template.afterPropertiesSet(); + return template; + } + + // shortcut for managed KeyValueAdapter/Template. + @Produces + @PersonDB + public RedisOperations redisOperationsProducerQualified(RedisOperations instance) { + return instance; + } + + public void closeRedisOperations(@Disposes RedisOperations redisOperations) throws Exception { + + if (redisOperations instanceof DisposableBean) { + ((DisposableBean) redisOperations).destroy(); + } + } + + /** + * Provides a producer method for {@link RedisKeyValueTemplate}. + */ + @Produces + public RedisKeyValueTemplate redisKeyValueAdapterDefault(RedisOperations redisOperations) { + + RedisKeyValueAdapter redisKeyValueAdapter = new RedisKeyValueAdapter(redisOperations); + RedisKeyValueTemplate keyValueTemplate = new RedisKeyValueTemplate(redisKeyValueAdapter, new RedisMappingContext()); + return keyValueTemplate; + } + +} diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/RepositoryConsumer.java b/src/test/java/org/springframework/data/redis/repository/cdi/RepositoryConsumer.java new file mode 100644 index 0000000000..d12edd465d --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/cdi/RepositoryConsumer.java @@ -0,0 +1,42 @@ +/* + * 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.repository.cdi; + +import javax.inject.Inject; + +/** + * @author Mark Paluch + */ +class RepositoryConsumer { + + @Inject PersonRepository unqualifiedRepo; + @Inject @PersonDB PersonRepository qualifiedRepo; + + public PersonRepository getUnqualifiedRepo() { + return unqualifiedRepo; + } + + public PersonRepository getQualifiedRepo() { + return qualifiedRepo; + } + + public void deleteAll() { + + unqualifiedRepo.deleteAll(); + qualifiedRepo.deleteAll(); + } + +} diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java new file mode 100644 index 0000000000..4c81841a8f --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java @@ -0,0 +1,121 @@ +/* + * 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.repository.configuration; + +import static org.junit.Assert.*; + +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.data.annotation.Id; +import org.springframework.data.keyvalue.repository.KeyValueRepository; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfiguration; +import org.springframework.data.repository.config.RepositoryConfigurationSource; + +/** + * @author Christoph Strobl + */ +public class RedisRepositoryConfigurationExtensionUnitTests { + + StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + ResourceLoader loader = new PathMatchingResourcePatternResolver(); + Environment environment = new StandardEnvironment(); + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableRedisRepositories.class, loader, environment); + + RedisRepositoryConfigurationExtension extension; + + @Before + public void setUp() { + extension = new RedisRepositoryConfigurationExtension(); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { + assertHasRepo(SampleRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() { + assertHasRepo(StoreRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() { + + assertDoesNotHaveRepo(UnannotatedRepository.class, + extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + private static void assertDoesNotHaveRepo(Class repositoryInterface, + Collection> configs) { + + try { + + assertHasRepo(repositoryInterface, configs); + fail("Expected not to find config for repository interface ".concat(repositoryInterface.getName())); + } catch (AssertionError error) { + // repo not there. we're fine. + } + } + + private static void assertHasRepo(Class repositoryInterface, + Collection> configs) { + + for (RepositoryConfiguration config : configs) { + if (config.getRepositoryInterface().equals(repositoryInterface.getName())) { + return; + } + } + + fail("Expected to find config for repository interface ".concat(repositoryInterface.getName()).concat(" but got ") + .concat(configs.toString())); + } + + @EnableRedisRepositories(considerNestedRepositories = true) + static class Config { + + } + + @RedisHash + static class Sample { + @Id String id; + } + + interface SampleRepository extends Repository {} + + interface UnannotatedRepository extends Repository {} + + interface StoreRepository extends KeyValueRepository {} +} diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java new file mode 100644 index 0000000000..6331713c62 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java @@ -0,0 +1,148 @@ +package org.springframework.data.redis.repository.configuration; +/* + * 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. + */ + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.convert.ReferenceResolver; +import org.springframework.data.redis.repository.configuration.RedisRepositoryConfigurationUnitTests.ContextWithCustomReferenceResolver; +import org.springframework.data.redis.repository.configuration.RedisRepositoryConfigurationUnitTests.ContextWithoutCustomization; +import org.springframework.data.repository.Repository; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * @author Christoph Strobl + */ +@RunWith(Suite.class) +@SuiteClasses({ ContextWithCustomReferenceResolver.class, ContextWithoutCustomization.class }) +public class RedisRepositoryConfigurationUnitTests { + + static RedisTemplate createTemplateMock() { + + RedisTemplate template = mock(RedisTemplate.class); + RedisConnectionFactory connectionFactory = mock(RedisConnectionFactory.class); + RedisConnection connection = mock(RedisConnection.class); + + when(template.getConnectionFactory()).thenReturn(connectionFactory); + when(connectionFactory.getConnection()).thenReturn(connection); + + return template; + } + + @RunWith(SpringJUnit4ClassRunner.class) + @DirtiesContext + @ContextConfiguration(classes = { ContextWithCustomReferenceResolver.Config.class }) + public static class ContextWithCustomReferenceResolver { + + @EnableRedisRepositories(considerNestedRepositories = true, + includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = { ".*ContextSampleRepository" }) }) + static class Config { + + @Bean + RedisTemplate redisTemplate() { + return createTemplateMock(); + } + + @Bean + ReferenceResolver redisReferenceResolver() { + return mock(ReferenceResolver.class); + } + + } + + @Autowired ApplicationContext ctx; + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldPickUpReferenceResolver() { + + RedisKeyValueAdapter adapter = (RedisKeyValueAdapter) ctx.getBean("redisKeyValueAdapter"); + + Object referenceResolver = ReflectionTestUtils.getField(adapter.getConverter(), "referenceResolver"); + + assertThat(referenceResolver, is(equalTo(ctx.getBean("redisReferenceResolver")))); + assertThat(mockingDetails(referenceResolver).isMock(), is(true)); + } + } + + @RunWith(SpringJUnit4ClassRunner.class) + @DirtiesContext + @ContextConfiguration(classes = { ContextWithoutCustomization.Config.class }) + public static class ContextWithoutCustomization { + + @EnableRedisRepositories(considerNestedRepositories = true, + includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = { ".*ContextSampleRepository" }) }) + static class Config { + + @Bean + RedisTemplate redisTemplate() { + return createTemplateMock(); + } + } + + @Autowired ApplicationContext ctx; + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldInitWithDefaults() { + assertThat(ctx.getBean(ContextSampleRepository.class), is(notNullValue())); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldRegisterDefaultBeans() { + + assertThat(ctx.getBean(ContextSampleRepository.class), is(notNullValue())); + assertThat(ctx.getBean("redisKeyValueAdapter"), is(notNullValue())); + assertThat(ctx.getBean("redisCustomConversions"), is(notNullValue())); + assertThat(ctx.getBean("redisReferenceResolver"), is(notNullValue())); + } + } + + @RedisHash + static class Sample { + String id; + } + + interface ContextSampleRepository extends Repository {} +} diff --git a/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java b/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java new file mode 100644 index 0000000000..120b2c6045 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java @@ -0,0 +1,49 @@ +/* + * 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.repository.core; + +import static org.mockito.Mockito.*; + +import java.io.Serializable; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.convert.ConversionTestEntities; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class MappingRedisEntityInformationUnitTests { + + @Mock RedisPersistentEntity entity; + + /** + * @see DATAREDIS-425 + */ + @Test(expected = MappingException.class) + @SuppressWarnings("unchecked") + public void throwsMappingExceptionWhenNoIdPropertyPresent() { + + when(entity.hasIdProperty()).thenReturn(false); + when(entity.getType()).thenReturn((Class) ConversionTestEntities.Person.class); + new MappingRedisEntityInformation(entity); + } +} diff --git a/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java b/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java new file mode 100644 index 0000000000..100174462f --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java @@ -0,0 +1,108 @@ +/* + * 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.repository.query; + +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.redis.core.convert.ConversionTestEntities; +import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * @author Christoph Strobl + */ +public class RedisQueryCreatorUnitTests { + + private @Mock RepositoryMetadata metadataMock; + + /** + * @see DATAREDIS-425 + */ + @Test + public void findBySingleSimpleProperty() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByFirstname", String.class), new Object[] { "eddard" }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getSismember(), hasSize(1)); + assertThat(query.getCritieria().getSismember(), hasItem(new PathAndValue("firstname", "eddard"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findByMultipleSimpleProperties() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByFirstnameAndAge", String.class, Integer.class), new Object[] { + "eddard", 43 }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getSismember(), hasSize(2)); + assertThat(query.getCritieria().getSismember(), hasItem(new PathAndValue("firstname", "eddard"))); + assertThat(query.getCritieria().getSismember(), hasItem(new PathAndValue("age", 43))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findByMultipleSimplePropertiesUsingOr() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByAgeOrFirstname", Integer.class, String.class), new Object[] { 43, + "eddard" }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getOrSismember(), hasSize(2)); + assertThat(query.getCritieria().getOrSismember(), hasItem(new PathAndValue("age", 43))); + assertThat(query.getCritieria().getOrSismember(), hasItem(new PathAndValue("firstname", "eddard"))); + } + + private RedisQueryCreator createQueryCreatorForMethodWithArgs(Method method, Object[] args) { + + PartTree partTree = new PartTree(method.getName(), method.getReturnType()); + RedisQueryCreator creator = new RedisQueryCreator(partTree, new ParametersParameterAccessor(new DefaultParameters( + method), args)); + + return creator; + } + + private interface SampleRepository extends Repository { + + ConversionTestEntities.Person findByFirstname(String firstname); + + ConversionTestEntities.Person findByFirstnameAndAge(String firstname, Integer age); + + ConversionTestEntities.Person findByAgeOrFirstname(Integer age, String firstname); + } +} diff --git a/src/test/java/org/springframework/data/redis/test/util/IsBucketMatcher.java b/src/test/java/org/springframework/data/redis/test/util/IsBucketMatcher.java new file mode 100644 index 0000000000..1598b196ce --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/IsBucketMatcher.java @@ -0,0 +1,201 @@ +/* + * Copyright 2015 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.test.util; + +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.springframework.data.redis.core.convert.Bucket; + +/** + * {@link TypeSafeMatcher} implementation for checking contents of {@link Bucket}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class IsBucketMatcher extends TypeSafeMatcher { + + Map expected = new LinkedHashMap(); + Set without = new LinkedHashSet(); + + /* + * (non-Javadoc) + * @see org.hamcrest.SelfDescribing#describeTo(org.hamcrest.Description) + */ + @Override + public void describeTo(Description description) { + + if (!expected.isEmpty()) { + description.appendValueList("Expected Bucket content [{", "},{", "}].", expected.entrySet()); + } + if (!without.isEmpty()) { + description.appendValueList("Expected Bucket to not include [", ",", "].", without); + } + + } + + /* + * (non-Javadoc) + * @see org.hamcrest.TypeSafeMatcher#matchesSafely(java.lang.Object) + */ + @Override + protected boolean matchesSafely(Bucket bucket) { + + if (bucket == null) { + return false; + } + + if (bucket.isEmpty() && expected.isEmpty()) { + return true; + } + + for (String notContained : without) { + byte[] value = bucket.get(notContained); + if (value != null || (value != null && value.length > 0)) { + return false; + } + } + + for (Map.Entry entry : expected.entrySet()) { + + byte[] actualValue = bucket.get(entry.getKey()); + Object expectedValue = entry.getValue(); + if (expectedValue == null && actualValue != null) { + return false; + } + + if (expectedValue != null && actualValue == null) { + return false; + } + + if (expectedValue instanceof byte[]) { + if (!Arrays.equals((byte[]) expectedValue, actualValue)) { + return false; + } + } else if (expectedValue instanceof String) { + if (!((String) expectedValue).equals(new String(actualValue, Bucket.CHARSET))) { + return false; + } + } else if (expectedValue instanceof Class) { + if (!((Class) expectedValue).getName().equals(new String(actualValue, Bucket.CHARSET))) { + return false; + } + } else if (expectedValue instanceof Date) { + if (((Date) expectedValue).getTime() != Long.valueOf(new String(actualValue, Bucket.CHARSET)).longValue()) { + return false; + } + } + + else if (expectedValue instanceof Matcher) { + if (!((Matcher) expectedValue).matches(actualValue)) { + return false; + } + } else { + if (!(expectedValue.toString()).equals(new String(actualValue, Bucket.CHARSET))) { + return false; + } + } + } + + return true; + } + + /** + * Creates new {@link IsBucketMatcher}. + * + * @return + */ + public static IsBucketMatcher isBucket() { + return new IsBucketMatcher(); + } + + /** + * Checks for presence of type hint at given path. + * + * @param path + * @param type + * @return + */ + public IsBucketMatcher containingTypeHint(String path, Class type) { + + this.expected.put(path, type); + return this; + } + + /** + * Checks for presence of equivalent String value at path. + * + * @param path + * @param value + * @return + */ + public IsBucketMatcher containingUtf8String(String path, String value) { + + this.expected.put(path, value); + return this; + } + + /** + * Checks for presence of given value at path. + * + * @param path + * @param value + * @return + */ + public IsBucketMatcher containing(String path, byte[] value) { + + this.expected.put(path, value); + return this; + } + + public IsBucketMatcher matchingPath(String path, Matcher matcher) { + + this.expected.put(path, matcher); + return this; + } + + /** + * Checks for presence of equivalent time in msec value at path. + * + * @param path + * @param date + * @return + */ + public IsBucketMatcher containingDateAsMsec(String path, Date date) { + + this.expected.put(path, date); + return this; + } + + /** + * Checks given path is not present. + * + * @param path + * @return + */ + public IsBucketMatcher without(String path) { + this.without.add(path); + return this; + } + +} diff --git a/src/test/resources/META-INF/beans.xml b/src/test/resources/META-INF/beans.xml new file mode 100644 index 0000000000..73ae3a2516 --- /dev/null +++ b/src/test/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + + diff --git a/template.mf b/template.mf index 984704fab5..d74b88be3e 100644 --- a/template.mf +++ b/template.mf @@ -21,10 +21,14 @@ Import-Template: org.springframework.cglib.*;version="${spring:[=.=.=.=,+1.1.0)}", org.springframework.oxm.*;resolution:="optional";version="${spring:[=.=.=.=,+1.1.0)}", org.springframework.transaction.support.*;version="${spring:[=.=.=.=,+1.1.0)}", + org.springframework.expression.*;version="${spring:[=.=.=.=,+1.1.0)}", + org.springframework.data.*;version="0", + org.slf4j.*;version="[1.7.12, 1.7.12]", 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", javax.xml.transform.*;resolution:="optional";version="0", + javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional, org.jredis.*;resolution:="optional";version="[1.0.0, 2.0.0)", redis.clients.*;resolution:="optional";version="${jedis}", org.apache.commons.pool2.*;resolution:="optional";version="${pool}",