diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoPage.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoPage.java new file mode 100644 index 000000000..7dbc501be --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoPage.java @@ -0,0 +1,63 @@ +/* + * Copyright 2011 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.mongodb.core.geo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +/** + * Custom {@link Page} to carry the average distance retrieved from the {@link GeoResults} the {@link GeoPage} is set up + * from. + * + * @author Oliver Gierke + */ +public class GeoPage extends PageImpl> { + + private static final long serialVersionUID = 23421312312412L; + private final Distance averageDistance; + + /** + * Creates a new {@link GeoPage} from the given {@link GeoResults}. + * + * @param content must not be {@literal null}. + */ + public GeoPage(GeoResults results) { + super(results.getContent()); + this.averageDistance = results.getAverageDistance(); + } + + /** + * Creates a new {@link GeoPage} from the given {@link GeoResults}, {@link Pageable} and total. + * + * @param results must not be {@literal null}. + * @param pageable must not be {@literal null}. + * @param total + */ + public GeoPage(GeoResults results, Pageable pageable, long total) { + super(results.getContent(), pageable, total); + this.averageDistance = results.getAverageDistance(); + } + + /** + * Returns the average distance of the underlying results. + * + * @return the averageDistance + */ + public Distance getAverageDistance() { + return averageDistance; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/AbstractMongoQuery.java index 46f2daacd..7cc335075 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/AbstractMongoQuery.java @@ -23,9 +23,15 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.GeoResult; +import org.springframework.data.mongodb.core.geo.GeoResults; +import org.springframework.data.mongodb.core.geo.Point; +import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import com.mongodb.DBCollection; @@ -72,10 +78,12 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { */ public Object execute(Object[] parameters) { - MongoParameterAccessor accessor = new MongoParametersParameterAccessor(method.getParameters(), parameters); + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(method, parameters); Query query = createQuery(new ConvertingParameterAccessor(template.getConverter(), accessor)); - if (method.isCollectionQuery()) { + if (method.isGeoNearQuery()) { + return new GeoNearExecution(accessor).execute(query); + } else if (method.isCollectionQuery()) { return new CollectionExecution().execute(query); } else if (method.isPageQuery()) { return new PagedExecution(accessor.getPageable()).execute(query); @@ -146,10 +154,9 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { } /* - * (non-Javadoc) - * - * @see org.springframework.data.mongodb.repository.MongoQuery.Execution #execute(com.mongodb.DBObject) - */ + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) + */ @Override @SuppressWarnings({ "rawtypes", "unchecked" }) Object execute(Query query) { @@ -193,4 +200,49 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { return template.findOne(query, entityInformation.getJavaType()); } } + + /** + * {@link Execution} to execute geo-near queries. + * + * @author Oliver Gierke + */ + class GeoNearExecution extends Execution { + + private final MongoParameterAccessor accessor; + + public GeoNearExecution(MongoParameterAccessor accessor) { + this.accessor = accessor; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query) + */ + @Override + Object execute(Query query) { + + Point nearLocation = accessor.getGeoNearLocation(); + NearQuery nearQuery = NearQuery.near(nearLocation); + + if (query != null) { + nearQuery.query(query); + } + + Distance maxDistance = accessor.getMaxDistance(); + if (maxDistance != null) { + nearQuery.maxDistance(maxDistance); + } + + MongoEntityInformation entityInformation = method.getEntityInformation(); + GeoResults results = template.geoNear(nearQuery, entityInformation.getJavaType(), entityInformation.getCollectionName()); + + return isListOfGeoResult() ? results.getContent() : results; + } + + private boolean isListOfGeoResult() { + + TypeInformation returnType = method.getReturnType(); + return returnType.getType().equals(List.class) && GeoResult.class.equals(returnType.getComponentType()); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java index 5ecc8c0fe..326ae4418 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java @@ -23,6 +23,7 @@ import org.springframework.data.mongodb.core.convert.MongoWriter; import org.springframework.data.mongodb.core.convert.TypeMapper; import org.springframework.data.mongodb.core.convert.TypeMapperProvider; import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.repository.query.ParameterAccessor; import com.mongodb.BasicDBList; @@ -90,6 +91,13 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { public Distance getMaxDistance() { return delegate.getMaxDistance(); } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoParameterAccessor#getGeoNearLocation() + */ + public Point getGeoNearLocation() { + return delegate.getGeoNearLocation(); + } /** * Converts the given value with the underlying {@link MongoWriter}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameterAccessor.java index 9ab252b83..25404cd1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameterAccessor.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.repository; import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.repository.query.ParameterAccessor; /** @@ -32,4 +33,11 @@ public interface MongoParameterAccessor extends ParameterAccessor { * at all or the given value for it was {@literal null}. */ Distance getMaxDistance(); + + /** + * Returns the {@link Point} to use for a geo-near query. + * + * @return + */ + Point getGeoNearLocation(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameters.java index 63a587060..3921c9325 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParameters.java @@ -17,9 +17,11 @@ package org.springframework.data.mongodb.repository; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.List; import org.springframework.core.MethodParameter; import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -30,40 +32,107 @@ import org.springframework.data.repository.query.Parameters; */ public class MongoParameters extends Parameters { - private int distanceIndex = -1; - - public MongoParameters(Method method) { - + private final Integer distanceIndex; + private Integer nearIndex; + + /** + * Creates a new {@link MongoParameters} instance from the given {@link Method} and {@link MongoQueryMethod}. + * + * @param method must not be {@literal null}. + * @param queryMethod must not be {@literal null}. + */ + public MongoParameters(Method method, boolean isGeoNearMethod) { + super(method); - this.distanceIndex = Arrays.asList(method.getParameterTypes()).indexOf(Distance.class); + List> parameterTypes = Arrays.asList(method.getParameterTypes()); + this.distanceIndex = parameterTypes.indexOf(Distance.class); + + if (this.nearIndex == null && isGeoNearMethod) { + this.nearIndex = getNearIndex(parameterTypes); + } else if (this.nearIndex == null) { + this.nearIndex = -1; + } } - - /* (non-Javadoc) + + @SuppressWarnings("unchecked") + private final int getNearIndex(List> parameterTypes) { + + for (Class reference : Arrays.asList(Point.class, double[].class)) { + + int nearIndex = parameterTypes.indexOf(reference); + + if (nearIndex == -1) { + continue; + } + + if (nearIndex == parameterTypes.lastIndexOf(reference)) { + return nearIndex; + } else { + throw new IllegalStateException("Multiple Point parameters found but none annotated with @Near!"); + } + } + + return -1; + } + + /* + * (non-Javadoc) * @see org.springframework.data.repository.query.Parameters#createParameter(org.springframework.core.MethodParameter) */ @Override protected Parameter createParameter(MethodParameter parameter) { - return new MongoParameter(parameter, this); + + MongoParameter mongoParameter = new MongoParameter(parameter); + + // Detect manually annotated @Near Point and reject multiple annotated ones + if (this.nearIndex == null && mongoParameter.isManuallyAnnotatedNearParameter()) { + this.nearIndex = mongoParameter.getIndex(); + } else if (mongoParameter.isManuallyAnnotatedNearParameter()) { + throw new IllegalStateException(String.format("Found multiple @Near annotations ond method %s! Only one allowed!", parameter.getMethod().toString())); + } + + return mongoParameter; } + /** + * Returns the index of a {@link Distance} parameter to be used for geo queries. + * + * @return + */ public int getDistanceIndex() { return distanceIndex; } - + /** - * Custom {@link Parameter} implementation adding parameters of type {@link Distance} to the special ones. + * Returns the index of the parameter to be used to start a geo-near query from. * + * @return + */ + public int getNearIndex() { + return nearIndex; + } + + /** + * Custom {@link Parameter} implementation adding parameters of type {@link Distance} to the special ones. + * * @author Oliver Gierke */ - static class MongoParameter extends Parameter { - + class MongoParameter extends Parameter { + + private final MethodParameter parameter; + /** + * Creates a new {@link MongoParameter}. * - * @param parameter - * @param parameters + * @param parameter must not be {@literal null}. */ - MongoParameter(MethodParameter parameter, Parameters parameters) { - super(parameter, parameters); + MongoParameter(MethodParameter parameter) { + super(parameter); + this.parameter = parameter; + + if (!isPoint() && hasNearAnnotation()) { + throw new IllegalArgumentException("Near annotation is only allowed at Point parameter!"); + } } /* @@ -72,7 +141,25 @@ public class MongoParameters extends Parameters { */ @Override public boolean isSpecialParameter() { - return super.isSpecialParameter() || getType().equals(Distance.class); + return super.isSpecialParameter() || getType().equals(Distance.class) + || isNearParameter(); + } + + private boolean isNearParameter() { + Integer nearIndex = MongoParameters.this.nearIndex; + return nearIndex != null && nearIndex.equals(getIndex()); + } + + private boolean isManuallyAnnotatedNearParameter() { + return isPoint() && hasNearAnnotation(); + } + + private boolean isPoint() { + return getType().equals(Point.class) || getType().equals(double[].class); + } + + private boolean hasNearAnnotation() { + return parameter.getParameterAnnotation(Near.class) != null; } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessor.java index dccf9bad8..7502d123c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessor.java @@ -16,24 +16,27 @@ package org.springframework.data.mongodb.repository; import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.repository.query.ParametersParameterAccessor; /** * Mongo-specific {@link ParametersParameterAccessor} to allow access to the {@link Distance} parameter. - * + * * @author Oliver Gierke */ public class MongoParametersParameterAccessor extends ParametersParameterAccessor implements MongoParameterAccessor { - private final MongoParameters parameters; - + private final MongoQueryMethod method; + /** - * @param parameters - * @param values + * Creates a new {@link MongoParametersParameterAccessor}. + * + * @param method must not be {@literal null}. + * @param values must not be {@@iteral null}. */ - public MongoParametersParameterAccessor(MongoParameters parameters, Object[] values) { - super(parameters, values); - this.parameters = parameters; + public MongoParametersParameterAccessor(MongoQueryMethod method, Object[] values) { + super(method.getParameters(), values); + this.method = method; } /* @@ -41,7 +44,37 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso * @see org.springframework.data.mongodb.repository.MongoParameterAccessor#getMaxDistance() */ public Distance getMaxDistance() { - int index = parameters.getDistanceIndex(); + int index = method.getParameters().getDistanceIndex(); return index == -1 ? null : (Distance) getValue(index); } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoParameterAccessor#getGeoNearLocation() + */ + public Point getGeoNearLocation() { + + int nearIndex = method.getParameters().getNearIndex(); + + if (nearIndex == -1) { + return null; + } + + Object value = getValue(nearIndex); + + if (value == null) { + return null; + } + + if (value instanceof double[]) { + double[] typedValue = (double[]) value; + if (typedValue.length != 2) { + throw new IllegalArgumentException("The given double[] must have exactly 2 elements!"); + } else { + return new Point(typedValue[0], typedValue[1]); + } + } + + return (Point) value; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryCreator.java index 3894864e2..5da44a038 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryCreator.java @@ -47,18 +47,26 @@ class MongoQueryCreator extends AbstractQueryCreator { private static final Log LOG = LogFactory.getLog(MongoQueryCreator.class); private final MongoParameterAccessor accessor; + private final boolean isGeoNearQuery; + public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor) { + this(tree, accessor, false); + } + /** * Creates a new {@link MongoQueryCreator} from the given {@link PartTree} and {@link ParametersParameterAccessor}. * * @param tree * @param accessor + * @param isGeoNearQuery */ - public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor) { + public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, boolean isGeoNearQuery) { super(tree, accessor); this.accessor = accessor; + this.isGeoNearQuery = isGeoNearQuery; } + /* * (non-Javadoc) @@ -66,6 +74,10 @@ class MongoQueryCreator extends AbstractQueryCreator { */ @Override protected Query create(Part part, Iterator iterator) { + + if (isGeoNearQuery && part.getType().equals(Type.NEAR)) { + return null; + } Criteria criteria = from(part.getType(), where(part.getProperty().toDotPath()), (PotentiallyConvertingIterator) iterator); @@ -79,6 +91,10 @@ class MongoQueryCreator extends AbstractQueryCreator { */ @Override protected Query and(Part part, Query base, Iterator iterator) { + + if (base == null) { + return create(part, iterator); + } Criteria criteria = from(part.getType(), where(part.getProperty().toDotPath()), (PotentiallyConvertingIterator) iterator); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryMethod.java index 2f15dfd22..50e4b6494 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoQueryMethod.java @@ -16,13 +16,21 @@ package org.springframework.data.mongodb.repository; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.mongodb.core.geo.GeoPage; +import org.springframework.data.mongodb.core.geo.GeoResult; +import org.springframework.data.mongodb.core.geo.GeoResults; import org.springframework.data.mongodb.repository.MongoRepositoryFactoryBean.EntityInformationCreator; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.util.ClassUtils; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -33,6 +41,10 @@ import org.springframework.util.StringUtils; */ class MongoQueryMethod extends QueryMethod { + @SuppressWarnings("unchecked") + private static final List> GEO_NEAR_RESULTS = Arrays + .asList(GeoResult.class, GeoResults.class, GeoPage.class); + private final Method method; private final MongoEntityInformation entityInformation; @@ -43,6 +55,7 @@ class MongoQueryMethod extends QueryMethod { */ public MongoQueryMethod(Method method, RepositoryMetadata metadata, EntityInformationCreator entityInformationCreator) { super(method, metadata); + Assert.notNull(entityInformationCreator, "EntityInformationCreator must not be null!"); this.method = method; this.entityInformation = entityInformationCreator.getEntityInformation(ClassUtils.getReturnedDomainClass(method), getDomainClass()); @@ -54,7 +67,7 @@ class MongoQueryMethod extends QueryMethod { */ @Override protected Parameters createParameters(Method method) { - return new MongoParameters(method); + return new MongoParameters(method, isGeoNearQuery(method)); } /** @@ -108,13 +121,40 @@ class MongoQueryMethod extends QueryMethod { return (MongoParameters) super.getParameters(); } + /** + * Returns whether te query is a geoNear query. + * + * @return + */ + public boolean isGeoNearQuery() { + + return isGeoNearQuery(this.method); + } + + private boolean isGeoNearQuery(Method method) { + + if (GEO_NEAR_RESULTS.contains(method.getReturnType())) { + return true; + } + + if (Iterable.class.isAssignableFrom(method.getReturnType())) { + TypeInformation from = ClassTypeInformation.fromReturnTypeOf(method); + return GeoResult.class.equals(from.getComponentType().getType()); + } + + return false; + } + /** * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available. * * @return */ private Query getQueryAnnotation() { - return method.getAnnotation(Query.class); } + + TypeInformation getReturnType() { + return ClassTypeInformation.fromReturnTypeOf(method); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Near.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Near.java new file mode 100644 index 000000000..f8ff08238 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Near.java @@ -0,0 +1,24 @@ +package org.springframework.data.mongodb.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.lang.annotation.Retention; + +import org.springframework.data.mongodb.core.geo.Distance; + +/** + * Annotation to be used for disambiguing method parameters that shall be used to trigger geo near queries. By default + * those parameters are found without the need for additional annotation if they are the only parameters of the + * according type (e.g. {@link Point}, {@code double[]}, {@link Distance}). + * + * @author Oliver Gierke + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Near { + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/PartTreeMongoQuery.java index f189fde6d..d3df7c147 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/PartTreeMongoQuery.java @@ -29,6 +29,7 @@ import org.springframework.data.repository.query.parser.PartTree; public class PartTreeMongoQuery extends AbstractMongoQuery { private final PartTree tree; + private final boolean isGeoNearQuery; /** * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. @@ -40,6 +41,7 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { super(method, template); this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); + this.isGeoNearQuery = method.isGeoNearQuery(); } /** @@ -59,7 +61,7 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { @Override protected Query createQuery(ConvertingParameterAccessor accessor) { - MongoQueryCreator creator = new MongoQueryCreator(tree, accessor); + MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, isGeoNearQuery); return creator.createQuery(); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/DistanceUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/DistanceUnitTests.java index ee6ef9465..624270efe 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/DistanceUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/DistanceUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2011 the original author or authors. + * Copyright 2011 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. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 9c656ae72..855dcf174 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -21,6 +21,9 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.geo.Box; import org.springframework.data.mongodb.core.geo.Circle; +import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.GeoResults; +import org.springframework.data.mongodb.core.geo.Metrics; import org.springframework.data.mongodb.core.geo.Point; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -317,4 +320,15 @@ public abstract class AbstractPersonRepositoryIntegrationTests { assertThat(result.get(4), is(leroi)); } + + @Test + public void executesGeoNearQueryForResultsCorrectly() { + + Point point = new Point(-73.99171, 40.738868); + dave.setLocation(point); + repository.save(dave); + + GeoResults results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, Metrics.KILOMETERS)); + assertThat(results.getContent().isEmpty(), is(false)); + } } \ No newline at end of file diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessorUnitTests.java index 5fd300953..10fdb7729 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersParameterAccessorUnitTests.java @@ -25,6 +25,11 @@ import org.junit.Test; import org.springframework.data.mongodb.core.geo.Distance; import org.springframework.data.mongodb.core.geo.Metrics; import org.springframework.data.mongodb.core.geo.Point; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.MongoRepositoryFactoryBean.EntityInformationCreator; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; /** * Unit tests for {@link MongoParametersParameterAccessor}. @@ -34,14 +39,17 @@ import org.springframework.data.mongodb.core.geo.Point; public class MongoParametersParameterAccessorUnitTests { private static final Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS); + private static final RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); + private static final MongoMappingContext context = new MongoMappingContext(); + private static final EntityInformationCreator creator = new EntityInformationCreator(context); @Test public void returnsNullForDistanceIfNoneAvailable() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class); - MongoParameters parameters = new MongoParameters(method); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, creator); - MongoParameterAccessor accessor = new MongoParametersParameterAccessor(parameters, + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { new Point(10, 20) }); assertThat(accessor.getMaxDistance(), is(nullValue())); } @@ -50,14 +58,14 @@ public class MongoParametersParameterAccessorUnitTests { public void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); - MongoParameters parameters = new MongoParameters(method); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, creator); - MongoParameterAccessor accessor = new MongoParametersParameterAccessor(parameters, new Object[] { + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { new Point(10, 20), DISTANCE }); assertThat(accessor.getMaxDistance(), is(DISTANCE)); } - interface PersonRepository { + interface PersonRepository extends Repository { List findByLocationNear(Point point); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersUnitTests.java index f85b22250..11bc176a5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoParametersUnitTests.java @@ -22,36 +22,92 @@ import java.lang.reflect.Method; import java.util.List; import org.junit.Test; -import org.springframework.core.MethodParameter; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.GeoResults; import org.springframework.data.mongodb.core.geo.Point; -import org.springframework.data.mongodb.repository.MongoParameters.MongoParameter; +import org.springframework.data.repository.query.Parameter; /** * Unit tests for {@link MongoParameters}. * * @author Oliver Gierke */ +@RunWith(MockitoJUnitRunner.class) public class MongoParametersUnitTests { + @Mock + MongoQueryMethod queryMethod; + @Test public void discoversDistanceParameter() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); - MongoParameters parameters = new MongoParameters(method); - + MongoParameters parameters = new MongoParameters(method, false); + assertThat(parameters.getNumberOfParameters(), is(2)); assertThat(parameters.getDistanceIndex(), is(1)); assertThat(parameters.getBindableParameters().getNumberOfParameters(), is(1)); - MongoParameter parameter = new MongoParameters.MongoParameter(new MethodParameter(method, - parameters.getDistanceIndex()), parameters); + Parameter parameter = parameters.getParameter(1); assertThat(parameter.isSpecialParameter(), is(true)); assertThat(parameter.isBindable(), is(false)); } + @Test + public void doesNotConsiderPointAsNearForSimpleQuery() throws Exception { + Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); + MongoParameters parameters = new MongoParameters(method, false); + + assertThat(parameters.getNearIndex(), is(-1)); + } + + @Test(expected = IllegalStateException.class) + public void rejectsMultiplePointsForGeoNearMethod() throws Exception { + Method method = PersonRepository.class.getMethod("findByLocationNearAndOtherLocation", Point.class, Point.class); + new MongoParameters(method, true); + } + + @Test(expected = IllegalStateException.class) + public void rejectsMultipleDoubleArraysForGeoNearMethod() throws Exception { + Method method = PersonRepository.class.getMethod("invalidDoubleArrays", double[].class, double[].class); + new MongoParameters(method, true); + } + + @Test + public void doesNotRejectMultiplePointsForSimpleQueryMethod() throws Exception { + Method method = PersonRepository.class.getMethod("someOtherMethod", Point.class, Point.class); + new MongoParameters(method, false); + } + + @Test + public void findsAnnotatedPointForGeoNearQuery() throws Exception { + Method method = PersonRepository.class.getMethod("findByOtherLocationAndLocationNear", Point.class, Point.class); + MongoParameters parameters = new MongoParameters(method, true); + assertThat(parameters.getNearIndex(), is(1)); + } + + @Test + public void findsAnnotatedDoubleArrayForGeoNearQuery() throws Exception { + Method method = PersonRepository.class.getMethod("validDoubleArrays", double[].class, double[].class); + MongoParameters parameters = new MongoParameters(method, true); + assertThat(parameters.getNearIndex(), is(1)); + } + interface PersonRepository { List findByLocationNear(Point point, Distance distance); + + GeoResults findByLocationNearAndOtherLocation(Point point, Point anotherLocation); + + GeoResults invalidDoubleArrays(double[] first, double[] second); + + List someOtherMethod(Point first, Point second); + + GeoResults findByOtherLocationAndLocationNear(Point point, @Near Point anotherLocation); + + GeoResults validDoubleArrays(double[] first, @Near double[] second); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryCreatorUnitTests.java index a9e480241..395043699 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryCreatorUnitTests.java @@ -15,12 +15,13 @@ */ package org.springframework.data.mongodb.repository; +import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; -import static org.hamcrest.CoreMatchers.*; -import static org.springframework.data.mongodb.repository.StubParameterAccessor.*; -import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; +import static org.springframework.data.mongodb.repository.StubParameterAccessor.*; import java.lang.reflect.Method; import java.util.List; @@ -38,9 +39,12 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.geo.Distance; import org.springframework.data.mongodb.core.geo.Metrics; import org.springframework.data.mongodb.core.geo.Point; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.repository.MongoQueryCreator; +import org.springframework.data.mongodb.repository.MongoRepositoryFactoryBean.EntityInformationCreator; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.parser.PartTree; import com.mongodb.BasicDBObject; @@ -103,42 +107,49 @@ public class MongoQueryCreatorUnitTests { assertThat(query.getQueryObject(), is(new Query(Criteria.where("firstName").is(null)).getQueryObject())); } - + @Test public void bindsMetricDistanceParameterToNearSphereCorrectly() throws Exception { Point point = new Point(10, 20); Distance distance = new Distance(2.5, Metrics.KILOMETERS); - - Query query = query(where("location").nearSphere(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave")); + + Query query = query(where("location").nearSphere(point).maxDistance(distance.getNormalizedValue()).and("firstname") + .is("Dave")); assertBindsDistanceToQuery(point, distance, query); } - + @Test public void bindsDistanceParameterToNearCorrectly() throws Exception { Point point = new Point(10, 20); Distance distance = new Distance(2.5); - - Query query = query(where("location").near(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave")); + + Query query = query(where("location").near(point).maxDistance(distance.getNormalizedValue()).and("firstname") + .is("Dave")); assertBindsDistanceToQuery(point, distance, query); } - + private void assertBindsDistanceToQuery(Point point, Distance distance, Query reference) throws Exception { - - when(converter.convertToMongoType("Dave")).thenReturn("Dave"); - - PartTree tree = new PartTree("findByLocationNearAndFirstname", org.springframework.data.mongodb.repository.Person.class); - MongoParameters parameters = new MongoParameters(PersonRepository.class.getMethod("findByLocationNearAndFirstname", Point.class, Distance.class, String.class)); - MongoParameterAccessor accessor = new MongoParametersParameterAccessor(parameters, new Object[] { point, distance, "Dave" }); - - Query query = new MongoQueryCreator(tree, new ConvertingParameterAccessor(converter, accessor)).createQuery(); - assertThat(query.getQueryObject(), is(query.getQueryObject())); - + + when(converter.convertToMongoType("Dave")).thenReturn("Dave"); + + PartTree tree = new PartTree("findByLocationNearAndFirstname", + org.springframework.data.mongodb.repository.Person.class); + Method method = PersonRepository.class.getMethod("findByLocationNearAndFirstname", Point.class, Distance.class, + String.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), + new EntityInformationCreator(new MongoMappingContext())); + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { point, distance, + "Dave" }); + + Query query = new MongoQueryCreator(tree, new ConvertingParameterAccessor(converter, accessor)).createQuery(); + assertThat(query.getQueryObject(), is(query.getQueryObject())); + } - - interface PersonRepository { - + + interface PersonRepository extends Repository { + List findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryMethodUnitTests.java index b153f29bf..8fcd765bf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoQueryMethodUnitTests.java @@ -15,15 +15,22 @@ */ package org.springframework.data.mongodb.repository; - -import static org.junit.Assert.*; import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import java.lang.reflect.Method; +import java.util.Collection; import java.util.List; import org.junit.Before; import org.junit.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.core.User; +import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.GeoPage; +import org.springframework.data.mongodb.core.geo.GeoResult; +import org.springframework.data.mongodb.core.geo.GeoResults; +import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.MongoRepositoryFactoryBean.EntityInformationCreator; import org.springframework.data.repository.Repository; @@ -31,13 +38,13 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat /** * Unit test for {@link MongoQueryMethod}. - * + * * @author Oliver Gierke */ public class MongoQueryMethodUnitTests { - + EntityInformationCreator creator; - + @Before public void setUp() { MongoMappingContext context = new MongoMappingContext(); @@ -46,36 +53,91 @@ public class MongoQueryMethodUnitTests { @Test public void detectsCollectionFromRepoTypeIfReturnTypeNotAssignable() throws Exception { - + Method method = SampleRepository.class.getMethod("method"); - - MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), creator); - MongoEntityInformation entityInformation = queryMethod.getEntityInformation(); - + + MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), + creator); + MongoEntityInformation entityInformation = queryMethod.getEntityInformation(); + assertThat(entityInformation.getJavaType(), is(typeCompatibleWith(Address.class))); assertThat(entityInformation.getCollectionName(), is("contact")); } - + @Test public void detectsCollectionFromReturnTypeIfReturnTypeAssignable() throws Exception { - + Method method = SampleRepository2.class.getMethod("method"); - - MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), creator); - MongoEntityInformation entityInformation = queryMethod.getEntityInformation(); - + + MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), + creator); + MongoEntityInformation entityInformation = queryMethod.getEntityInformation(); + assertThat(entityInformation.getJavaType(), is(typeCompatibleWith(Person.class))); assertThat(entityInformation.getCollectionName(), is("person")); } + + @Test + public void discoversUserAsDomainTypeForGeoPageQueryMethod() throws Exception { + + MongoQueryMethod queryMethod = queryMethod("findByLocationNear", Point.class, Distance.class, Pageable.class); + assertThat(queryMethod.isGeoNearQuery(), is(true)); + assertThat(queryMethod.isPageQuery(), is(true)); + + queryMethod = queryMethod("findByFirstname", String.class, Point.class); + assertThat(queryMethod.isGeoNearQuery(), is(true)); + assertThat(queryMethod.isPageQuery(), is(false)); + assertThat(queryMethod.getEntityInformation().getJavaType(), is(typeCompatibleWith(User.class))); + + assertThat(queryMethod("findByEmailAddress", String.class, Point.class).isGeoNearQuery(), is(true)); + assertThat(queryMethod("findByFirstname", String.class, Point.class).isGeoNearQuery(), is(true)); + assertThat(queryMethod("findByLastname", String.class, Point.class).isGeoNearQuery(), is(true)); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsGeoPageQueryWithoutPageable() throws Exception { + queryMethod("findByLocationNear", Point.class, Distance.class); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullEntityCreator() throws Exception { + Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Point.class); + new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), null); + } - + @Test + public void considersMethodReturningGeoPageAsPagingMethod() throws Exception { + MongoQueryMethod method = queryMethod("findByLocationNear", Point.class, Distance.class, Pageable.class); + assertThat(method.isPageQuery(), is(true)); + assertThat(method.isCollectionQuery(), is(false)); + } + + private MongoQueryMethod queryMethod(String name, Class... parameters) throws Exception { + Method method = PersonRepository.class.getMethod(name, parameters); + return new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), creator); + } + + interface PersonRepository extends Repository { + + // Misses Pageable + GeoPage findByLocationNear(Point point, Distance distance); + + GeoPage findByLocationNear(Point point, Distance distance, Pageable pageable); + + GeoResult findByEmailAddress(String lastname, Point location); + + GeoResults findByFirstname(String firstname, Point location); + + Collection> findByLastname(String lastname, Point location); + } + interface SampleRepository extends Repository { - + List
method(); } - + interface SampleRepository2 extends Repository { - + List method(); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 7dac2f006..0edfd0882 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -23,6 +23,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.geo.Box; import org.springframework.data.mongodb.core.geo.Circle; +import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.GeoResults; import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; @@ -140,4 +142,6 @@ public interface PersonRepository extends MongoRepository, Query List findBySex(Sex sex); List findByNamedQuery(String firstname); + + GeoResults findByLocationNear(Point point, Distance maxDistance); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StubParameterAccessor.java index 95bbfa30d..09562c488 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StubParameterAccessor.java @@ -22,6 +22,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.convert.MongoWriter; import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.mongodb.repository.ConvertingParameterAccessor; import org.springframework.data.repository.query.ParameterAccessor; @@ -92,4 +93,11 @@ class StubParameterAccessor implements MongoParameterAccessor { public Iterator iterator() { return Arrays.asList(values).iterator(); } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoParameterAccessor#getGeoNearLocation() + */ + public Point getGeoNearLocation() { + return null; + } } \ No newline at end of file diff --git a/src/docbkx/reference/mongo-repositories.xml b/src/docbkx/reference/mongo-repositories.xml index f01be9132..7d2b279dd 100644 --- a/src/docbkx/reference/mongo-repositories.xml +++ b/src/docbkx/reference/mongo-repositories.xml @@ -285,6 +285,66 @@ public class PersonRepositoryTests { +
+ Geo-spatial repository queries + + As you've just seen there are a few keywords triggering + geo-spatial operations within a MongoDB query. The Near + keyword allows some further modification. Let's have look at some + examples: + + + Advanced <code>Near</code> queries + + public interface PersonRepository extends MongoRepository<Person, String> + + // { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}} + List<Person> findByLocationNear(Point location, Distance distance); +} + + + Adding a Distance parameter to the query + method allows restricting results to those within the given distance. If + the Distance was set up containing a + Metric we will transparently use + $nearSphere instead of $code. + + + Using <code>Distance</code> with <code>Metrics</code> + + Point point = new Point(43.7, 48.8); +Distance distance = new Distance(200, Metrics.KILOMETERS); +… = repository.findByLocationNear(point, distance); +// {'location' : {'$nearSphere' : [43.7, 48.8], '$maxDistance' : 0.03135711885774796}} + + + As you can see using a Distance equipped + with a Metric causes + $nearSphere clause to be added instead of a plain + $near. Beyond that the actual distance gets calculated + according to the Metrics used. + + + Geo-near queries + + + + public interface PersonRepository extends MongoRepository<Person, String> + + // {'geoNear' : 'location', 'near' : [x, y] } + GeoResults<Person> findByLocationNear(Point location); + + // No metric: {'geoNear' : 'person', 'near' : [x, y], maxDistance : distance } + // Metric: {'geoNear' : 'person', 'near' : [x, y], 'maxDistance' : distance, + // 'distanceMultiplier' : metric.multiplier, 'spherical' : true } + GeoResults<Person> findByLocationNear(Point location, Distance distance); + + // {'geoNear' : 'location', 'near' : [x, y] } + GeoResults<Person> findByLocationNear(Point location); +} + +
+
Mongo JSON based query methods and field restriction