From fedcbdae4feb0e623ebcf568742c6a2f7b5152de Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 20 Jul 2011 18:46:14 +0200 Subject: [PATCH] DATADOC-68 - Added support for geoNear command. Introduced GeoResult value object as well as NearQuery. NearQuery allows definition of an origin and distances. Introduced a Metric interface and Metrics enum to carry commonly used metrics like kilometers and miles to ease the handling in NearQueries. Introduced Distance value object to capture distances in Metrics. --- .../data/mongodb/core/MongoOperations.java | 23 ++ .../data/mongodb/core/MongoTemplate.java | 63 ++++- .../data/mongodb/core/geo/Distance.java | 145 ++++++++++ .../data/mongodb/core/geo/GeoResult.java | 102 +++++++ .../data/mongodb/core/geo/GeoResults.java | 143 ++++++++++ .../data/mongodb/core/geo/Metric.java | 16 ++ .../data/mongodb/core/geo/Metrics.java | 27 ++ .../data/mongodb/core/query/NearQuery.java | 266 ++++++++++++++++++ .../mongodb/core/geo/DistanceUnitTests.java | 50 ++++ .../mongodb/core/geo/GeoResultUnitTests.java | 55 ++++ .../mongodb/core/geo/GeoResultsUnitTests.java | 24 ++ .../mongodb/core/geo/GeoSpatialAppConfig.java | 2 +- .../mongodb/core/geo/GeoSpatialTests.java | 16 +- .../core/mapping/GeoIndexedAppConfig.java | 2 +- .../core/query/NearQueryUnitTests.java | 44 +++ 15 files changed, 967 insertions(+), 11 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Distance.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResult.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResults.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metric.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metrics.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/DistanceUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultsUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 1b4814120..a21638f21 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -25,7 +25,10 @@ import com.mongodb.DBObject; import com.mongodb.WriteResult; import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.geo.GeoResult; +import org.springframework.data.mongodb.core.geo.GeoResults; import org.springframework.data.mongodb.core.index.IndexDefinition; +import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; @@ -255,6 +258,26 @@ public interface MongoOperations { * @return the converted collection */ List findAll(Class entityClass, String collectionName); + + /** + * Returns {@link GeoResult} for all entities matching the given {@link NearQuery}. Will consider entity mapping + * information to determine the collection the query is ran against. + * + * @param near must not be {@literal null}. + * @param entityClass must not be {@literal null}. + * @return + */ + GeoResults geoNear(NearQuery near, Class entityClass); + + /** + * Returns {@link GeoResult} for all entities matching the given {@link NearQuery}. + * + * @param near must not be {@literal null}. + * @param entityClass must not be {@literal null}. + * @param collectionName the collection to trigger the query against. If no collection name is given the entity class will be inspected. + * @return + */ + GeoResults geoNear(NearQuery near, Class entityClass, String collectionName); /** * Ensure that an index for the provided {@link IndexDefinition} exists for the collection indicated by the entity class. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index eaa206b72..14bf789e2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.CommandResult; import com.mongodb.DB; @@ -60,6 +61,10 @@ import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoReader; import org.springframework.data.mongodb.core.convert.MongoWriter; +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.Metric; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator; @@ -72,10 +77,12 @@ import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; +import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.jca.cci.core.ConnectionCallback; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Primary implementation of {@link MongoOperations}. @@ -430,6 +437,30 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { return doFindOne(collectionName, new BasicDBObject(idKey, id), null, entityClass); } + public GeoResults geoNear(NearQuery near, Class entityClass) { + return geoNear(near, entityClass, determineCollectionName(entityClass)); + } + + public GeoResults geoNear(NearQuery near, Class entityClass, String collectionName) { + + String collection = StringUtils.hasText(collectionName) ? collectionName : determineCollectionName(entityClass); + BasicDBObject command = new BasicDBObject("geoNear", collection); + command.putAll(near.toDBObject()); + + CommandResult commandResult = executeCommand(command); + BasicDBList results = (BasicDBList) commandResult.get("results"); + DbObjectCallback> callback = new GeoNearResultDbObjectCallback(new ReadDbObjectCallback(mongoConverter, entityClass), near.getMetric()); + List> result = new ArrayList>(results.size()); + + for (Object element : results) { + result.add(callback.doWith((DBObject) element)); + } + + double averageDistance = (Double) ((DBObject) commandResult.get("stats")).get("avgDistance"); + return new GeoResults(result, new Distance(averageDistance, near.getMetric())); + } + + // Find methods that take a Query to express the query and that return a single object that is also removed from the // collection in the database. @@ -843,7 +874,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { entityClass)); } - protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass, + protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass, CursorPreparer preparer, DbObjectCallback objectCallback) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); if (LOGGER.isDebugEnabled()) { @@ -1258,7 +1289,37 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { } } + /** + * {@link DbObjectCallback} that assumes a {@link GeoResult} to be created, delegates actual content unmarshalling to + * a delegate and creates a {@link GeoResult} from the result. + * + * @author Oliver Gierke + */ + static class GeoNearResultDbObjectCallback implements DbObjectCallback> { + private final DbObjectCallback delegate; + private final Metric metric; + /** + * Creates a new {@link GeoNearResultDbObjectCallback} using the given {@link DbObjectCallback} delegate for + * {@link GeoResult} content unmarshalling. + * + * @param delegate + */ + public GeoNearResultDbObjectCallback(DbObjectCallback delegate, Metric metric) { + Assert.notNull(delegate); + this.delegate = delegate; + this.metric = metric; + } + public GeoResult doWith(DBObject object) { + + double distance = ((Double) object.get("dis")).doubleValue(); + DBObject content = (DBObject) object.get("obj"); + + T doWith = delegate.doWith(content); + + return new GeoResult(doWith, new Distance(distance, metric)); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Distance.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Distance.java new file mode 100644 index 000000000..65d553d4d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Distance.java @@ -0,0 +1,145 @@ +/* + * Copyright 2010-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.util.ObjectUtils; + +/** + * Value object to represent distances in a given metric. + * + * @author Oliver Gierke + */ +public class Distance { + + private final double value; + private final Metric metric; + + /** + * Creates a new {@link Distance}. + * + * @param value + */ + public Distance(double value) { + this(value, Metrics.NEUTRAL); + } + + /** + * Creates a new {@link Distance} with the given {@link Metric}. + * + * @param value + * @param metric + */ + public Distance(double value, Metric metric) { + this.value = value; + this.metric = metric == null ? Metrics.NEUTRAL : metric; + } + + /** + * @return the value + */ + public double getValue() { + return value; + } + + /** + * Returns the normalized value regarding the underlying {@link Metric}. + * + * @return + */ + public double getNormalizedValue() { + return value / metric.getMultiplier(); + } + + /** + * @return the metric + */ + public Metric getMetric() { + return metric; + } + + /** + * Adds the given distance to the current one. The resulting {@link Distance} will be in the same metric as the + * current one. + * + * @param other + * @return + */ + public Distance add(Distance other) { + double newNormalizedValue = getNormalizedValue() + other.getNormalizedValue(); + return new Distance(newNormalizedValue * metric.getMultiplier(), metric); + } + + /** + * Adds the given {@link Distance} to the current one and forces the result to be in a given {@link Metric}. + * + * @param other + * @param metric + * @return + */ + public Distance add(Distance other, Metric metric) { + double newLeft = getNormalizedValue() * metric.getMultiplier(); + double newRight = other.getNormalizedValue() * metric.getMultiplier(); + return new Distance(newLeft + newRight, metric); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + + Distance that = (Distance) obj; + + return this.value == that.value && ObjectUtils.nullSafeEquals(this.metric, that.metric); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = 17; + result += 31 * Double.doubleToLongBits(value); + result += 31 * ObjectUtils.nullSafeHashCode(metric); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + StringBuilder builder = new StringBuilder(); + builder.append(value); + + if (metric != null) { + builder.append(" ").append(metric.toString()); + } + + return builder.toString(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResult.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResult.java new file mode 100644 index 000000000..6ba6fb82d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResult.java @@ -0,0 +1,102 @@ +/* + * 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.util.Assert; + +/** + * Calue object capturing some arbitrary object plus a distance. + * + * @author Oliver Gierke + */ +public class GeoResult { + + private final T content; + private final Distance distance; + + /** + * Creates a new {@link GeoResult} for the given content and distance. + * + * @param content must not be {@literal null}. + * @param distance must not be {@literal null}. + */ + public GeoResult(T content, Distance distance) { + Assert.notNull(content); + Assert.notNull(distance); + this.content = content; + this.distance = distance; + } + + /** + * Returns the actual content object. + * + * @return the content + */ + public T getContent() { + return content; + } + + /** + * Returns the distance the actual content object has from the origin. + * + * @return the distance + */ + public Distance getDistance() { + return distance; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + + GeoResult that = (GeoResult) obj; + + return this.content.equals(that.content) && this.distance.equals(that.distance); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + result += 31 * distance.hashCode(); + result += 31 * content.hashCode(); + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("GeoResult [content: %s, distance: %s, ]", content.toString(), distance.toString()); + } +} \ No newline at end of file diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResults.java new file mode 100644 index 000000000..cb14118cb --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoResults.java @@ -0,0 +1,143 @@ +/* + * 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 java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Value object to capture {@link GeoResult}s as well as the average distance they have. + * + * @author Oliver Gierke + */ +public class GeoResults implements Iterable> { + + private final List> results; + private final Distance averageDistance; + + /** + * Creates a new {@link GeoResults} instance manually calculating the average distance from the distance values of the + * given {@link GeoResult}s. + * + * @param results must not be {@literal null}. + */ + public GeoResults(List> results) { + this(results, (Metric) null); + } + + public GeoResults(List> results, Metric metric) { + this(results, calculateAverageDistance(results, metric)); + } + + /** + * Creates a new {@link GeoResults} instance from the given {@link GeoResult}s and average distance. + * + * @param results must not be {@literal null}. + * @param averageDistance + */ + @PersistenceConstructor + public GeoResults(List> results, Distance averageDistance) { + Assert.notNull(results); + this.results = results; + this.averageDistance = averageDistance; + } + + /** + * @return the averageDistance + */ + public Distance getAverageDistance() { + return averageDistance; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + public Iterator> iterator() { + return results.iterator(); + } + + /** + * Returns the actual + * + * @return + */ + public List> getContent() { + return Collections.unmodifiableList(results); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + + GeoResults that = (GeoResults) obj; + + return this.results.equals(that.results) && this.averageDistance == that.averageDistance; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = 17; + result += 31 * results.hashCode(); + result += 31 * averageDistance.hashCode(); + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("GeoResults: [averageDistance: %s, results: %s]", averageDistance.toString(), + StringUtils.collectionToCommaDelimitedString(results)); + } + + private static Distance calculateAverageDistance(List> results, Metric metric) { + + if (results.isEmpty()) { + return new Distance(0, null); + } + + double averageDistance = 0; + + for (GeoResult result : results) { + averageDistance += result.getDistance().getValue(); + } + + return new Distance(averageDistance / results.size(), metric); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metric.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metric.java new file mode 100644 index 000000000..300c19cc0 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metric.java @@ -0,0 +1,16 @@ +package org.springframework.data.mongodb.core.geo; + +/** + * Interface for {@link Metric}s that can be applied to a base scale. + * + * @author Oliver Gierke + */ +public interface Metric { + + /** + * Returns the multiplier to calculate metrics values from a base scale. + * + * @return + */ + double getMultiplier(); +} \ No newline at end of file diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metrics.java new file mode 100644 index 000000000..3a04995fa --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Metrics.java @@ -0,0 +1,27 @@ +package org.springframework.data.mongodb.core.geo; + +import org.springframework.data.mongodb.core.query.NearQuery; + +/** + * Commonly used {@link Metrics} for {@link NearQuery}s. + * + * @author Oliver Gierke + */ +public enum Metrics implements Metric { + + KILOMETERS(6378.137), MILES(3963.191), NEUTRAL(1); + + private final double multiplier; + + private Metrics(double multiplier) { + this.multiplier = multiplier; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.Metric#getMultiplier() + */ + public double getMultiplier() { + return multiplier; + } +} \ No newline at end of file diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java new file mode 100644 index 000000000..e5b6add78 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -0,0 +1,266 @@ +/* + * 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.query; + +import org.springframework.data.mongodb.core.geo.Distance; +import org.springframework.data.mongodb.core.geo.Metric; +import org.springframework.data.mongodb.core.geo.Metrics; +import org.springframework.data.mongodb.core.geo.Point; +import org.springframework.util.Assert; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Builder class to build near-queries. + * + * @author Oliver Gierke + */ +public class NearQuery { + + private final DBObject criteria; + private Query query; + private Double maxDistance; + private Metric metric; + + /** + * Creates a new {@link NearQuery}. + * + * @param point + */ + private NearQuery(Point point, Metric metric) { + + Assert.notNull(point); + + this.criteria = new BasicDBObject(); + this.criteria.put("near", point.asArray()); + + this.metric = metric; + if (metric != null) { + spherical(true); + distanceMultiplier(metric); + } + } + + /** + * Creates a new {@link NearQuery} starting near the given coordinates. + * + * @param i + * @param j + * @return + */ + public static NearQuery near(double x, double y) { + return near(x, y, null); + } + + /** + * Creates a new {@link NearQuery} starting at the given coordinates using the given {@link Metric} to adapt given + * values to further configuration. E.g. setting a {@link #maxDistance(double)} will be interpreted as a value of the + * initially set {@link Metric}. + * + * @param x + * @param y + * @param metric + * @return + */ + public static NearQuery near(double x, double y, Metric metric) { + return near(new Point(x, y), metric); + } + + /** + * Creates a new {@link NearQuery} starting at the given {@link Point}. + * + * @param point must not be {@literal null}. + * @return + */ + public static NearQuery near(Point point) { + return near(point, null); + } + + /** + * Creates a {@link NearQuery} starting near the given {@link Point} using the given {@link Metric} to adapt given + * values to further configuration. E.g. setting a {@link #maxDistance(double)} will be interpreted as a value of the + * initially set {@link Metric}. + * + * @param point must not be {@literal null}. + * @param metric + * @return + */ + public static NearQuery near(Point point, Metric metric) { + Assert.notNull(point); + return new NearQuery(point, metric); + } + + /** + * Returns the {@link Metric} underlying the actual query. + * + * @return + */ + public Metric getMetric() { + return metric; + } + + /** + * Configures the number of results to return. + * + * @param num + * @return + */ + public NearQuery num(int num) { + this.criteria.put("num", num); + return this; + } + + /** + * Sets the max distance results shall have from the configured origin. Will normalize the given value using a + * potentially already configured {@link Metric}. + * + * @param maxDistance + * @return + */ + public NearQuery maxDistance(double maxDistance) { + this.maxDistance = getNormalizedDistance(maxDistance, this.metric); + return this; + } + + /** + * Sets the maximum distance supplied in a given metric. Will normalize the distance but not reconfigure the query's + * {@link Metric}. + * + * @param maxDistance + * @param metric must not be {@literal null}. + * @return + */ + public NearQuery maxDistance(double maxDistance, Metric metric) { + Assert.notNull(metric); + this.spherical(true); + return maxDistance(getNormalizedDistance(maxDistance, metric)); + } + + /** + * Sets the maximum distance to the given {@link Distance}. + * + * @param distance + * @return + */ + public NearQuery maxDistance(Distance distance) { + Assert.notNull(distance); + return maxDistance(distance.getValue(), distance.getMetric()); + } + + /** + * Configures a distance multiplier the resulting distances get applied. + * + * @param distanceMultiplier + * @return + */ + public NearQuery distanceMultiplier(double distanceMultiplier) { + this.criteria.put("distanceMultiplier", distanceMultiplier); + return this; + } + + /** + * Configures the distance multiplier to the multiplier of the given {@link Metric}. Does not recalculate the + * {@link #maxDistance(double)}. + * + * @param metric must not be {@literal null}. + * @return + */ + public NearQuery distanceMultiplier(Metric metric) { + Assert.notNull(metric); + return distanceMultiplier(metric.getMultiplier()); + } + + /** + * Configures whether to return spherical values for the actual distance. + * + * @param spherical + * @return + */ + public NearQuery spherical(boolean spherical) { + this.criteria.put("spherical", spherical); + return this; + } + + /** + * Will cause the results' distances being returned in kilometers. Sets {@link #distanceMultiplier(double)} and + * {@link #spherical(boolean)} accordingly. + * + * @return + */ + public NearQuery inKilometers() { + return adaptMetric(Metrics.KILOMETERS); + } + + /** + * Will cause the results' distances being returned in miles. Sets {@link #distanceMultiplier(double)} and + * {@link #spherical(boolean)} accordingly. + * + * @return + */ + public NearQuery inMiles() { + return adaptMetric(Metrics.MILES); + } + + /** + * Configures the given {@link Metric} to be used as base on for this query and recalculate the maximum distance if no + * metric was set before. + * + * @param metric + */ + private NearQuery adaptMetric(Metric metric) { + + if (this.metric == null && maxDistance != null) { + maxDistance(this.maxDistance, metric); + } + + spherical(true); + return distanceMultiplier(metric); + } + + /** + * Adds an actual query to the {@link NearQuery} to restrict the objects considered for the actual near operation. + * + * @param query + * @return + */ + public NearQuery query(Query query) { + this.query = query; + return this; + } + + /** + * Returns the {@link DBObject} built by the {@link NearQuery}. + * + * @return + */ + public DBObject toDBObject() { + + BasicDBObject dbObject = new BasicDBObject(criteria.toMap()); + if (query != null) { + dbObject.put("query", query.getQueryObject()); + } + if (maxDistance != null) { + dbObject.put("maxDistance", maxDistance); + } + + return dbObject; + } + + private double getNormalizedDistance(double distance, Metric metric) { + return metric == null ? distance : distance / metric.getMultiplier(); + } +} 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 new file mode 100644 index 000000000..ee6ef9465 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/DistanceUnitTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-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 static org.springframework.data.mongodb.core.geo.Metrics.*; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link Distance}. + * + * @author Oliver Gierke + */ +public class DistanceUnitTests { + + @Test + public void defaultsMetricToNeutralOne() { + assertThat(new Distance(2.5).getMetric(), is((Metric) Metrics.NEUTRAL)); + assertThat(new Distance(2.5, null).getMetric(), is((Metric) Metrics.NEUTRAL)); + } + + @Test + public void addsDistancesWithoutExplicitMetric() { + Distance left = new Distance(2.5, KILOMETERS); + Distance right = new Distance(2.5, KILOMETERS); + assertThat(left.add(right), is(new Distance(5.0, KILOMETERS))); + } + + @Test + public void addsDistancesWithExplicitMetric() { + Distance left = new Distance(2.5, KILOMETERS); + Distance right = new Distance(2.5, KILOMETERS); + assertThat(left.add(right, MILES), is(new Distance(3.106856281073925, MILES))); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultUnitTests.java new file mode 100644 index 000000000..7d4bb2cae --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultUnitTests.java @@ -0,0 +1,55 @@ +/* + * 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 static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.*; +import org.junit.Test; + +/** + * Unit tests for {@link GeoResult}. + * + * @author Oliver Gierke + */ +public class GeoResultUnitTests { + + GeoResult first = new GeoResult("Foo", new Distance(2.5)); + GeoResult second = new GeoResult("Foo", new Distance(2.5)); + GeoResult third = new GeoResult("Bar", new Distance(2.5)); + GeoResult fourth = new GeoResult("Foo", new Distance(5.2)); + + @Test + public void considersSameInstanceEqual() { + + assertThat(first.equals(first), is(true)); + } + + @Test + public void considersSameValuesAsEqual() { + assertThat(first.equals(second), is(true)); + assertThat(second.equals(first), is(true)); + assertThat(first.equals(third), is(false)); + assertThat(third.equals(first), is(false)); + assertThat(first.equals(fourth), is(false)); + assertThat(fourth.equals(first), is(false)); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test(expected = IllegalArgumentException.class) + public void rejectsNullContent() { + new GeoResult(null, new Distance(2.5)); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultsUnitTests.java new file mode 100644 index 000000000..9e622292f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoResultsUnitTests.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * + * @author Oliver Gierke + */ +public class GeoResultsUnitTests { + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialAppConfig.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialAppConfig.java index 89f24a24a..bca1f4bc3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialAppConfig.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialAppConfig.java @@ -33,7 +33,7 @@ public class GeoSpatialAppConfig extends AbstractMongoConfiguration { @Override @Bean public Mongo mongo() throws Exception { - return new Mongo("localhost"); + return new Mongo("127.0.0.1"); } @Bean diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialTests.java index 9da4d0175..f55ce95c7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialTests.java @@ -17,7 +17,7 @@ package org.springframework.data.mongodb.core.geo; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import java.net.UnknownHostException; import java.util.Collection; @@ -34,11 +34,9 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.Venue; -import org.springframework.data.mongodb.core.geo.Box; -import org.springframework.data.mongodb.core.geo.Circle; -import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.mongodb.core.index.GeospatialIndex; import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.monitor.ServerInfo; import org.springframework.expression.ExpressionParser; @@ -111,11 +109,13 @@ public class GeoSpatialTests { template.insert(new Venue("Maplewood, NJ", -74.2713, 40.73137)); } - /* + @Test public void geoNear() { - GeoNearResult geoNearResult = template.geoNear(new Query(Criteria.where("type").is("Office")), Venue.class, - GeoNearCriteria.near(2,3).num(10).maxDistance(10).distanceMultiplier(10).spherical(true)); - }*/ + NearQuery geoNear = NearQuery.near(-73,40, Metrics.KILOMETERS).num(10).maxDistance(150); + GeoResults geoNearResult = template.geoNear(geoNear, Venue.class); + + assertThat(geoNearResult.getContent().size(), is(not(0))); + } @Test public void withinCenter() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/GeoIndexedAppConfig.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/GeoIndexedAppConfig.java index 05a5eedc9..7d6f24f31 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/GeoIndexedAppConfig.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/GeoIndexedAppConfig.java @@ -19,7 +19,7 @@ public class GeoIndexedAppConfig extends AbstractMongoConfiguration { @Override @Bean public Mongo mongo() throws Exception { - return new Mongo("localhost"); + return new Mongo("127.0.0.1"); } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java new file mode 100644 index 000000000..f5b701388 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java @@ -0,0 +1,44 @@ +package org.springframework.data.mongodb.core.query; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.mongodb.core.geo.Metrics; + +/** + * + * @author Oliver Gierke + */ +public class NearQueryUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullPoint() { + NearQuery.near(null); + } + + @Test + public void settingUpNearWithMetricRecalculatesDistance() { + + NearQuery query = NearQuery.near(2.5, 2.5, Metrics.KILOMETERS).maxDistance(150); + + assertThat((Double) query.toDBObject().get("maxDistance"), is(0.02351783914331097)); + assertThat((Boolean) query.toDBObject().get("spherical"), is(true)); + assertThat((Double) query.toDBObject().get("distanceMultiplier"), is(Metrics.KILOMETERS.getMultiplier())); + } + + @Test + public void settingMetricRecalculatesMaxDistance() { + + NearQuery query = NearQuery.near(2.5, 2.5, Metrics.KILOMETERS).maxDistance(150); + + assertThat((Double) query.toDBObject().get("maxDistance"), is(0.02351783914331097)); + assertThat((Double) query.toDBObject().get("distanceMultiplier"), is(Metrics.KILOMETERS.getMultiplier())); + + query.inMiles(); + assertThat((Double) query.toDBObject().get("distanceMultiplier"), is(Metrics.MILES.getMultiplier())); + + NearQuery.near(2.5, 2.5).maxDistance(150).inKilometers(); + assertThat((Double) query.toDBObject().get("maxDistance"), is(0.02351783914331097)); + } +}