DATAREDIS-425 - Add Support for basic CRUD and finder Operations backed by Hashes and Sets.

We now enable storing domain object as a flat Redis 'HASH' and maintain additional 'SET' structures to enable finder operations on simple properties.

  @RedisHash("persons");
  class Person {

    @id String id;
    @Indexed String firstname;
    String lastname;

    Map<String, String> attributes;

    City city;

    @Reference Person mother;
  }

The above is stored in the HASH with key 'persons:1' as

  _class = org.example.Person
  id = 1
  firstname = rand
  lastname = al’thor
  attributes.[eye-color] = grey
  attributes.[hair-color] = red
  city.name = emond's field
  city.region = two rivers
  mother = persons:2

Complex types are flattened out to their full property path for each of the values provided. If the properties actual value type does not match the declared one the '_class' type hint is added to the entry.

  city._class = CityInAndor.class
  city.name = emond's field
  city.region = two rivers
  city.country = andor

Map and Collection like structures are stored with their key/index values as part of the property path. If the map/collection value type does not match the actutal objects one the '_class' type hint is added to the entry.

  list.[0]._class = DomainType.class
  list.[0].property1 = ...

  map.[key-1]._class = DomainType.class
  map.[key-1].property1 = ...

Properties marked with '@Reference' are stored as semantic references by just storing the key to the referenced object 'HASH' instead of embedding its values.

   mother = persons:2

Please note that referenced objects are not transitively updated/saved and that lazy loading of references will be part of future development.

A 'save' operation therefore executes the following:

  # flatten domain type and add as hash
  HMSET persons:1 id 1 firstname rand …

  # add the newly inserted entry to the list of all entries of that type
  SADD persons 1

  # index the firstname for finder lookup
  SADD persons.firstname:rand 1

Simple finder operation like 'findByFirstname' use 'SINTER' to find matching

  SINTER persons.firstname:rand
  HGETALL persons:1

Besides resolving an index via the '@Index' annotation we also allow to add custom configuration via the 'indexConfiguration' attribute of '@EnableRedisRepositories'.

  @Configuration
  @EnableRedisRepositories(indexConfiguration = CustomIndexConfiguration.class)
  class Config { }

  static class CustomIndexConfiguration extends IndexConfiguration {

    @Override
    protected Iterable<RedisIndexDefinition> initialConfiguration() {
      return Arrays.asList(
        new SimpleIndexDefinition("persons", "lastname"),
      );
    }
  }

The '@TimeToLive' annotation allows to define a property or method providing an expiration time when storing the key in redis.

@RedisHash
class Person {

  @Id String id;
  @TimeToLive Long ttl;
}

Original Pull Request: #156
This commit is contained in:
Christoph Strobl
2015-08-31 15:02:40 +02:00
parent 5a85ac47a3
commit bd7728e6ad
57 changed files with 9911 additions and 6 deletions

View File

@@ -4,8 +4,6 @@ jdk:
- oraclejdk8
env:
matrix:
- PROFILE=ci
- PROFILE=spring41-next
- PROFILE=spring42
- PROFILE=spring42-next
- PROFILE=spring43-next

View File

@@ -17,7 +17,7 @@
<properties>
<dist.key>DATAREDIS</dist.key>
<springdata.commons>1.12.0.BUILD-SNAPSHOT</springdata.commons>
<springdata.keyvalue>1.1.0.BUILD-SNAPSHOT</springdata.keyvalue>
<jta>1.1</jta>
<beanutils>1.9.2</beanutils>
<xstream>1.4.8</xstream>
@@ -53,8 +53,8 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${springdata.commons}</version>
<artifactId>spring-data-keyvalue</artifactId>
<version>${springdata.keyvalue}</version>
</dependency>
<dependency>

View File

