From 1c43a3d1eef90b51d18942d86b2e7b9fc4b27c1c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Jan 2015 13:55:03 +0100 Subject: [PATCH] DATAMONGO-1135 - Add support for GeoJson. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ve added special types representing GeoJson structures. This allows to use those within both queries and domain types. GeoJson types should only be used in combination with a 2dsphere index as 2d index is not able to handle the structure. Though legacy coordinate pairs and GeoJson types can be mixed inside MongoDB, we currently do not support conversion of legacy coordinates to GeoJson types. --- .../mongodb/core/convert/GeoConverters.java | 360 +++++++++++++- .../data/mongodb/core/geo/GeoJson.java | 42 ++ .../core/geo/GeoJsonGeometryCollection.java | 92 ++++ .../mongodb/core/geo/GeoJsonLineString.java | 58 +++ .../core/geo/GeoJsonMultiLineString.java | 106 +++++ .../mongodb/core/geo/GeoJsonMultiPoint.java | 105 ++++ .../mongodb/core/geo/GeoJsonMultiPolygon.java | 90 ++++ .../data/mongodb/core/geo/GeoJsonPoint.java | 72 +++ .../data/mongodb/core/geo/GeoJsonPolygon.java | 93 ++++ .../data/mongodb/core/query/Criteria.java | 37 ++ .../convert/GeoJsonConverterUnitTests.java | 450 ++++++++++++++++++ .../core/convert/QueryMapperUnitTests.java | 81 ++++ .../data/mongodb/core/geo/GeoJsonTests.java | 360 ++++++++++++++ .../mongodb/core/query/CriteriaTests.java | 47 ++ .../mongodb/test/util/BasicDbListBuilder.java | 41 ++ .../data/mongodb/test/util/IsBsonObject.java | 189 ++++++++ 16 files changed, 2210 insertions(+), 13 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJson.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonLineString.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPoint.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoJsonConverterUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/BasicDbListBuilder.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java index c6c395f1c..8b7f0f1b4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -30,9 +30,18 @@ import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; import org.springframework.data.geo.Shape; +import org.springframework.data.mongodb.core.geo.GeoJson; +import org.springframework.data.mongodb.core.geo.GeoJsonGeometryCollection; +import org.springframework.data.mongodb.core.geo.GeoJsonLineString; +import org.springframework.data.mongodb.core.geo.GeoJsonMultiLineString; +import org.springframework.data.mongodb.core.geo.GeoJsonMultiPoint; +import org.springframework.data.mongodb.core.geo.GeoJsonMultiPolygon; +import org.springframework.data.mongodb.core.geo.GeoJsonPoint; +import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.query.GeoCommand; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; @@ -43,6 +52,7 @@ import com.mongodb.DBObject; * * @author Thomas Darimont * @author Oliver Gierke + * @author Christoph Strobl * @since 1.5 */ abstract class GeoConverters { @@ -69,7 +79,17 @@ abstract class GeoConverters { , DbObjectToSphereConverter.INSTANCE // , DbObjectToPointConverter.INSTANCE // , PointToDbObjectConverter.INSTANCE // - , GeoCommandToDbObjectConverter.INSTANCE); + , GeoCommandToDbObjectConverter.INSTANCE // + , GeoJsonToDbObjectConverter.INSTANCE // + , GeoJsonPointToDbObjectConverter.INSTANCE // + , GeoJsonPolygonToDbObjectConverter.INSTANCE // + , DbObjectToGeoJsonPointConverter.INSTANCE // + , DbObjectToGeoJsonPolygonConverter.INSTANCE // + , DbObjectToGeoJsonLineStringConverter.INSTANCE // + , DbObjectToGeoJsonMultiLineStringConverter.INSTANCE // + , DbObjectToGeoJsonMultiPointConverter.INSTANCE // + , DbObjectToGeoJsonMultiPolygonConverter.INSTANCE // + , DbObjectToGeoJsonGeometryCollectionConverter.INSTANCE); } /** @@ -79,7 +99,7 @@ abstract class GeoConverters { * @since 1.5 */ @ReadingConverter - public static enum DbObjectToPointConverter implements Converter { + static enum DbObjectToPointConverter implements Converter { INSTANCE; @@ -102,7 +122,7 @@ abstract class GeoConverters { * @author Thomas Darimont * @since 1.5 */ - public static enum PointToDbObjectConverter implements Converter { + static enum PointToDbObjectConverter implements Converter { INSTANCE; @@ -123,7 +143,7 @@ abstract class GeoConverters { * @since 1.5 */ @WritingConverter - public static enum BoxToDbObjectConverter implements Converter { + static enum BoxToDbObjectConverter implements Converter { INSTANCE; @@ -152,7 +172,7 @@ abstract class GeoConverters { * @since 1.5 */ @ReadingConverter - public static enum DbObjectToBoxConverter implements Converter { + static enum DbObjectToBoxConverter implements Converter { INSTANCE; @@ -180,7 +200,7 @@ abstract class GeoConverters { * @author Thomas Darimont * @since 1.5 */ - public static enum CircleToDbObjectConverter implements Converter { + static enum CircleToDbObjectConverter implements Converter { INSTANCE; @@ -210,7 +230,7 @@ abstract class GeoConverters { * @since 1.5 */ @ReadingConverter - public static enum DbObjectToCircleConverter implements Converter { + static enum DbObjectToCircleConverter implements Converter { INSTANCE; @@ -251,7 +271,7 @@ abstract class GeoConverters { * @author Thomas Darimont * @since 1.5 */ - public static enum SphereToDbObjectConverter implements Converter { + static enum SphereToDbObjectConverter implements Converter { INSTANCE; @@ -281,7 +301,7 @@ abstract class GeoConverters { * @since 1.5 */ @ReadingConverter - public static enum DbObjectToSphereConverter implements Converter { + static enum DbObjectToSphereConverter implements Converter { INSTANCE; @@ -322,7 +342,7 @@ abstract class GeoConverters { * @author Thomas Darimont * @since 1.5 */ - public static enum PolygonToDbObjectConverter implements Converter { + static enum PolygonToDbObjectConverter implements Converter { INSTANCE; @@ -357,7 +377,7 @@ abstract class GeoConverters { * @since 1.5 */ @ReadingConverter - public static enum DbObjectToPolygonConverter implements Converter { + static enum DbObjectToPolygonConverter implements Converter { INSTANCE; @@ -392,7 +412,7 @@ abstract class GeoConverters { * @author Thomas Darimont * @since 1.5 */ - public static enum GeoCommandToDbObjectConverter implements Converter { + static enum GeoCommandToDbObjectConverter implements Converter { INSTANCE; @@ -411,6 +431,10 @@ abstract class GeoConverters { Shape shape = source.getShape(); + if (shape instanceof GeoJson) { + return GeoJsonToDbObjectConverter.INSTANCE.convert((GeoJson) shape); + } + if (shape instanceof Box) { argument.add(toList(((Box) shape).getFirst())); @@ -442,7 +466,317 @@ abstract class GeoConverters { } } + /** + * @author Christoph Strobl + * @since 1.7 + */ + @SuppressWarnings("rawtypes") + static enum GeoJsonToDbObjectConverter implements Converter { + INSTANCE; + + @Override + public DBObject convert(GeoJson source) { + + if (source == null) { + return null; + } + + DBObject dbo = new BasicDBObject("type", source.getType()); + + if (source instanceof GeoJsonGeometryCollection) { + BasicDBList dbl = new BasicDBList(); + for (GeoJson geometry : ((GeoJsonGeometryCollection) source).getCoordinates()) { + dbl.add(convert(geometry)); + } + dbo.put("geometries", dbl); + } else { + dbo.put("coordinates", convertIfNecessarry(source.getCoordinates())); + } + + return dbo; + } + + private Object convertIfNecessarry(Object candidate) { + + if (candidate instanceof GeoJson) { + return convertIfNecessarry(((GeoJson) candidate).getCoordinates()); + } + + if (candidate instanceof Iterable) { + BasicDBList dbl = new BasicDBList(); + for (Object element : (Iterable) candidate) { + dbl.add(convertIfNecessarry(element)); + } + return dbl; + } + + if (candidate instanceof Point) { + return toList((Point) candidate); + } + + return candidate; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum GeoJsonPointToDbObjectConverter implements Converter { + INSTANCE; + + @Override + public DBObject convert(GeoJsonPoint source) { + return GeoJsonToDbObjectConverter.INSTANCE.convert(source); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum GeoJsonPolygonToDbObjectConverter implements Converter { + INSTANCE; + + @Override + public DBObject convert(GeoJsonPolygon source) { + return GeoJsonToDbObjectConverter.INSTANCE.convert(source); + } + + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum DbObjectToGeoJsonPointConverter implements Converter { + INSTANCE; + + @Override + public GeoJsonPoint convert(DBObject source) { + + if (source == null) { + return null; + } + + Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "Point"), + String.format("Cannot convert type '%s' to Point.", source.get("type"))); + + List dbl = (List) source.get("coordinates"); + return new GeoJsonPoint(dbl.get(0).doubleValue(), dbl.get(1).doubleValue()); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum DbObjectToGeoJsonPolygonConverter implements Converter { + INSTANCE; + + @SuppressWarnings("rawtypes") + @Override + public GeoJsonPolygon convert(DBObject source) { + + if (source == null) { + return null; + } + + Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "Polygon"), + String.format("Cannot convert type '%s' to Polygon.", source.get("type"))); + + return toGeoJsonPolygon((BasicDBList) source.get("coordinates")); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum DbObjectToGeoJsonMultiPolygonConverter implements Converter { + INSTANCE; + + @Override + public GeoJsonMultiPolygon convert(DBObject source) { + + if (source == null) { + return null; + } + + Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiPolygon"), + String.format("Cannot convert type '%s' to MultiPolygon.", source.get("type"))); + + BasicDBList dbl = (BasicDBList) source.get("coordinates"); + List polygones = new ArrayList(); + for (Object polygon : dbl) { + polygones.add(toGeoJsonPolygon((BasicDBList) polygon)); + } + return new GeoJsonMultiPolygon(polygones); + + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum DbObjectToGeoJsonLineStringConverter implements Converter { + INSTANCE; + + @Override + public GeoJsonLineString convert(DBObject source) { + + if (source == null) { + return null; + } + + Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "LineString"), + String.format("Cannot convert type '%s' to LineString.", source.get("type"))); + + BasicDBList cords = (BasicDBList) source.get("coordinates"); + return new GeoJsonLineString(toListOfPoint(cords)); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum DbObjectToGeoJsonMultiPointConverter implements Converter { + INSTANCE; + + @Override + public GeoJsonMultiPoint convert(DBObject source) { + + if (source == null) { + return null; + } + + Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiPoint"), + String.format("Cannot convert type '%s' to MultiPoint.", source.get("type"))); + + BasicDBList cords = (BasicDBList) source.get("coordinates"); + return new GeoJsonMultiPoint(toListOfPoint(cords)); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum DbObjectToGeoJsonMultiLineStringConverter implements Converter { + INSTANCE; + + @Override + public GeoJsonMultiLineString convert(DBObject source) { + + if (source == null) { + return null; + } + + Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiLineString"), + String.format("Cannot convert type '%s' to MultiLineString.", source.get("type"))); + + List lines = new ArrayList(); + BasicDBList cords = (BasicDBList) source.get("coordinates"); + for (Object line : cords) { + lines.add(new GeoJsonLineString(toListOfPoint((BasicDBList) line))); + } + return new GeoJsonMultiLineString(lines); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static enum DbObjectToGeoJsonGeometryCollectionConverter implements Converter { + INSTANCE; + + @Override + public GeoJsonGeometryCollection convert(DBObject source) { + + if (source == null) { + return null; + } + + Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"), + String.format("Cannot convert type '%s' to GeometryCollection.", source.get("type"))); + + List> geometries = new ArrayList>(); + for (Object o : (List) source.get("geometries")) { + geometries.add(convertGeometries((DBObject) o)); + } + return new GeoJsonGeometryCollection(geometries); + + } + + private GeoJson convertGeometries(DBObject source) { + + Object type = source.get("type"); + if (ObjectUtils.nullSafeEquals(type, "Point")) { + return DbObjectToGeoJsonPointConverter.INSTANCE.convert(source); + } + + if (ObjectUtils.nullSafeEquals(type, "MultiPoint")) { + return DbObjectToGeoJsonMultiPointConverter.INSTANCE.convert(source); + } + + if (ObjectUtils.nullSafeEquals(type, "LineString")) { + return DbObjectToGeoJsonLineStringConverter.INSTANCE.convert(source); + } + + if (ObjectUtils.nullSafeEquals(type, "MultiLineString")) { + return DbObjectToGeoJsonMultiLineStringConverter.INSTANCE.convert(source); + } + + if (ObjectUtils.nullSafeEquals(type, "Polygon")) { + return DbObjectToGeoJsonPolygonConverter.INSTANCE.convert(source); + } + if (ObjectUtils.nullSafeEquals(type, "MultiPolygon")) { + return DbObjectToGeoJsonMultiPolygonConverter.INSTANCE.convert(source); + } + + throw new IllegalArgumentException(String.format("Cannot convert unknown GeoJson type %s", type)); + } + } + static List toList(Point point) { return Arrays.asList(point.getX(), point.getY()); } + + /** + * Converts a coordinate pairs nested in in {@link BasicDBList} into {@link GeoJsonPoint}s. + * + * @param listOfCoordinatePairs + * @return + * @since 1.7 + */ + @SuppressWarnings("unchecked") + static List toListOfPoint(BasicDBList listOfCoordinatePairs) { + + List points = new ArrayList(); + for (Object point : listOfCoordinatePairs) { + + Assert.isInstanceOf(List.class, point); + + List coordinatesList = (List) point; + points.add(new GeoJsonPoint(coordinatesList.get(0).doubleValue(), coordinatesList.get(1).doubleValue())); + } + return points; + } + + /** + * Converts a coordinate pairs nested in in {@link BasicDBList} into {@link GeoJsonPolygon}. + * + * @param dbl + * @return + * @since 1.7 + */ + static GeoJsonPolygon toGeoJsonPolygon(BasicDBList dbl) { + + List outer = toListOfPoint((BasicDBList) dbl.get(0)); + + return new GeoJsonPolygon(outer); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJson.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJson.java new file mode 100644 index 000000000..92e4ae2ef --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJson.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 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; + +/** + * Interface definition for structures defined in GeoJSON ({@link http://geojson.org/}) format. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface GeoJson> { + + /** + * String value representing the type of the {@link GeoJson} object. + * + * @return never {@literal null}. + * @see http://geojson.org/geojson-spec.html#geojson-objects + */ + String getType(); + + /** + * The value of the coordinates member is always an {@link Iterable}. The structure for the elements within is + * determined by {@link #getType()} of geometry. + * + * @return never {@literal null}. + * @see http://geojson.org/geojson-spec.html#geometry-objects + */ + T getCoordinates(); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java new file mode 100644 index 000000000..1b98f312b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java @@ -0,0 +1,92 @@ +/* + * Copyright 2015 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.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Defines a {@link GeoJsonGeometryCollection} that consists of a {@link List} of {@link GeoJson} objects. + * + * @author Christoph Strobl + * @since 1.7 + * @see http://geojson.org/geojson-spec.html#geometry-collection + */ +public class GeoJsonGeometryCollection implements GeoJson>> { + + private static final String TYPE = "GeometryCollection"; + private final List> geometries = new ArrayList>(); + + /** + * @param geometries + */ + public GeoJsonGeometryCollection(List> geometries) { + + Assert.notNull(geometries); + this.geometries.addAll(geometries); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getType() + */ + @Override + public String getType() { + return TYPE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getCoordinates() + */ + @Override + public Iterable> getCoordinates() { + return Collections.unmodifiableList(this.geometries); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.geometries); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof GeoJsonGeometryCollection)) { + return false; + } + GeoJsonGeometryCollection other = (GeoJsonGeometryCollection) obj; + return ObjectUtils.nullSafeEquals(this.geometries, other.geometries); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonLineString.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonLineString.java new file mode 100644 index 000000000..98a8b9901 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonLineString.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 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.List; + +import org.springframework.data.geo.Point; + +/** + * {@link GeoJsonLineString} is defined as list of at least 2 {@link Point}s. + * + * @author Christoph Strobl + * @since 1.7 + * @see http://geojson.org/geojson-spec.html#linestring + */ +public class GeoJsonLineString extends GeoJsonMultiPoint { + + private static final String TYPE = "LineString"; + + /** + * @param points must not be {@literal null} and have at least 2 entries. + */ + public GeoJsonLineString(List points) { + super(points); + } + + /** + * @param p0 must not be {@literal null} + * @param p1 must not be {@literal null} + * @param others can be {@literal null} + */ + public GeoJsonLineString(Point p0, Point p1, Point... others) { + super(p0, p1, others); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJsonMultiPoint#getType() + */ + @Override + public String getType() { + return TYPE; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java new file mode 100644 index 000000000..1bc11d4c2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 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.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.geo.Point; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link GeoJsonMultiLineString} is defined as list of {@link GeoJsonLineString}s. + * + * @author Christoph Strobl + * @since 1.7 + * @see http://geojson.org/geojson-spec.html#multilinestring + */ +public class GeoJsonMultiLineString implements GeoJson> { + + private static final String TYPE = "MultiLineString"; + private List coordinates = new ArrayList(); + + /** + * Creates new {@link GeoJsonMultiLineString}. + * + * @param lines must not be {@literal null}. + */ + @SuppressWarnings("unchecked") + public GeoJsonMultiLineString(List... lines) { + + Assert.notEmpty(lines, "Lines for MultiLineString must not be null!"); + for (List line : lines) { + this.coordinates.add(new GeoJsonLineString(line)); + } + } + + /** + * @param lines must not be {@literal null}. + */ + public GeoJsonMultiLineString(List lines) { + + Assert.notNull(lines, "Lines for MultiLineString must not be null!"); + this.coordinates.addAll(lines); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getType() + */ + @Override + public String getType() { + return TYPE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getCoordinates() + */ + @Override + public Iterable getCoordinates() { + return Collections.unmodifiableList(this.coordinates); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.coordinates); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof GeoJsonMultiLineString)) { + return false; + } + return ObjectUtils.nullSafeEquals(this.coordinates, ((GeoJsonMultiLineString) obj).coordinates); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java new file mode 100644 index 000000000..d7b974443 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015 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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.geo.Point; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link GeoJsonMultiPoint} is defined as list of {@link Point}s. + * + * @author Christoph Strobl + * @since 1.7 + * @see http://geojson.org/geojson-spec.html#multipoint + */ +public class GeoJsonMultiPoint implements GeoJson> { + + private static final String TYPE = "MultiPoint"; + private final List points; + + /** + * @param points points must not be {@literal null} and have at least 2 entries. + */ + public GeoJsonMultiPoint(List points) { + + Assert.notNull(points, "Points must not be null."); + Assert.isTrue(points.size() >= 2, "Minimum of 2 Points required."); + + this.points = new ArrayList(points); + } + + public GeoJsonMultiPoint(Point p0, Point p1, Point... others) { + + this.points = new ArrayList(); + this.points.add(p0); + this.points.add(p1); + if (!ObjectUtils.isEmpty(others)) { + this.points.addAll(Arrays.asList(others)); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getType() + */ + @Override + public String getType() { + return TYPE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getCoordinates() + */ + @Override + public List getCoordinates() { + return Collections.unmodifiableList(this.points); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.points); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof GeoJsonMultiPoint)) { + return false; + } + return ObjectUtils.nullSafeEquals(this.points, ((GeoJsonMultiPoint) obj).points); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java new file mode 100644 index 000000000..f0254394c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015 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.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link GeoJsonMultiPolygon} is defined as a list of {@link GeoJsonPolygon}s. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class GeoJsonMultiPolygon implements GeoJson> { + + private static final String TYPE = "MultiPolygon"; + private List coordinates = new ArrayList(); + + /** + * @param polygons must not be {@literal null}. + */ + public GeoJsonMultiPolygon(List polygons) { + + Assert.notNull(polygons, "Polygons for MultiPolygon must not be null!"); + this.coordinates.addAll(polygons); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getType() + */ + @Override + public String getType() { + return TYPE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getCoordinates() + */ + @Override + public List getCoordinates() { + return Collections.unmodifiableList(this.coordinates); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.coordinates); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof GeoJsonMultiPolygon)) { + return false; + } + return ObjectUtils.nullSafeEquals(this.coordinates, ((GeoJsonMultiPolygon) obj).coordinates); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPoint.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPoint.java new file mode 100644 index 000000000..ab83adb3a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPoint.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 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.Arrays; +import java.util.List; + +import org.springframework.data.geo.Point; + +/** + * {@link GeoJson} representation of {@link Point}. + * + * @author Christoph Strobl + * @since 1.7 + * @see http://geojson.org/geojson-spec.html#point + */ +public class GeoJsonPoint extends Point implements GeoJson> { + + private static final long serialVersionUID = -8026303425147474002L; + private static final String TYPE = "Point"; + + /** + * Creates {@link GeoJsonPoint} for given coordinates. + * + * @param x + * @param y + */ + public GeoJsonPoint(double x, double y) { + super(x, y); + } + + /** + * Creates {@link GeoJsonPoint} for given {@link Point}. + * + * @param point must not be {@literal null}. + */ + public GeoJsonPoint(Point point) { + super(point); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getType() + */ + @Override + public String getType() { + return TYPE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getCoordinates() + */ + @Override + public List getCoordinates() { + return Arrays.asList(Double.valueOf(getX()), Double.valueOf(getY())); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java new file mode 100644 index 000000000..5738efc4f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.geo.Point; +import org.springframework.data.geo.Polygon; +import org.springframework.util.ObjectUtils; + +/** + * {@link GeoJson} representation of {@link Polygon}. Unlike {@link Polygon} the {@link GeoJsonPolygon} requires a + * closed border. Which means that the first and last {@link Point} have to have same coordinate pairs. + * + * @author Christoph Strobl + * @since 1.7 + * @see http://geojson.org/geojson-spec.html#polygon + */ +public class GeoJsonPolygon extends Polygon implements GeoJson> { + + private static final long serialVersionUID = 3936163018187247185L; + private static final String TYPE = "Polygon"; + private List coordinates = new ArrayList(); + + /** + * Creates new {@link GeoJsonPolygon}. + * + * @param p0 must not be {@literal null}. + * @param p1 must not be {@literal null}. + * @param p2 must not be {@literal null}. + * @param p3 must not be {@literal null}. + * @param others can be {@literal null}. + */ + public GeoJsonPolygon(Point p0, Point p1, Point p2, Point p3, final Point... others) { + + this(new ArrayList(Arrays.asList(p0, p1, p2, p3)) { + private static final long serialVersionUID = 3143657022446395361L; + + { + if (!ObjectUtils.isEmpty(others)) { + for (Point p : others) { + add(p); + } + } + } + }); + } + + /** + * Creates new {@link GeoJsonPolygon}. + * + * @param points must not be {@literal null}. + */ + public GeoJsonPolygon(List points) { + + super(points); + this.coordinates.add(new GeoJsonLineString(points)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getType() + */ + @Override + public String getType() { + return TYPE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJson#getCoordinates() + */ + @Override + public List getCoordinates() { + return Collections.unmodifiableList(this.coordinates); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index f8190738f..a0066a34f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -29,6 +29,7 @@ import org.springframework.data.geo.Circle; import org.springframework.data.geo.Point; import org.springframework.data.geo.Shape; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.geo.GeoJson; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -439,6 +440,11 @@ public class Criteria implements CriteriaDefinition { * @return */ public Criteria maxDistance(double maxDistance) { + + if (createNearCriteriaForCommand("$near", maxDistance) || createNearCriteriaForCommand("$nearSphere", maxDistance)) { + return this; + } + criteria.put("$maxDistance", maxDistance); return this; } @@ -540,7 +546,13 @@ public class Criteria implements CriteriaDefinition { boolean not = false; for (String k : this.criteria.keySet()) { + Object value = this.criteria.get(k); + + if (requiresGeoJsonFormat(value)) { + value = new BasicDBObject("$geometry", value); + } + if (not) { DBObject notDbo = new BasicDBObject(); notDbo.put(k, value); @@ -593,6 +605,31 @@ public class Criteria implements CriteriaDefinition { } } + private boolean requiresGeoJsonFormat(Object value) { + return value instanceof GeoJson + || (value instanceof GeoCommand && ((GeoCommand) value).getShape() instanceof GeoJson); + } + + private boolean createNearCriteriaForCommand(String command, double maxDistance) { + + if (!criteria.containsKey(command)) { + return false; + } + + Object existingNearOperationValue = criteria.get(command); + if (existingNearOperationValue instanceof DBObject) { + ((DBObject) existingNearOperationValue).put("$maxDistance", maxDistance); + return true; + } else if (existingNearOperationValue instanceof GeoJson) { + + BasicDBObject dbo = new BasicDBObject("$geometry", existingNearOperationValue); + dbo.put("$maxDistance", maxDistance); + criteria.put(command, dbo); + return true; + } + return false; + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoJsonConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoJsonConverterUnitTests.java new file mode 100644 index 000000000..33fb1bf2c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoJsonConverterUnitTests.java @@ -0,0 +1,450 @@ +/* + * Copyright 2015 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.convert; + +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.convert.GeoConverters.DbObjectToGeoJsonLineStringConverter; +import org.springframework.data.mongodb.core.convert.GeoConverters.DbObjectToGeoJsonMultiLineStringConverter; +import org.springframework.data.mongodb.core.convert.GeoConverters.DbObjectToGeoJsonMultiPointConverter; +import org.springframework.data.mongodb.core.convert.GeoConverters.DbObjectToGeoJsonMultiPolygonConverter; +import org.springframework.data.mongodb.core.convert.GeoConverters.DbObjectToGeoJsonPointConverter; +import org.springframework.data.mongodb.core.convert.GeoConverters.DbObjectToGeoJsonPolygonConverter; +import org.springframework.data.mongodb.core.convert.GeoConverters.GeoJsonToDbObjectConverter; +import org.springframework.data.mongodb.core.geo.GeoJson; +import org.springframework.data.mongodb.core.geo.GeoJsonGeometryCollection; +import org.springframework.data.mongodb.core.geo.GeoJsonLineString; +import org.springframework.data.mongodb.core.geo.GeoJsonMultiLineString; +import org.springframework.data.mongodb.core.geo.GeoJsonMultiPoint; +import org.springframework.data.mongodb.core.geo.GeoJsonMultiPolygon; +import org.springframework.data.mongodb.core.geo.GeoJsonPoint; +import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; +import org.springframework.data.mongodb.test.util.BasicDbListBuilder; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + */ +@RunWith(Suite.class) +@SuiteClasses({ GeoJsonConverterUnitTests.GeoJsonToDbObjectConverterUnitTests.class, + GeoJsonConverterUnitTests.DbObjectToGeoJsonPointConverterUnitTests.class, + GeoJsonConverterUnitTests.DbObjectToGeoJsonPolygonConverterUnitTests.class, + GeoJsonConverterUnitTests.DbObjectToGeoJsonLineStringConverterUnitTests.class, + GeoJsonConverterUnitTests.DbObjectToGeoJsonMultiPolygonConverterUnitTests.class, + GeoJsonConverterUnitTests.DbObjectToGeoJsonMultiLineStringConverterUnitTests.class, + GeoJsonConverterUnitTests.DbObjectToGeoJsonMultiPointConverterUnitTests.class }) +public class GeoJsonConverterUnitTests { + + /* + * --- GeoJson + */ + static final GeoJsonPoint SINGLE_POINT = new GeoJsonPoint(100, 50); + + static final Point POINT_0 = new Point(0, 0); + static final Point POINT_1 = new Point(100, 0); + static final Point POINT_2 = new Point(100, 100); + static final Point POINT_3 = new Point(0, 100); + + static final GeoJsonMultiPoint MULTI_POINT = new GeoJsonMultiPoint(POINT_0, POINT_2, POINT_3); + static final GeoJsonLineString LINE_STRING = new GeoJsonLineString(POINT_0, POINT_1, POINT_2); + @SuppressWarnings("unchecked") static final GeoJsonMultiLineString MULTI_LINE_STRING = new GeoJsonMultiLineString( + Arrays.asList(POINT_0, POINT_1, POINT_2), Arrays.asList(POINT_3, POINT_0)); + static final GeoJsonPolygon POLYGON = new GeoJsonPolygon(POINT_0, POINT_1, POINT_2, POINT_3, POINT_0); + static final GeoJsonMultiPolygon MULTI_POLYGON = new GeoJsonMultiPolygon(Arrays.asList(POLYGON)); + static final GeoJsonGeometryCollection GEOMETRY_COLLECTION = new GeoJsonGeometryCollection( + Arrays.> asList(SINGLE_POINT, POLYGON)); + /* + * -- GeoJson DBObjects + */ + + // Point + static final BasicDBList SINGE_POINT_CORDS = new BasicDbListBuilder() // + .add(SINGLE_POINT.getX()) // + .add(SINGLE_POINT.getY()) // + .get(); // + static final DBObject SINGLE_POINT_DBO = new BasicDBObjectBuilder() // + .add("type", "Point") // + .add("coordinates", SINGE_POINT_CORDS)// + .get(); + + // MultiPoint + static final BasicDBList MULTI_POINT_CORDS = new BasicDbListBuilder() // + .add(new BasicDbListBuilder().add(POINT_0.getX()).add(POINT_0.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_2.getX()).add(POINT_2.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_3.getX()).add(POINT_3.getY()).get()) // + .get(); + static final DBObject MULTI_POINT_DBO = new BasicDBObjectBuilder() // + .add("type", "MultiPoint")// + .add("coordinates", MULTI_POINT_CORDS)// + .get(); + + // Polygon + static final BasicDBList POLYGON_OUTER_CORDS = new BasicDbListBuilder() // + .add(new BasicDbListBuilder().add(POINT_0.getX()).add(POINT_0.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_1.getX()).add(POINT_1.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_2.getX()).add(POINT_2.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_3.getX()).add(POINT_3.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_0.getX()).add(POINT_0.getY()).get()) // + .get(); + static final BasicDBList POLYGON_CORDS = new BasicDbListBuilder().add(POLYGON_OUTER_CORDS).get(); + static final DBObject POLYGON_DBO = new BasicDBObjectBuilder() // + .add("type", "Polygon") // + .add("coordinates", POLYGON_CORDS) // + .get(); + + // LineString + static final BasicDBList LINE_STRING_CORDS_0 = new BasicDbListBuilder() // + .add(new BasicDbListBuilder().add(POINT_0.getX()).add(POINT_0.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_1.getX()).add(POINT_1.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_2.getX()).add(POINT_2.getY()).get()) // + .get(); + static final BasicDBList LINE_STRING_CORDS_1 = new BasicDbListBuilder() // + .add(new BasicDbListBuilder().add(POINT_3.getX()).add(POINT_3.getY()).get()) // + .add(new BasicDbListBuilder().add(POINT_0.getX()).add(POINT_0.getY()).get()) // + .get(); + static final DBObject LINE_STRING_DBO = new BasicDBObjectBuilder().add("type", "LineString") + .add("coordinates", LINE_STRING_CORDS_0).get(); + + // MultiLineString + static final BasicDBList MUILT_LINE_STRING_CORDS = new BasicDbListBuilder() // + .add(LINE_STRING_CORDS_0) // + .add(LINE_STRING_CORDS_1) // + .get(); + static final DBObject MULTI_LINE_STRING_DBO = new BasicDBObjectBuilder().add("type", "MultiLineString") + .add("coordinates", MUILT_LINE_STRING_CORDS).get(); + + // MultiPolygoin + static final BasicDBList MULTI_POLYGON_CORDS = new BasicDbListBuilder().add(POLYGON_CORDS).get(); + static final DBObject MULTI_POLYGON_DBO = new BasicDBObjectBuilder().add("type", "MultiPolygon") + .add("coordinates", MULTI_POLYGON_CORDS).get(); + + // GeometryCollection + static final BasicDBList GEOMETRY_COLLECTION_GEOMETRIES = new BasicDbListBuilder() // + .add(SINGLE_POINT_DBO)// + .add(POLYGON_DBO)// + .get(); + static final DBObject GEOMETRY_COLLECTION_DBO = new BasicDBObjectBuilder().add("type", "GeometryCollection") + .add("geometries", GEOMETRY_COLLECTION_GEOMETRIES).get(); + + /** + * @author Christoph Strobl + */ + public static class DbObjectToGeoJsonPolygonConverterUnitTests { + + DbObjectToGeoJsonPolygonConverter converter = DbObjectToGeoJsonPolygonConverter.INSTANCE; + public @Rule ExpectedException expectedException = ExpectedException.none(); + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertDboCorrectly() { + assertThat(converter.convert(POLYGON_DBO), equalTo(POLYGON)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldReturnNullWhenConvertIsGivenNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldThrowExceptionWhenTypeDoesNotMatchPolygon() { + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'YouDontKonwMe' to Polygon"); + + converter.convert(new BasicDBObject("type", "YouDontKonwMe")); + } + + } + + /** + * @author Christoph Strobl + */ + public static class DbObjectToGeoJsonPointConverterUnitTests { + + DbObjectToGeoJsonPointConverter converter = DbObjectToGeoJsonPointConverter.INSTANCE; + public @Rule ExpectedException expectedException = ExpectedException.none(); + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertDboCorrectly() { + assertThat(converter.convert(SINGLE_POINT_DBO), equalTo(SINGLE_POINT)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldReturnNullWhenConvertIsGivenNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldThrowExceptionWhenTypeDoesNotMatchPoint() { + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'YouDontKonwMe' to Point"); + + converter.convert(new BasicDBObject("type", "YouDontKonwMe")); + } + } + + /** + * @author Christoph Strobl + */ + public static class DbObjectToGeoJsonLineStringConverterUnitTests { + + DbObjectToGeoJsonLineStringConverter converter = DbObjectToGeoJsonLineStringConverter.INSTANCE; + public @Rule ExpectedException expectedException = ExpectedException.none(); + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertDboCorrectly() { + assertThat(converter.convert(LINE_STRING_DBO), equalTo(LINE_STRING)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldReturnNullWhenConvertIsGivenNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldThrowExceptionWhenTypeDoesNotMatchPoint() { + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'YouDontKonwMe' to LineString"); + + converter.convert(new BasicDBObject("type", "YouDontKonwMe")); + } + } + + /** + * @author Christoph Strobl + */ + public static class DbObjectToGeoJsonMultiLineStringConverterUnitTests { + + DbObjectToGeoJsonMultiLineStringConverter converter = DbObjectToGeoJsonMultiLineStringConverter.INSTANCE; + public @Rule ExpectedException expectedException = ExpectedException.none(); + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertDboCorrectly() { + assertThat(converter.convert(MULTI_LINE_STRING_DBO), equalTo(MULTI_LINE_STRING)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldReturnNullWhenConvertIsGivenNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldThrowExceptionWhenTypeDoesNotMatchPoint() { + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'YouDontKonwMe' to MultiLineString"); + + converter.convert(new BasicDBObject("type", "YouDontKonwMe")); + } + } + + /** + * @author Christoph Strobl + */ + public static class DbObjectToGeoJsonMultiPointConverterUnitTests { + + DbObjectToGeoJsonMultiPointConverter converter = DbObjectToGeoJsonMultiPointConverter.INSTANCE; + public @Rule ExpectedException expectedException = ExpectedException.none(); + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertDboCorrectly() { + assertThat(converter.convert(MULTI_POINT_DBO), equalTo(MULTI_POINT)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldReturnNullWhenConvertIsGivenNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldThrowExceptionWhenTypeDoesNotMatchPoint() { + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'YouDontKonwMe' to MultiPoint"); + + converter.convert(new BasicDBObject("type", "YouDontKonwMe")); + } + } + + /** + * @author Christoph Strobl + */ + public static class DbObjectToGeoJsonMultiPolygonConverterUnitTests { + + DbObjectToGeoJsonMultiPolygonConverter converter = DbObjectToGeoJsonMultiPolygonConverter.INSTANCE; + public @Rule ExpectedException expectedException = ExpectedException.none(); + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertDboCorrectly() { + assertThat(converter.convert(MULTI_POLYGON_DBO), equalTo(MULTI_POLYGON)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldReturnNullWhenConvertIsGivenNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldThrowExceptionWhenTypeDoesNotMatchPoint() { + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'YouDontKonwMe' to MultiPolygon"); + + converter.convert(new BasicDBObject("type", "YouDontKonwMe")); + } + } + + /** + * @author Christoph Strobl + */ + public static class GeoJsonToDbObjectConverterUnitTests { + + GeoJsonToDbObjectConverter converter = GeoJsonToDbObjectConverter.INSTANCE; + + /** + * @see DATAMONGO-1135 + */ + public void convertShouldReturnNullWhenGivenNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void shouldConvertGeoJsonPointCorrectly() { + assertThat(converter.convert(SINGLE_POINT), equalTo(SINGLE_POINT_DBO)); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void shouldConvertGeoJsonPolygonCorrectly() { + assertThat(converter.convert(POLYGON), equalTo(POLYGON_DBO)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertGeoJsonLineStringCorrectly() { + assertThat(converter.convert(LINE_STRING), equalTo(LINE_STRING_DBO)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertGeoJsonMultiLineStringCorrectly() { + assertThat(converter.convert(MULTI_LINE_STRING), equalTo(MULTI_LINE_STRING_DBO)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertGeoJsonMultiPointCorrectly() { + assertThat(converter.convert(MULTI_POINT), equalTo(MULTI_POINT_DBO)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertGeoJsonMultiPolygonCorrectly() { + assertThat(converter.convert(MULTI_POLYGON), equalTo(MULTI_POLYGON_DBO)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouldConvertGeometryCollectionCorrectly() { + assertThat(converter.convert(GEOMETRY_COLLECTION), equalTo(GEOMETRY_COLLECTION_DBO)); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index eaf70bda0..c0ef246a9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -20,6 +20,7 @@ import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import java.math.BigInteger; import java.util.ArrayList; @@ -36,9 +37,12 @@ import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.geo.Point; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.Person; +import org.springframework.data.mongodb.core.geo.GeoJsonPoint; +import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; @@ -702,6 +706,75 @@ public class QueryMapperUnitTests { assertThat(dbo, equalTo(new BasicDBObjectBuilder().add("nested.id", 1).get())); } + /** + * @see DATAMONGO-1135 + */ + @Test + public void nearShouldUseGeoJsonRepresentationOnUnmappedProperty() { + + Query query = query(where("foo").near(new GeoJsonPoint(100, 50))); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(ClassWithGeoTypes.class)); + + assertThat(dbo, isBsonObject().containing("foo.$near.$geometry.type", "Point")); + assertThat(dbo, isBsonObject().containing("foo.$near.$geometry.coordinates.[0]", 100D)); + assertThat(dbo, isBsonObject().containing("foo.$near.$geometry.coordinates.[1]", 50D)); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void nearShouldUseGeoJsonRepresentationWhenMappingToGoJsonType() { + + Query query = query(where("geoJsonPoint").near(new GeoJsonPoint(100, 50))); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(ClassWithGeoTypes.class)); + + assertThat(dbo, isBsonObject().containing("geoJsonPoint.$near.$geometry.type", "Point")); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void nearSphereShouldUseGeoJsonRepresentationWhenMappingToGoJsonType() { + + Query query = query(where("geoJsonPoint").nearSphere(new GeoJsonPoint(100, 50))); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(ClassWithGeoTypes.class)); + + assertThat(dbo, isBsonObject().containing("geoJsonPoint.$nearSphere.$geometry.type", "Point")); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void shouldMapNameCorrectlyForGeoJsonType() { + + Query query = query(where("namedGeoJsonPoint").nearSphere(new GeoJsonPoint(100, 50))); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(ClassWithGeoTypes.class)); + + assertThat(dbo, + isBsonObject().containing("geoJsonPointWithNameViaFieldAnnotation.$nearSphere.$geometry.type", "Point")); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void withinShouldUseGeoJsonPolygonWhenMappingPolygonOn2DSphereIndex() { + + Query query = query(where("geoJsonPoint").within( + new GeoJsonPolygon(new Point(0, 0), new Point(100, 100), new Point(100, 0), new Point(0, 0)))); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(ClassWithGeoTypes.class)); + + assertThat(dbo, isBsonObject().containing("geoJsonPoint.$geoWithin.$geometry.type", "Polygon")); + } + @Document public class Foo { @Id private ObjectId id; @@ -794,4 +867,12 @@ public class QueryMapperUnitTests { @Field("id") String id; } + + static class ClassWithGeoTypes { + + double[] justAnArray; + Point legacyPoint; + GeoJsonPoint geoJsonPoint; + @Field("geoJsonPointWithNameViaFieldAnnotation") GeoJsonPoint namedGeoJsonPoint; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java new file mode 100644 index 000000000..0d97e73d5 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java @@ -0,0 +1,360 @@ +/* + * Copyright 2015 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.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Metric; +import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.config.AbstractMongoConfiguration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; +import org.springframework.data.mongodb.core.index.GeospatialIndex; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.query.NearQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.mongodb.Mongo; +import com.mongodb.MongoClient; +import com.mongodb.WriteConcern; + +/** + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class GeoJsonTests { + + @Configuration + static class TestConfig extends AbstractMongoConfiguration { + + @Override + protected String getDatabaseName() { + return "database"; + } + + @Override + public Mongo mongo() throws Exception { + return new MongoClient(); + } + } + + @Autowired MongoTemplate template; + + @Before + public void setUp() { + + template.setWriteConcern(WriteConcern.FSYNC_SAFE); + + createIndex(); + addVenues(); + } + + @After + public void tearDown() { + + dropIndex(); + removeCollections(); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void geoNear() { + + NearQuery geoNear = NearQuery.near(new GeoJsonPoint(-73, 40), Metrics.KILOMETERS).num(10).maxDistance(150); + + GeoResults result = template.geoNear(geoNear, Venue2DSphere.class); + + assertThat(result.getContent().size(), is(not(0))); + assertThat(result.getAverageDistance().getMetric(), is((Metric) Metrics.KILOMETERS)); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void withinPolygon() { + + Point first = new Point(-73.99756, 40.73083); + Point second = new Point(-73.99756, 40.741404); + Point third = new Point(-73.988135, 40.741404); + Point fourth = new Point(-73.988135, 40.73083); + + GeoJsonPolygon polygon = new GeoJsonPolygon(first, second, third, fourth, first); + + List venues = template.find(query(where("location").within(polygon)), Venue2DSphere.class); + assertThat(venues.size(), is(4)); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void nearPoint() { + + GeoJsonPoint point = new GeoJsonPoint(-73.99171, 40.738868); + + Query query = query(where("location").near(point).maxDistance(0.01)); + List venues = template.find(query, Venue2DSphere.class); + assertThat(venues.size(), is(1)); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void nearSphere() { + + GeoJsonPoint point = new GeoJsonPoint(-73.99171, 40.738868); + + Query query = query(where("location").nearSphere(point).maxDistance(0.003712240453784)); + List venues = template.find(query, Venue2DSphere.class); + + assertThat(venues.size(), is(1)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouleSaveAndRetrieveDocumentWithGeoJsonPointTypeCorrectly() { + + DocumentWithPropertyUsingGeoJsonType obj = new DocumentWithPropertyUsingGeoJsonType(); + obj.id = "geoJsonPoint"; + obj.geoJsonPoint = new GeoJsonPoint(100, 50); + + template.save(obj); + + DocumentWithPropertyUsingGeoJsonType result = template.findOne(query(where("id").is(obj.id)), + DocumentWithPropertyUsingGeoJsonType.class); + + assertThat(result.geoJsonPoint, equalTo(obj.geoJsonPoint)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouleSaveAndRetrieveDocumentWithGeoJsonPolygonTypeCorrectly() { + + DocumentWithPropertyUsingGeoJsonType obj = new DocumentWithPropertyUsingGeoJsonType(); + obj.id = "geoJsonPolygon"; + obj.geoJsonPolygon = new GeoJsonPolygon(new Point(0, 0), new Point(0, 1), new Point(1, 1), new Point(1, 0), + new Point(0, 0)); + + template.save(obj); + + DocumentWithPropertyUsingGeoJsonType result = template.findOne(query(where("id").is(obj.id)), + DocumentWithPropertyUsingGeoJsonType.class); + + assertThat(result.geoJsonPolygon, equalTo(obj.geoJsonPolygon)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouleSaveAndRetrieveDocumentWithGeoJsonLineStringTypeCorrectly() { + + DocumentWithPropertyUsingGeoJsonType obj = new DocumentWithPropertyUsingGeoJsonType(); + obj.id = "geoJsonLineString"; + obj.geoJsonLineString = new GeoJsonLineString(new Point(0, 0), new Point(0, 1), new Point(1, 1)); + + template.save(obj); + + DocumentWithPropertyUsingGeoJsonType result = template.findOne(query(where("id").is(obj.id)), + DocumentWithPropertyUsingGeoJsonType.class); + + assertThat(result.geoJsonLineString, equalTo(obj.geoJsonLineString)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouleSaveAndRetrieveDocumentWithGeoJsonMultiLineStringTypeCorrectly() { + + DocumentWithPropertyUsingGeoJsonType obj = new DocumentWithPropertyUsingGeoJsonType(); + obj.id = "geoJsonMultiLineString"; + obj.geoJsonMultiLineString = new GeoJsonMultiLineString(Arrays.asList(new GeoJsonLineString(new Point(0, 0), + new Point(0, 1), new Point(1, 1)), new GeoJsonLineString(new Point(199, 0), new Point(2, 3)))); + + template.save(obj); + + DocumentWithPropertyUsingGeoJsonType result = template.findOne(query(where("id").is(obj.id)), + DocumentWithPropertyUsingGeoJsonType.class); + + assertThat(result.geoJsonMultiLineString, equalTo(obj.geoJsonMultiLineString)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouleSaveAndRetrieveDocumentWithGeoJsonMultiPointTypeCorrectly() { + + DocumentWithPropertyUsingGeoJsonType obj = new DocumentWithPropertyUsingGeoJsonType(); + obj.id = "geoJsonMultiPoint"; + obj.geoJsonMultiPoint = new GeoJsonMultiPoint(Arrays.asList(new Point(0, 0), new Point(0, 1), new Point(1, 1))); + + template.save(obj); + + DocumentWithPropertyUsingGeoJsonType result = template.findOne(query(where("id").is(obj.id)), + DocumentWithPropertyUsingGeoJsonType.class); + + assertThat(result.geoJsonMultiPoint, equalTo(obj.geoJsonMultiPoint)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouleSaveAndRetrieveDocumentWithGeoJsonMultiPolygonTypeCorrectly() { + + DocumentWithPropertyUsingGeoJsonType obj = new DocumentWithPropertyUsingGeoJsonType(); + obj.id = "geoJsonMultiPolygon"; + obj.geoJsonMultiPolygon = new GeoJsonMultiPolygon(Arrays.asList(new GeoJsonPolygon(new Point(0, 0), + new Point(0, 1), new Point(1, 1), new Point(0, 0)))); + + template.save(obj); + + DocumentWithPropertyUsingGeoJsonType result = template.findOne(query(where("id").is(obj.id)), + DocumentWithPropertyUsingGeoJsonType.class); + + assertThat(result.geoJsonMultiPolygon, equalTo(obj.geoJsonMultiPolygon)); + } + + /** + * @see DATAMONGO-1137 + */ + @Test + public void shouleSaveAndRetrieveDocumentWithGeoJsonGeometryCollectionTypeCorrectly() { + + DocumentWithPropertyUsingGeoJsonType obj = new DocumentWithPropertyUsingGeoJsonType(); + obj.id = "geoJsonGeometryCollection"; + obj.geoJsonGeometryCollection = new GeoJsonGeometryCollection(Arrays.> asList( + new GeoJsonPoint(100, 200), new GeoJsonPolygon(new Point(0, 0), new Point(0, 1), new Point(1, 1), new Point(1, + 0), new Point(0, 0)))); + + template.save(obj); + + DocumentWithPropertyUsingGeoJsonType result = template.findOne(query(where("id").is(obj.id)), + DocumentWithPropertyUsingGeoJsonType.class); + + assertThat(result.geoJsonGeometryCollection, equalTo(obj.geoJsonGeometryCollection)); + } + + private void addVenues() { + + template.insert(new Venue2DSphere("Penn Station", -73.99408, 40.75057)); + template.insert(new Venue2DSphere("10gen Office", -73.99171, 40.738868)); + template.insert(new Venue2DSphere("Flatiron Building", -73.988135, 40.741404)); + template.insert(new Venue2DSphere("Players Club", -73.997812, 40.739128)); + template.insert(new Venue2DSphere("City Bakery ", -73.992491, 40.738673)); + template.insert(new Venue2DSphere("Splash Bar", -73.992491, 40.738673)); + template.insert(new Venue2DSphere("Momofuku Milk Bar", -73.985839, 40.731698)); + template.insert(new Venue2DSphere("Shake Shack", -73.98820, 40.74164)); + template.insert(new Venue2DSphere("Penn Station", -73.99408, 40.75057)); + template.insert(new Venue2DSphere("Empire State Building", -73.98602, 40.74894)); + template.insert(new Venue2DSphere("Ulaanbaatar, Mongolia", 106.9154, 47.9245)); + template.insert(new Venue2DSphere("Maplewood, NJ", -74.2713, 40.73137)); + } + + protected void createIndex() { + dropIndex(); + template.indexOps(Venue2DSphere.class).ensureIndex( + new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_2DSPHERE)); + } + + protected void dropIndex() { + try { + template.indexOps(Venue2DSphere.class).dropIndex("location"); + } catch (Exception e) { + + } + } + + protected void removeCollections() { + template.dropCollection(Venue2DSphere.class); + template.dropCollection(DocumentWithPropertyUsingGeoJsonType.class); + } + + @Document(collection = "venue2dsphere") + static class Venue2DSphere { + + @Id private String id; + private String name; + private @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) double[] location; + + @PersistenceConstructor + public Venue2DSphere(String name, double[] location) { + this.name = name; + this.location = location; + } + + public Venue2DSphere(String name, double x, double y) { + this.name = name; + this.location = new double[] { x, y }; + } + + public String getName() { + return name; + } + + public double[] getLocation() { + return location; + } + + @Override + public String toString() { + return "Venue2DSphere [id=" + id + ", name=" + name + ", location=" + Arrays.toString(location) + "]"; + } + } + + static class DocumentWithPropertyUsingGeoJsonType { + + String id; + GeoJsonPoint geoJsonPoint; + GeoJsonPolygon geoJsonPolygon; + GeoJsonLineString geoJsonLineString; + GeoJsonMultiLineString geoJsonMultiLineString; + GeoJsonMultiPoint geoJsonMultiPoint; + GeoJsonMultiPolygon geoJsonMultiPolygon; + GeoJsonGeometryCollection geoJsonGeometryCollection; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java index a4fea2457..e42cd5365 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java @@ -19,7 +19,10 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; +import org.springframework.data.geo.Point; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.geo.GeoJsonPoint; +import org.springframework.data.mongodb.test.util.IsBsonObject; import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObjectBuilder; @@ -164,4 +167,48 @@ public class CriteriaTests { assertThat(dbo, equalTo(new BasicDBObjectBuilder().add("$not", new BasicDBObject("$lt", "foo")).get())); } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void geoJsonTypesShouldBeWrappedInGeometry() { + + DBObject dbo = new Criteria("foo").near(new GeoJsonPoint(100, 200)).getCriteriaObject(); + + assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$near.$geometry", new GeoJsonPoint(100, 200))); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void legacyCoordinateTypesShouldNotBeWrappedInGeometry() { + + DBObject dbo = new Criteria("foo").near(new Point(100, 200)).getCriteriaObject(); + + assertThat(dbo, IsBsonObject.isBsonObject().notContaining("foo.$near.$geometry")); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void maxDistanceShouldBeMappedInsideNearWhenUsedAlongWithGeoJsonType() { + + DBObject dbo = new Criteria("foo").near(new GeoJsonPoint(100, 200)).maxDistance(50D).getCriteriaObject(); + + assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$near.$maxDistance", 50D)); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void maxDistanceShouldBeMappedInsideNearSphereWhenUsedAlongWithGeoJsonType() { + + DBObject dbo = new Criteria("foo").nearSphere(new GeoJsonPoint(100, 200)).maxDistance(50D).getCriteriaObject(); + + assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$nearSphere.$maxDistance", 50D)); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/BasicDbListBuilder.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/BasicDbListBuilder.java new file mode 100644 index 000000000..f34e6650d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/BasicDbListBuilder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 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.test.util; + +import com.mongodb.BasicDBList; + +/** + * @author Christoph Strobl + */ +public class BasicDbListBuilder { + + private final BasicDBList dbl; + + public BasicDbListBuilder() { + this.dbl = new BasicDBList(); + } + + public BasicDbListBuilder add(Object value) { + + this.dbl.add(value); + return this; + } + + public BasicDBList get() { + return this.dbl; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java new file mode 100644 index 000000000..04f4b4ce9 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java @@ -0,0 +1,189 @@ +/* + * Copyright 2015 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.test.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.bson.BSONObject; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.hamcrest.core.IsEqual; +import org.springframework.data.mongodb.core.query.SerializationUtils; +import org.springframework.util.ClassUtils; + +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + * @param + */ +public class IsBsonObject extends TypeSafeMatcher { + + private List expectations = new ArrayList();; + + public static IsBsonObject isBsonObject() { + return new IsBsonObject(); + } + + @Override + protected void describeMismatchSafely(T item, Description mismatchDescription) { + mismatchDescription.appendText("was ").appendValue(SerializationUtils.serializeToJsonSafely(item)); + } + + @Override + public void describeTo(Description description) { + + for (ExpectedBsonContent expectation : expectations) { + + if (expectation.not) { + description.appendText(String.format("Path %s should not be present.", expectation.path)); + } else if (expectation.value == null) { + description.appendText(String.format("Expected to find path %s.", expectation.path)); + } else { + description.appendText(String.format("Expected to find %s for path %s.", expectation.value, expectation.path)); + } + } + + } + + @Override + protected boolean matchesSafely(T item) { + + if (expectations.isEmpty()) { + return true; + } + + for (ExpectedBsonContent expectation : expectations) { + + Object o = getValue(item, expectation.path); + + if (o == null && expectation.not) { + return true; + } + + if (o == null) { + return false; + } + + if (expectation.type != null && !ClassUtils.isAssignable(expectation.type, o.getClass())) { + return false; + } + + if (expectation.value != null && !new IsEqual(expectation.value).matches(o)) { + return false; + } + + } + return true; + } + + public IsBsonObject containing(String key) { + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + + this.expectations.add(expected); + return this; + } + + public IsBsonObject containing(String key, Class type) { + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + expected.type = type; + + this.expectations.add(expected); + return this; + } + + public IsBsonObject containing(String key, Object value) { + + if (value == null) { + return notContaining(key); + } + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + expected.type = ClassUtils.getUserClass(value); + expected.value = value; + + this.expectations.add(expected); + return this; + } + + public IsBsonObject notContaining(String key) { + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + expected.not = true; + + this.expectations.add(expected); + return this; + } + + static class ExpectedBsonContent { + String path; + Class type; + Object value; + boolean not = false; + } + + Object getValue(BSONObject source, String path) { + + String[] fragments = path.split("\\."); + if (fragments.length == 1) { + return source.get(path); + } + + Iterator it = Arrays.asList(fragments).iterator(); + + Object current = source; + while (it.hasNext()) { + + String key = it.next(); + + if (!(current instanceof BSONObject) && !key.startsWith("[")) { + return null; + } + + if (key.startsWith("[")) { + String indexNumber = key.substring(1, key.indexOf("]")); + if (current instanceof List) { + current = ((List) current).get(Integer.valueOf(indexNumber)); + } + if (!it.hasNext()) { + return current; + } + } else { + + if (current instanceof DBObject) { + current = ((DBObject) current).get(key); + } + + if (!it.hasNext()) { + return current; + } + + } + } + + throw new NoSuchElementException(String.format("Unable to find '%s' in %s.", path, source)); + } +}