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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user