@@ -0,0 +1,521 @@
[[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.
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<Person, String> {
}
----
====
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<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
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 = althor
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<String> nicknames = asList("dragon reborn", "lews therin");
| nicknames.[0] = "dragon reborn", +
nicknames.[1] = "lews therin"
| Map +
of Simple Type
| Map<String, String> atts = asMap({"eye-color", "grey"}, {"...
| atts.[eye-color] = "grey", +
atts.[hair-color] = "...
| List +
of Complex Type
| List<Address> addresses = asList(new Address("em...
| addresses.[0].city = "emond's field", +
addresses.[1].city = "...
| Map +
of Complex Type
| Map<String, Address> 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<String,byte[]>` whereas the first one is suitable for eg. converting one complex type to eg. a binary JSON representation that still uses the default mappings hash structure, whereas the second options offers full control over the resulting hash.
.Sample byte[] Converters
====
[source,java]
----
@WritingConverter
public class AddressToBytesConverter implements Converter<Address, byte[]> {
private final Jackson2JsonRedisSerializer<Address> serializer;
public AddressToBytesConverter() {
serializer = new Jackson2JsonRedisSerializer<Address>(Address.class);
serializer.setObjectMapper(new ObjectMapper());
}
@Override
public byte[] convert(Address value) {
return serializer.serialize(value);
}
}
@ReadingConverter
public class BytesToAddressConverter implements Converter<byte[], Address> {
private final Jackson2JsonRedisSerializer<Address> serializer;
public BytesToAddressConverter() {
serializer = new Jackson2JsonRedisSerializer<Address>(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 = althor
address = { city : "emond's field", country : "andor" }
----
====
.Sample Map<String,byte[]> Converters
====
[source,java]
----
@WritingConverter
public class AddressToMapConverter implements Converter<Address, Map<String,byte[]>> {
@Override
public Map<String,byte[]> convert(Address source) {
return singletonMap("ciudad", source.getCity().getBytes());
}
}
@ReadingConverter
public class MapToAddressConverter implements Converter<Address, Map<String, byte[]>> {
@Override
public Address convert(Map<String,byte[]> 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 = althor
ciudad = "emond's field"
----
====
NOTE: Custom conversions have no effect on index resolution. <<redis.repositories.indexes>> 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<KeyspaceSettings> 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<KeyspaceSettings> 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 <<redis.repositories.expirations,expire>>.
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<String,String> attributes; <1>
Map<String Person> relatives; <2>
List<Address> 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 <<redis.repositories.references,References>>.
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<RedisIndexSetting> 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<RedisIndexSetting> 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 <<redis.repositories.keyspaces>>).
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 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.
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.
[[redis.repositories.references]]
== Persisting References
Marking properties with `@Reference` allows to store a simple key reference instead of copying the all values into the hash itself.
On loading from Redis references are resolved automatically and mapped back into the object.
.Sample Property Reference
====
[source,text]
----
_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = althor
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<Person, String> {
List<Person> findByFirstname(String firstname);
}
----
====
NOTE: Please make sure properties used in finder methods are set up for indexing.
Using derived finder methods might not always be sufficient to model the queries to execute. `RedisCallback` offers more control over the actual matching of index structures or even customly added ones. All it takes is providing a `RedisCallback` that returns a single or `Iterable` set of _id_ values.
.Sample finder using RedisCallback
====
[source,java]
----
String user = //...
List<RedisSession> sessionsByUser = template.find(new RedisCallback<Set<byte[]>>() {
public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
return connection
.sMembers("sessions:securityContext.authentication.principal.username:" + user);
}}, RedisSession.class);
----
====

View File

@@ -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]]

View File

@@ -150,7 +150,7 @@ abstract public class LettuceConverters extends Converters {
public Map<byte[], byte[]> convert(final List<byte[]> source) {
if (CollectionUtils.isEmpty(source)) {
Collections.emptyMap();
return Collections.emptyMap();
}
Map<byte[], byte[]> target = new LinkedHashMap<byte[], byte[]>();

View File

@@ -0,0 +1,183 @@
/*
* 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.Set;
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 <a href="http://redis.io/topics/indexes">secondary index</a> 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
* @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<IndexedData> 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<byte[]> potentialIndex = connection.keys(toBytes(keyspace + ":*"));
if (!potentialIndex.isEmpty()) {
connection.del(potentialIndex.toArray(new byte[potentialIndex.size()][]));
}
}
private void removeKeyFromExistingIndexes(byte[] key, Iterable<IndexedData> indexValues) {
for (IndexedData indexData : indexValues) {
removeKeyFromExistingIndexes(key, indexData);
}
}
/**
* Remove given key from all indexes matching {@link IndexedData#getPath()}:
*
* @param key
* @param indexedData
*/
protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData) {
Assert.notNull(indexedData, "IndexedData must not be null!");
Set<byte[]> existingKeys = connection.keys(toBytes(indexedData.getKeySpace() + ":" + indexedData.getPath() + ":*"));
if (!CollectionUtils.isEmpty(existingKeys)) {
for (byte[] existingKey : existingKeys) {
connection.sRem(existingKey, key);
}
}
}
private void addKeyToIndexes(byte[] key, Iterable<IndexedData> indexValues) {
for (IndexedData indexData : indexValues) {
addKeyToIndex(key, indexData);
}
}
/**
* Adds a given key to the index for {@link IndexedData#getPath()}.
*
* @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.getPath() + ":");
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;
}
return converter.getConversionService().convert(source, byte[].class);
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,98 @@
/*
* 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<T> extends RedisKeyspaceEvent {
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.forName("UTF-8"));
}
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() + "]";
}
}

View File

@@ -0,0 +1,608 @@
/*
* 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.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.IndexResolverImpl;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.convert.MappingRedisConverter;
import org.springframework.data.redis.core.convert.RedisConverter;
import org.springframework.data.redis.core.convert.RedisData;
import org.springframework.data.redis.core.convert.ReferenceResolver;
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. <br />
* <strong>Example</strong>
*
* <pre>
* <code>
* &#64;RedisHash("persons")
* class Person {
* &#64;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
* </code>
* </pre>
*
* <br />
* The {@link KeyValueAdapter} is <strong>not</strong> 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<RedisKeyspaceEvent> {
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 IndexResolverImpl(
mappingContext), new ReferenceResolverImpl(this));
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();
}
/*
* (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(id);
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<Object>() {
@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<Boolean>() {
@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> T get(Serializable id, Serializable keyspace, Class<T> type) {
final byte[] binId = createKey(keyspace, id);
Map<byte[], byte[]> raw = redisOps.execute(new RedisCallback<Map<byte[], byte[]>>() {
@Override
public Map<byte[], byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
return connection.hGetAll(binId);
}
});
RedisData data = new RedisData(raw);
data.setId(id);
data.setKeyspace(keyspace.toString());
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> T delete(final Serializable id, final Serializable keyspace, final Class<T> type) {
final byte[] binId = toBytes(id);
final byte[] binKeyspace = toBytes(keyspace);
T o = get(id, keyspace, type);
if (o != null) {
redisOps.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.del(createKey(keyspace, id));
connection.sRem(binKeyspace, binId);
new IndexWriter(connection, converter).removeKeyFromIndexes(keyspace.toString(), 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<Map<byte[], byte[]>> raw = redisOps.execute(new RedisCallback<List<Map<byte[], byte[]>>>() {
@Override
public List<Map<byte[], byte[]>> doInRedis(RedisConnection connection) throws DataAccessException {
final List<Map<byte[], byte[]>> rawData = new ArrayList<Map<byte[], byte[]>>();
Set<byte[]> members = connection.sMembers(binKeyspace);
for (byte[] id : members) {
rawData.add(connection.hGetAll(createKey(binKeyspace, id)));
}
return rawData;
}
});
List<Object> result = new ArrayList<Object>(raw.size());
for (Map<byte[], byte[]> 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<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.del(toBytes(keyspace));
new IndexWriter(connection, converter).removeAllIndexes(keyspace.toString());
return null;
}
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#entries(java.io.Serializable)
*/
public CloseableIterator<Entry<Serializable, Object>> 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<Long>() {
@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> T execute(RedisCallback<T> 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
}
public byte[] createKey(Serializable keyspace, Serializable 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<Void>() {
@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<byte[], byte[]> hash = ops.execute(new RedisCallback<Map<byte[], byte[]>>() {
@Override
public Map<byte[], byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
Map<byte[], byte[]> 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;
}
}
/**
* {@link ReferenceResolver} using {@link RedisKeyValueAdapter} to read and convert referenced entities.
*
* @author Christoph Strobl
* @since 1.7
*/
static class ReferenceResolverImpl implements ReferenceResolver {
private RedisKeyValueAdapter adapter;
ReferenceResolverImpl() {}
/**
* @param adapter must not be {@literal null}.
*/
public ReferenceResolverImpl(RedisKeyValueAdapter adapter) {
this.adapter = adapter;
}
/**
* @param adapter
*/
public void setAdapter(RedisKeyValueAdapter adapter) {
this.adapter = adapter;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.convert.ReferenceResolver#resolveReference(java.io.Serializable, java.io.Serializable, java.lang.Class)
*/
@Override
public <T> T resolveReference(Serializable id, Serializable keyspace, Class<T> type) {
return (T) adapter.get(id, keyspace, type);
}
}
}

View File

@@ -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. <br />
* 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}.
*
* <pre>
* <code>
* List&#60;RedisSession&#62; sessions = template.find(new RedisCallback&#60;Set&#60;byte[]&#62;&#62;() {
* public Set&#60;byte[]&#60; doInRedis(RedisConnection connection) throws DataAccessException {
* return connection
* .sMembers("spring:session:sessions:securityContext.authentication.principal.username:user"
* .getBytes());
* }
* }, RedisSession.class);
* </code>
*
* <pre>
* @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 <T> List<T> find(final RedisCallback<?> callback, final Class<T> type) {
Assert.notNull(callback, "Callback must not be null.");
return execute(new RedisKeyValueCallback<List<T>>() {
@Override
public List<T> 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<T> result = new ArrayList<T>();
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 <T>
* @since 1.7
*/
public static abstract class RedisKeyValueCallback<T> implements KeyValueCallback<T> {
/*
* (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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.util.ByteUtils;
/**
* Redis specific {@link QueryEngine} implementation.
*
* @author Christoph Strobl
* @since 1.7
*/
class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationChain, Comparator<?>> {
/**
* 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<RedisOperationChain> criteriaAccessor,
SortAccessor<Comparator<?>> 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)
*/
public <T> Collection<T> execute(final RedisOperationChain criteria, final Comparator<?> sort, int offset, int rows,
final Serializable keyspace, Class<T> type) {
RedisCallback<Map<byte[], Map<byte[], byte[]>>> callback = new RedisCallback<Map<byte[], Map<byte[], byte[]>>>() {
@Override
public Map<byte[], Map<byte[], byte[]>> 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++;
}
Set<byte[]> allKeys = connection.sInter(keys);
byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class);
final Map<byte[], Map<byte[], byte[]>> rawData = new LinkedHashMap<byte[], Map<byte[], byte[]>>();
for (byte[] id : allKeys) {
byte[] singleKey = ByteUtils.concat(keyspaceBin, id);
rawData.put(id, connection.hGetAll(singleKey));
}
return rawData;
}
};
Map<byte[], Map<byte[], byte[]>> raw = this.getAdapter().execute(callback);
List<T> result = new ArrayList<T>(raw.size());
for (Map.Entry<byte[], Map<byte[], byte[]>> 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<Long>() {
@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();
}
});
}
/**
* @author Christoph Strobl
* @since 1.7
*/
static class RedisCriteriaAccessor implements CriteriaAccessor<RedisOperationChain> {
@Override
public RedisOperationChain resolve(KeyValueQuery<?> query) {
return (RedisOperationChain) query.getCritieria();
}
}
}

View File

@@ -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.
*
* <pre>
* <code>
* &#64;RedisHash
* class Person {
* &#64;Id String id;
* String name;
* &#64;TimeToLive Long timeout;
* }
* </code>
* </pre>
*
* @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;
}

View File

@@ -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);
}

View File

@@ -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
*/
private 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<String, byte[]> {
@Override
public byte[] convert(String source) {
return fromString(source);
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
@ReadingConverter
static class BytesToStringConverter extends StringBasedConverter implements Converter<byte[], String> {
@Override
public String convert(byte[] source) {
return toString(source);
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
@WritingConverter
static class NumberToBytesConverter extends StringBasedConverter implements Converter<Number, byte[]> {
@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<Enum<?>, 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<byte[], Enum<?>> {
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T extends Enum<?>> Converter<byte[], T> getConverter(Class<T> 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<T extends Enum<T>> extends StringBasedConverter implements Converter<byte[], T> {
private final Class<T> enumType;
public BytesToEnum(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(byte[] source) {
String value = toString(source);
if (value == null || value.length() == 0) {
return null;
}
return (T) Enum.valueOf(this.enumType, value.trim());
}
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
@ReadingConverter
static class BytesToNumberConverterFactory implements ConverterFactory<byte[], Number> {
@Override
public <T extends Number> Converter<byte[], T> getConverter(Class<T> targetType) {
return new BytesToNumberConverter<T>(targetType);
}
private static final class BytesToNumberConverter<T extends Number> extends StringBasedConverter implements
Converter<byte[], T> {
private final Class<T> targetType;
public BytesToNumberConverter(Class<T> 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<Boolean, byte[]> {
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<byte[], Boolean> {
@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<Date, byte[]> {
@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<byte[], Date> {
@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);
}
}
}

View File

@@ -0,0 +1,250 @@
/*
* 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<String, byte[]> data;
/**
* Creates new empty bucket
*/
public Bucket() {
data = new LinkedHashMap<String, byte[]>();
}
Bucket(Map<String, byte[]> data) {
Assert.notNull(data, "Inital data must not be null!");
this.data = new LinkedHashMap<String, byte[]>(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<Entry<String, byte[]>> entrySet() {
return data.entrySet();
}
/**
* @return {@literal true} when no data present in {@link Bucket}.
*/
public boolean isEmpty() {
return data.isEmpty();
}
/**
* @return never {@literal null}.
*/
public Collection<byte[]> values() {
return data.values();
}
/**
* @return never {@literal null}.
*/
public Set<String> keySet() {
return data.keySet();
}
/**
* Key/value pairs contained in the {@link Bucket}.
*
* @return never {@literal null}.
*/
public Map<String, byte[]> asMap() {
return Collections.unmodifiableMap(this.data);
}
public Bucket extract(String prefix) {
Bucket partial = new Bucket();
for (Map.Entry<String, byte[]> 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<String> extractAllKeysFor(String path) {
if (!StringUtils.hasText(path)) {
return keySet();
}
Pattern pattern = Pattern.compile("(" + Pattern.quote(path) + ")\\.\\[.*?\\]");
Set<String> keys = new LinkedHashSet<String>();
for (Map.Entry<String, byte[]> 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<byte[], byte[]> rawMap() {
Map<byte[], byte[]> raw = new LinkedHashMap<byte[], byte[]>(data.size());
for (Map.Entry<String, byte[]> 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<byte[], byte[]> source) {
Bucket bucket = new Bucket();
if (source == null) {
return bucket;
}
for (Map.Entry<byte[], byte[]> 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<String, String> source) {
Bucket bucket = new Bucket();
if (source == null) {
return bucket;
}
for (Map.Entry<String, String> 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<String, String> serialized = new LinkedHashMap<String, String>();
for (Map.Entry<String, byte[]> 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;
}
}

View File

@@ -0,0 +1,485 @@
/*
* 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<ConvertiblePair> readingPairs;
private final Set<ConvertiblePair> writingPairs;
private final Set<Class<?>> customSimpleTypes;
private final SimpleTypeHolder simpleTypeHolder;
private final List<Object> converters;
private final Map<ConvertiblePair, CacheValue<Class<?>>> customReadTargetTypes;
private final Map<ConvertiblePair, CacheValue<Class<?>>> customWriteTargetTypes;
private final Map<Class<?>, CacheValue<Class<?>>> rawWriteTargetTypes;
/**
* Creates an empty {@link CustomConversions} object.
*/
public CustomConversions() {
this(new ArrayList<Object>());
}
/**
* Creates a new {@link CustomConversions} instance registering the given converters.
*
* @param converters
*/
public CustomConversions(List<?> converters) {
Assert.notNull(converters);
this.readingPairs = new LinkedHashSet<ConvertiblePair>();
this.writingPairs = new LinkedHashSet<ConvertiblePair>();
this.customSimpleTypes = new HashSet<Class<?>>();
this.customReadTargetTypes = new ConcurrentHashMap<ConvertiblePair, CacheValue<Class<?>>>();
this.customWriteTargetTypes = new ConcurrentHashMap<ConvertiblePair, CacheValue<Class<?>>>();
this.rawWriteTargetTypes = new ConcurrentHashMap<Class<?>, CacheValue<Class<?>>>();
List<Object> toRegister = new ArrayList<Object>();
// 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());
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<ConvertiblePair> 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 <T> Class<?> getOrCreateAndCache(T key, Map<T, CacheValue<Class<?>>> cache, Producer producer) {
CacheValue<Class<?>> cacheValue = cache.get(key);
if (cacheValue != null) {
return cacheValue.getValue();
}
Class<?> type = producer.get();
cache.put(key, CacheValue.<Class<?>> 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));
}
}
}

View File

@@ -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<IndexedData> resolveIndexesFor(TypeInformation<?> typeInformation, Object value);
}

View File

@@ -0,0 +1,204 @@
/*
* 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.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.IndexConfiguration;
import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting;
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;
/**
* {@link IndexResolver} implementation considering properties annotated with {@link Indexed} or paths set up in
* {@link IndexConfiguration}.
*
* @author Christoph Strobl
* @since 1.7
*/
public class IndexResolverImpl implements IndexResolver {
private IndexConfiguration indexConfiguration;
private RedisMappingContext mappingContext;
/**
* Creates new {@link IndexResolverImpl} with empty {@link IndexConfiguration}.
*/
public IndexResolverImpl() {
this(new RedisMappingContext());
}
/**
* Creates new {@link IndexResolverImpl} with given {@link IndexConfiguration}.
*
* @param mapppingContext must not be {@literal null}.
*/
public IndexResolverImpl(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<IndexedData> resolveIndexesFor(TypeInformation<?> typeInformation, Object value) {
return doResolveIndexesFor(mappingContext.getPersistentEntity(typeInformation).getKeySpace(), "", typeInformation,
value);
}
private Set<IndexedData> doResolveIndexesFor(final String keyspace, final String path,
TypeInformation<?> typeInformation, Object value) {
RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(typeInformation);
if (entity == null) {
IndexedData index = resolveIndex(keyspace, path, null, value);
if (index != null) {
return Collections.singleton(index);
}
return Collections.emptySet();
}
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value);
final Set<IndexedData> indexes = new LinkedHashSet<IndexedData>();
entity.doWithProperties(new PropertyHandler<KeyValuePersistentProperty>() {
@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();
IndexedData index = resolveIndex(keyspace, currentPath, persistentProperty, propertyValue);
if (index != null) {
indexes.add(index);
}
if (persistentProperty.isMap()) {
for (Entry<?, ?> entry : ((Map<?, ?>) propertyValue).entrySet()) {
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, entry.getValue());
indexes.addAll(doResolveIndexesFor(keyspace, currentPath + "." + entry.getKey(),
typeToUse.getActualType(), entry.getValue()));
}
} else if (persistentProperty.isCollectionLike()) {
for (Object listValue : (Iterable<?>) propertyValue) {
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, listValue);
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeToUse.getActualType(), listValue));
}
}
else if (persistentProperty.isEntity()
|| persistentProperty.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)) {
typeHint = updateTypeHintForActualValue(typeHint, propertyValue);
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeHint.getActualType(), 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 IndexedData resolveIndex(String keyspace, String propertyPath, PersistentProperty<?> property, Object value) {
if (value == null) {
return null;
}
String path = normalizeIndexPath(propertyPath, property);
if (indexConfiguration.hasIndexFor(keyspace, path)) {
return new SimpleIndexedPropertyValue(keyspace, path, value);
}
else if (property != null && property.isAnnotationPresent(Indexed.class)) {
Indexed indexed = property.findAnnotation(Indexed.class);
indexConfiguration.addIndexDefinition(new RedisIndexSetting(keyspace, path, indexed.type()));
switch (indexed.type()) {
case SIMPLE:
return new SimpleIndexedPropertyValue(keyspace, path, value);
default:
throw new IllegalArgumentException(String.format("Unsupported index type '%s' for path '%s'.",
indexed.type(), path));
}
}
return null;
}
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;
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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;
/**
* {@link IndexedData} represents a secondary index for a property path in a given keyspace.
*
* @author Christoph Strobl
* @since 1.7
*/
public interface IndexedData {
/**
* Get the {@link String} representation of the dot path to the referenced property.
*
* @return never {@literal null}.
*/
String getPath();
/**
* Get the associated keyspace the index resides in.
*
* @return
*/
String getKeySpace();
}

View File

@@ -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<Class<?>, KeyspaceSettings> settingsMap;
public KeyspaceConfiguration() {
this.settingsMap = new ConcurrentHashMap<Class<?>, 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<KeyspaceSettings> 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);
}
}
}

View File

@@ -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.convert;
import org.springframework.data.redis.core.index.IndexConfiguration;
/**
* {@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 IndexConfiguration 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(IndexConfiguration indexConfiguration, KeyspaceConfiguration keyspaceConfiguration) {
this.indexConfiguration = indexConfiguration;
this.keyspaceConfiguration = keyspaceConfiguration;
}
/**
* @return never {@literal null}.
*/
public IndexConfiguration getIndexConfiguration() {
return indexConfiguration;
}
/**
* @return never {@literal null}.
*/
public KeyspaceConfiguration getKeyspaceConfiguration() {
return keyspaceConfiguration;
}
}

View File

@@ -0,0 +1,815 @@
/*
* 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.Collection;
import java.util.HashMap;
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;
/**
* {@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. <br />
* <br />
* <strong>NOTE</strong> {@link MappingRedisConverter} is an {@link InitializingBean} and requires
* {@link MappingRedisConverter#afterPropertiesSet()} to be called.
*
* <pre>
* <code>
* &#64;RedisHash("persons")
* class Person {
*
* &#64;Id String id;
* String firstname;
*
* List<String> nicknames;
* List<Person> coworkers;
*
* Address address;
* &#64;Reference Country nationality;
* }
* </code>
* </pre>
*
* The above is represented as:
*
* <pre>
* <code>
* _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
* </code>
* </pre>
*
* @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<RedisData> typeMapper;
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<RedisData>(new RedisTypeAliasAccessor(this.conversionService));
this.referenceResolver = referenceResolver;
this.indexResolver = indexResolver != null ? indexResolver : new IndexResolverImpl(this.mappingContext);
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityReader#read(java.lang.Class, java.lang.Object)
*/
public <R> R read(Class<R> type, final RedisData source) {
return readInternal("", type, source);
}
@SuppressWarnings("unchecked")
private <R> R readInternal(final String path, Class<R> 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<String, byte[]> partial = new HashMap<String, byte[]>();
if (!path.isEmpty()) {
for (Entry<String, byte[]> 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<KeyValuePersistentProperty>(entity,
new ConverterAwareParameterValueProvider(source, conversionService), null));
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
entity.doWithProperties(new PropertyHandler<KeyValuePersistentProperty>() {
@Override
public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) {
String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName();
PreferredConstructor<?, KeyValuePersistentProperty> 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<KeyValuePersistentProperty>() {
@Override
public void doWithAssociation(Association<KeyValuePersistentProperty> association) {
String currentPath = !path.isEmpty() ? path + "." + association.getInverse().getName() : association
.getInverse().getName();
if (association.getInverse().isCollectionLike()) {
Collection<Object> target = CollectionFactory.createCollection(association.getInverse().getType(),
association.getInverse().getComponentType(), 10);
Bucket bucket = source.getBucket().extract(currentPath + ".[");
for (Entry<String, byte[]> entry : bucket.entrySet()) {
String referenceKey = fromBytes(entry.getValue(), String.class);
String[] args = referenceKey.split(":");
Object loadedObject = referenceResolver.resolveReference(args[1], args[0], association.getInverse()
.getActualType());
if (loadedObject != null) {
target.add(loadedObject);
}
}
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(":");
Object loadedObject = referenceResolver.resolveReference(args[1], args[0], association.getInverse()
.getActualType());
if (loadedObject != null) {
accessor.setProperty(association.getInverse(), loadedObject);
}
}
}
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityWriter#write(java.lang.Object, java.lang.Object)
*/
@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((Serializable) entity.getIdentifierAccessor(source).getIdentifier());
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<KeyValuePersistentProperty>() {
@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<KeyValuePersistentProperty>() {
@Override
public void doWithAssociation(Association<KeyValuePersistentProperty> 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) {
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, 10);
Bucket partial = source.getBucket().extract(path + ".[");
for (byte[] value : partial.values()) {
target.add(fromBytes(value, valueType));
}
return target;
}
/**
* @param path
* @param collectionType
* @param valueType
* @param source
* @return
*/
private Collection<?> readCollectionOfComplexTypes(String path, Class<?> collectionType, Class<?> valueType,
Bucket source) {
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, 10);
Set<String> keys = source.extractAllKeysFor(path);
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) {
Map<Object, Object> target = CollectionFactory.createMap(mapType, 10);
Bucket partial = source.getBucket().extract(path + ".[");
for (Entry<String, byte[]> 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) {
Map<Object, Object> target = CollectionFactory.createMap(mapType, 10);
Set<String> keys = source.getBucket().extractAllKeysFor(path);
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> T fromBytes(byte[] source, Class<T> 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()
*/
public RedisMappingContext getMappingContext() {
return this.mappingContext;
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityConverter#getConversionService()
*/
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<KeyValuePersistentProperty> {
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> T getPropertyValue(KeyValuePersistentProperty property) {
return (T) conversionService.convert(source.getBucket().get(property.getName()), property.getActualType());
}
}
/**
* @author Christoph Strobl
*/
private static class RedisTypeAliasAccessor implements TypeAliasAccessor<RedisData> {
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();
}
}
}

View File

@@ -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 sepcific {@link EntityConverter}.
*
* @author Christoph Strobl
* @since 1.7
*/
public interface RedisConverter extends
EntityConverter<KeyValuePersistentEntity<?>, KeyValuePersistentProperty, Object, RedisData> {
}

View File

@@ -0,0 +1,166 @@
/*
* 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.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 Serializable id;
private Bucket bucket;
private Set<IndexedData> indexedData;
private Long timeToLive;
/**
* Creates new {@link RedisData} with empty {@link Bucket}.
*/
public RedisData() {
this(Collections.<byte[], byte[]> emptyMap());
}
/**
* Creates new {@link RedisData} with {@link Bucket} holding provided values.
*
* @param raw should not be {@literal null}.
*/
public RedisData(Map<byte[], byte[]> 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<IndexedData>();
}
/**
* Set the id to be used as part of the key.
*
* @param id
*/
public void setId(Serializable id) {
this.id = id;
}
/**
* @return
*/
public Serializable 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<IndexedData> 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 + "]";
}
}

View File

@@ -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 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}.
* @param type must not be {@literal null}.
* @return {@literal null} if referenced object does not exist.
*/
<T> T resolveReference(Serializable id, Serializable keyspace, Class<T> type);
}

View File

@@ -0,0 +1,123 @@
/*
* 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.util.ObjectUtils;
/**
* {@link IndexedData} implementation indicating storage of data within a Redis Set.
*
* @author Christoph Strobl
* @since 1.7
*/
public class SimpleIndexedPropertyValue implements IndexedData {
private final String keyspace;
private final String path;
private final Object value;
/**
* Creates new {@link SimpleIndexedPropertyValue}.
*
* @param keyspace must not be {@literal null}.
* @param path must not be {@literal null}.
* @param value can be {@literal null}.
*/
public SimpleIndexedPropertyValue(String keyspace, String path, Object value) {
this.keyspace = keyspace;
this.path = path;
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#getPath()
*/
@Override
public String getPath() {
return path;
}
/*
* (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(path);
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.path, that.path)) {
return false;
}
return ObjectUtils.nullSafeEquals(this.value, that.value);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "SimpleIndexedPropertyValue [keyspace=" + keyspace + ", path=" + path + ", value=" + value + "]";
}
}

View File

@@ -0,0 +1,189 @@
/*
* 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.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.util.Assert;
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
* @since 1.7
*/
public class IndexConfiguration {
private final Set<RedisIndexSetting> definitions;
/**
* Creates new empty {@link IndexConfiguration}.
*/
public IndexConfiguration() {
this.definitions = new CopyOnWriteArraySet<RedisIndexSetting>();
for (RedisIndexSetting initial : initialConfiguration()) {
addIndexDefinition(initial);
}
}
/**
* Checks if an index is defined for a given keyspace and property path.
*
* @param keyspace
* @param path
* @return true if index is defined.
*/
public boolean hasIndexFor(Serializable keyspace, String path) {
for (IndexType type : IndexType.values()) {
RedisIndexSetting def = getIndexDefinition(keyspace, path, type);
if (def != null) {
return true;
}
}
return false;
}
/**
* Get the list of {@link RedisIndexSetting} for a given keyspace and property path.
*
* @param keyspace
* @param path
* @return never {@literal null}.
*/
public List<RedisIndexSetting> getIndexDefinitionsFor(Serializable keyspace, String path) {
List<RedisIndexSetting> indexDefinitions = new ArrayList<RedisIndexSetting>();
for (IndexType type : IndexType.values()) {
RedisIndexSetting def = getIndexDefinition(keyspace, path, type);
if (def != null) {
indexDefinitions.add(def);
}
}
return indexDefinitions;
}
/**
* Add given {@link RedisIndexSetting}.
*
* @param indexDefinition must not be {@literal null}.
*/
public void addIndexDefinition(RedisIndexSetting indexDefinition) {
Assert.notNull(indexDefinition, "RedisIndexDefinition must not be null in order to be added.");
this.definitions.add(indexDefinition);
}
private RedisIndexSetting getIndexDefinition(Serializable keyspace, String path, IndexType type) {
for (RedisIndexSetting indexDef : definitions) {
if (indexDef.getKeyspace().equals(keyspace) && indexDef.getPath().equals(path) && indexDef.getType().equals(type)) {
return indexDef;
}
}
return null;
}
/**
* Customization hook.
*
* @return must not return {@literal null}.
*/
protected Iterable<RedisIndexSetting> initialConfiguration() {
return Collections.emptySet();
}
/**
* @author Christoph Strobl
* @since 1.7
*/
public static class RedisIndexSetting {
private final Serializable keyspace;
private final String path;
private final IndexType type;
public RedisIndexSetting(Serializable keyspace, String path) {
this(keyspace, path, null);
}
public RedisIndexSetting(Serializable keyspace, String path, IndexType type) {
this.keyspace = keyspace;
this.path = path;
this.type = type == null ? IndexType.SIMPLE : type;
}
public Serializable getKeyspace() {
return keyspace;
}
public String getPath() {
return path;
}
public IndexType getType() {
return type;
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(keyspace);
result += ObjectUtils.nullSafeHashCode(path);
result += ObjectUtils.nullSafeHashCode(type);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof RedisIndexSetting)) {
return false;
}
RedisIndexSetting that = (RedisIndexSetting) obj;
if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(this.path, that.path)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(this.type, that.type)) {
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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;
/**
* {@link IndexType} indicates structure of index.
*
* @author Christoph Strobl
* @since 1.7
*/
public enum IndexType {
/**
* Simple indicates usage of Redis Set for persisting data.
*/
SIMPLE
}

View File

@@ -0,0 +1,42 @@
/*
* 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 property to be included in a secondary index.
*
* @author Christoph Strobl
* @since 1.7
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
public @interface Indexed {
/**
* Type of index to use.
*
* @return
*/
IndexType type() default IndexType.SIMPLE;
}

View File

@@ -0,0 +1,58 @@
/*
* 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.BasicKeyValuePersistentEntity;
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
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 <T>
*/
public class BasicRedisPersistentEntity<T> extends BasicKeyValuePersistentEntity<T> implements RedisPersistentEntity<T> {
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<T> 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;
}
}

View File

@@ -0,0 +1,369 @@
/*
* 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
* @since 1.7
*/
public class RedisMappingContext extends KeyValueMappingContext {
private MappingConfiguration mappingConfiguration;
private KeySpaceResolver fallbackKeySpaceResolver;
private TimeToLiveAccessor timeToLiveAccessor;
/**
* 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 <T> RedisPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
return new BasicRedisPersistentEntity<T>(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<Class<?>, Long> defaultTimeouts;
private final Map<Class<?>, PersistentProperty<?>> timeoutProperties;
private final Map<Class<?>, 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<Class<?>, Long>();
this.timeoutProperties = new HashMap<Class<?>, PersistentProperty<?>>();
this.timeoutMethods = new HashMap<Class<?>, 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);
}
}
}

View File

@@ -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 <T>
* @since 1.7
*/
public interface RedisPersistentEntity<T> extends KeyValuePersistentEntity<T> {
/**
* Get the {@link TimeToLiveAccessor} associated with the entity.
*
* @return never {@literal null}.
*/
TimeToLiveAccessor getTimeToLiveAccessor();
}

View File

@@ -0,0 +1,47 @@
/*
* 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 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 {
/**
* Creates new {@link RedisPersistentProperty}.
*
* @param field
* @param propertyDescriptor
* @param owner
* @param simpleTypeHolder
*/
public RedisPersistentProperty(Field field, PropertyDescriptor propertyDescriptor,
PersistentEntity<?, KeyValuePersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) {
super(field, propertyDescriptor, owner, simpleTypeHolder);
}
}

View File

@@ -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;
}
}

View File

@@ -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<String> 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();
}
}

View File

@@ -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 @EnableJpaRepositories("org.my.pkg")} instead of {@code @EnableJpaRepositories(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<? extends IndexConfiguration> indexConfiguration() default IndexConfiguration.class;
/**
* Set up keyspaces for specific types.
*
* @return
*/
Class<? extends KeyspaceConfiguration> keyspaceConfiguration() default KeyspaceConfiguration.class;
}

View File

@@ -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<? extends Annotation> getAnnotation() {
return EnableRedisRepositories.class;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension()
*/
@Override
protected RepositoryConfigurationExtension getExtension() {
return new RedisRepositoryConfigurationExtension();
}
}

View File

@@ -0,0 +1,208 @@
/*
* 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 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.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) {
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();
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();
String redisTemplateRef = configurationSource.getAttribute("redisTemplateRef");
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() {
RootBeanDefinition beanDef = new RootBeanDefinition();
beanDef.setBeanClassName("org.springframework.data.redis.core.RedisKeyValueAdapter.ReferenceResolverImpl");
MutablePropertyValues props = new MutablePropertyValues();
props.add("adapter", new RuntimeBeanReference(REDIS_ADAPTER_BEAN_NAME));
beanDef.setPropertyValues(props);
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;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.LinkedHashSet;
import java.util.Set;
/**
* Simple set of operations requried to run queries against Redis.
*
* @author Christoph Strobl
* @since 1.7
*/
public class RedisOperationChain {
Set<Object> sismember = new LinkedHashSet<Object>();
Set<Object> orSismember = new LinkedHashSet<Object>();
public void sismember(Object next) {
sismember.add(next);
}
public Set<Object> getSismember() {
return sismember;
}
public void orSismember(Object next) {
orSismember.add(next);
}
public void orSismember(Collection<Object> next) {
orSismember.addAll(next);
}
public Set<Object> getOrSismember() {
return orSismember;
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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<KeyValueQuery<RedisOperationChain>, 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<Object> iterator) {
return from(part, iterator, new RedisOperationChain());
}
private RedisOperationChain from(Part part, Iterator<Object> 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<Object> 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.sismember);
return base;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort)
*/
@Override
protected KeyValueQuery<RedisOperationChain> complete(final RedisOperationChain criteria, Sort sort) {
KeyValueQuery<RedisOperationChain> query = new KeyValueQuery<RedisOperationChain>(criteria);
if (sort != null) {
query.setSort(sort);
}
return query;
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.support;
import java.lang.reflect.Method;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery.QueryInitialization;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.util.Assert;
/**
* {@link RepositoryFactorySupport} specific of handing Redis
* {@link org.springframework.data.keyvalue.repository.KeyValueRepository}.
*
* @author Christoph Strobl
* @since 1.7
*/
public class RedisRepositoryFactory extends KeyValueRepositoryFactory {
/**
* @param keyValueOperations
* @see KeyValueRepositoryFactory#KeyValueRepositoryFactory(KeyValueOperations)
*/
public RedisRepositoryFactory(KeyValueOperations keyValueOperations) {
super(keyValueOperations);
}
/**
* @param keyValueOperations
* @param queryCreator
* @see KeyValueRepositoryFactory#KeyValueRepositoryFactory(KeyValueOperations, Class)
*/
public RedisRepositoryFactory(KeyValueOperations keyValueOperations,
Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
super(keyValueOperations, queryCreator);
}
/*
* (non-Javadoc)
* @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider)
*/
@Override
protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) {
return new RedisQueryLookupStrategy(key, evaluationContextProvider, getKeyValueOperations(), getQueryCreator());
}
/**
* @author Christoph Strobl
* @since 1.7
*/
private static class RedisQueryLookupStrategy implements QueryLookupStrategy {
private EvaluationContextProvider evaluationContextProvider;
private KeyValueOperations keyValueOperations;
private Class<? extends AbstractQueryCreator<?, ?>> queryCreator;
/**
* Creates a new {@link RedisQueryLookupStrategy} for the given {@link Key}, {@link EvaluationContextProvider},
* {@link KeyValueOperations} and query creator type.
* <p>
*
* @param key
* @param evaluationContextProvider must not be {@literal null}.
* @param keyValueOperations must not be {@literal null}.
* @param queryCreator must not be {@literal null}.
*/
public RedisQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider,
KeyValueOperations keyValueOperations, Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
Assert.notNull(keyValueOperations, "KeyValueOperations must not be null!");
Assert.notNull(queryCreator, "Query creator type must not be null!");
this.evaluationContextProvider = evaluationContextProvider;
this.keyValueOperations = keyValueOperations;
this.queryCreator = queryCreator;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.repository.core.NamedQueries)
*/
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
QueryMethod queryMethod = new QueryMethod(method, metadata, factory);
KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(queryMethod, evaluationContextProvider,
this.keyValueOperations, this.queryCreator);
partTreeQuery.setQueryIntialization(QueryInitialization.NEW);
return partTreeQuery;
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.support;
import java.io.Serializable;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
/**
* Adapter for Springs {@link FactoryBean} interface to allow easy setup of {@link RedisRepositoryFactory} via Spring
* configuration.
*
* @author Christoph Strobl
* @param <T> The repository type.
* @param <S> The repository domain type.
* @param <ID> The repository id type.
* @since 1.7
*/
public class RedisRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> extends
KeyValueRepositoryFactoryBean<T, S, ID> {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createRepositoryFactory()
*/
@Override
protected RepositoryFactorySupport createRepositoryFactory() {
return new RedisRepositoryFactory(getOperations(), getQueryCreator());
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Some handy methods for dealing with byte arrays.
*
* @author Christoph Strobl
* @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<byte[]> bytes = new ArrayList<byte[]>();
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()][]);
}
}

View File

@@ -0,0 +1,158 @@
/*
* 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.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.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.convert.IndexResolverImpl;
import org.springframework.data.redis.core.convert.IndexedData;
import org.springframework.data.redis.core.convert.MappingRedisConverter;
import org.springframework.data.redis.core.convert.ReferenceResolver;
import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
/**
* @author Christoph Strobl
*/
@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;
@Mock RedisConnection connectionMock;
@Mock ReferenceResolver referenceResolverMock;
@Before
public void setUp() {
MappingRedisConverter converter = new MappingRedisConverter(new RedisMappingContext(), new IndexResolverImpl(),
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<byte[]>(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<byte[]>(Arrays.asList(indexKey1, indexKey2)));
writer.removeAllIndexes(KEYSPACE);
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
verify(connectionMock, times(1)).del(captor.capture());
assertThat(captor.getAllValues(), hasItems(indexKey1, indexKey2));
}
static class StubIndxedData implements IndexedData {
@Override
public String getPath() {
return "address.city";
}
@Override
public String getKeySpace() {
return KEYSPACE;
}
}
}

View File

@@ -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<Void>() {
@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<String, String> map = new LinkedHashMap<String, String>();
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<String, String> map = new LinkedHashMap<String, String>();
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<String, String> map = new LinkedHashMap<String, String>();
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<String, String> map = new LinkedHashMap<String, String>();
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<String, String> map = new LinkedHashMap<String, String>();
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<String, String> map = new LinkedHashMap<String, String>();
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<String> nicknames;
List<Person> coworkers;
Integer age;
Boolean alive;
Date birthdate;
Address address;
Map<String, String> physicalAttributes;
Map<String, Person> relatives;
@Reference Location location;
@Reference List<Location> 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;
}
}

View File

@@ -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<Object, Object> nativeTemplate;
public RedisKeyValueTemplateTests(RedisConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
ConnectionFactoryTracker.add(connectionFactory);
}
@Parameters
public static List<RedisConnectionFactory> params() {
JedisConnectionFactory jedis = new JedisConnectionFactory();
jedis.afterPropertiesSet();
return Collections.<RedisConnectionFactory> singletonList(jedis);
}
@AfterClass
public static void cleanUp() {
ConnectionFactoryTracker.cleanUp();
}
@Before
public void setUp() {
nativeTemplate = new RedisTemplate<Object, Object>();
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<Void>() {
@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<Void>() {
@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<Person> result = template.find(new RedisCallback<byte[]>() {
@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<Person> result = template.find(new RedisCallback<List<byte[]>>() {
@Override
public List<byte[]> 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<Person> result = template.find(new RedisCallback<List<byte[]>>() {
@Override
public List<byte[]> 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);
}
}
}

View File

@@ -0,0 +1,142 @@
/*
* 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.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;
/**
* @author Christoph Strobl
*/
class ConversionTestEntities {
static final String KEYSPACE_PERSON = "persons";
static final String KEYSPACE_TWOT = "twot";
static final String KEYSPACE_LOCATION = "locations";
@RedisHash(KEYSPACE_PERSON)
static class Person {
@Id String id;
String firstname;
Gender gender;
List<String> nicknames;
List<Person> coworkers;
Integer age;
Boolean alive;
Date birthdate;
Address address;
Map<String, String> physicalAttributes;
Map<String, Person> relatives;
@Reference Location location;
@Reference List<Location> visited;
Species species;
}
static class PersonWithAddressReference extends Person {
@Reference AddressWithId addressRef;
}
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 {
Object feature;
Map<String, Object> characteristics;
List<Object> items;
}
@RedisHash(KEYSPACE_LOCATION)
static class Location {
@Id String id;
String name;
Address address;
}
@RedisHash(timeToLive = 5)
static class ExpiringPerson {
@Id String id;
String name;
}
static class ExipringPersonWithExplicitProperty extends ExpiringPerson {
@TimeToLive(unit = TimeUnit.MINUTES) Long ttl;
}
static class Species {
String name;
List<String> alsoKnownAs;
}
@RedisHash(KEYSPACE_TWOT)
static class TheWheelOfTime {
List<Person> mainCharacters;
List<Species> species;
Map<String, Location> places;
}
static class Item {
@Indexed String type;
String description;
Size size;
}
static class Size {
int width;
int height;
int length;
}
}

View File

@@ -0,0 +1,427 @@
/*
* 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 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.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.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.IndexConfiguration.RedisIndexSetting;
import org.springframework.data.redis.core.index.IndexType;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.util.ClassTypeInformation;
/**
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class IndexResolverImplUnitTests {
IndexConfiguration indexConfig;
IndexResolverImpl indexResolver;
@Mock PersistentProperty<?> propertyMock;
@Before
public void setUp() {
indexConfig = new IndexConfiguration();
this.indexResolver = new IndexResolverImpl(new RedisMappingContext(new MappingConfiguration(indexConfig,
new KeyspaceConfiguration())));
}
/**
* @see DATAREDIS-425
*/
@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionOnNullMappingContext() {
new IndexResolverImpl(null);
}
/**
* @see DATAREDIS-425
*/
@Test
public void shouldResolveAnnotatedIndexOnRootWhenValueIsNotNull() {
Address address = new Address();
address.country = "andor";
Set<IndexedData> 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<IndexedData> 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<IndexedData> 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>();
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<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TheWheelOfTime.class), twot);
assertThat(indexes.size(), is(2));
assertThat(indexes, IsCollectionContaining.<IndexedData> 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<String, ConversionTestEntities.Location>();
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<IndexedData> 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 RedisIndexSetting(KEYSPACE_PERSON, "physicalAttributes.eye-color"));
Person rand = new Person();
rand.physicalAttributes = new LinkedHashMap<String, String>();
rand.physicalAttributes.put("eye-color", "grey");
Set<IndexedData> 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 RedisIndexSetting(KEYSPACE_PERSON, "relatives.father.firstname"));
Person rand = new Person();
rand.relatives = new LinkedHashMap<String, Person>();
Person janduin = new Person();
janduin.firstname = "janduin";
rand.relatives.put("father", janduin);
Set<IndexedData> 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 RedisIndexSetting(KEYSPACE_PERSON, "physicalAttributes.eye-color"));
Person rand = new Person();
rand.physicalAttributes = new LinkedHashMap<String, String>();
rand.physicalAttributes.put("eye-color", null);
Set<IndexedData> 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<IndexedData> 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 RedisIndexSetting(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(IndexType.SIMPLE));
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(IndexType.SIMPLE));
IndexedData index = resolve("list.[0].name", "rand");
assertThat(index.getPath(), 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(IndexType.SIMPLE));
IndexedData index = resolve("map.[foo].name", "rand");
assertThat(index.getPath(), 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(IndexType.SIMPLE));
IndexedData index = resolve("map.[0].name", "rand");
assertThat(index.getPath(), 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<IndexedData> 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<IndexedData> 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<String, Object>(2);
mat.characteristics.put("clothing", hat);
mat.characteristics.put("gambling", "owns the dark one's luck");
Set<IndexedData> 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<Object>(2);
mat.items.add(hat);
mat.items.add("foxhead medallion");
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat);
assertThat(indexes.size(), is(1));
assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "items.type", "hat")));
}
private IndexedData resolve(String path, Object value) {
return indexResolver.resolveIndex(KEYSPACE_PERSON, path, propertyMock, value);
}
private Indexed createIndexedInstance(final IndexType type) {
return new Indexed() {
@Override
public Class<? extends Annotation> annotationType() {
return Indexed.class;
}
@Override
public IndexType type() {
return type == null ? IndexType.SIMPLE : type;
}
};
}
}

View File

@@ -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 {
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<ApplicationEvent> 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);
}
}

View File

@@ -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<ApplicationEvent> 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);
}
}

View File

@@ -0,0 +1,331 @@
/*
* 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.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.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
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.Indexed;
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)
static class Config {
@Bean
RedisTemplate<?, ?> redisTemplate() {
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
connectionFactory.afterPropertiesSet();
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
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 simpleFindSouldReturnEntitiesCorrectly() {
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.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());
}
public static interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByFirstname(String firstname);
List<Person> findByLastname(String lastname);
List<Person> findByFirstnameAndLastname(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<RedisIndexSetting> initialConfiguration() {
return Collections.singleton(new RedisIndexSetting("persons", "lastname"));
}
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> 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 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;
}
}
}

View File

@@ -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<Bucket> {
Map<String, Object> expected = new LinkedHashMap<String, Object>();
Set<String> without = new LinkedHashSet<String>();
/*
* (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<String, Object> 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<byte[]>) 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<byte[]> 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;
}
}

View File

@@ -21,6 +21,9 @@ 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",