/*
* Copyright 2015-2017 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.Matchers.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.geo.Point;
import org.springframework.data.keyvalue.annotation.KeySpace;
import org.springframework.data.redis.ConnectionFactoryTracker;
import org.springframework.data.redis.RedisTestProfileValueSource;
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.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources;
import org.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.convert.MappingConfiguration;
import org.springframework.data.redis.core.index.GeoIndexed;
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
* @author Mark Paluch
*/
@RunWith(Parameterized.class)
public class RedisKeyValueAdapterTests {
private static Set<RedisConnectionFactory> initializedFactories = new HashSet<RedisConnectionFactory>();
RedisKeyValueAdapter adapter;
StringRedisTemplate template;
RedisConnectionFactory connectionFactory;
public RedisKeyValueAdapterTests(RedisConnectionFactory connectionFactory) throws Exception {
if (connectionFactory instanceof InitializingBean && initializedFactories.add(connectionFactory)) {
((InitializingBean) connectionFactory).afterPropertiesSet();
}
this.connectionFactory = connectionFactory;
ConnectionFactoryTracker.add(connectionFactory);
}
@Parameters
public static List<RedisConnectionFactory> params() {
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory();
lettuceConnectionFactory.setClientResources(LettuceTestClientResources.getSharedClientResources());
return Arrays.<RedisConnectionFactory> asList(new JedisConnectionFactory(), lettuceConnectionFactory);
}
@AfterClass
public static void cleanUp() {
initializedFactories.clear();
ConnectionFactoryTracker.cleanUp();
}
@Before
public void setUp() {
template = new StringRedisTemplate(connectionFactory);
template.afterPropertiesSet();
RedisMappingContext mappingContext = new RedisMappingContext(
new MappingConfiguration(new IndexConfiguration(), new KeyspaceConfiguration()));
mappingContext.afterPropertiesSet();
adapter = new RedisKeyValueAdapter(template, mappingContext);
adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP);
adapter.afterPropertiesSet();
template.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.flushDb();
return null;
}
});
RedisConnection connection = template.getConnectionFactory().getConnection();
try {
connection.setConfig("notify-keyspace-events", "KEA");
} finally {
connection.close();
}
}
@After
public void tearDown() {
try {
adapter.destroy();
} catch (Exception e) {
// ignore
}
}
@Test // DATAREDIS-425
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));
}
@Test // DATAREDIS-425
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"));
}
@Test // DATAREDIS-425
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));
}
@Test // DATAREDIS-425
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"));
}
@Test // DATAREDIS-425
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));
}
@Test // DATAREDIS-425
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"));
}
@Test // DATAREDIS-425
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));
}
@Test // DATAREDIS-425
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));
}
@Test // DATAREDIS-425
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")));
}
@Test // DATAREDIS-425
public void keyExpiredEventShouldRemoveHelperStructures() throws Exception {
assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true"));
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:firstname:rand", "1");
template.opsForSet().add("persons:1:idx", "persons:firstname:rand");
template.expire("persons:1", 100, TimeUnit.MILLISECONDS);
waitUntilKeyIsGone(template, "persons:1");
waitUntilKeyIsGone(template, "persons:1:phantom");
waitUntilKeyIsGone(template, "persons:firstname:rand");
assertThat(template.hasKey("persons:1"), is(false));
assertThat(template.hasKey("persons:firstname:rand"), is(false));
assertThat(template.hasKey("persons:1:idx"), is(false));
assertThat(template.opsForSet().members("persons"), not(hasItem("1")));
}
@Test // DATAREDIS-589
public void keyExpiredEventWithoutKeyspaceShouldBeIgnored() throws Exception {
assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true"));
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.opsForHash().putAll("1", map);
template.opsForSet().add("persons", "1");
template.opsForSet().add("persons:firstname:rand", "1");
template.opsForSet().add("persons:1:idx", "persons:firstname:rand");
template.expire("1", 100, TimeUnit.MILLISECONDS);
waitUntilKeyIsGone(template, "1");
assertThat(template.hasKey("persons:1"), is(true));
assertThat(template.hasKey("persons:firstname:rand"), is(true));
assertThat(template.hasKey("persons:1:idx"), is(true));
assertThat(template.opsForSet().members("persons"), hasItem("1"));
}
@Test // DATAREDIS-512
public void putWritesIndexDataCorrectly() {
Person rand = new Person();
rand.age = 24;
rand.firstname = "rand";
adapter.put("rand", rand, "persons");
assertThat(template.hasKey("persons:firstname:rand"), is(true));
assertThat(template.hasKey("persons:rand:idx"), is(true));
assertThat(template.opsForSet().isMember("persons:rand:idx", "persons:firstname:rand"), is(true));
Person mat = new Person();
mat.age = 22;
mat.firstname = "mat";
adapter.put("mat", mat, "persons");
assertThat(template.hasKey("persons:firstname:rand"), is(true));
assertThat(template.hasKey("persons:firstname:mat"), is(true));
assertThat(template.hasKey("persons:rand:idx"), is(true));
assertThat(template.hasKey("persons:mat:idx"), is(true));
assertThat(template.opsForSet().isMember("persons:rand:idx", "persons:firstname:rand"), is(true));
assertThat(template.opsForSet().isMember("persons:mat:idx", "persons:firstname:mat"), is(true));
rand.firstname = "frodo";
adapter.put("rand", rand, "persons");
assertThat(template.hasKey("persons:firstname:rand"), is(false));
assertThat(template.hasKey("persons:firstname:mat"), is(true));
assertThat(template.hasKey("persons:firstname:frodo"), is(true));
assertThat(template.hasKey("persons:rand:idx"), is(true));
assertThat(template.opsForSet().isMember("persons:rand:idx", "persons:firstname:frodo"), is(true));
assertThat(template.opsForSet().isMember("persons:mat:idx", "persons:firstname:mat"), is(true));
}
@Test // DATAREDIS-471
public void updateShouldAlterIndexDataCorrectly() {
Person rand = new Person();
rand.firstname = "rand";
adapter.put("1", rand, "persons");
assertThat(template.hasKey("persons:firstname:rand"), is(true));
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.set("firstname", "mat");
adapter.update(update);
assertThat(template.hasKey("persons:firstname:rand"), is(false));
assertThat(template.hasKey("persons:firstname:mat"), is(true));
}
@Test // DATAREDIS-471
public void updateShouldAlterIndexDataOnNestedObjectCorrectly() {
Person rand = new Person();
rand.address = new Address();
rand.address.country = "andor";
adapter.put("1", rand, "persons");
assertThat(template.hasKey("persons:address.country:andor"), is(true));
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class);
Address addressUpdate = new Address();
addressUpdate.country = "tear";
update = update.set("address", addressUpdate);
adapter.update(update);
assertThat(template.hasKey("persons:address.country:andor"), is(false));
assertThat(template.hasKey("persons:address.country:tear"), is(true));
}
@Test // DATAREDIS-471
public void updateShouldAlterIndexDataOnNestedObjectPathCorrectly() {
Person rand = new Person();
rand.address = new Address();
rand.address.country = "andor";
adapter.put("1", rand, "persons");
assertThat(template.hasKey("persons:address.country:andor"), is(true));
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.set("address.country", "tear");
adapter.update(update);
assertThat(template.hasKey("persons:address.country:andor"), is(false));
assertThat(template.hasKey("persons:address.country:tear"), is(true));
}
@Test // DATAREDIS-471
public void updateShouldRemoveComplexObjectCorrectly() {
Person rand = new Person();
rand.address = new Address();
rand.address.country = "andor";
rand.address.city = "emond's field";
adapter.put("1", rand, "persons");
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.del("address");
adapter.update(update);
assertThat(template.opsForHash().hasKey("persons:1", "address.country"), is(false));
assertThat(template.opsForHash().hasKey("persons:1", "address.city"), is(false));
assertThat(template.opsForSet().isMember("persons:address.country:andor", "1"), is(false));
}
@Test // DATAREDIS-471
public void updateShouldRemoveSimpleListValuesCorrectly() {
Person rand = new Person();
rand.nicknames = Arrays.asList("lews therin", "dragon reborn");
adapter.put("1", rand, "persons");
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.del("nicknames");
adapter.update(update);
assertThat(template.opsForHash().hasKey("persons:1", "nicknames.[0]"), is(false));
assertThat(template.opsForHash().hasKey("persons:1", "nicknames.[1]"), is(false));
}
@Test // DATAREDIS-471
public void updateShouldRemoveComplexListValuesCorrectly() {
Person mat = new Person();
mat.firstname = "mat";
mat.nicknames = Collections.singletonList("prince of ravens");
Person perrin = new Person();
perrin.firstname = "mat";
perrin.nicknames = Collections.singletonList("lord of the two rivers");
Person rand = new Person();
rand.coworkers = Arrays.asList(mat, perrin);
adapter.put("1", rand, "persons");
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.del("coworkers");
adapter.update(update);
assertThat(template.opsForHash().hasKey("persons:1", "coworkers.[0].firstname"), is(false));
assertThat(template.opsForHash().hasKey("persons:1", "coworkers.[0].nicknames.[0]"), is(false));
assertThat(template.opsForHash().hasKey("persons:1", "coworkers.[1].firstname"), is(false));
assertThat(template.opsForHash().hasKey("persons:1", "coworkers.[1].nicknames.[0]"), is(false));
}
@Test // DATAREDIS-471
public void updateShouldRemoveSimpleMapValuesCorrectly() {
Person rand = new Person();
rand.physicalAttributes = Collections.singletonMap("eye-color", "grey");
adapter.put("1", rand, "persons");
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.del("physicalAttributes");
adapter.update(update);
assertThat(template.opsForHash().hasKey("persons:1", "physicalAttributes.[eye-color]"), is(false));
}
@Test // DATAREDIS-471
public void updateShouldRemoveComplexMapValuesCorrectly() {
Person tam = new Person();
tam.firstname = "tam";
Person rand = new Person();
rand.relatives = Collections.singletonMap("stepfather", tam);
adapter.put("1", rand, "persons");
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.del("relatives");
adapter.update(update);
assertThat(template.opsForHash().hasKey("persons:1", "relatives.[stepfather].firstname"), is(false));
}
@Test // DATAREDIS-533
public void putShouldCreateGeoIndexCorrectly() {
Person tam = new Person();
tam.id = "1";
tam.firstname = "tam";
tam.address = new Address();
tam.address.location = new Point(10, 20);
adapter.put("1", tam, "persons");
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(notNullValue()));
}
@Test // DATAREDIS-533
public void deleteShouldRemoveGeoIndexCorrectly() {
Person tam = new Person();
tam.id = "1";
tam.firstname = "tam";
tam.address = new Address();
tam.address.location = new Point(10, 20);
adapter.put("1", tam, "persons");
adapter.delete("1", "persons", Person.class);
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(nullValue()));
}
@Test // DATAREDIS-533
public void updateShouldAlterGeoIndexCorrectlyOnDelete() {
Person tam = new Person();
tam.id = "1";
tam.firstname = "tam";
tam.address = new Address();
tam.address.location = new Point(10, 20);
adapter.put("1", tam, "persons");
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.del("address.location");
adapter.update(update);
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(nullValue()));
}
@Test // DATAREDIS-533
public void updateShouldAlterGeoIndexCorrectlyOnUpdate() {
Person tam = new Person();
tam.id = "1";
tam.firstname = "tam";
tam.address = new Address();
tam.address.location = new Point(10, 20);
adapter.put("1", tam, "persons");
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
.set("address.location", new Point(17, 18));
adapter.update(update);
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(notNullValue()));
Point updatedLocation = template.opsForGeo().geoPos("persons:address:location", "1").iterator().next();
assertThat(updatedLocation.getX(), is(closeTo(17D, 0.005)));
assertThat(updatedLocation.getY(), is(closeTo(18D, 0.005)));
}
/**
* Wait up to 5 seconds until {@code key} is no longer available in Redis.
*
* @param template must not be {@literal null}.
* @param key must not be {@literal null}.
* @throws TimeoutException
* @throws InterruptedException
*/
private static void waitUntilKeyIsGone(RedisTemplate<String, ?> template, String key)
throws TimeoutException, InterruptedException {
waitUntilKeyIsGone(template, key, 5, TimeUnit.SECONDS);
}
/**
* Wait up to {@code timeout} until {@code key} is no longer available in Redis.
*
* @param template must not be {@literal null}.
* @param key must not be {@literal null}.
* @param timeout
* @param timeUnit must not be {@literal null}.
* @throws InterruptedException
* @throws TimeoutException
*/
private static void waitUntilKeyIsGone(RedisTemplate<String, ?> template, String key, long timeout, TimeUnit timeUnit)
throws InterruptedException, TimeoutException {
long limitMs = TimeUnit.MILLISECONDS.convert(timeout, timeUnit);
long sleepMs = 100;
long waitedMs = 0;
while (template.hasKey(key)) {
if (waitedMs > limitMs) {
throw new TimeoutException(String.format("Key '%s' after %d %s still present", key, timeout, timeUnit));
}
Thread.sleep(sleepMs);
waitedMs += sleepMs;
}
}
@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;
@GeoIndexed Point location;
}
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;
}
}