diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc index 18010e8b3..f339d36fd 100644 --- a/src/main/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -310,6 +310,8 @@ public class ApplicationConfig { == Secondary Indexes http://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native Redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or <>. +=== Simple Property Index + Given the sample `Person` entity we can create an index for _firstname_ by annotating the property with `@Indexed`. .Annotation driven indexing @@ -419,6 +421,51 @@ public class ApplicationConfig { ---- ==== +=== Geospatial Index + +Assume the `Address` type contains a property `location` of type `Point` that holds the geo coordinates of the particular address. By annotating the property with `@GeoIndexed` those values will be added using Redis `GEO` commands. + +==== +[source,java] +---- +@RedisHash("persons") +public class Person { + + Address address; + + // ... other properties omitted +} + +public class Address { + + @GeoIndexed Point location; + + // ... other properties omitted +} + +public interface PersonRepository extends CrudRepository { + + List findByAddressLocationNear(Point point, Distance distance); <1> + List findByAddressLocationWithin(Circle circle); <2> +} + +Person rand = new Person("rand", "al'thor"); +rand.setAddress(new Address(new Point(13.361389D, 38.115556D))); + +repository.save(rand); <3> + +repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200)); <4> +---- +<1> finder declaration on nested property using Point and Distance. +<2> finder declaration on nested property using Circle to search within. +<3> `GEOADD persons:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e` +<4> `GEORADIUS persons:address:location 15.0 37.0 200.0 km` +==== + +In the above example the lon/lat values are stored using `GEOADD` using the objects `id` as the members name. The finder methods allow usage of `Circle` or `Point, Distance` combinations for querying those values. + +NOTE: It is **not** possible to combine `near`/`within` with other criteria. + [[redis.repositories.expirations]] == Time To Live diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 48fc7cad7..f0ce25f8f 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -2431,7 +2431,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco Map byteMap = new HashMap(); for (Entry entry : memberCoordinateMap.entrySet()) { - byteMap.put(serialize(entry.getKey()), memberCoordinateMap.get(entry.getValue())); + byteMap.put(serialize(entry.getKey()), entry.getValue()); } return geoAdd(serialize(key), byteMap); 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 cc4ff0df5..8b6d3cde2 100644 --- a/src/main/java/org/springframework/data/redis/core/IndexWriter.java +++ b/src/main/java/org/springframework/data/redis/core/IndexWriter.java @@ -18,7 +18,9 @@ package org.springframework.data.redis.core; import java.util.Set; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue; import org.springframework.data.redis.core.convert.IndexedData; import org.springframework.data.redis.core.convert.RedisConverter; import org.springframework.data.redis.core.convert.RemoveIndexedData; @@ -126,7 +128,13 @@ class IndexWriter { byte[] indexHelperKey = ByteUtils.concatAll(toBytes(keyspace + ":"), binKey, toBytes(":idx")); for (byte[] indexKey : connection.sMembers(indexHelperKey)) { - connection.sRem(indexKey, binKey); + + DataType type = connection.type(indexKey); + if (DataType.ZSET.equals(type)) { + connection.zRem(indexKey, binKey); + } else { + connection.sRem(indexKey, binKey); + } } connection.del(indexHelperKey); @@ -166,7 +174,12 @@ class IndexWriter { if (!CollectionUtils.isEmpty(existingKeys)) { for (byte[] existingKey : existingKeys) { - connection.sRem(existingKey, key); + + if (indexedData instanceof GeoIndexedPropertyValue) { + connection.geoRemove(existingKey, key); + } else { + connection.sRem(existingKey, key); + } } } } @@ -207,7 +220,23 @@ class IndexWriter { // keep track of indexes used for the object connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey); - } else { + } else if (indexedData instanceof GeoIndexedPropertyValue) { + + GeoIndexedPropertyValue geoIndexedData = ((GeoIndexedPropertyValue) indexedData); + + Object value = geoIndexedData.getValue(); + if (value == null) { + return; + } + + byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName()); + connection.geoAdd(indexKey, geoIndexedData.getPoint(), key); + + // keep track of indexes used for the object + 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())); } 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 292acc99f..194617b9a 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -42,12 +42,14 @@ import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.PartialUpdate.PropertyUpdate; import org.springframework.data.redis.core.PartialUpdate.UpdateCommand; import org.springframework.data.redis.core.convert.CustomConversions; +import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.convert.MappingRedisConverter; import org.springframework.data.redis.core.convert.PathIndexResolver; @@ -461,8 +463,13 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter redisUpdateObject.fieldsToRemove.toArray(new byte[redisUpdateObject.fieldsToRemove.size()][])); } - for (byte[] index : redisUpdateObject.indexesToUpdate) { - connection.sRem(index, toBytes(redisUpdateObject.targetId)); + for (RedisUpdateObject.Index index : redisUpdateObject.indexesToUpdate) { + + if (ObjectUtils.nullSafeEquals(DataType.ZSET, index.type)) { + connection.zRem(index.key, toBytes(redisUpdateObject.targetId)); + } else { + connection.sRem(index.key, toBytes(redisUpdateObject.targetId)); + } } if (!rdo.getBucket().isEmpty()) { @@ -509,7 +516,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter ? ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes((":" + path)), toBytes(":"), value) : null; if (connection.exists(existingValueIndexKey)) { - redisUpdateObject.addIndexToUpdate(existingValueIndexKey); + + redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET)); } return redisUpdateObject; } @@ -530,12 +538,22 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter : null; if (connection.exists(existingValueIndexKey)) { - redisUpdateObject.addIndexToUpdate(existingValueIndexKey); + redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET)); } } } } + String pathToUse = GeoIndexedPropertyValue.geoIndexName(path); + if (connection.zRank(ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)), + toBytes(redisUpdateObject.targetId)) != null) { + + redisUpdateObject + .addIndexToUpdate(new org.springframework.data.redis.core.RedisKeyValueAdapter.RedisUpdateObject.Index( + ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)), + DataType.ZSET)); + } + return redisUpdateObject; } @@ -855,7 +873,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter private final byte[] targetKey; private Set fieldsToRemove = new LinkedHashSet(); - private Set indexesToUpdate = new LinkedHashSet(); + private Set indexesToUpdate = new LinkedHashSet(); RedisUpdateObject(byte[] targetKey, String keyspace, Object targetId) { @@ -868,8 +886,19 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter fieldsToRemove.add(field); } - void addIndexToUpdate(byte[] indexName) { - indexesToUpdate.add(indexName); + void addIndexToUpdate(Index index) { + indexesToUpdate.add(index); + } + + static class Index { + final DataType type; + final byte[] key; + + public Index(byte[] key, DataType type) { + this.key = key; + this.type = 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 b174da221..95e81e196 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java +++ b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java @@ -25,13 +25,19 @@ import java.util.List; import java.util.Map; import org.springframework.dao.DataAccessException; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.GeoResult; +import org.springframework.data.geo.GeoResults; import org.springframework.data.keyvalue.core.CriteriaAccessor; import org.springframework.data.keyvalue.core.QueryEngine; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue; import org.springframework.data.redis.core.convert.RedisData; import org.springframework.data.redis.repository.query.RedisOperationChain; +import org.springframework.data.redis.repository.query.RedisOperationChain.NearPath; import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue; import org.springframework.data.redis.util.ByteUtils; import org.springframework.util.CollectionUtils; @@ -74,7 +80,8 @@ class RedisQueryEngine extends QueryEngine type) { if (criteria == null - || (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember()))) { + || (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember())) + && criteria.getNear() == null) { return (Collection) getAdapter().getAllOf(keyspace, offset, rows); } @@ -99,6 +106,15 @@ class RedisQueryEngine extends QueryEngine> x = connection.geoRadius(geoKey(keyspace + ":", criteria.getNear()), + new Circle(criteria.getNear().getPoint(), criteria.getNear().getDistance())); + for (GeoResult> y : x) { + allKeys.add(y.getContent().getName()); + } + } + byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class); final Map> rawData = new LinkedHashMap>(); @@ -195,6 +211,13 @@ class RedisQueryEngine extends QueryEngine) entity, new PersistentEntityParameterValueProvider(entity, - new ConverterAwareParameterValueProvider(source, conversionService), null)); + new ConverterAwareParameterValueProvider(path, source, conversionService), this.conversionService)); final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); @@ -259,14 +259,14 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { Bucket bucket = source.getBucket().extract(currentPath + "."); - RedisData source = new RedisData(bucket); + RedisData newBucket = new RedisData(bucket); byte[] type = bucket.get(currentPath + "." + TYPE_HINT_ALIAS); if (type != null && type.length > 0) { - source.getBucket().put(TYPE_HINT_ALIAS, type); + newBucket.getBucket().put(TYPE_HINT_ALIAS, type); } - accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, source)); + accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, newBucket)); } else { if (persistentProperty.isIdProperty() && StringUtils.isEmpty(path.isEmpty())) { @@ -602,8 +602,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { writeAssociation(path, entity, value, sink); } - private void writeAssociation(final String path, final KeyValuePersistentEntity entity, - final Object value, final RedisData sink) { + private void writeAssociation(final String path, final KeyValuePersistentEntity entity, final Object value, + final RedisData sink) { if (value == null) { return; @@ -992,10 +992,13 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { private static class ConverterAwareParameterValueProvider implements PropertyValueProvider { + private final String path; private final RedisData source; private final ConversionService conversionService; - public ConverterAwareParameterValueProvider(RedisData source, ConversionService conversionService) { + public ConverterAwareParameterValueProvider(String path, RedisData source, ConversionService conversionService) { + + this.path = path; this.source = source; this.conversionService = conversionService; } @@ -1003,7 +1006,9 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { @Override @SuppressWarnings("unchecked") public T getPropertyValue(KeyValuePersistentProperty property) { - return (T) conversionService.convert(source.getBucket().get(property.getName()), property.getActualType()); + + String name = StringUtils.hasText(path) ? path + "." + property.getName() : property.getName(); + return (T) conversionService.convert(source.getBucket().get(name), property.getActualType()); } } diff --git a/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java index 649866394..3e5846cb5 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java @@ -15,16 +15,22 @@ */ package org.springframework.data.redis.core.convert; +import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import org.springframework.data.geo.Point; 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.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider; +import org.springframework.data.redis.core.index.GeoIndexDefinition; +import org.springframework.data.redis.core.index.GeoIndexed; import org.springframework.data.redis.core.index.IndexConfiguration; import org.springframework.data.redis.core.index.IndexDefinition; import org.springframework.data.redis.core.index.IndexDefinition.Condition; @@ -49,8 +55,12 @@ import org.springframework.util.CollectionUtils; */ public class PathIndexResolver implements IndexResolver { + private final Set> VALUE_TYPES = new HashSet>( + Arrays.> asList(Point.class, GeoLocation.class)); + private ConfigurableIndexDefinitionProvider indexConfiguration; private RedisMappingContext mappingContext; + private IndexedDataFactoryProvider indexedDataFactoryProvider; /** * Creates new {@link PathIndexResolver} with empty {@link IndexConfiguration}. @@ -69,6 +79,7 @@ public class PathIndexResolver implements IndexResolver { Assert.notNull(mappingContext, "MappingContext must not be null!"); this.mappingContext = mappingContext; this.indexConfiguration = mappingContext.getMappingConfiguration().getIndexConfiguration(); + this.indexedDataFactoryProvider = new IndexedDataFactoryProvider(); } /* @@ -94,7 +105,7 @@ public class PathIndexResolver implements IndexResolver { RedisPersistentEntity entity = mappingContext.getPersistentEntity(typeInformation); - if (entity == null) { + if (entity == null || (value != null && VALUE_TYPES.contains(value.getClass()))) { return resolveIndex(keyspace, path, fallback, value); } @@ -206,10 +217,11 @@ public class PathIndexResolver implements IndexResolver { Object transformedValue = indexDefinition.valueTransformer().convert(value); - IndexedData indexedData = new SimpleIndexedPropertyValue(keyspace, indexDefinition.getIndexName(), - transformedValue); + IndexedData indexedData = null; if (transformedValue == null) { indexedData = new RemoveIndexedData(indexedData); + } else { + indexedData = indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value); } data.add(indexedData); } @@ -220,7 +232,13 @@ public class PathIndexResolver implements IndexResolver { SimpleIndexDefinition indexDefinition = new SimpleIndexDefinition(keyspace, path); indexConfiguration.addIndexDefinition(indexDefinition); - data.add(new SimpleIndexedPropertyValue(keyspace, path, indexDefinition.valueTransformer().convert(value))); + data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value)); + } else if (property != null && property.isAnnotationPresent(GeoIndexed.class)) { + + GeoIndexDefinition indexDefinition = new GeoIndexDefinition(keyspace, path); + indexConfiguration.addIndexDefinition(indexDefinition); + + data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value)); } return data; diff --git a/src/main/java/org/springframework/data/redis/core/index/GeoIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/GeoIndexDefinition.java new file mode 100644 index 000000000..04d149db0 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/GeoIndexDefinition.java @@ -0,0 +1,72 @@ +/* + * 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.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; + +/** + * @author Christoph Strobl + * @since 1.8 + */ +public class GeoIndexDefinition extends RedisIndexDefinition implements PathBasedRedisIndexDefinition { + + /** + * Creates new {@link GeoIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param path + */ + public GeoIndexDefinition(String keyspace, String path) { + this(keyspace, path, path); + } + + /** + * Creates new {@link GeoIndexDefinition}. + * + * @param keyspace must not be {@literal null}. + * @param path + * @param name must not be {@literal null}. + */ + public GeoIndexDefinition(String keyspace, String path, String name) { + super(keyspace, path, name); + addCondition(new PathCondition(path)); + setValueTransformer(new PointValueTransformer()); + } + + /** + * @author Christoph Strobl + * @since 1.8 + */ + static class PointValueTransformer implements IndexValueTransformer { + + @Override + public Point convert(Object source) { + + if (source == null || source instanceof Point) { + return (Point) source; + } + + if (source instanceof GeoLocation) { + return ((GeoLocation) source).getPoint(); + } + + throw new IllegalArgumentException( + String.format("Cannot convert %s to %s. GeoIndexed property needs to be of type Point or GeoLocation!", + source.getClass(), Point.class)); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/index/GeoIndexed.java b/src/main/java/org/springframework/data/redis/core/index/GeoIndexed.java new file mode 100644 index 000000000..7e9a5e3d5 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/index/GeoIndexed.java @@ -0,0 +1,37 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark properties value to be included in a secondary index.
+ * Uses Redis {@literal GEO} structures for storage.
+ * The value will be part of the key built for the index. + * + * @author Christoph Strobl + * @since 1.8 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface GeoIndexed { + +} 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 563f21bf6..d455e8009 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 @@ -23,7 +23,7 @@ import java.lang.annotation.Target; /** * Mark properties value to be included in a secondary index.
- * Uses Redos {@literal SET} for storage.
+ * Uses Redis {@literal SET} for storage.
* The value will be part of the key built for the index. * * @author Christoph Strobl 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 f272052bc..ff5132daf 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 @@ -15,11 +15,15 @@ */ package org.springframework.data.redis.repository.query; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Point; import org.springframework.util.ObjectUtils; /** @@ -33,6 +37,8 @@ public class RedisOperationChain { private Set sismember = new LinkedHashSet(); private Set orSismember = new LinkedHashSet(); + private NearPath near; + public void sismember(String path, Object value) { sismember(new PathAndValue(path, value)); } @@ -61,6 +67,14 @@ public class RedisOperationChain { return orSismember; } + public void near(NearPath near) { + this.near = near; + } + + public NearPath getNear() { + return near; + } + public static class PathAndValue { private final String path; @@ -128,4 +142,26 @@ public class RedisOperationChain { } + /** + * @since 1.8 + * @author Christoph Strobl + */ + public static class NearPath extends PathAndValue { + + public NearPath(String path, Point point, Distance distance) { + super(path, Arrays. asList(point, distance)); + } + + public Point getPoint() { + return (Point) getFirstValue(); + } + + public Distance getDistance() { + + Iterator it = values().iterator(); + it.next(); + return (Distance) it.next(); + } + } + } 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 4475d54f9..ba3b9b9fa 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 @@ -17,8 +17,14 @@ package org.springframework.data.redis.repository.query; import java.util.Iterator; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.redis.repository.query.RedisOperationChain.NearPath; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; @@ -35,6 +41,7 @@ public class RedisQueryCreator extends AbstractQueryCreator iterator) { + + Object o = iterator.next(); + + Point point = null; + Distance distance = null; + + if (o instanceof Circle) { + + point = ((Circle) o).getCenter(); + distance = ((Circle) o).getRadius(); + } else if (o instanceof Point) { + + point = (Point) o; + + if (!iterator.hasNext()) { + throw new InvalidDataAccessApiUsageException( + "Expected to find distance value for geo query. Are you missing a parameter?"); + } + + Object distObject = iterator.next(); + if (distObject instanceof Distance) { + distance = (Distance) distObject; + } else if (distObject instanceof Number) { + distance = new Distance(((Number) distObject).doubleValue(), Metrics.KILOMETERS); + } else { + throw new InvalidDataAccessApiUsageException(String + .format("Expected to find Distance or Numeric value for geo query but was %s.", distObject.getClass())); + } + } else { + throw new InvalidDataAccessApiUsageException( + String.format("Expected to find a Circle or Point/Distance for geo query but was %s.", o.getClass())); + } + + return new NearPath(part.getProperty().toDotPath(), point, distance); + } + } 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 53f1a65f4..4d96b1a77 100644 --- a/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java @@ -35,6 +35,7 @@ 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.GeoIndexedPropertyValue; import org.springframework.data.redis.core.convert.IndexedData; import org.springframework.data.redis.core.convert.MappingRedisConverter; import org.springframework.data.redis.core.convert.PathIndexResolver; @@ -215,6 +216,21 @@ public class IndexWriterUnitTests { verify(connectionMock, times(1)).sRem(any(byte[].class), eq(KEY_BIN)); } + /** + * @see DATAREDIS-533 + */ + @Test + public void removeGeoIndexShouldCallGeoRemove() { + + byte[] indexKey1 = "persons:location".getBytes(CHARSET); + + when(connectionMock.keys(any(byte[].class))).thenReturn(new LinkedHashSet(Arrays.asList(indexKey1))); + + writer.removeKeyFromExistingIndexes(KEY_BIN, new GeoIndexedPropertyValue(KEYSPACE, "address.city", null)); + + verify(connectionMock).geoRemove(indexKey1, KEY_BIN); + } + static class StubIndxedData implements IndexedData { @Override diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java index beb46d389..45c0490c8 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Reference; +import org.springframework.data.geo.Point; import org.springframework.data.keyvalue.annotation.KeySpace; import org.springframework.data.redis.ConnectionFactoryTracker; import org.springframework.data.redis.connection.RedisConnection; @@ -45,6 +46,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor import org.springframework.data.redis.core.convert.Bucket; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.convert.MappingConfiguration; +import org.springframework.data.redis.core.index.GeoIndexed; import org.springframework.data.redis.core.index.IndexConfiguration; import org.springframework.data.redis.core.index.Indexed; import org.springframework.data.redis.core.mapping.RedisMappingContext; @@ -510,6 +512,90 @@ public class RedisKeyValueAdapterTests { assertThat(template.opsForHash().hasKey("persons:1", "relatives.[stepfather].firstname"), is(false)); } + /** + * @see DATAREDIS-533 + */ + @Test + public void putShouldCreateGeoIndexCorrectly() { + + Person tam = new Person(); + tam.id = "1"; + tam.firstname = "tam"; + tam.address = new Address(); + tam.address.location = new Point(10, 20); + + adapter.put("1", tam, "persons"); + + assertThat(template.opsForZSet().score("persons:address:location", "1"), is(notNullValue())); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void deleteShouldRemoveGeoIndexCorrectly() { + + Person tam = new Person(); + tam.id = "1"; + tam.firstname = "tam"; + tam.address = new Address(); + tam.address.location = new Point(10, 20); + + adapter.put("1", tam, "persons"); + + adapter.delete("1", "persons", Person.class); + + assertThat(template.opsForZSet().score("persons:address:location", "1"), is(nullValue())); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void updateShouldAlterGeoIndexCorrectlyOnDelete() { + + Person tam = new Person(); + tam.id = "1"; + tam.firstname = "tam"; + tam.address = new Address(); + tam.address.location = new Point(10, 20); + + adapter.put("1", tam, "persons"); + + PartialUpdate update = new PartialUpdate("1", Person.class) // + .del("address.location"); + + adapter.update(update); + + assertThat(template.opsForZSet().score("persons:address:location", "1"), is(nullValue())); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void updateShouldAlterGeoIndexCorrectlyOnUpdate() { + + Person tam = new Person(); + tam.id = "1"; + tam.firstname = "tam"; + tam.address = new Address(); + tam.address.location = new Point(10, 20); + + adapter.put("1", tam, "persons"); + + PartialUpdate update = new PartialUpdate("1", Person.class) // + .set("address.location", new Point(17, 18)); + + adapter.update(update); + + assertThat(template.opsForZSet().score("persons:address:location", "1"), is(notNullValue())); + Point updatedLocation = template.opsForGeo().geoPos("persons:address:location", "1").iterator().next(); + + assertThat(updatedLocation.getX(), is(closeTo(17D, 0.005))); + assertThat(updatedLocation.getY(), is(closeTo(18D, 0.005))); + } + @KeySpace("persons") static class Person { @@ -536,6 +622,8 @@ public class RedisKeyValueAdapterTests { String city; @Indexed String country; + @GeoIndexed Point location; + } static class AddressWithId extends Address { diff --git a/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java index c9f96b2ad..f918c5fb1 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java @@ -34,10 +34,13 @@ import java.util.Set; import org.hamcrest.core.IsCollectionContaining; import org.hamcrest.core.IsInstanceOf; 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.geo.Point; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.redis.core.convert.ConversionTestEntities.Address; import org.springframework.data.redis.core.convert.ConversionTestEntities.AddressWithId; @@ -48,6 +51,7 @@ import org.springframework.data.redis.core.convert.ConversionTestEntities.Person 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.GeoIndexed; import org.springframework.data.redis.core.index.IndexConfiguration; import org.springframework.data.redis.core.index.Indexed; import org.springframework.data.redis.core.index.SimpleIndexDefinition; @@ -61,6 +65,8 @@ import org.springframework.data.util.ClassTypeInformation; @RunWith(MockitoJUnitRunner.class) public class PathIndexResolverUnitTests { + public @Rule ExpectedException exception = ExpectedException.none(); + IndexConfiguration indexConfig; PathIndexResolver indexResolver; @@ -514,6 +520,68 @@ public class PathIndexResolverUnitTests { new SimpleIndexedPropertyValue(IndexedOnPrimitiveArrayField.class.getName(), "values", 3))); } + /** + * @see DATAREDIS-533 + */ + @Test + public void resolveGeoIndexShouldMapNameCorrectly() { + + when(propertyMock.isMap()).thenReturn(true); + when(propertyMock.isAnnotationPresent(eq(GeoIndexed.class))).thenReturn(true); + when(propertyMock.findAnnotation(eq(GeoIndexed.class))).thenReturn(createGeoIndexedInstance()); + + IndexedData index = resolve("location", new Point(1D, 2D)); + + assertThat(index.getIndexName(), is("location")); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void resolveGeoIndexShouldMapNameForNestedPropertyCorrectly() { + + when(propertyMock.isMap()).thenReturn(true); + when(propertyMock.isAnnotationPresent(eq(GeoIndexed.class))).thenReturn(true); + when(propertyMock.findAnnotation(eq(GeoIndexed.class))).thenReturn(createGeoIndexedInstance()); + + IndexedData index = resolve("property.location", new Point(1D, 2D)); + + assertThat(index.getIndexName(), is("property:location")); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void resolveGeoIndexOnPointField() { + + GeoIndexedOnPoint source = new GeoIndexedOnPoint(); + source.location = new Point(1D, 2D); + + Set indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(GeoIndexedOnPoint.class), + source); + + assertThat(indexes.size(), is(1)); + assertThat(indexes, IsCollectionContaining. hasItems( + new GeoIndexedPropertyValue(GeoIndexedOnPoint.class.getName(), "location", source.location))); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void resolveGeoIndexOnArrayFieldThrowsError() { + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("GeoIndexed property needs to be of type Point or GeoLocation"); + + GeoIndexedOnArray source = new GeoIndexedOnArray(); + source.location = new double[] { 10D, 20D }; + + indexResolver.resolveIndexesFor(ClassTypeInformation.from(GeoIndexedOnArray.class), source); + } + private IndexedData resolve(String path, Object value) { Set data = indexResolver.resolveIndex(KEYSPACE_PERSON, path, propertyMock, value); @@ -534,7 +602,16 @@ public class PathIndexResolverUnitTests { public Class annotationType() { return Indexed.class; } + }; + } + private GeoIndexed createGeoIndexedInstance() { + + return new GeoIndexed() { + @Override + public Class annotationType() { + return GeoIndexed.class; + } }; } @@ -553,4 +630,12 @@ public class PathIndexResolverUnitTests { @Indexed Map values; } + static class GeoIndexedOnPoint { + @GeoIndexed Point location; + } + + static class GeoIndexedOnArray { + @GeoIndexed double[] location; + } + } diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java index e9d7c22df..556ed5267 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java @@ -51,8 +51,8 @@ public class RedisRepositoryClusterIntegrationTests extends RedisRepositoryInteg @Configuration @EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class, - keyspaceConfiguration = MyKeyspaceConfiguration.class, - includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") }) + keyspaceConfiguration = MyKeyspaceConfiguration.class, includeFilters = { + @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository|.*CityRepository") }) static class Config { @Bean diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java index 63bc7704b..3c608077e 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java @@ -15,9 +15,7 @@ */ package org.springframework.data.redis.repository; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.collection.IsCollectionWithSize.*; -import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.io.Serializable; @@ -34,13 +32,18 @@ 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.geo.Distance; +import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.index.GeoIndexed; 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.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; /** @@ -52,6 +55,7 @@ import org.springframework.data.repository.PagingAndSortingRepository; public abstract class RedisRepositoryIntegrationTestBase { @Autowired PersonRepository repo; + @Autowired CityRepository cityRepo; @Autowired KeyValueTemplate kvTemplate; @Before @@ -318,6 +322,74 @@ public abstract class RedisRepositoryIntegrationTestBase { } } + /** + * @see DATAREDIS-533 + */ + @Test + public void nearQueryShouldReturnResultsCorrectly() { + + City palermo = new City(); + palermo.location = new Point(13.361389D, 38.115556D); + + City catania = new City(); + catania.location = new Point(15.087269D, 37.502669D); + + cityRepo.save(Arrays.asList(palermo, catania)); + + List result = cityRepo.findByLocationNear(new Point(15D, 37D), new Distance(200, Metrics.KILOMETERS)); + assertThat(result, hasItems(palermo, catania)); + + result = cityRepo.findByLocationNear(new Point(15D, 37D), new Distance(100, Metrics.KILOMETERS)); + assertThat(result, hasItems(catania)); + assertThat(result, not(hasItems(palermo))); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void nearQueryShouldFindNothingIfOutOfRange() { + + City palermo = new City(); + palermo.location = new Point(13.361389D, 38.115556D); + + City catania = new City(); + catania.location = new Point(15.087269D, 37.502669D); + + cityRepo.save(Arrays.asList(palermo, catania)); + + List result = cityRepo.findByLocationNear(new Point(15D, 37D), new Distance(10, Metrics.KILOMETERS)); + assertThat(result, is(empty())); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void nearQueryShouldReturnResultsCorrectlyOnNestedProperty() { + + City palermo = new City(); + palermo.location = new Point(13.361389D, 38.115556D); + + City catania = new City(); + catania.location = new Point(15.087269D, 37.502669D); + + Person p1 = new Person("foo", "bar"); + p1.hometown = palermo; + + Person p2 = new Person("two", "two"); + p2.hometown = catania; + + repo.save(Arrays.asList(p1, p2)); + + List result = repo.findByHometownLocationNear(new Point(15D, 37D), new Distance(200, Metrics.KILOMETERS)); + assertThat(result, hasItems(p1, p2)); + + result = repo.findByHometownLocationNear(new Point(15D, 37D), new Distance(100, Metrics.KILOMETERS)); + assertThat(result, hasItems(p2)); + assertThat(result, not(hasItems(p1))); + } + public static interface PersonRepository extends PagingAndSortingRepository { List findByFirstname(String firstname); @@ -337,6 +409,13 @@ public abstract class RedisRepositoryIntegrationTestBase { List findTop2ByLastname(String lastname); Page findBy(Pageable page); + + List findByHometownLocationNear(Point point, Distance distance); + } + + public static interface CityRepository extends CrudRepository { + + List findByLocationNear(Point point, Distance distance); } /** @@ -373,6 +452,7 @@ public abstract class RedisRepositoryIntegrationTestBase { @Indexed String firstname; String lastname; @Reference City city; + City hometown; public Person() {} @@ -460,9 +540,12 @@ public abstract class RedisRepositoryIntegrationTestBase { } public static class City { + @Id String id; String name; + @GeoIndexed Point location; + @Override public int hashCode() { final int prime = 31; 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 961ecea59..06fb56fb4 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java @@ -35,8 +35,8 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT @Configuration @EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class, - keyspaceConfiguration = MyKeyspaceConfiguration.class, - includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") }) + keyspaceConfiguration = MyKeyspaceConfiguration.class, includeFilters = { + @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository|.*CityRepository") }) static class Config { @Bean 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 index 100174462..312ca0771 100644 --- a/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java @@ -16,15 +16,26 @@ package org.springframework.data.redis.repository.query; import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsCollectionContaining.*; +import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; import java.lang.reflect.Method; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.mockito.Mock; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.geo.Box; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; +import org.springframework.data.geo.Shape; import org.springframework.data.keyvalue.core.query.KeyValueQuery; -import org.springframework.data.redis.core.convert.ConversionTestEntities; +import org.springframework.data.redis.core.convert.ConversionTestEntities.Person; import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; @@ -37,6 +48,8 @@ import org.springframework.data.repository.query.parser.PartTree; */ public class RedisQueryCreatorUnitTests { + public @Rule ExpectedException exception = ExpectedException.none(); + private @Mock RepositoryMetadata metadataMock; /** @@ -61,8 +74,8 @@ public class RedisQueryCreatorUnitTests { public void findByMultipleSimpleProperties() throws SecurityException, NoSuchMethodException { RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( - SampleRepository.class.getMethod("findByFirstnameAndAge", String.class, Integer.class), new Object[] { - "eddard", 43 }); + SampleRepository.class.getMethod("findByFirstnameAndAge", String.class, Integer.class), + new Object[] { "eddard", 43 }); KeyValueQuery query = creator.createQuery(); @@ -78,8 +91,8 @@ public class RedisQueryCreatorUnitTests { public void findByMultipleSimplePropertiesUsingOr() throws SecurityException, NoSuchMethodException { RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( - SampleRepository.class.getMethod("findByAgeOrFirstname", Integer.class, String.class), new Object[] { 43, - "eddard" }); + SampleRepository.class.getMethod("findByAgeOrFirstname", Integer.class, String.class), + new Object[] { 43, "eddard" }); KeyValueQuery query = creator.createQuery(); @@ -88,21 +101,127 @@ public class RedisQueryCreatorUnitTests { assertThat(query.getCritieria().getOrSismember(), hasItem(new PathAndValue("firstname", "eddard"))); } + /** + * @see DATAREDIS-533 + */ + @Test + public void findWithinCircle() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByLocationWithin", Circle.class), + new Object[] { new Circle(new Point(1, 2), new Distance(200, Metrics.KILOMETERS)) }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getNear(), is(notNullValue())); + assertThat(query.getCritieria().getNear().getPoint(), is(new Point(1, 2))); + assertThat(query.getCritieria().getNear().getDistance(), is(new Distance(200, Metrics.KILOMETERS))); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void findNearWithPointAndDistance() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByLocationNear", Point.class, Distance.class), + new Object[] { new Point(1, 2), new Distance(200, Metrics.KILOMETERS) }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getNear(), is(notNullValue())); + assertThat(query.getCritieria().getNear().getPoint(), is(new Point(1, 2))); + assertThat(query.getCritieria().getNear().getDistance(), is(new Distance(200, Metrics.KILOMETERS))); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void findNearWithPointAndNumericValueDefaultsToKilometers() throws SecurityException, NoSuchMethodException { + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByLocationNear", Shape.class, Object.class), + new Object[] { new Point(1, 2), 200F }); + + KeyValueQuery query = creator.createQuery(); + + assertThat(query.getCritieria().getNear(), is(notNullValue())); + assertThat(query.getCritieria().getNear().getPoint(), is(new Point(1, 2))); + assertThat(query.getCritieria().getNear().getDistance(), is(new Distance(200, Metrics.KILOMETERS))); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void findNearWithInvalidShapeParameter() throws SecurityException, NoSuchMethodException { + + exception.expect(InvalidDataAccessApiUsageException.class); + exception.expectMessage("Expected to find a Circle or Point/Distance"); + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByLocationNear", Shape.class, Object.class), + new Object[] { new Box(new Point(0, 0), new Point(1, 1)), 200F }); + + creator.createQuery(); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void findNearWithInvalidDistanceParameter() throws SecurityException, NoSuchMethodException { + + exception.expect(InvalidDataAccessApiUsageException.class); + exception.expectMessage("Expected to find Distance or Numeric value"); + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByLocationNear", Shape.class, Object.class), + new Object[] { new Point(0, 0), "200" }); + + creator.createQuery(); + } + + /** + * @see DATAREDIS-533 + */ + @Test + public void findNearWithMissingDistanceParameter() throws SecurityException, NoSuchMethodException { + + exception.expect(InvalidDataAccessApiUsageException.class); + exception.expectMessage("Are you missing a parameter?"); + + RedisQueryCreator creator = createQueryCreatorForMethodWithArgs( + SampleRepository.class.getMethod("findByLocationNear", Shape.class), new Object[] { new Point(0, 0) }); + + creator.createQuery(); + } + 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)); + RedisQueryCreator creator = new RedisQueryCreator(partTree, + new ParametersParameterAccessor(new DefaultParameters(method), args)); return creator; } - private interface SampleRepository extends Repository { + private interface SampleRepository extends Repository { - ConversionTestEntities.Person findByFirstname(String firstname); + Person findByFirstname(String firstname); - ConversionTestEntities.Person findByFirstnameAndAge(String firstname, Integer age); + Person findByFirstnameAndAge(String firstname, Integer age); - ConversionTestEntities.Person findByAgeOrFirstname(Integer age, String firstname); + Person findByAgeOrFirstname(Integer age, String firstname); + + Person findByLocationWithin(Circle circle); + + Person findByLocationNear(Point point, Distance distance); + + Person findByLocationNear(Shape point, Object distance); + + Person findByLocationNear(Shape point); } }