diff --git a/src/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc similarity index 98% rename from src/asciidoc/reference/redis-repositories.adoc rename to src/main/asciidoc/reference/redis-repositories.adoc index 78bfa084b..d125dbe2a 100644 --- a/src/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -28,6 +28,8 @@ public class Person { We have a pretty simple domain object here. Note that it has a property named `id` annotated with `org.springframework.data.annotation.Id` and a `@RedisHash` annotation on its type. Those two are responsible for creating the actual key used to persist the hash. +NOTE: Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties. Those with the annotation are favored over others. + To now actually have a component responsible for storage and retrieval we need to define a repository interface. .Basic Repository Interface To Persist Person Entities diff --git a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java index 47866c7ce..f6cf551dc 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java @@ -35,14 +35,13 @@ import org.springframework.data.redis.connection.RedisClusterNode.RedisClusterNo import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.RedisNode.NodeType; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; -import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.NumberUtils; import org.springframework.util.StringUtils; /** * Common type converters - * + * * @author Jennifer Hickey * @author Thomas Darimont * @author Mark Paluch @@ -85,7 +84,7 @@ abstract public class Converters { Set flags = parseFlags(args); String portPart = hostAndPort[1]; - if(portPart.contains("@")){ + if (portPart.contains("@")) { portPart = portPart.substring(0, portPart.indexOf('@')); } @@ -195,7 +194,7 @@ abstract public class Converters { /** * Converts the result of a single line of {@code CLUSTER NODES} into a {@link RedisClusterNode}. - * + * * @param clusterNodesLine * @return * @since 1.7 @@ -206,7 +205,7 @@ abstract public class Converters { /** * Converts lines from the result of {@code CLUSTER NODES} into {@link RedisClusterNode}s. - * + * * @param clusterNodes * @return * @since 1.7 @@ -228,6 +227,7 @@ abstract public class Converters { /** * Converts the result of {@code CLUSTER NODES} into {@link RedisClusterNode}s. + * * @param clusterNodes * @return * @since 1.7 @@ -259,24 +259,8 @@ abstract public class Converters { * @return */ public static Long toTimeMillis(String seconds, String microseconds) { - return NumberUtils.parseNumber(seconds, Long.class) * 1000L + NumberUtils.parseNumber(microseconds, Long.class) - / 1000L; + return NumberUtils.parseNumber(seconds, Long.class) * 1000L + + NumberUtils.parseNumber(microseconds, Long.class) / 1000L; } - /** - * Merge multiple {@code byte} arrays into one array - * @param firstArray must not be {@literal null} - * @param additionalArrays must not be {@literal null} - * @return - */ - public static byte[][] mergeArrays(byte[] firstArray, byte[]... additionalArrays){ - Assert.notNull(firstArray, "first array must not be null"); - Assert.notNull(additionalArrays, "additional arrays must not be null"); - - byte[][] result = new byte[additionalArrays.length + 1][]; - result[0] = firstArray; - System.arraycopy(additionalArrays, 0, result, 1, additionalArrays.length); - - return result; - } } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java index cea347005..b1c3aaa57 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java @@ -65,6 +65,7 @@ import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.util.ByteUtils; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -79,7 +80,7 @@ import redis.clients.jedis.ZParams; * {@link RedisClusterConnection} implementation on top of {@link JedisCluster}.
* Uses the native {@link JedisCluster} api where possible and falls back to direct node communication using * {@link Jedis} where needed. - * + * * @author Christoph Strobl * @author Mark Paluch * @since 1.7 @@ -126,7 +127,7 @@ public class JedisClusterConnection implements RedisClusterConnection { /** * Create new {@link JedisClusterConnection} utilizing native connections via {@link JedisCluster} running commands * across the cluster via given {@link ClusterCommandExecutor}. - * + * * @param cluster must not be {@literal null}. * @param executor must not be {@literal null}. */ @@ -894,7 +895,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long bitOp(BitOperation op, byte[] destination, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destination, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destination, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1332,7 +1333,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long sInterStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1392,7 +1393,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long sUnionStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1455,7 +1456,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long sDiffStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -2083,7 +2084,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long zUnionStore(byte[] destKey, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -2104,7 +2105,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -2123,7 +2124,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long zInterStore(byte[] destKey, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -2140,7 +2141,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { - byte[][] allKeys = Converters.mergeArrays(destKey, sets); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { @@ -3298,7 +3299,7 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { - byte[][] allKeys = Converters.mergeArrays(destinationKey, sourceKeys); + byte[][] allKeys = ByteUtils.mergeArrays(destinationKey, sourceKeys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -3592,6 +3593,7 @@ public class JedisClusterConnection implements RedisClusterConnection { * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetMasterSlaveMap() */ + @Override public Map> clusterGetMasterSlaveMap() { List>> nodeResults = clusterCommandExecutor @@ -3753,7 +3755,7 @@ public class JedisClusterConnection implements RedisClusterConnection { /** * {@link Jedis} specific {@link ClusterCommandCallback}. - * + * * @author Christoph Strobl * @param * @since 1.7 @@ -3771,7 +3773,7 @@ public class JedisClusterConnection implements RedisClusterConnection { /** * Jedis specific implementation of {@link ClusterNodeResourceProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ @@ -3781,7 +3783,7 @@ public class JedisClusterConnection implements RedisClusterConnection { /** * Creates new {@link JedisClusterNodeResourceProvider}. - * + * * @param cluster must not be {@literal null}. */ public JedisClusterNodeResourceProvider(JedisCluster cluster) { @@ -3829,20 +3831,20 @@ public class JedisClusterConnection implements RedisClusterConnection { /** * Jedis specific implementation of {@link ClusterTopologyProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ static class JedisClusterTopologyProvider implements ClusterTopologyProvider { - private Object lock = new Object(); + private final Object lock = new Object(); private final JedisCluster cluster; private long time = 0; private ClusterTopology cached; /** * Create new {@link JedisClusterTopologyProvider}.s - * + * * @param cluster */ public JedisClusterTopologyProvider(JedisCluster cluster) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java index be02fdc41..c84461524 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java @@ -51,6 +51,7 @@ import org.springframework.data.redis.connection.util.ByteArraySet; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.util.ByteUtils; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -102,7 +103,7 @@ public class LettuceClusterConnection extends LettuceConnection /** * Creates new {@link LettuceClusterConnection} using {@link RedisClusterClient} running commands across the cluster * via given {@link ClusterCommandExecutor}. - * + * * @param clusterClient must not be {@literal null}. * @param executor must not be {@literal null}. */ @@ -131,6 +132,7 @@ public class LettuceClusterConnection extends LettuceConnection * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#keys(byte[]) */ + @Override public Set keys(final byte[] pattern) { Assert.notNull(pattern, "Pattern must not be null!"); @@ -156,6 +158,7 @@ public class LettuceClusterConnection extends LettuceConnection * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#flushAll() */ + @Override public void flushAll() { clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { @@ -1109,7 +1112,7 @@ public class LettuceClusterConnection extends LettuceConnection @Override public Long sInterStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { return super.sInterStore(destKey, keys); @@ -1161,7 +1164,7 @@ public class LettuceClusterConnection extends LettuceConnection @Override public Long sUnionStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { return super.sUnionStore(destKey, keys); @@ -1216,7 +1219,7 @@ public class LettuceClusterConnection extends LettuceConnection @Override public Long sDiffStore(byte[] destKey, byte[]... keys) { - byte[][] allKeys = Converters.mergeArrays(destKey, keys); + byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { return super.sDiffStore(destKey, keys); @@ -1276,7 +1279,7 @@ public class LettuceClusterConnection extends LettuceConnection @Override public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { - byte[][] allKeys = Converters.mergeArrays(destinationKey, sourceKeys); + byte[][] allKeys = ByteUtils.mergeArrays(destinationKey, sourceKeys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { @@ -1318,7 +1321,7 @@ public class LettuceClusterConnection extends LettuceConnection } /* - * + * * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#getConfig(java.lang.String) */ @@ -1538,7 +1541,7 @@ public class LettuceClusterConnection extends LettuceConnection /** * Lettuce specific implementation of {@link ClusterCommandCallback}. - * + * * @author Christoph Strobl * @param * @since 1.7 @@ -1548,7 +1551,7 @@ public class LettuceClusterConnection extends LettuceConnection /** * Lettuce specific implementation of {@link MultiKeyClusterCommandCallback}. - * + * * @author Christoph Strobl * @param * @since 1.7 @@ -1560,7 +1563,7 @@ public class LettuceClusterConnection extends LettuceConnection /** * Lettuce specific implementation of {@link ClusterNodeResourceProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ @@ -1605,7 +1608,7 @@ public class LettuceClusterConnection extends LettuceConnection /** * Lettuce specific implementation of {@link ClusterTopologyProvider}. - * + * * @author Christoph Strobl * @since 1.7 */ diff --git a/src/main/java/org/springframework/data/redis/core/IndexWriter.java b/src/main/java/org/springframework/data/redis/core/IndexWriter.java index 2e2d6527f..b179180fd 100644 --- a/src/main/java/org/springframework/data/redis/core/IndexWriter.java +++ b/src/main/java/org/springframework/data/redis/core/IndexWriter.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import java.util.Set; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.convert.IndexedData; import org.springframework.data.redis.core.convert.RedisConverter; @@ -122,7 +123,7 @@ class IndexWriter { protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData) { Assert.notNull(indexedData, "IndexedData must not be null!"); - Set existingKeys = connection.keys(toBytes(indexedData.getKeySpace() + ":" + indexedData.getIndexName() + Set existingKeys = connection.keys(toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + ":*")); if (!CollectionUtils.isEmpty(existingKeys)) { @@ -158,12 +159,12 @@ class IndexWriter { return; } - byte[] indexKey = toBytes(indexedData.getKeySpace() + ":" + indexedData.getIndexName() + ":"); + byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + ":"); indexKey = ByteUtils.concat(indexKey, toBytes(value)); connection.sAdd(indexKey, key); // keep track of indexes used for the object - connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeySpace() + ":"), key, toBytes(":idx")), indexKey); + connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey); } else { throw new IllegalArgumentException(String.format("Cannot write index data for unknown index type %s", indexedData.getClass())); @@ -180,6 +181,14 @@ class IndexWriter { return (byte[]) source; } - return converter.getConversionService().convert(source, byte[].class); + if (converter.getConversionService().canConvert(source.getClass(), byte[].class)) { + return converter.getConversionService().convert(source, byte[].class); + } + + throw new InvalidDataAccessApiUsageException( + String + .format( + "Cannot convert %s to binary representation for index key generation. Are you missing a Converter? Did you register a non PathBasedRedisIndexDefinition that might apply to a complex type?", + source.getClass())); } } diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java index e8ec920b2..43c2339f6 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -40,12 +40,12 @@ import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.convert.CustomConversions; -import org.springframework.data.redis.core.convert.IndexResolverImpl; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.convert.MappingRedisConverter; +import org.springframework.data.redis.core.convert.PathIndexResolver; import org.springframework.data.redis.core.convert.RedisConverter; import org.springframework.data.redis.core.convert.RedisData; -import org.springframework.data.redis.core.convert.ReferenceResolver; +import org.springframework.data.redis.core.convert.ReferenceResolverImpl; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.RedisMessageListenerContainer; @@ -86,8 +86,8 @@ import org.springframework.util.Assert; * @author Christoph Strobl * @since 1.7 */ -public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements ApplicationContextAware, - ApplicationListener { +public class RedisKeyValueAdapter extends AbstractKeyValueAdapter + implements ApplicationContextAware, ApplicationListener { private static final Logger LOGGER = LoggerFactory.getLogger(RedisKeyValueAdapter.class); @@ -131,8 +131,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App Assert.notNull(redisOps, "RedisOperations must not be null!"); Assert.notNull(mappingContext, "RedisMappingContext must not be null!"); - MappingRedisConverter mappingConverter = new MappingRedisConverter(mappingContext, new IndexResolverImpl( - mappingContext), new ReferenceResolverImpl(this)); + MappingRedisConverter mappingConverter = new MappingRedisConverter(mappingContext, + new PathIndexResolver(mappingContext), new ReferenceResolverImpl(redisOps)); mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions); mappingConverter.afterPropertiesSet(); @@ -173,7 +173,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App if (rdo.getId() == null) { - rdo.setId(id); + rdo.setId(converter.getConversionService().convert(id, String.class)); if (!(item instanceof RedisData)) { KeyValuePersistentProperty idProperty = converter.getMappingContext().getPersistentEntity(item.getClass()) @@ -249,7 +249,10 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App */ public T get(Serializable id, Serializable keyspace, Class type) { - final byte[] binId = createKey(keyspace, id); + String stringId = asString(id); + String stringKeyspace = asString(keyspace); + + final byte[] binId = createKey(stringKeyspace, stringId); Map raw = redisOps.execute(new RedisCallback>() { @@ -260,8 +263,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App }); RedisData data = new RedisData(raw); - data.setId(id); - data.setKeyspace(keyspace.toString()); + data.setId(stringId); + data.setKeyspace(stringKeyspace); return converter.read(type, data); } @@ -287,15 +290,17 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App if (o != null) { + final byte[] keyToDelete = createKey(asString(keyspace), asString(id)); + redisOps.execute(new RedisCallback() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { - connection.del(createKey(keyspace, id)); + connection.del(keyToDelete); connection.sRem(binKeyspace, binId); - new IndexWriter(connection, converter).removeKeyFromIndexes(keyspace.toString(), binId); + new IndexWriter(connection, converter).removeKeyFromIndexes(asString(keyspace), binId); return null; } }); @@ -322,7 +327,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App Set members = connection.sMembers(binKeyspace); for (byte[] id : members) { - rawData.add(connection.hGetAll(createKey(binKeyspace, id))); + rawData.add(connection + .hGetAll(createKey(asString(keyspace), getConverter().getConversionService().convert(id, String.class)))); } return rawData; @@ -349,7 +355,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.del(toBytes(keyspace)); - new IndexWriter(connection, converter).removeAllIndexes(keyspace.toString()); + new IndexWriter(connection, converter).removeAllIndexes(asString(keyspace)); return null; } }); @@ -404,7 +410,12 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App // nothing to do } - public byte[] createKey(Serializable keyspace, Serializable id) { + private String asString(Serializable value) { + return value instanceof String ? (String) value + : getConverter().getConversionService().convert(value, String.class); + } + + public byte[] createKey(String keyspace, String id) { return toBytes(keyspace + ":" + id); } @@ -533,8 +544,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App byte[] key = message.getBody(); - final byte[] phantomKey = ByteUtils.concat(key, converter.getConversionService() - .convert(":phantom", byte[].class)); + final byte[] phantomKey = ByteUtils.concat(key, + converter.getConversionService().convert(":phantom", byte[].class)); Map hash = ops.execute(new RedisCallback>() { @@ -569,40 +580,4 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements App } } - /** - * {@link ReferenceResolver} using {@link RedisKeyValueAdapter} to read and convert referenced entities. - * - * @author Christoph Strobl - * @since 1.7 - */ - static class ReferenceResolverImpl implements ReferenceResolver { - - private RedisKeyValueAdapter adapter; - - ReferenceResolverImpl() {} - - /** - * @param adapter must not be {@literal null}. - */ - public ReferenceResolverImpl(RedisKeyValueAdapter adapter) { - this.adapter = adapter; - } - - /** - * @param adapter - */ - public void setAdapter(RedisKeyValueAdapter adapter) { - this.adapter = adapter; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.redis.core.convert.ReferenceResolver#resolveReference(java.io.Serializable, java.io.Serializable, java.lang.Class) - */ - @Override - public T resolveReference(Serializable id, Serializable keyspace, Class type) { - return (T) adapter.get(id, keyspace, type); - } - } - } diff --git a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java index 82c250e29..f97aeeefa 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java +++ b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java @@ -18,11 +18,11 @@ package org.springframework.data.redis.core; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.springframework.dao.DataAccessException; import org.springframework.data.keyvalue.core.CriteriaAccessor; @@ -32,6 +32,7 @@ import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.convert.RedisData; import org.springframework.data.redis.repository.query.RedisOperationChain; +import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue; import org.springframework.data.redis.util.ByteUtils; /** @@ -65,8 +66,8 @@ class RedisQueryEngine extends QueryEngine Collection execute(final RedisOperationChain criteria, final Comparator sort, int offset, int rows, - final Serializable keyspace, Class type) { + public Collection execute(final RedisOperationChain criteria, final Comparator sort, final int offset, + final int rows, final Serializable keyspace, Class type) { RedisCallback>> callback = new RedisCallback>>() { @@ -81,12 +82,25 @@ class RedisQueryEngine extends QueryEngine allKeys = connection.sInter(keys); + List allKeys = new ArrayList(); + if (!criteria.getSismember().isEmpty()) { + allKeys.addAll(connection.sInter(keys(keyspace + ":", criteria.getSismember()))); + } + if (!criteria.getOrSismember().isEmpty()) { + allKeys.addAll(connection.sUnion(keys(keyspace + ":", criteria.getOrSismember()))); + } byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class); final Map> rawData = new LinkedHashMap>(); + if (allKeys.size() == 0 || allKeys.size() < offset) { + return Collections.emptyMap(); + } + + if (offset >= 0 && rows > 0) { + allKeys = allKeys.subList(Math.max(0, offset), Math.min(offset + rows, allKeys.size())); + } for (byte[] id : allKeys) { byte[] singleKey = ByteUtils.concat(keyspaceBin, id); @@ -150,6 +164,23 @@ class RedisQueryEngine extends QueryEngine source) { + + byte[][] keys = new byte[source.size()][]; + int i = 0; + for (PathAndValue pathAndValue : source) { + + byte[] convertedValue = getAdapter().getConverter().getConversionService() + .convert(pathAndValue.getFirstValue(), byte[].class); + byte[] fullPath = getAdapter().getConverter().getConversionService() + .convert(prefix + pathAndValue.getPath() + ":", byte[].class); + + keys[i] = ByteUtils.concat(fullPath, convertedValue); + i++; + } + return keys; + } + /** * @author Christoph Strobl * @since 1.7 diff --git a/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java new file mode 100644 index 000000000..2ebe21b3f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Composite {@link IndexResolver} implementation that iterates over a given collection of delegate + * {@link IndexResolver} instances.
+ *
+ * NOTE {@link IndexedData} created by an {@link IndexResolver} can be overwritten by subsequent + * {@link IndexResolver}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class CompositeIndexResolver implements IndexResolver { + + private final List resolvers; + + /** + * Create new {@link CompositeIndexResolver}. + * + * @param resolvers must not be {@literal null}. + */ + public CompositeIndexResolver(Collection resolvers) { + + Assert.notNull(resolvers, "Resolvers must not be null!"); + if (CollectionUtils.contains(resolvers.iterator(), null)) { + throw new IllegalArgumentException("Resolvers must no contain null values"); + } + this.resolvers = new ArrayList(resolvers); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(org.springframework.data.util.TypeInformation, java.lang.Object) + */ + @Override + public Set resolveIndexesFor(TypeInformation typeInformation, Object value) { + + if (resolvers.isEmpty()) { + return Collections.emptySet(); + } + + Set data = new LinkedHashSet(); + for (IndexResolver resolver : resolvers) { + data.addAll(resolver.resolveIndexesFor(typeInformation, value)); + } + return data; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java b/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java index 9a19f79ff..37584a9d0 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java +++ b/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java @@ -36,6 +36,6 @@ public interface IndexedData { * * @return */ - String getKeySpace(); + String getKeyspace(); } diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java b/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java index 1eaced36b..908f96a0b 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -15,7 +15,7 @@ */ package org.springframework.data.redis.core.convert; -import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider; /** * {@link MappingConfiguration} is used for programmatic configuration of secondary indexes, key prefixes, expirations @@ -26,7 +26,7 @@ import org.springframework.data.redis.core.index.IndexConfiguration; */ public class MappingConfiguration { - private final IndexConfiguration indexConfiguration; + private final ConfigurableIndexDefinitionProvider indexConfiguration; private final KeyspaceConfiguration keyspaceConfiguration; /** @@ -35,7 +35,8 @@ public class MappingConfiguration { * @param indexConfiguration must not be {@literal null}. * @param keyspaceConfiguration must not be {@literal null}. */ - public MappingConfiguration(IndexConfiguration indexConfiguration, KeyspaceConfiguration keyspaceConfiguration) { + public MappingConfiguration(ConfigurableIndexDefinitionProvider indexConfiguration, + KeyspaceConfiguration keyspaceConfiguration) { this.indexConfiguration = indexConfiguration; this.keyspaceConfiguration = keyspaceConfiguration; @@ -44,7 +45,7 @@ public class MappingConfiguration { /** * @return never {@literal null}. */ - public IndexConfiguration getIndexConfiguration() { + public ConfigurableIndexDefinitionProvider getIndexConfiguration() { return indexConfiguration; } diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java index 1af07f488..5c3921bb3 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -15,7 +15,6 @@ */ package org.springframework.data.redis.core.convert; -import java.io.Serializable; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -138,7 +137,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { this.referenceResolver = referenceResolver; - this.indexResolver = indexResolver != null ? indexResolver : new IndexResolverImpl(this.mappingContext); + this.indexResolver = indexResolver != null ? indexResolver : new PathIndexResolver(this.mappingContext); } /* @@ -210,35 +209,29 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { if (persistentProperty.isMap()) { if (conversionService.canConvert(byte[].class, persistentProperty.getMapValueType())) { - accessor.setProperty( - persistentProperty, - readMapOfSimpleTypes(currentPath, persistentProperty.getType(), persistentProperty.getComponentType(), - persistentProperty.getMapValueType(), source)); + accessor.setProperty(persistentProperty, readMapOfSimpleTypes(currentPath, persistentProperty.getType(), + persistentProperty.getComponentType(), persistentProperty.getMapValueType(), source)); } else { - accessor.setProperty( - persistentProperty, - readMapOfComplexTypes(currentPath, persistentProperty.getType(), persistentProperty.getComponentType(), - persistentProperty.getMapValueType(), source)); + accessor.setProperty(persistentProperty, readMapOfComplexTypes(currentPath, persistentProperty.getType(), + persistentProperty.getComponentType(), persistentProperty.getMapValueType(), source)); } } else if (persistentProperty.isCollectionLike()) { if (conversionService.canConvert(byte[].class, persistentProperty.getComponentType())) { - accessor.setProperty( - persistentProperty, - readCollectionOfSimpleTypes(currentPath, persistentProperty.getType(), persistentProperty - .getTypeInformation().getComponentType().getActualType().getType(), source)); + accessor.setProperty(persistentProperty, + readCollectionOfSimpleTypes(currentPath, persistentProperty.getType(), + persistentProperty.getTypeInformation().getComponentType().getActualType().getType(), source)); } else { - accessor.setProperty( - persistentProperty, - readCollectionOfComplexTypes(currentPath, persistentProperty.getType(), persistentProperty - .getTypeInformation().getComponentType().getActualType().getType(), source.getBucket())); + accessor.setProperty(persistentProperty, + readCollectionOfComplexTypes(currentPath, persistentProperty.getType(), + persistentProperty.getTypeInformation().getComponentType().getActualType().getType(), + source.getBucket())); } - } else if (persistentProperty.isEntity() - && !conversionService.canConvert(byte[].class, persistentProperty.getTypeInformation().getActualType() - .getType())) { + } else if (persistentProperty.isEntity() && !conversionService.canConvert(byte[].class, + persistentProperty.getTypeInformation().getActualType().getType())) { Class targetType = persistentProperty.getTypeInformation().getActualType().getType(); @@ -284,8 +277,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { @Override public void doWithAssociation(Association association) { - String currentPath = !path.isEmpty() ? path + "." + association.getInverse().getName() : association - .getInverse().getName(); + String currentPath = !path.isEmpty() ? path + "." + association.getInverse().getName() + : association.getInverse().getName(); if (association.getInverse().isCollectionLike()) { @@ -299,11 +292,10 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { String referenceKey = fromBytes(entry.getValue(), String.class); String[] args = referenceKey.split(":"); - Object loadedObject = referenceResolver.resolveReference(args[1], args[0], association.getInverse() - .getActualType()); + Map rawHash = referenceResolver.resolveReference(args[1], args[0]); - if (loadedObject != null) { - target.add(loadedObject); + if (!CollectionUtils.isEmpty(rawHash)) { + target.add(read(association.getInverse().getActualType(), new RedisData(rawHash))); } } @@ -319,11 +311,12 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { String key = fromBytes(binKey, String.class); String[] args = key.split(":"); - Object loadedObject = referenceResolver.resolveReference(args[1], args[0], association.getInverse() - .getActualType()); - if (loadedObject != null) { - accessor.setProperty(association.getInverse(), loadedObject); + Map rawHash = referenceResolver.resolveReference(args[1], args[0]); + + if (!CollectionUtils.isEmpty(rawHash)) { + accessor.setProperty(association.getInverse(), + read(association.getInverse().getActualType(), new RedisData(rawHash))); } } } @@ -345,7 +338,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { sink.setKeyspace(entity.getKeySpace()); writeInternal(entity.getKeySpace(), "", source, entity.getTypeInformation(), sink); - sink.setId((Serializable) entity.getIdentifierAccessor(source).getIdentifier()); + sink.setId(getConversionService().convert(entity.getIdentifierAccessor(source).getIdentifier(), String.class)); Long ttl = entity.getTimeToLiveAccessor().getTimeToLive(source); if (ttl != null && ttl > 0) { @@ -748,8 +741,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { /** * @author Christoph Strobl */ - private static class ConverterAwareParameterValueProvider implements - PropertyValueProvider { + private static class ConverterAwareParameterValueProvider + implements PropertyValueProvider { private final RedisData source; private final ConversionService conversionService; diff --git a/src/main/java/org/springframework/data/redis/core/convert/IndexResolverImpl.java b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java similarity index 65% rename from src/main/java/org/springframework/data/redis/core/convert/IndexResolverImpl.java rename to src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java index a08e0323b..94c533421 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/IndexResolverImpl.java +++ b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -25,9 +25,13 @@ import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider; import org.springframework.data.redis.core.index.IndexConfiguration; -import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting; +import org.springframework.data.redis.core.index.IndexDefinition; +import org.springframework.data.redis.core.index.IndexDefinition.Condition; +import org.springframework.data.redis.core.index.IndexDefinition.IndexingContext; import org.springframework.data.redis.core.index.Indexed; +import org.springframework.data.redis.core.index.SimpleIndexDefinition; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.redis.core.mapping.RedisPersistentEntity; import org.springframework.data.util.ClassTypeInformation; @@ -41,24 +45,24 @@ import org.springframework.util.Assert; * @author Christoph Strobl * @since 1.7 */ -public class IndexResolverImpl implements IndexResolver { +public class PathIndexResolver implements IndexResolver { - private IndexConfiguration indexConfiguration; + private ConfigurableIndexDefinitionProvider indexConfiguration; private RedisMappingContext mappingContext; /** - * Creates new {@link IndexResolverImpl} with empty {@link IndexConfiguration}. + * Creates new {@link PathIndexResolver} with empty {@link IndexConfiguration}. */ - public IndexResolverImpl() { + public PathIndexResolver() { this(new RedisMappingContext()); } /** - * Creates new {@link IndexResolverImpl} with given {@link IndexConfiguration}. + * Creates new {@link PathIndexResolver} with given {@link IndexConfiguration}. * * @param mapppingContext must not be {@literal null}. */ - public IndexResolverImpl(RedisMappingContext mappingContext) { + public PathIndexResolver(RedisMappingContext mappingContext) { Assert.notNull(mappingContext, "MappingContext must not be null!"); this.mappingContext = mappingContext; @@ -71,21 +75,16 @@ public class IndexResolverImpl implements IndexResolver { */ public Set resolveIndexesFor(TypeInformation typeInformation, Object value) { return doResolveIndexesFor(mappingContext.getPersistentEntity(typeInformation).getKeySpace(), "", typeInformation, - value); + null, value); } private Set doResolveIndexesFor(final String keyspace, final String path, - TypeInformation typeInformation, Object value) { + TypeInformation typeInformation, PersistentProperty fallback, Object value) { RedisPersistentEntity entity = mappingContext.getPersistentEntity(typeInformation); if (entity == null) { - - IndexedData index = resolveIndex(keyspace, path, null, value); - if (index != null) { - return Collections.singleton(index); - } - return Collections.emptySet(); + return resolveIndex(keyspace, path, fallback, value); } final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); @@ -102,14 +101,9 @@ public class IndexResolverImpl implements IndexResolver { if (propertyValue != null) { - TypeInformation typeHint = persistentProperty.isMap() ? persistentProperty.getTypeInformation() - .getMapValueType() : persistentProperty.getTypeInformation().getActualType(); - - IndexedData index = resolveIndex(keyspace, currentPath, persistentProperty, propertyValue); - - if (index != null) { - indexes.add(index); - } + TypeInformation typeHint = persistentProperty.isMap() + ? persistentProperty.getTypeInformation().getMapValueType() + : persistentProperty.getTypeInformation().getActualType(); if (persistentProperty.isMap()) { @@ -117,7 +111,7 @@ public class IndexResolverImpl implements IndexResolver { TypeInformation typeToUse = updateTypeHintForActualValue(typeHint, entry.getValue()); indexes.addAll(doResolveIndexesFor(keyspace, currentPath + "." + entry.getKey(), - typeToUse.getActualType(), entry.getValue())); + typeToUse.getActualType(), persistentProperty, entry.getValue())); } } else if (persistentProperty.isCollectionLike()) { @@ -125,7 +119,8 @@ public class IndexResolverImpl implements IndexResolver { for (Object listValue : (Iterable) propertyValue) { TypeInformation typeToUse = updateTypeHintForActualValue(typeHint, listValue); - indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeToUse.getActualType(), listValue)); + indexes.addAll( + doResolveIndexesFor(keyspace, currentPath, typeToUse.getActualType(), persistentProperty, listValue)); } } @@ -133,7 +128,10 @@ public class IndexResolverImpl implements IndexResolver { || persistentProperty.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)) { typeHint = updateTypeHintForActualValue(typeHint, propertyValue); - indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeHint.getActualType(), propertyValue)); + indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeHint.getActualType(), persistentProperty, + propertyValue)); + } else { + indexes.addAll(resolveIndex(keyspace, currentPath, persistentProperty, propertyValue)); } } @@ -157,37 +155,55 @@ public class IndexResolverImpl implements IndexResolver { return indexes; } - protected IndexedData resolveIndex(String keyspace, String propertyPath, PersistentProperty property, Object value) { + protected Set resolveIndex(String keyspace, String propertyPath, PersistentProperty property, + Object value) { if (value == null) { - return null; + return Collections.emptySet(); } String path = normalizeIndexPath(propertyPath, property); + Set data = new LinkedHashSet(); + if (indexConfiguration.hasIndexFor(keyspace, path)) { - // FIXME it seems there is a mis-match between IndexConfiguration - // resolving many RedisIndexSetting objects to resolving a single - // IndexData in this method. - RedisIndexSetting indexSetting = indexConfiguration.getIndexDefinitionsFor(keyspace, path).iterator().next(); - return new SimpleIndexedPropertyValue(keyspace, indexSetting.getIndexName(), value); + + IndexingContext context = new IndexingContext(keyspace, path, + property != null ? property.getTypeInformation() : ClassTypeInformation.OBJECT); + + for (IndexDefinition indexDefinition : indexConfiguration.getIndexDefinitionsFor(keyspace, path)) { + + if (!verifyConditions(indexDefinition.getConditions(), value, context)) { + continue; + } + + data.add(new SimpleIndexedPropertyValue(keyspace, indexDefinition.getIndexName(), + indexDefinition.valueTransformer().convert(value))); + } } else if (property != null && property.isAnnotationPresent(Indexed.class)) { - Indexed indexed = property.findAnnotation(Indexed.class); + SimpleIndexDefinition indexDefinition = new SimpleIndexDefinition(keyspace, path); + indexConfiguration.addIndexDefinition(indexDefinition); - indexConfiguration.addIndexDefinition(new RedisIndexSetting(keyspace, path, indexed.type())); + data.add(new SimpleIndexedPropertyValue(keyspace, path, indexDefinition.valueTransformer().convert(value))); + } + return data; + } - switch (indexed.type()) { - case SIMPLE: - return new SimpleIndexedPropertyValue(keyspace, path, value); - default: - throw new IllegalArgumentException(String.format("Unsupported index type '%s' for path '%s'.", - indexed.type(), path)); + @SuppressWarnings({ "rawtypes", "unchecked" }) + private boolean verifyConditions(Iterable> conditions, Object value, IndexingContext context) { + + for (Condition condition : conditions) { + + // TODO: generics lookup + if (!condition.matches(value, context)) { + return false; } } - return null; + + return true; } private String normalizeIndexPath(String path, PersistentProperty property) { diff --git a/src/main/java/org/springframework/data/redis/core/convert/RedisData.java b/src/main/java/org/springframework/data/redis/core/convert/RedisData.java index 975fc7cad..0bfd78b6c 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/RedisData.java +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisData.java @@ -15,7 +15,6 @@ */ package org.springframework.data.redis.core.convert; -import java.io.Serializable; import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -34,7 +33,7 @@ import org.springframework.util.Assert; public class RedisData { private String keyspace; - private Serializable id; + private String id; private Bucket bucket; private Set indexedData; @@ -74,14 +73,14 @@ public class RedisData { * * @param id */ - public void setId(Serializable id) { + public void setId(String id) { this.id = id; } /** * @return */ - public Serializable getId() { + public String getId() { return this.id; } diff --git a/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java index 5d18b20f1..1e29e08b3 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core.convert; import java.io.Serializable; +import java.util.Map; import org.springframework.data.annotation.Reference; @@ -30,8 +31,7 @@ public interface ReferenceResolver { /** * @param id must not be {@literal null}. * @param keyspace must not be {@literal null}. - * @param type must not be {@literal null}. * @return {@literal null} if referenced object does not exist. */ - T resolveReference(Serializable id, Serializable keyspace, Class type); + Map resolveReference(Serializable id, String keyspace); } diff --git a/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java new file mode 100644 index 000000000..e4240de70 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.convert; + +import java.io.Serializable; +import java.util.Map; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.convert.BinaryConverters.StringToBytesConverter; +import org.springframework.util.Assert; + +/** + * {@link ReferenceResolver} using {@link RedisKeyValueAdapter} to read raw data. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ReferenceResolverImpl implements ReferenceResolver { + + private final RedisOperations redisOps; + private final StringToBytesConverter converter; + + /** + * @param redisOperations must not be {@literal null}. + */ + public ReferenceResolverImpl(RedisOperations redisOperations) { + + Assert.notNull(redisOperations); + + this.redisOps = redisOperations; + this.converter = new StringToBytesConverter(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.convert.ReferenceResolver#resolveReference(java.io.Serializable, java.io.Serializable, java.lang.Class) + */ + @Override + public Map resolveReference(Serializable id, String keyspace) { + + final byte[] key = converter.convert(keyspace + ":" + id); + + return redisOps.execute(new RedisCallback>() { + + @Override + public Map doInRedis(RedisConnection connection) throws DataAccessException { + return connection.hGetAll(key); + } + }); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java b/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java index a1ebf841b..0c936c1cc 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java +++ b/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java @@ -67,7 +67,7 @@ public class SimpleIndexedPropertyValue implements IndexedData { * @see org.springframework.data.redis.core.convert.IndexedData#getKeySpace() */ @Override - public String getKeySpace() { + public String getKeyspace() { return this.keyspace; } diff --git a/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java index 8dd1ce9c8..b4203f243 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java @@ -16,13 +16,16 @@ package org.springframework.data.redis.core.convert; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; -import org.springframework.data.redis.core.index.IndexConfiguration; -import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting; +import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider; +import org.springframework.data.redis.core.index.IndexDefinition; +import org.springframework.data.redis.core.index.SpelIndexDefinition; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.util.TypeInformation; import org.springframework.expression.BeanResolver; @@ -35,16 +38,19 @@ import org.springframework.util.Assert; * An {@link IndexResolver} that resolves {@link IndexedData} using a {@link SpelExpressionParser}. * * @author Rob Winch + * @author Christoph Strobl * @since 1.7 */ public class SpelIndexResolver implements IndexResolver { - private final IndexConfiguration settings; + private final ConfigurableIndexDefinitionProvider settings; private final SpelExpressionParser parser; private final RedisMappingContext mappingContext; private BeanResolver beanResolver; + private Map expressionCache; + /** * Creates a new instance using a default {@link SpelExpressionParser}. * @@ -66,6 +72,7 @@ public class SpelIndexResolver implements IndexResolver { Assert.notNull(parser, "SpelExpressionParser must not be null!"); this.mappingContext = mappingContext; this.settings = mappingContext.getMappingConfiguration().getIndexConfiguration(); + this.expressionCache = new HashMap(); this.parser = parser; } @@ -88,26 +95,41 @@ public class SpelIndexResolver implements IndexResolver { Set indexes = new HashSet(); - for (RedisIndexSetting setting : settings.getIndexDefinitionsFor(keyspace)) { + for (IndexDefinition setting : settings.getIndexDefinitionsFor(keyspace)) { - Expression expression = parser.parseExpression(setting.getPath()); - StandardEvaluationContext context = new StandardEvaluationContext(); - context.setRootObject(value); - context.setVariable("this", value); + if (setting instanceof SpelIndexDefinition) { - if (beanResolver != null) { - context.setBeanResolver(beanResolver); - } + Expression expression = getAndCacheIfAbsent((SpelIndexDefinition) setting); - Object index = expression.getValue(context); - if (index != null) { - indexes.add(new SimpleIndexedPropertyValue(keyspace, setting.getIndexName(), index)); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(value); + context.setVariable("this", value); + + if (beanResolver != null) { + context.setBeanResolver(beanResolver); + } + + Object index = expression.getValue(context); + if (index != null) { + indexes.add(new SimpleIndexedPropertyValue(keyspace, setting.getIndexName(), index)); + } } } return indexes; } + private Expression getAndCacheIfAbsent(SpelIndexDefinition indexDefinition) { + + if (expressionCache.containsKey(indexDefinition)) { + return expressionCache.get(indexDefinition); + } + + Expression expression = parser.parseExpression(indexDefinition.getExpression()); + expressionCache.put(indexDefinition, expression); + return expression; + } + /** * Allows setting the BeanResolver * diff --git a/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java b/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java new file mode 100644 index 000000000..f1f478329 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +/** + * {@link IndexDefinitionProvider} that allows registering new {@link IndexDefinition} via + * {@link IndexDefinitionRegistry}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface ConfigurableIndexDefinitionProvider extends IndexDefinitionProvider, IndexDefinitionRegistry { + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java index da33933b6..614f996aa 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java +++ b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java @@ -16,13 +16,13 @@ package org.springframework.data.redis.core.index; import java.io.Serializable; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; +import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** @@ -33,70 +33,55 @@ import org.springframework.util.ObjectUtils; * @author Rob Winch * @since 1.7 */ -public class IndexConfiguration { +public class IndexConfiguration implements ConfigurableIndexDefinitionProvider { - private final Set definitions; + private final Set definitions; /** * Creates new empty {@link IndexConfiguration}. */ public IndexConfiguration() { - this.definitions = new CopyOnWriteArraySet(); - for (RedisIndexSetting initial : initialConfiguration()) { + this.definitions = new CopyOnWriteArraySet(); + for (IndexDefinition initial : initialConfiguration()) { addIndexDefinition(initial); } } - /** - * Checks if an index is defined for a given keyspace and property path. - * - * @param keyspace - * @param path - * @return true if index is defined. + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#hasIndexFor(java.io.Serializable) + */ + @Override + public boolean hasIndexFor(Serializable keyspace) { + return !getIndexDefinitionsFor(keyspace).isEmpty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#hasIndexFor(java.io.Serializable, java.lang.String) */ public boolean hasIndexFor(Serializable keyspace, String path) { - - for (IndexType type : IndexType.values()) { - RedisIndexSetting def = getIndexDefinition(keyspace, path, type); - if (def != null) { - return true; - } - } - return false; + return !getIndexDefinitionsFor(keyspace, path).isEmpty(); } - /** - * Get the list of {@link RedisIndexSetting} for a given keyspace and property path. - * - * @param keyspace - * @param path - * @return never {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#getIndexDefinitionsFor(java.io.Serializable, java.lang.String) */ - public List getIndexDefinitionsFor(Serializable keyspace, String path) { - - List indexDefinitions = new ArrayList(); - for (IndexType type : IndexType.values()) { - RedisIndexSetting def = getIndexDefinition(keyspace, path, type); - if (def != null) { - indexDefinitions.add(def); - } - } - - return indexDefinitions; + public Set getIndexDefinitionsFor(Serializable keyspace, String path) { + return getIndexDefinitions(keyspace, path, Object.class); } - /** - * Gets all of the {@link RedisIndexSetting} for a given keyspace. - * - * @param keyspace the keyspace to get - * @return never {@literal null} + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionProvider#getIndexDefinitionsFor(java.io.Serializable) */ - public List getIndexDefinitionsFor(Serializable keyspace) { + public Set getIndexDefinitionsFor(Serializable keyspace) { - List indexDefinitions = new ArrayList(); + Set indexDefinitions = new LinkedHashSet(); - for (RedisIndexSetting indexDef : definitions) { + for (IndexDefinition indexDef : definitions) { if (indexDef.getKeyspace().equals(keyspace)) { indexDefinitions.add(indexDef); } @@ -105,26 +90,33 @@ public class IndexConfiguration { return indexDefinitions; } - /** - * Add given {@link RedisIndexSetting}. - * - * @param indexDefinition must not be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinitionRegistry#addIndexDefinition(org.springframework.data.redis.core.index.IndexDefinition) */ - public void addIndexDefinition(RedisIndexSetting indexDefinition) { + public void addIndexDefinition(IndexDefinition indexDefinition) { Assert.notNull(indexDefinition, "RedisIndexDefinition must not be null in order to be added."); this.definitions.add(indexDefinition); } - private RedisIndexSetting getIndexDefinition(Serializable keyspace, String path, IndexType type) { + private Set getIndexDefinitions(Serializable keyspace, String path, Class type) { - for (RedisIndexSetting indexDef : definitions) { - if (indexDef.getKeyspace().equals(keyspace) && indexDef.getPath().equals(path) && indexDef.getType().equals(type)) { - return indexDef; + Set def = new LinkedHashSet(); + for (IndexDefinition indexDef : definitions) { + if (ClassUtils.isAssignable(type, indexDef.getClass()) && indexDef.getKeyspace().equals(keyspace)) { + + if (indexDef instanceof PathBasedRedisIndexDefinition) { + if (ObjectUtils.nullSafeEquals(((PathBasedRedisIndexDefinition) indexDef).getPath(), path)) { + def.add(indexDef); + } + } else { + def.add(indexDef); + } } } - return null; + return def; } /** @@ -132,93 +124,8 @@ public class IndexConfiguration { * * @return must not return {@literal null}. */ - protected Iterable initialConfiguration() { + protected Iterable initialConfiguration() { return Collections.emptySet(); } - /** - * @author Christoph Strobl - * @author Rob Winch - * @since 1.7 - */ - public static class RedisIndexSetting { - - private final Serializable keyspace; - private final String path; - private final String indexName; - private final IndexType type; - - public RedisIndexSetting(Serializable keyspace, String path) { - this(keyspace, path, null); - } - - public RedisIndexSetting(Serializable keyspace, String path, IndexType type) { - this(keyspace, path, path, type); - } - - public RedisIndexSetting(Serializable keyspace, String path, String indexName, IndexType type) { - this.keyspace = keyspace; - this.path = path; - this.indexName = indexName; - this.type = type == null ? IndexType.SIMPLE : type; - } - - public String getIndexName() { - return indexName; - } - - public Serializable getKeyspace() { - return keyspace; - } - - public String getPath() { - return path; - } - - public IndexType getType() { - return type; - } - - @Override - public int hashCode() { - - int result = ObjectUtils.nullSafeHashCode(keyspace); - result += ObjectUtils.nullSafeHashCode(path); - result += ObjectUtils.nullSafeHashCode(indexName); - result += ObjectUtils.nullSafeHashCode(type); - return result; - } - - @Override - public boolean equals(Object obj) { - - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof RedisIndexSetting)) { - return false; - } - - RedisIndexSetting that = (RedisIndexSetting) obj; - - if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) { - return false; - } - if (!ObjectUtils.nullSafeEquals(this.path, that.path)) { - return false; - } - if (!ObjectUtils.nullSafeEquals(this.indexName, that.indexName)) { - return false; - } - if (!ObjectUtils.nullSafeEquals(this.type, that.type)) { - return false; - } - return true; - } - - } - } diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java new file mode 100644 index 000000000..5da0e32d8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +import java.util.Collection; + +import org.springframework.data.util.TypeInformation; + +/** + * {@link IndexDefinition} allow to set up a blueprint for creating secondary index structures in Redis. Setting up + * conditions allows to define {@link Condition} that have to be passed in order to add a value to the index. This + * allows to fine grained tune the I index structure. {@link IndexValueTransformer} gets applied to the raw value for + * creating the actual index entry. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexDefinition { + + /** + * @return never {@literal null}. + */ + String getKeyspace(); + + /** + * @return never {@literal null}. + */ + Collection> getConditions(); + + /** + * @return never {@literal null}. + */ + IndexValueTransformer valueTransformer(); + + /** + * @return never {@literal null}. + */ + String getIndexName(); + + /** + * @author Christoph Strobl + * @since 1.7 + * @param + */ + public static interface Condition { + boolean matches(T value, IndexingContext context); + } + + /** + * Context in which a particular value is about to get indexed. + * + * @author Christoph Strobl + * @since 1.7 + */ + public class IndexingContext { + + private final String keyspace; + private final String path; + private final TypeInformation typeInformation; + + public IndexingContext(String keyspace, String path, TypeInformation typeInformation) { + + this.keyspace = keyspace; + this.path = path; + this.typeInformation = typeInformation; + } + + public String getKeyspace() { + return keyspace; + } + + public String getPath() { + return path; + } + + public TypeInformation getTypeInformation() { + return typeInformation; + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java new file mode 100644 index 000000000..366386beb --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java @@ -0,0 +1,62 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +import java.io.Serializable; +import java.util.Set; + +/** + * {@link IndexDefinitionProvider} give access to {@link IndexDefinition}s for creating secondary index structures. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexDefinitionProvider { + + /** + * Gets all of the {@link RedisIndexSetting} for a given keyspace. + * + * @param keyspace the keyspace to get + * @return never {@literal null} + */ + boolean hasIndexFor(Serializable keyspace); + + /** + * Checks if an index is defined for a given keyspace and property path. + * + * @param keyspace + * @param path + * @return true if index is defined. + */ + boolean hasIndexFor(Serializable keyspace, String path); + + /** + * Get the list of {@link IndexDefinition} for a given keyspace. + * + * @param keyspace + * @return never {@literal null}. + */ + Set getIndexDefinitionsFor(Serializable keyspace); + + /** + * Get the list of {@link IndexDefinition} for a given keyspace and property path. + * + * @param keyspace + * @param path + * @return never {@literal null}. + */ + Set getIndexDefinitionsFor(Serializable keyspace, String path); +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java new file mode 100644 index 000000000..6eba2274a --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +/** + * Registry that allows adding {@link IndexDefinition}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexDefinitionRegistry { + + /** + * Add given {@link RedisIndexSetting}. + * + * @param indexDefinition must not be {@literal null}. + */ + void addIndexDefinition(IndexDefinition indexDefinition); +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java b/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java new file mode 100644 index 000000000..748e72ff3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +import org.springframework.core.convert.converter.Converter; + +/** + * {@link Converter} implementation that is used to transform values for usage in a particular secondary index. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface IndexValueTransformer extends Converter { + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/Indexed.java b/src/main/java/org/springframework/data/redis/core/index/Indexed.java index 0bfa454f1..563f21bf6 100644 --- a/src/main/java/org/springframework/data/redis/core/index/Indexed.java +++ b/src/main/java/org/springframework/data/redis/core/index/Indexed.java @@ -22,7 +22,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Mark property to be included in a secondary index. + * Mark properties value to be included in a secondary index.
+ * Uses Redos {@literal SET} for storage.
+ * The value will be part of the key built for the index. * * @author Christoph Strobl * @since 1.7 @@ -32,11 +34,4 @@ import java.lang.annotation.Target; @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) public @interface Indexed { - /** - * Type of index to use. - * - * @return - */ - IndexType type() default IndexType.SIMPLE; - } diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexType.java b/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java similarity index 73% rename from src/main/java/org/springframework/data/redis/core/index/IndexType.java rename to src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java index 5e35bdd99..1411a9e34 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexType.java +++ b/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2016 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. @@ -16,15 +16,16 @@ package org.springframework.data.redis.core.index; /** - * {@link IndexType} indicates structure of index. + * {@link IndexDefinition} that is based on a property paths. * * @author Christoph Strobl * @since 1.7 */ -public enum IndexType { +public interface PathBasedRedisIndexDefinition extends IndexDefinition { /** - * Simple indicates usage of Redis Set for persisting data. + * @return can be {@literal null}. */ - SIMPLE + String getPath(); + } diff --git a/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java new file mode 100644 index 000000000..96a628f4c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java @@ -0,0 +1,263 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base {@link IndexDefinition} implementation. + * + * @author Christoph Strobl + * @since 1.7 + */ +public abstract class RedisIndexDefinition implements IndexDefinition { + + private final String keyspace; + private final String indexName; + private final String path; + private List> conditions; + private IndexValueTransformer valueTransformer; + + /** + * Creates new {@link RedisIndexDefinition}. + * + * @param keyspace + * @param path + * @param indexName + */ + protected RedisIndexDefinition(String keyspace, String path, String indexName) { + + this.keyspace = keyspace; + this.indexName = indexName; + this.path = path; + this.conditions = new ArrayList>(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#getKeyspace() + */ + @Override + public String getKeyspace() { + return keyspace; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#getConditions() + */ + @Override + public Collection> getConditions() { + return Collections.unmodifiableCollection(conditions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#valueTransformer() + */ + @Override + public IndexValueTransformer valueTransformer() { + return valueTransformer != null ? valueTransformer : NoOpValueTransformer.INSTANCE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition#getIndexName() + */ + @Override + public String getIndexName() { + return indexName; + } + + public String getPath() { + return this.path; + } + + protected void addCondition(Condition condition) { + + Assert.notNull(condition, "Condition must not be null!"); + this.conditions.add(condition); + } + + public void setValueTransformer(IndexValueTransformer valueTransformer) { + this.valueTransformer = valueTransformer; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(indexName); + return result + ObjectUtils.nullSafeHashCode(keyspace); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof RedisIndexDefinition)) { + return false; + } + RedisIndexDefinition that = (RedisIndexDefinition) obj; + + if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) { + return false; + } + + return ObjectUtils.nullSafeEquals(this.indexName, that.indexName); + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static enum NoOpValueTransformer implements IndexValueTransformer { + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + return source; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static enum LowercaseIndexValueTransformer implements IndexValueTransformer { + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (!(source instanceof String)) { + return source; + } + + return ((String) source).toLowerCase(); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static class CompositeValueTransformer implements IndexValueTransformer { + + private final List transformers = new ArrayList(); + + public CompositeValueTransformer(Collection transformers) { + this.transformers.addAll(transformers); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + Object tmp = source; + for (IndexValueTransformer transformer : transformers) { + tmp = transformer.convert(tmp); + } + return tmp; + } + + } + + /** + * @author Christoph Strobl + * @since 1.7 + * @param + */ + public static class OrCondition implements Condition { + + private final List> conditions = new ArrayList>(); + + public OrCondition(Collection> conditions) { + this.conditions.addAll(conditions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition.Condition#matches(java.lang.Object, org.springframework.data.redis.core.index.IndexDefinition.IndexingContext) + */ + @Override + public boolean matches(T value, IndexingContext context) { + + for (Condition condition : conditions) { + if (condition.matches(value, context)) { + return true; + } + } + return false; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static class PathCondition implements Condition { + + private final String path; + + public PathCondition(String path) { + this.path = path; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.IndexDefinition.Condition#matches(java.lang.Object, org.springframework.data.redis.core.index.IndexDefinition.IndexingContext) + */ + @Override + public boolean matches(Object value, IndexingContext context) { + + if (!StringUtils.hasText(path)) { + return true; + } + + return ObjectUtils.nullSafeEquals(context.getPath(), path); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java new file mode 100644 index 000000000..31c3f7be4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +/** + * {@link PathBasedRedisIndexDefinition} for including property values in a secondary index.
+ * Uses Redis {@literal SET} for storage.
+ * + * @author Christoph Strobl + * @since 1.7 + */ +public class SimpleIndexDefinition extends RedisIndexDefinition implements PathBasedRedisIndexDefinition { + + /** + * Creates new {@link SimpleIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param path + */ + public SimpleIndexDefinition(String keyspace, String path) { + this(keyspace, path, path); + } + + /** + * Creates new {@link SimpleIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param path + * @param name must not be {@literal null}. + */ + public SimpleIndexDefinition(String keyspace, String path, String name) { + super(keyspace, path, name); + addCondition(new PathCondition(path)); + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java new file mode 100644 index 000000000..8eaadeff6 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.index; + +import org.springframework.data.redis.core.convert.SpelIndexResolver; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.util.ObjectUtils; + +/** + * {@link SpelIndexDefinition} defines index that is evaluated based on a {@link SpelExpression} requires the + * {@link SpelIndexResolver} to be evaluated. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class SpelIndexDefinition extends RedisIndexDefinition { + + private final String expression; + + /** + * Creates new {@link SpelIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param expression must not be {@literal null}. + * @param indexName must not be {@literal null}. + */ + public SpelIndexDefinition(String keyspace, String expression, String indexName) { + super(keyspace, null, indexName); + this.expression = expression; + } + + /** + * Get the raw expression. + * + * @return + */ + public String getExpression() { + return expression; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.RedisIndexDefinition#hashCode() + */ + @Override + public int hashCode() { + int result = super.hashCode(); + result += ObjectUtils.nullSafeHashCode(expression); + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.index.RedisIndexDefinition#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof SpelIndexDefinition)) { + return false; + } + SpelIndexDefinition that = (SpelIndexDefinition) obj; + return ObjectUtils.nullSafeEquals(this.expression, that.expression); + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java index 1508b16f2..b2579f32b 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java @@ -15,8 +15,11 @@ */ package org.springframework.data.redis.core.mapping; +import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEntity; import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.model.MappingException; import org.springframework.data.redis.core.TimeToLiveAccessor; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -27,7 +30,8 @@ import org.springframework.util.Assert; * @author Christoph Strobl * @param */ -public class BasicRedisPersistentEntity extends BasicKeyValuePersistentEntity implements RedisPersistentEntity { +public class BasicRedisPersistentEntity extends BasicKeyValuePersistentEntity + implements RedisPersistentEntity { private TimeToLiveAccessor timeToLiveAccessor; @@ -55,4 +59,47 @@ public class BasicRedisPersistentEntity extends BasicKeyValuePersistentEntity return this.timeToLiveAccessor; } + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.BasicPersistentEntity#returnPropertyIfBetterIdPropertyCandidateOrNull(org.springframework.data.mapping.PersistentProperty) + */ + @Override + protected KeyValuePersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull( + KeyValuePersistentProperty property) { + + Assert.notNull(property); + + if (!property.isIdProperty()) { + return null; + } + + KeyValuePersistentProperty currentIdProperty = getIdProperty(); + boolean currentIdPropertyIsSet = currentIdProperty != null; + + if (!currentIdPropertyIsSet) { + return property; + } + + boolean currentIdPropertyIsExplicit = currentIdProperty.isAnnotationPresent(Id.class); + boolean newIdPropertyIsExplicit = property.isAnnotationPresent(Id.class); + + if (currentIdPropertyIsExplicit && newIdPropertyIsExplicit) { + throw new MappingException(String.format( + "Attempt to add explicit id property %s but already have an property %s registered " + + "as explicit id. Check your mapping configuration!", + property.getField(), currentIdProperty.getField())); + } + + if (!currentIdPropertyIsExplicit && !newIdPropertyIsExplicit) { + throw new MappingException( + String.format("Attempt to add id property %s but already have an property %s registered " + + "as id. Check your mapping configuration!", property.getField(), currentIdProperty.getField())); + } + + if (newIdPropertyIsExplicit) { + return property; + } + + return null; + } } diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java index ec032fcff..f856053ca 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -17,6 +17,8 @@ package org.springframework.data.redis.core.mapping; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; import org.springframework.data.mapping.PersistentEntity; @@ -31,6 +33,12 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; */ public class RedisPersistentProperty extends KeyValuePersistentProperty { + private static final Set SUPPORTED_ID_PROPERTY_NAMES = new HashSet(); + + static { + SUPPORTED_ID_PROPERTY_NAMES.add("id"); + } + /** * Creates new {@link RedisPersistentProperty}. * @@ -44,4 +52,17 @@ public class RedisPersistentProperty extends KeyValuePersistentProperty { super(field, propertyDescriptor, owner, simpleTypeHolder); } + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.AnnotationBasedPersistentProperty#isIdProperty() + */ + @Override + public boolean isIdProperty() { + + if (super.isIdProperty()) { + return true; + } + + return SUPPORTED_ID_PROPERTY_NAMES.contains(getName()); + } } diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java index fd673462d..fd34a64f1 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java @@ -15,6 +15,10 @@ */ package org.springframework.data.redis.repository.configuration; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; @@ -26,6 +30,7 @@ import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; +import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.RedisKeyValueAdapter; import org.springframework.data.redis.core.RedisKeyValueTemplate; import org.springframework.data.redis.core.convert.CustomConversions; @@ -79,6 +84,8 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon @Override public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) { + String redisTemplateRef = configurationSource.getAttribute("redisTemplateRef"); + RootBeanDefinition mappingContextDefinition = createRedisMappingContext(configurationSource); mappingContextDefinition.setSource(configurationSource.getSource()); @@ -86,10 +93,11 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon // register coustom conversions RootBeanDefinition customConversions = new RootBeanDefinition(CustomConversions.class); - registerIfNotAlreadyRegistered(customConversions, registry, REDIS_CUSTOM_CONVERSIONS_BEAN_NAME, configurationSource); + registerIfNotAlreadyRegistered(customConversions, registry, REDIS_CUSTOM_CONVERSIONS_BEAN_NAME, + configurationSource); // Register referenceResolver - RootBeanDefinition redisReferenceResolver = createRedisReferenceResolverDefinition(); + RootBeanDefinition redisReferenceResolver = createRedisReferenceResolverDefinition(redisTemplateRef); redisReferenceResolver.setSource(configurationSource.getSource()); registerIfNotAlreadyRegistered(redisReferenceResolver, registry, REDIS_REFERENCE_RESOLVER_BEAN_NAME, configurationSource); @@ -104,16 +112,14 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon RootBeanDefinition redisKeyValueAdapterDefinition = new RootBeanDefinition(RedisKeyValueAdapter.class); ConstructorArgumentValues constructorArgumentValuesForRedisKeyValueAdapter = new ConstructorArgumentValues(); - - String redisTemplateRef = configurationSource.getAttribute("redisTemplateRef"); if (StringUtils.hasText(redisTemplateRef)) { - constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(0, new RuntimeBeanReference( - redisTemplateRef)); + constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(0, + new RuntimeBeanReference(redisTemplateRef)); } - constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(1, new RuntimeBeanReference( - REDIS_CONVERTER_BEAN_NAME)); + constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(1, + new RuntimeBeanReference(REDIS_CONVERTER_BEAN_NAME)); redisKeyValueAdapterDefinition.setConstructorArgumentValues(constructorArgumentValuesForRedisKeyValueAdapter); registerIfNotAlreadyRegistered(redisKeyValueAdapterDefinition, registry, REDIS_ADAPTER_BEAN_NAME, @@ -122,14 +128,15 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon super.registerBeansForRoot(registry, configurationSource); } - private RootBeanDefinition createRedisReferenceResolverDefinition() { + private RootBeanDefinition createRedisReferenceResolverDefinition(String redisTemplateRef) { RootBeanDefinition beanDef = new RootBeanDefinition(); - beanDef.setBeanClassName("org.springframework.data.redis.core.RedisKeyValueAdapter.ReferenceResolverImpl"); + beanDef.setBeanClassName("org.springframework.data.redis.core.convert.ReferenceResolverImpl"); - MutablePropertyValues props = new MutablePropertyValues(); - props.add("adapter", new RuntimeBeanReference(REDIS_ADAPTER_BEAN_NAME)); - beanDef.setPropertyValues(props); + ConstructorArgumentValues constructorArgs = new ConstructorArgumentValues(); + constructorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference(redisTemplateRef)); + + beanDef.setConstructorArgumentValues(constructorArgs); return beanDef; } @@ -178,10 +185,10 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition(RedisKeyValueTemplate.class); ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues(); - constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(0, new RuntimeBeanReference( - REDIS_ADAPTER_BEAN_NAME)); - constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(1, new RuntimeBeanReference( - MAPPING_CONTEXT_BEAN_NAME)); + constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(0, + new RuntimeBeanReference(REDIS_ADAPTER_BEAN_NAME)); + constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(1, + new RuntimeBeanReference(MAPPING_CONTEXT_BEAN_NAME)); keyValueTemplateDefinition.setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate); @@ -205,4 +212,13 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon return beanDef; } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingAnnotations() + */ + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.> singleton(RedisHash.class); + } + } diff --git a/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java b/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java new file mode 100644 index 000000000..74f538e67 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 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.core; + +import java.io.Serializable; + +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.repository.core.support.PersistentEntityInformation; + +/** + * {@link RedisEntityInformation} implementation using a {@link MongoPersistentEntity} instance to lookup the necessary + * information. Can be configured with a custom collection to be returned which will trump the one returned by the + * {@link MongoPersistentEntity} if given. + * + * @author Christoph Strobl + * @param + * @param + */ +public class MappingRedisEntityInformation + extends PersistentEntityInformation implements RedisEntityInformation { + + private final RedisPersistentEntity entityMetadata; + + /** + * @param entity + */ + public MappingRedisEntityInformation(RedisPersistentEntity entity) { + super(entity); + + this.entityMetadata = entity; + + if (!entityMetadata.hasIdProperty()) { + + throw new MappingException( + String.format("Entity %s requires to have an explicit id field. Did you forget to provide one using @Id?", + entity.getName())); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java b/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java new file mode 100644 index 000000000..804e1d4c9 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 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.core; + +import java.io.Serializable; + +import org.springframework.data.repository.core.EntityInformation; + +/** + * @author Christoph Strobl + * @param + * @param + */ +public interface RedisEntityInformation extends EntityInformation { + +} diff --git a/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java b/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java index 4c4ecca97..f272052bc 100644 --- a/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java @@ -16,9 +16,12 @@ package org.springframework.data.redis.repository.query; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import org.springframework.util.ObjectUtils; + /** * Simple set of operations requried to run queries against Redis. * @@ -27,27 +30,102 @@ import java.util.Set; */ public class RedisOperationChain { - Set sismember = new LinkedHashSet(); - Set orSismember = new LinkedHashSet(); + private Set sismember = new LinkedHashSet(); + private Set orSismember = new LinkedHashSet(); - public void sismember(Object next) { - sismember.add(next); + public void sismember(String path, Object value) { + sismember(new PathAndValue(path, value)); } - public Set getSismember() { + public void sismember(PathAndValue pathAndValue) { + sismember.add(pathAndValue); + } + + public Set getSismember() { return sismember; } - public void orSismember(Object next) { - orSismember.add(next); + public void orSismember(String path, Object value) { + orSismember(new PathAndValue(path, value)); } - public void orSismember(Collection next) { + public void orSismember(PathAndValue pathAndValue) { + orSismember.add(pathAndValue); + } + + public void orSismember(Collection next) { orSismember.addAll(next); } - public Set getOrSismember() { + public Set getOrSismember() { return orSismember; } + public static class PathAndValue { + + private final String path; + private final Collection values; + + public PathAndValue(String path, Object singleValue) { + + this.path = path; + this.values = Collections.singleton(singleValue); + } + + public PathAndValue(String path, Collection values) { + + this.path = path; + this.values = values != null ? values : Collections.emptySet(); + } + + public boolean isSingleValue() { + return values.size() == 1; + } + + public String getPath() { + return path; + } + + public Collection values() { + return values; + } + + public Object getFirstValue() { + return values.isEmpty() ? null : values.iterator().next(); + } + + @Override + public String toString() { + return path + ":" + (isSingleValue() ? getFirstValue() : values); + } + + @Override + public int hashCode() { + + int result = ObjectUtils.nullSafeHashCode(path); + result += ObjectUtils.nullSafeHashCode(values); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof PathAndValue)) { + return false; + } + PathAndValue that = (PathAndValue) obj; + if (!ObjectUtils.nullSafeEquals(this.path, that.path)) { + return false; + } + + return ObjectUtils.nullSafeEquals(this.values, that.values); + } + + } + } diff --git a/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java b/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java index 7c12fdb03..b051dc316 100644 --- a/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java @@ -49,7 +49,7 @@ public class RedisQueryCreator extends AbstractQueryCreator query = new KeyValueQuery(criteria); + if (query.getCritieria().getSismember().size() == 1 && query.getCritieria().getOrSismember().size() == 1) { + + query.getCritieria().getOrSismember().add(query.getCritieria().getSismember().iterator().next()); + query.getCritieria().getSismember().clear(); + } + if (sort != null) { query.setSort(sort); } diff --git a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java index 66c4b81a5..3760c8b4d 100644 --- a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.repository.support; +import java.io.Serializable; import java.lang.reflect.Method; import org.springframework.data.keyvalue.core.KeyValueOperations; @@ -22,6 +23,9 @@ import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery.QueryInitialization; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.redis.repository.core.MappingRedisEntityInformation; +import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -69,6 +73,21 @@ public class RedisRepositoryFactory extends KeyValueRepositoryFactory { return new RedisQueryLookupStrategy(key, evaluationContextProvider, getKeyValueOperations(), getQueryCreator()); } + /* + * (non-Javadoc) + * @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory#getEntityInformation(java.lang.Class) + */ + @Override + @SuppressWarnings("unchecked") + public EntityInformation getEntityInformation(Class domainClass) { + + RedisPersistentEntity entity = (RedisPersistentEntity) getMappingContext().getPersistentEntity(domainClass); + EntityInformation entityInformation = (EntityInformation) new MappingRedisEntityInformation( + entity); + + return entityInformation; + } + /** * @author Christoph Strobl * @since 1.7 diff --git a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java index e867c831d..b9275fd1c 100644 --- a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -18,9 +18,10 @@ package org.springframework.data.redis.repository.support; import java.io.Serializable; import org.springframework.beans.factory.FactoryBean; +import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; /** * Adapter for Springs {@link FactoryBean} interface to allow easy setup of {@link RedisRepositoryFactory} via Spring @@ -37,10 +38,11 @@ public class RedisRepositoryFactoryBean, S, ID exten /* * (non-Javadoc) - * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createRepositoryFactory() + * @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean#createRepositoryFactory(org.springframework.data.keyvalue.core.KeyValueOperations, java.lang.Class) */ @Override - protected RepositoryFactorySupport createRepositoryFactory() { - return new RedisRepositoryFactory(getOperations(), getQueryCreator()); + protected RedisRepositoryFactory createRepositoryFactory(KeyValueOperations operations, + Class> queryCreator) { + return new RedisRepositoryFactory(operations, queryCreator); } } diff --git a/src/main/java/org/springframework/data/redis/util/ByteUtils.java b/src/main/java/org/springframework/data/redis/util/ByteUtils.java index 437320a23..1c9235dc2 100644 --- a/src/main/java/org/springframework/data/redis/util/ByteUtils.java +++ b/src/main/java/org/springframework/data/redis/util/ByteUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -19,10 +19,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.springframework.util.Assert; + /** * Some handy methods for dealing with byte arrays. - * + * * @author Christoph Strobl + * @author Mark Paluch * @since 1.7 */ public final class ByteUtils { @@ -76,4 +79,23 @@ public final class ByteUtils { } return bytes.toArray(new byte[bytes.size()][]); } + + /** + * Merge multiple {@code byte} arrays into one array + * + * @param firstArray must not be {@literal null} + * @param additionalArrays must not be {@literal null} + * @return + */ + public static byte[][] mergeArrays(byte[] firstArray, byte[]... additionalArrays) { + + Assert.notNull(firstArray, "first array must not be null"); + Assert.notNull(additionalArrays, "additional arrays must not be null"); + + byte[][] result = new byte[additionalArrays.length + 1][]; + result[0] = firstArray; + System.arraycopy(additionalArrays, 0, result, 1, additionalArrays.length); + + return result; + } } diff --git a/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java b/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java index 5a7ad9faf..ba512f9a1 100644 --- a/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java @@ -30,13 +30,17 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.dao.InvalidDataAccessApiUsageException; 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.PathIndexResolver; import org.springframework.data.redis.core.convert.ReferenceResolver; import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue; import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl @@ -50,6 +54,7 @@ public class IndexWriterUnitTests { private static final String KEY = "key-1"; private static final byte[] KEY_BIN = KEY.getBytes(CHARSET); IndexWriter writer; + MappingRedisConverter converter; @Mock RedisConnection connectionMock; @Mock ReferenceResolver referenceResolverMock; @@ -57,8 +62,7 @@ public class IndexWriterUnitTests { @Before public void setUp() { - MappingRedisConverter converter = new MappingRedisConverter(new RedisMappingContext(), new IndexResolverImpl(), - referenceResolverMock); + converter = new MappingRedisConverter(new RedisMappingContext(), new PathIndexResolver(), referenceResolverMock); converter.afterPropertiesSet(); writer = new IndexWriter(connectionMock, converter); @@ -143,6 +147,37 @@ public class IndexWriterUnitTests { assertThat(captor.getAllValues(), hasItems(indexKey1, indexKey2)); } + /** + * @see DATAREDIS-425 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void addToIndexShouldThrowDataAccessExceptionWhenAddingDataThatConnotBeConverted() { + writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", new DummyObject())); + + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void addToIndexShouldUseRegisteredConverterWhenAddingData() { + + DummyObject value = new DummyObject(); + final String identityHexString = ObjectUtils.getIdentityHexString(value); + + ((GenericConversionService) converter.getConversionService()).addConverter(new Converter() { + + @Override + public byte[] convert(DummyObject source) { + return identityHexString.getBytes(CHARSET); + } + }); + + writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", value)); + + verify(connectionMock).sAdd(eq(("persons:firstname:" + identityHexString).getBytes(CHARSET)), eq(KEY_BIN)); + } + static class StubIndxedData implements IndexedData { @Override @@ -151,9 +186,12 @@ public class IndexWriterUnitTests { } @Override - public String getKeySpace() { + public String getKeyspace() { return KEYSPACE; } + } + + static class DummyObject { } } diff --git a/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java new file mode 100644 index 000000000..acea3e774 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 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.IsEqual.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class CompositeIndexResolverUnitTests { + + @Mock IndexResolver resolver1; + @Mock IndexResolver resolver2; + @Mock TypeInformation typeInfoMock; + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectNull() { + new CompositeIndexResolver(null); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectCollectionWithNullValues() { + new CompositeIndexResolver(Arrays.asList(resolver1, null, resolver2)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldCollectionIndexesFromResolvers() { + + when(resolver1.resolveIndexesFor(any(TypeInformation.class), anyObject())).thenReturn( + Collections. singleton(new SimpleIndexedPropertyValue("spring", "data", "redis"))); + when(resolver2.resolveIndexesFor(any(TypeInformation.class), anyObject())).thenReturn( + Collections. singleton(new SimpleIndexedPropertyValue("redis", "data", "spring"))); + + CompositeIndexResolver resolver = new CompositeIndexResolver(Arrays.asList(resolver1, resolver2)); + + assertThat(resolver.resolveIndexesFor(typeInfoMock, "o.O").size(), equalTo(2)); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java b/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java index 95915f2ff..82b6357f7 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java +++ b/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java @@ -26,17 +26,19 @@ import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.TimeToLive; import org.springframework.data.redis.core.index.Indexed; +import lombok.EqualsAndHashCode; + /** * @author Christoph Strobl */ -class ConversionTestEntities { +public 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 { + public static class Person { @Id String id; String firstname; @@ -59,80 +61,82 @@ class ConversionTestEntities { Species species; } - static class PersonWithAddressReference extends Person { + public static class PersonWithAddressReference extends Person { @Reference AddressWithId addressRef; } - static class Address { + public static class Address { String city; @Indexed String country; } - static class AddressWithId extends Address { + public static class AddressWithId extends Address { @Id String id; } - static enum Gender { + public static enum Gender { MALE, FEMALE } - static class AddressWithPostcode extends Address { + public static class AddressWithPostcode extends Address { String postcode; } - static class TaVeren extends Person { + public static class TaVeren extends Person { Object feature; Map characteristics; List items; } + @EqualsAndHashCode @RedisHash(KEYSPACE_LOCATION) - static class Location { + public static class Location { @Id String id; String name; Address address; + } @RedisHash(timeToLive = 5) - static class ExpiringPerson { + public static class ExpiringPerson { @Id String id; String name; } - static class ExipringPersonWithExplicitProperty extends ExpiringPerson { + public static class ExipringPersonWithExplicitProperty extends ExpiringPerson { @TimeToLive(unit = TimeUnit.MINUTES) Long ttl; } - static class Species { + public static class Species { String name; List alsoKnownAs; } @RedisHash(KEYSPACE_TWOT) - static class TheWheelOfTime { + public static class TheWheelOfTime { List mainCharacters; List species; Map places; } - static class Item { + public static class Item { @Indexed String type; String description; Size size; } - static class Size { + public static class Size { int width; int height; diff --git a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java index a0a44fa1e..6eb1c2c0a 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java @@ -629,7 +629,12 @@ public class MappingRedisConverterUnitTests { location.id = "1"; location.name = "tar valon"; - when(resolverMock. resolveReference(eq("1"), eq("locations"), eq(Location.class))).thenReturn(location); + Map locationMap = new LinkedHashMap(); + locationMap.put("id", location.id); + locationMap.put("name", location.name); + + when(resolverMock.resolveReference(eq("1"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(locationMap).rawMap()); Map map = new LinkedHashMap(); map.put("location", "locations:1"); @@ -671,7 +676,12 @@ public class MappingRedisConverterUnitTests { location.id = "1"; location.name = "tar valon"; - when(resolverMock. resolveReference(eq("1"), eq("locations"), eq(Location.class))).thenReturn(location); + Map locationMap = new LinkedHashMap(); + locationMap.put("id", location.id); + locationMap.put("name", location.name); + + when(resolverMock.resolveReference(eq("1"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(locationMap).rawMap()); Map map = new LinkedHashMap(); map.put("coworkers.[0].location", "locations:1"); @@ -726,9 +736,26 @@ public class MappingRedisConverterUnitTests { tear.id = "3"; tear.name = "city of tear"; - when(resolverMock. resolveReference(eq("1"), eq("locations"), eq(Location.class))).thenReturn(tarValon); - when(resolverMock. resolveReference(eq("2"), eq("locations"), eq(Location.class))).thenReturn(falme); - when(resolverMock. resolveReference(eq("3"), eq("locations"), eq(Location.class))).thenReturn(tear); + Map tarValonMap = new LinkedHashMap(); + tarValonMap.put("id", tarValon.id); + tarValonMap.put("name", tarValon.name); + + Map falmeMap = new LinkedHashMap(); + falmeMap.put("id", falme.id); + falmeMap.put("name", falme.name); + + Map tearMap = new LinkedHashMap(); + tearMap.put("id", tear.id); + tearMap.put("name", tear.name); + + Bucket.newBucketFromStringMap(tearMap).rawMap(); + + when(resolverMock.resolveReference(eq("1"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(tarValonMap).rawMap()); + when(resolverMock.resolveReference(eq("2"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(falmeMap).rawMap()); + when(resolverMock.resolveReference(eq("3"), eq("locations"))) + .thenReturn(Bucket.newBucketFromStringMap(tearMap).rawMap()); Map map = new LinkedHashMap(); map.put("visited.[0]", "locations:1"); diff --git a/src/test/java/org/springframework/data/redis/core/convert/IndexResolverImplUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java similarity index 75% rename from src/test/java/org/springframework/data/redis/core/convert/IndexResolverImplUnitTests.java rename to src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java index c37273170..40bb1a826 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/IndexResolverImplUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-216 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core.convert; +import static org.hamcrest.collection.IsEmptyCollection.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsCollectionContaining.*; import static org.hamcrest.core.IsNull.*; @@ -26,6 +27,8 @@ 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.List; +import java.util.Map; import java.util.Set; import org.hamcrest.core.IsCollectionContaining; @@ -41,12 +44,12 @@ 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.Size; 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.index.SimpleIndexDefinition; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.util.ClassTypeInformation; @@ -54,10 +57,10 @@ import org.springframework.data.util.ClassTypeInformation; * @author Christoph Strobl */ @RunWith(MockitoJUnitRunner.class) -public class IndexResolverImplUnitTests { +public class PathIndexResolverUnitTests { IndexConfiguration indexConfig; - IndexResolverImpl indexResolver; + PathIndexResolver indexResolver; @Mock PersistentProperty propertyMock; @@ -65,8 +68,8 @@ public class IndexResolverImplUnitTests { public void setUp() { indexConfig = new IndexConfiguration(); - this.indexResolver = new IndexResolverImpl(new RedisMappingContext(new MappingConfiguration(indexConfig, - new KeyspaceConfiguration()))); + this.indexResolver = new PathIndexResolver( + new RedisMappingContext(new MappingConfiguration(indexConfig, new KeyspaceConfiguration()))); } /** @@ -74,7 +77,7 @@ public class IndexResolverImplUnitTests { */ @Test(expected = IllegalArgumentException.class) public void shouldThrowExceptionOnNullMappingContext() { - new IndexResolverImpl(null); + new PathIndexResolver(null); } /** @@ -145,9 +148,10 @@ public class IndexResolverImplUnitTests { Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TheWheelOfTime.class), twot); assertThat(indexes.size(), is(2)); - assertThat(indexes, IsCollectionContaining. hasItems(new SimpleIndexedPropertyValue(KEYSPACE_TWOT, - "mainCharacters.address.country", "andor"), new SimpleIndexedPropertyValue(KEYSPACE_TWOT, - "mainCharacters.address.country", "saldaea"))); + assertThat(indexes, + IsCollectionContaining. hasItems( + new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "mainCharacters.address.country", "andor"), + new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "mainCharacters.address.country", "saldaea"))); } /** @@ -170,8 +174,8 @@ public class IndexResolverImplUnitTests { Set 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"))); + assertThat(indexes, + hasItem(new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "places.stone-of-tear.address.country", "illian"))); } /** @@ -180,7 +184,7 @@ public class IndexResolverImplUnitTests { @Test public void shouldResolveConfiguredIndexesInMapOfSimpleTypes() { - indexConfig.addIndexDefinition(new RedisIndexSetting(KEYSPACE_PERSON, "physicalAttributes.eye-color")); + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "physicalAttributes.eye-color")); Person rand = new Person(); rand.physicalAttributes = new LinkedHashMap(); @@ -199,7 +203,7 @@ public class IndexResolverImplUnitTests { @Test public void shouldResolveConfiguredIndexesInMapOfComplexTypes() { - indexConfig.addIndexDefinition(new RedisIndexSetting(KEYSPACE_PERSON, "relatives.father.firstname")); + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "relatives.father.firstname")); Person rand = new Person(); rand.relatives = new LinkedHashMap(); @@ -222,7 +226,7 @@ public class IndexResolverImplUnitTests { @Test public void shouldIgnoreConfiguredIndexesInMapWhenValueIsNull() { - indexConfig.addIndexDefinition(new RedisIndexSetting(KEYSPACE_PERSON, "physicalAttributes.eye-color")); + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "physicalAttributes.eye-color")); Person rand = new Person(); rand.physicalAttributes = new LinkedHashMap(); @@ -244,8 +248,8 @@ public class IndexResolverImplUnitTests { rand.addressRef.id = "emond_s_field"; rand.addressRef.country = "andor"; - Set indexes = indexResolver.resolveIndexesFor( - ClassTypeInformation.from(PersonWithAddressReference.class), rand); + Set indexes = indexResolver + .resolveIndexesFor(ClassTypeInformation.from(PersonWithAddressReference.class), rand); assertThat(indexes.size(), is(0)); } @@ -267,7 +271,7 @@ public class IndexResolverImplUnitTests { public void resolveIndexShouldReturnDataWhenIndexConfigured() { when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(false); - indexConfig.addIndexDefinition(new RedisIndexSetting(KEYSPACE_PERSON, "foo")); + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "foo")); assertThat(resolve("foo", "rand"), notNullValue()); } @@ -279,7 +283,7 @@ public class IndexResolverImplUnitTests { public void resolveIndexShouldReturnDataWhenNoIndexConfiguredButPropertyAnnotated() { when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); - when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance(IndexType.SIMPLE)); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); assertThat(resolve("foo", "rand"), notNullValue()); } @@ -292,7 +296,7 @@ public class IndexResolverImplUnitTests { when(propertyMock.isCollectionLike()).thenReturn(true); when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); - when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance(IndexType.SIMPLE)); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); IndexedData index = resolve("list.[0].name", "rand"); @@ -307,7 +311,7 @@ public class IndexResolverImplUnitTests { when(propertyMock.isMap()).thenReturn(true); when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); - when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance(IndexType.SIMPLE)); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); IndexedData index = resolve("map.[foo].name", "rand"); @@ -322,7 +326,7 @@ public class IndexResolverImplUnitTests { when(propertyMock.isMap()).thenReturn(true); when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true); - when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance(IndexType.SIMPLE)); + when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(createIndexedInstance()); IndexedData index = resolve("map.[0].name", "rand"); @@ -410,7 +414,8 @@ public class IndexResolverImplUnitTests { */ @Test public void resolveIndexAllowCustomIndexName() { - indexConfig.addIndexDefinition(new RedisIndexSetting(KEYSPACE_PERSON, "items.type", "itemsType", IndexType.SIMPLE)); + + indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "items.type", "itemsType")); Item hat = new Item(); hat.type = "hat"; @@ -426,11 +431,78 @@ public class IndexResolverImplUnitTests { assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "itemsType", "hat"))); } - private IndexedData resolve(String path, Object value) { - return indexResolver.resolveIndex(KEYSPACE_PERSON, path, propertyMock, value); + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexForTypeThatHasNoIndexDefined() { + + Size size = new Size(); + size.height = 10; + size.length = 20; + size.width = 30; + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Size.class), size); + assertThat(indexes, is(empty())); } - private Indexed createIndexedInstance(final IndexType type) { + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexOnMapField() { + + IndexedOnMapField source = new IndexedOnMapField(); + source.values = new LinkedHashMap(); + + source.values.put("jon", "snow"); + source.values.put("arya", "stark"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(IndexedOnMapField.class), + source); + + assertThat(indexes.size(), is(2)); + assertThat(indexes, + IsCollectionContaining. hasItems( + new SimpleIndexedPropertyValue(IndexedOnMapField.class.getName(), "values.jon", "snow"), + new SimpleIndexedPropertyValue(IndexedOnMapField.class.getName(), "values.arya", "stark"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolveIndexOnListField() { + + IndexedOnListField source = new IndexedOnListField(); + source.values = new ArrayList(); + + source.values.add("jon"); + source.values.add("arya"); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(IndexedOnListField.class), + source); + + assertThat(indexes.size(), is(2)); + assertThat(indexes, + IsCollectionContaining. hasItems( + new SimpleIndexedPropertyValue(IndexedOnListField.class.getName(), "values", "jon"), + new SimpleIndexedPropertyValue(IndexedOnListField.class.getName(), "values", "arya"))); + } + + private IndexedData resolve(String path, Object value) { + + Set data = indexResolver.resolveIndex(KEYSPACE_PERSON, path, propertyMock, value); + + if (data.isEmpty()) { + return null; + } + + assertThat(data.size(), is(1)); + return data.iterator().next(); + } + + private Indexed createIndexedInstance() { return new Indexed() { @@ -439,10 +511,17 @@ public class IndexResolverImplUnitTests { return Indexed.class; } - @Override - public IndexType type() { - return type == null ? IndexType.SIMPLE : type; - } }; } + + static class IndexedOnListField { + + @Indexed List values; + } + + static class IndexedOnMapField { + + @Indexed Map values; + } + } diff --git a/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java index 939105599..5ce91e385 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java @@ -27,8 +27,7 @@ import org.junit.Test; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; 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.SpelIndexDefinition; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.util.ClassTypeInformation; import org.springframework.expression.AccessException; @@ -175,7 +174,7 @@ public class SpelIndexResolverUnitTests { private SpelIndexResolver createWithExpression(String expression) { - RedisIndexSetting principalIndex = new RedisIndexSetting(keyspace, expression, indexName, IndexType.SIMPLE); + SpelIndexDefinition principalIndex = new SpelIndexDefinition(keyspace, expression, indexName); IndexConfiguration configuration = new IndexConfiguration(); configuration.addIndexDefinition(principalIndex); diff --git a/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java index 584b5d13b..7c873e8a4 100644 --- a/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java @@ -19,10 +19,10 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; -import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting; /** * @author Rob Winch + * @author Christoph Strobl */ public class IndexConfigurationUnitTests { @@ -33,7 +33,7 @@ public class IndexConfigurationUnitTests { public void redisIndexSettingIndexNameDefaulted() { String path = "path"; - RedisIndexSetting setting = new RedisIndexSetting("keyspace", path); + SimpleIndexDefinition setting = new SimpleIndexDefinition("keyspace", path); assertThat(setting.getIndexName(), equalTo(path)); } @@ -44,7 +44,7 @@ public class IndexConfigurationUnitTests { public void redisIndexSettingIndexNameExplicit() { String indexName = "indexName"; - RedisIndexSetting setting = new RedisIndexSetting("keyspace", "index", indexName, IndexType.SIMPLE); + SimpleIndexDefinition setting = new SimpleIndexDefinition("keyspace", "index", indexName); assertThat(setting.getIndexName(), equalTo(indexName)); } @@ -54,9 +54,9 @@ public class IndexConfigurationUnitTests { @Test public void redisIndexSettingIndexNameUsedInEquals() { - RedisIndexSetting setting1 = new RedisIndexSetting("keyspace", "path", "indexName1", IndexType.SIMPLE); - RedisIndexSetting setting2 = new RedisIndexSetting(setting1.getKeyspace(), setting1.getPath(), - setting1.getIndexName() + "other", setting1.getType()); + SimpleIndexDefinition setting1 = new SimpleIndexDefinition("keyspace", "path", "indexName1"); + SimpleIndexDefinition setting2 = new SimpleIndexDefinition(setting1.getKeyspace(), "path", setting1.getIndexName() + + "other"); assertThat(setting1, not(equalTo(setting2))); } @@ -67,9 +67,9 @@ public class IndexConfigurationUnitTests { @Test public void redisIndexSettingIndexNameUsedInHashCode() { - RedisIndexSetting setting1 = new RedisIndexSetting("keyspace", "path", "indexName1", IndexType.SIMPLE); - RedisIndexSetting setting2 = new RedisIndexSetting(setting1.getKeyspace(), setting1.getPath(), - setting1.getIndexName() + "other", setting1.getType()); + SimpleIndexDefinition setting1 = new SimpleIndexDefinition("keyspace", "path", "indexName1"); + SimpleIndexDefinition setting2 = new SimpleIndexDefinition(setting1.getKeyspace(), "path", setting1.getIndexName() + + "other"); assertThat(setting1.hashCode(), not(equalTo(setting2.hashCode()))); } diff --git a/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java new file mode 100644 index 000000000..5fa8fb6a6 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2016 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.IsEqual.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.TimeToLiveAccessor; +import org.springframework.data.redis.core.convert.ConversionTestEntities; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + * @param + * @param + */ +@RunWith(MockitoJUnitRunner.class) +public class BasicRedisPersistentEntityUnitTests { + + public @Rule ExpectedException expectedException = ExpectedException.none(); + + @Mock TypeInformation entityInformation; + @Mock KeySpaceResolver keySpaceResolver; + @Mock TimeToLiveAccessor ttlAccessor; + + BasicRedisPersistentEntity entity; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + + when(entityInformation.getType()).thenReturn((Class) ConversionTestEntities.Person.class); + entity = new BasicRedisPersistentEntity(entityInformation, keySpaceResolver, ttlAccessor); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void addingMultipleIdPropertiesWithoutAnExplicitOneThrowsException() { + + expectedException.expect(MappingException.class); + expectedException.expectMessage("Attempt to add id property"); + expectedException.expectMessage("but already have an property"); + + KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class); + when(property1.isIdProperty()).thenReturn(true); + + KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class); + when(property2.isIdProperty()).thenReturn(true); + + entity.addPersistentProperty(property1); + entity.addPersistentProperty(property2); + } + + /** + * @see DATAREDIS-425 + */ + @Test + @SuppressWarnings("unchecked") + public void addingMultipleExplicitIdPropertiesThrowsException() { + + expectedException.expect(MappingException.class); + expectedException.expectMessage("Attempt to add explicit id property"); + expectedException.expectMessage("but already have an property"); + + KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class); + when(property1.isIdProperty()).thenReturn(true); + when(property1.isAnnotationPresent(any(Class.class))).thenReturn(true); + + KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class); + when(property2.isIdProperty()).thenReturn(true); + when(property2.isAnnotationPresent(any(Class.class))).thenReturn(true); + + entity.addPersistentProperty(property1); + entity.addPersistentProperty(property2); + } + + /** + * @see DATAREDIS-425 + */ + @Test + @SuppressWarnings("unchecked") + public void explicitIdPropertiyShouldBeFavoredOverNonExplicit() { + + KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class); + when(property1.isIdProperty()).thenReturn(true); + + KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class); + when(property2.isIdProperty()).thenReturn(true); + when(property2.isAnnotationPresent(any(Class.class))).thenReturn(true); + + entity.addPersistentProperty(property1); + entity.addPersistentProperty(property2); + + assertThat(entity.getIdProperty(), is(equalTo(property2))); + } +} diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java index 1dc7120d5..4d3139788 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.repository; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsCollectionContaining.*; import static org.junit.Assert.*; @@ -30,16 +32,23 @@ 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.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Reference; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; 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.IndexDefinition; import org.springframework.data.redis.core.index.Indexed; +import org.springframework.data.redis.core.index.SimpleIndexDefinition; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.repository.CrudRepository; import org.springframework.test.context.ContextConfiguration; @@ -54,7 +63,8 @@ public class RedisRepositoryIntegrationTests { @Configuration @EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class, - keyspaceConfiguration = MyKeyspaceConfiguration.class) + keyspaceConfiguration = MyKeyspaceConfiguration.class, + includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") }) static class Config { @Bean @@ -161,7 +171,50 @@ public class RedisRepositoryIntegrationTests { // find and assert the location is gone Person reLoaded = repo.findOne(moiraine.getId()); assertThat(reLoaded.city, IsNull.nullValue()); + } + /** + * @see DATAREDIS-425 + */ + @Test + public void findReturnsPageCorrectly() { + + Person eddard = new Person("eddard", "stark"); + Person robb = new Person("robb", "stark"); + Person sansa = new Person("sansa", "stark"); + Person arya = new Person("arya", "stark"); + Person bran = new Person("bran", "stark"); + Person rickon = new Person("rickon", "stark"); + + repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon)); + + Page page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5)); + + assertThat(page1.getNumberOfElements(), is(5)); + assertThat(page1.getTotalElements(), is(6L)); + + Page page2 = repo.findPersonByLastname("stark", page1.nextPageable()); + + assertThat(page2.getNumberOfElements(), is(1)); + assertThat(page2.getTotalElements(), is(6L)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findUsingOrReturnsResultCorrectly() { + + Person eddard = new Person("eddard", "stark"); + Person robb = new Person("robb", "stark"); + Person jon = new Person("jon", "snow"); + + repo.save(Arrays.asList(eddard, robb, jon)); + + List eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow"); + + assertThat(eddardAndJon, hasSize(2)); + assertThat(eddardAndJon, containsInAnyOrder(eddard, jon)); } public static interface PersonRepository extends CrudRepository { @@ -170,7 +223,11 @@ public class RedisRepositoryIntegrationTests { List findByLastname(String lastname); + Page findPersonByLastname(String lastname, Pageable page); + List findByFirstnameAndLastname(String firstname, String lastname); + + List findByFirstnameOrLastname(String firstname, String lastname); } /** @@ -181,8 +238,8 @@ public class RedisRepositoryIntegrationTests { static class MyIndexConfiguration extends IndexConfiguration { @Override - protected Iterable initialConfiguration() { - return Collections.singleton(new RedisIndexSetting("persons", "lastname")); + protected Iterable initialConfiguration() { + return Collections. singleton(new SimpleIndexDefinition("persons", "lastname")); } } @@ -208,6 +265,14 @@ public class RedisRepositoryIntegrationTests { String lastname; @Reference City city; + public Person() {} + + public Person(String firstname, String lastname) { + + this.firstname = firstname; + this.lastname = lastname; + } + public City getCity() { return city; } diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java new file mode 100644 index 000000000..4c81841a8 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.repository.configuration; + +import static org.junit.Assert.*; + +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.data.annotation.Id; +import org.springframework.data.keyvalue.repository.KeyValueRepository; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfiguration; +import org.springframework.data.repository.config.RepositoryConfigurationSource; + +/** + * @author Christoph Strobl + */ +public class RedisRepositoryConfigurationExtensionUnitTests { + + StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + ResourceLoader loader = new PathMatchingResourcePatternResolver(); + Environment environment = new StandardEnvironment(); + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableRedisRepositories.class, loader, environment); + + RedisRepositoryConfigurationExtension extension; + + @Before + public void setUp() { + extension = new RedisRepositoryConfigurationExtension(); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { + assertHasRepo(SampleRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() { + assertHasRepo(StoreRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() { + + assertDoesNotHaveRepo(UnannotatedRepository.class, + extension.getRepositoryConfigurations(configurationSource, loader, true)); + } + + private static void assertDoesNotHaveRepo(Class repositoryInterface, + Collection> configs) { + + try { + + assertHasRepo(repositoryInterface, configs); + fail("Expected not to find config for repository interface ".concat(repositoryInterface.getName())); + } catch (AssertionError error) { + // repo not there. we're fine. + } + } + + private static void assertHasRepo(Class repositoryInterface, + Collection> configs) { + + for (RepositoryConfiguration config : configs) { + if (config.getRepositoryInterface().equals(repositoryInterface.getName())) { + return; + } + } + + fail("Expected to find config for repository interface ".concat(repositoryInterface.getName()).concat(" but got ") + .concat(configs.toString())); + } + + @EnableRedisRepositories(considerNestedRepositories = true) + static class Config { + + } + + @RedisHash + static class Sample { + @Id String id; + } + + interface SampleRepository extends Repository {} + + interface UnannotatedRepository extends Repository {} + + interface StoreRepository extends KeyValueRepository {} +} diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java new file mode 100644 index 000000000..6331713c6 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java @@ -0,0 +1,148 @@ +package org.springframework.data.redis.repository.configuration; +/* + * Copyright 2016 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. + */ + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.convert.ReferenceResolver; +import org.springframework.data.redis.repository.configuration.RedisRepositoryConfigurationUnitTests.ContextWithCustomReferenceResolver; +import org.springframework.data.redis.repository.configuration.RedisRepositoryConfigurationUnitTests.ContextWithoutCustomization; +import org.springframework.data.repository.Repository; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * @author Christoph Strobl + */ +@RunWith(Suite.class) +@SuiteClasses({ ContextWithCustomReferenceResolver.class, ContextWithoutCustomization.class }) +public class RedisRepositoryConfigurationUnitTests { + + static RedisTemplate createTemplateMock() { + + RedisTemplate template = mock(RedisTemplate.class); + RedisConnectionFactory connectionFactory = mock(RedisConnectionFactory.class); + RedisConnection connection = mock(RedisConnection.class); + + when(template.getConnectionFactory()).thenReturn(connectionFactory); + when(connectionFactory.getConnection()).thenReturn(connection); + + return template; + } + + @RunWith(SpringJUnit4ClassRunner.class) + @DirtiesContext + @ContextConfiguration(classes = { ContextWithCustomReferenceResolver.Config.class }) + public static class ContextWithCustomReferenceResolver { + + @EnableRedisRepositories(considerNestedRepositories = true, + includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = { ".*ContextSampleRepository" }) }) + static class Config { + + @Bean + RedisTemplate redisTemplate() { + return createTemplateMock(); + } + + @Bean + ReferenceResolver redisReferenceResolver() { + return mock(ReferenceResolver.class); + } + + } + + @Autowired ApplicationContext ctx; + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldPickUpReferenceResolver() { + + RedisKeyValueAdapter adapter = (RedisKeyValueAdapter) ctx.getBean("redisKeyValueAdapter"); + + Object referenceResolver = ReflectionTestUtils.getField(adapter.getConverter(), "referenceResolver"); + + assertThat(referenceResolver, is(equalTo(ctx.getBean("redisReferenceResolver")))); + assertThat(mockingDetails(referenceResolver).isMock(), is(true)); + } + } + + @RunWith(SpringJUnit4ClassRunner.class) + @DirtiesContext + @ContextConfiguration(classes = { ContextWithoutCustomization.Config.class }) + public static class ContextWithoutCustomization { + + @EnableRedisRepositories(considerNestedRepositories = true, + includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = { ".*ContextSampleRepository" }) }) + static class Config { + + @Bean + RedisTemplate redisTemplate() { + return createTemplateMock(); + } + } + + @Autowired ApplicationContext ctx; + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldInitWithDefaults() { + assertThat(ctx.getBean(ContextSampleRepository.class), is(notNullValue())); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void shouldRegisterDefaultBeans() { + + assertThat(ctx.getBean(ContextSampleRepository.class), is(notNullValue())); + assertThat(ctx.getBean("redisKeyValueAdapter"), is(notNullValue())); + assertThat(ctx.getBean("redisCustomConversions"), is(notNullValue())); + assertThat(ctx.getBean("redisReferenceResolver"), is(notNullValue())); + } + } + + @RedisHash + static class Sample { + String id; + } + + interface ContextSampleRepository extends Repository {} +} diff --git a/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java b/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java new file mode 100644 index 000000000..120b2c604 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 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.core; + +import static org.mockito.Mockito.*; + +import java.io.Serializable; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.convert.ConversionTestEntities; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class MappingRedisEntityInformationUnitTests { + + @Mock RedisPersistentEntity entity; + + /** + * @see DATAREDIS-425 + */ + @Test(expected = MappingException.class) + @SuppressWarnings("unchecked") + public void throwsMappingExceptionWhenNoIdPropertyPresent() { + + when(entity.hasIdProperty()).thenReturn(false); + when(entity.getType()).thenReturn((Class) ConversionTestEntities.Person.class); + new MappingRedisEntityInformation(entity); + } +} diff --git a/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java b/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java new file mode 100644 index 000000000..100174462 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.repository.query; + +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.redis.core.convert.ConversionTestEntities; +import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * @author Christoph Strobl + */ +public class RedisQueryCreatorUnitTests { + + private @Mock RepositoryMetadata metadataMock; + + /** + * @see DATAREDIS-425 + */ + @Test + public void findBySingleSimpleProperty() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByFirstname", String.class), new Object[] { "eddard" }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getSismember(), hasSize(1)); + assertThat(query.getCritieria().getSismember(), hasItem(new PathAndValue("firstname", "eddard"))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findByMultipleSimpleProperties() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByFirstnameAndAge", String.class, Integer.class), new Object[] { + "eddard", 43 }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getSismember(), hasSize(2)); + assertThat(query.getCritieria().getSismember(), hasItem(new PathAndValue("firstname", "eddard"))); + assertThat(query.getCritieria().getSismember(), hasItem(new PathAndValue("age", 43))); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void findByMultipleSimplePropertiesUsingOr() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByAgeOrFirstname", Integer.class, String.class), new Object[] { 43, + "eddard" }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getOrSismember(), hasSize(2)); + assertThat(query.getCritieria().getOrSismember(), hasItem(new PathAndValue("age", 43))); + assertThat(query.getCritieria().getOrSismember(), hasItem(new PathAndValue("firstname", "eddard"))); + } + + private RedisQueryCreator createQueryCreatorForMethodWithArgs(Method method, Object[] args) { + + PartTree partTree = new PartTree(method.getName(), method.getReturnType()); + RedisQueryCreator creator = new RedisQueryCreator(partTree, new ParametersParameterAccessor(new DefaultParameters( + method), args)); + + return creator; + } + + private interface SampleRepository extends Repository { + + ConversionTestEntities.Person findByFirstname(String firstname); + + ConversionTestEntities.Person findByFirstnameAndAge(String firstname, Integer age); + + ConversionTestEntities.Person findByAgeOrFirstname(Integer age, String firstname); + } +}