/*
* Copyright 2011-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.support.collections;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import static org.springframework.data.redis.matcher.RedisTestMatchers.*;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.ConnectionFactoryTracker;
import org.springframework.data.redis.DoubleAsStringObjectFactory;
import org.springframework.data.redis.LongAsStringObjectFactory;
import org.springframework.data.redis.ObjectFactory;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.test.util.MinimumRedisVersionRule;
import org.springframework.data.redis.test.util.RedisClientRule;
import org.springframework.data.redis.test.util.RedisDriver;
import org.springframework.data.redis.test.util.WithRedisDriver;
import org.springframework.test.annotation.IfProfileValue;
/**
* Integration test for Redis Map.
*
* @author Costin Leau
* @author Jennifer Hickey
* @author Christoph Strobl
* @auhtor Thomas Darimont
*/
@RunWith(Parameterized.class)
public abstract class AbstractRedisMapTests<K, V> {
public @Rule RedisClientRule clientRule = new RedisClientRule() {
public RedisConnectionFactory getConnectionFactory() {
return template.getConnectionFactory();
}
};
public @Rule MinimumRedisVersionRule versionRule = new MinimumRedisVersionRule();
protected RedisMap<K, V> map;
protected ObjectFactory<K> keyFactory;
protected ObjectFactory<V> valueFactory;
@SuppressWarnings("rawtypes") protected RedisTemplate template;
abstract RedisMap<K, V> createMap();
@Before
public void setUp() throws Exception {
map = createMap();
}
@SuppressWarnings("rawtypes")
public AbstractRedisMapTests(ObjectFactory<K> keyFactory, ObjectFactory<V> valueFactory, RedisTemplate template) {
this.keyFactory = keyFactory;
this.valueFactory = valueFactory;
this.template = template;
ConnectionFactoryTracker.add(template.getConnectionFactory());
}
@AfterClass
public static void cleanUp() {
ConnectionFactoryTracker.cleanUp();
}
protected K getKey() {
return keyFactory.instance();
}
protected V getValue() {
return valueFactory.instance();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected RedisStore copyStore(RedisStore store) {
return new DefaultRedisMap(store.getKey(), store.getOperations());
}
@SuppressWarnings("unchecked")
@After
public void tearDown() throws Exception {
// remove the collection entirely since clear() doesn't always work
map.getOperations().delete(Collections.singleton(map.getKey()));
template.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) {
connection.flushDb();
return null;
}
});
}
@Test
public void testClear() {
map.clear();
assertEquals(0, map.size());
map.put(getKey(), getValue());
assertEquals(1, map.size());
map.clear();
assertEquals(0, map.size());
}
@Test
public void testContainsKey() {
K k1 = getKey();
K k2 = getKey();
assertFalse(map.containsKey(k1));
assertFalse(map.containsKey(k2));
map.put(k1, getValue());
assertTrue(map.containsKey(k1));
map.put(k2, getValue());
assertTrue(map.containsKey(k2));
}
@Test(expected = UnsupportedOperationException.class)
public void testContainsValue() {
V v1 = getValue();
V v2 = getValue();
assertFalse(map.containsValue(v1));
assertFalse(map.containsValue(v2));
map.put(getKey(), v1);
assertTrue(map.containsValue(v1));
map.put(getKey(), v2);
assertTrue(map.containsValue(v2));
}
public Set<Entry<K, V>> entrySet() {
return map.entrySet();
}
@Test
public void testEquals() {
RedisStore clone = copyStore(map);
assertEquals(clone, map);
assertEquals(clone, clone);
assertEquals(map, map);
}
@Test
public void testNotEquals() {
RedisOperations<String, ?> ops = map.getOperations();
RedisStore newInstance = new DefaultRedisMap<K, V>(ops.<K, V> boundHashOps(map.getKey() + ":new"));
assertFalse(map.equals(newInstance));
assertFalse(newInstance.equals(map));
}
@Test
public void testGet() {
K k1 = getKey();
V v1 = getValue();
assertNull(map.get(k1));
map.put(k1, v1);
assertThat(map.get(k1), isEqual(v1));
}
@Test
public void testGetKey() {
assertNotNull(map.getKey());
}
@Test
public void testGetOperations() {
assertEquals(template, map.getOperations());
}
@Test
public void testHashCode() {
assertThat(map.hashCode(), not(equalTo(map.getKey().hashCode())));
assertEquals(map.hashCode(), copyStore(map).hashCode());
}
@Test
public void testIncrementNotNumber() {
assumeTrue(!(valueFactory instanceof LongAsStringObjectFactory));
K k1 = getKey();
V v1 = getValue();
map.put(k1, v1);
try {
Long value = map.increment(k1, 1);
} catch (InvalidDataAccessApiUsageException ex) {
// expected
} catch (RedisSystemException ex) {
// expected for SRP and Lettuce
}
}
@Test
public void testIncrement() {
assumeTrue(valueFactory instanceof LongAsStringObjectFactory);
K k1 = getKey();
V v1 = getValue();
map.put(k1, v1);
assertEquals(Long.valueOf(Long.valueOf((String) v1) + 10), map.increment(k1, 10));
}
@Test
@IfProfileValue(name = "redisVersion", value = "2.6+")
public void testIncrementDouble() {
assumeTrue(valueFactory instanceof DoubleAsStringObjectFactory);
K k1 = getKey();
V v1 = getValue();
map.put(k1, v1);
DecimalFormat twoDForm = new DecimalFormat("#.##");
assertEquals(twoDForm.format(Double.valueOf((String) v1) + 3.4), twoDForm.format(map.increment(k1, 3.4)));
}
@Test
public void testIsEmpty() {
map.clear();
assertTrue(map.isEmpty());
map.put(getKey(), getValue());
assertFalse(map.isEmpty());
map.clear();
assertTrue(map.isEmpty());
}
@SuppressWarnings("unchecked")
@Test
public void testKeySet() {
map.clear();
assertTrue(map.keySet().isEmpty());
K k1 = getKey();
K k2 = getKey();
K k3 = getKey();
map.put(k1, getValue());
map.put(k2, getValue());
map.put(k3, getValue());
Set<K> keySet = map.keySet();
assertThat(keySet, hasItems(k1, k2, k3));
assertEquals(3, keySet.size());
}
@Test
public void testPut() {
K k1 = getKey();
K k2 = getKey();
V v1 = getValue();
V v2 = getValue();
map.put(k1, v1);
map.put(k2, v2);
assertThat(map.get(k1), isEqual(v1));
assertThat(map.get(k2), isEqual(v2));
}
@Test
public void testPutAll() {
Map<K, V> m = new LinkedHashMap<K, V>();
K k1 = getKey();
K k2 = getKey();
V v1 = getValue();
V v2 = getValue();
m.put(k1, v1);
m.put(k2, v2);
assertNull(map.get(k1));
assertNull(map.get(k2));
map.putAll(m);
assertThat(map.get(k1), isEqual(v1));
assertThat(map.get(k2), isEqual(v2));
}
@Test
public void testRemove() {
K k1 = getKey();
K k2 = getKey();
V v1 = getValue();
V v2 = getValue();
assertNull(map.remove(k1));
assertNull(map.remove(k2));
map.put(k1, v1);
map.put(k2, v2);
assertThat(map.remove(k1), isEqual(v1));
assertNull(map.remove(k1));
assertNull(map.get(k1));
assertThat(map.remove(k2), isEqual(v2));
assertNull(map.remove(k2));
assertNull(map.get(k2));
}
@Test
public void testSize() {
assertEquals(0, map.size());
map.put(getKey(), getValue());
assertEquals(1, map.size());
K k = getKey();
map.put(k, getValue());
assertEquals(2, map.size());
map.remove(k);
assertEquals(1, map.size());
map.clear();
assertEquals(0, map.size());
}
@SuppressWarnings("unchecked")
@Test
public void testValues() {
V v1 = getValue();
V v2 = getValue();
V v3 = getValue();
map.put(getKey(), v1);
map.put(getKey(), v2);
Collection<V> values = map.values();
assertEquals(2, values.size());
assertThat(values, hasItems(v1, v2));
map.put(getKey(), v3);
values = map.values();
assertEquals(3, values.size());
assertThat(values, hasItems(v1, v2, v3));
}
@SuppressWarnings("unchecked")
@Test
public void testEntrySet() {
Set<Entry<K, V>> entries = map.entrySet();
assertTrue(entries.isEmpty());
K k1 = getKey();
K k2 = getKey();
V v1 = getValue();
V v2 = getValue();
map.put(k1, v1);
map.put(k2, v1);
entries = map.entrySet();
Set<K> keys = new LinkedHashSet<K>();
Collection<V> values = new ArrayList<V>();
for (Entry<K, V> entry : entries) {
keys.add(entry.getKey());
values.add(entry.getValue());
}
assertEquals(2, keys.size());
assertThat(keys, hasItems(k1, k2));
assertThat(values, hasItem(v1));
assertThat(values, not(hasItem(v2)));
}
@Test
public void testPutIfAbsent() {
K k1 = getKey();
K k2 = getKey();
V v1 = getValue();
V v2 = getValue();
assertNull(map.get(k1));
assertNull(map.putIfAbsent(k1, v1));
assertThat(map.putIfAbsent(k1, v2), isEqual(v1));
assertThat(map.get(k1), isEqual(v1));
assertNull(map.putIfAbsent(k2, v2));
assertThat(map.putIfAbsent(k2, v1), isEqual(v2));
assertThat(map.get(k2), isEqual(v2));
}
@Test
public void testConcurrentRemove() {
K k1 = getKey();
V v1 = getValue();
V v2 = getValue();
// No point testing this with byte[], they will never be equal
assumeTrue(!(v1 instanceof byte[]));
map.put(k1, v2);
assertFalse(map.remove(k1, v1));
assertThat(map.get(k1), isEqual(v2));
assertTrue(map.remove(k1, v2));
assertNull(map.get(k1));
}
@Test(expected = NullPointerException.class)
public void testRemoveNullValue() {
map.remove(getKey(), null);
}
@Test
public void testConcurrentReplaceTwoArgs() {
K k1 = getKey();
V v1 = getValue();
V v2 = getValue();
// No point testing binary data here, as equals will always be false
assumeTrue(!(v1 instanceof byte[]));
map.put(k1, v1);
assertFalse(map.replace(k1, v2, v1));
assertThat(map.get(k1), isEqual(v1));
assertTrue(map.replace(k1, v1, v2));
assertThat(map.get(k1), isEqual(v2));
}
@Test(expected = NullPointerException.class)
public void testReplaceNullOldValue() {
map.replace(getKey(), null, getValue());
}
@Test(expected = NullPointerException.class)
public void testReplaceNullNewValue() {
map.replace(getKey(), getValue(), null);
}
@Test
public void testConcurrentReplaceOneArg() {
K k1 = getKey();
V v1 = getValue();
V v2 = getValue();
assertNull(map.replace(k1, v1));
map.put(k1, v1);
assertNull(map.replace(getKey(), v1));
assertThat(map.replace(k1, v2), isEqual(v1));
assertThat(map.get(k1), isEqual(v2));
}
@Test(expected = NullPointerException.class)
public void testReplaceNullValue() {
map.replace(getKey(), null);
}
@Test // DATAREDIS-314
@IfProfileValue(name = "redisVersion", value = "2.8+")
@WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE })
public void testScanWorksCorrectly() throws IOException {
K k1 = getKey();
K k2 = getKey();
V v1 = getValue();
V v2 = getValue();
map.put(k1, v1);
map.put(k2, v2);
Cursor<Entry<K, V>> cursor = (Cursor<Entry<K, V>>) map.scan();
while (cursor.hasNext()) {
Entry<K, V> entry = cursor.next();
assertThat(entry.getKey(), anyOf(equalTo(k1), equalTo(k2)));
assertThat(entry.getValue(), anyOf(equalTo(v1), equalTo(v2)));
}
cursor.close();
}
}