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

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