diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 7c2d818c4..d0f386e7e 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -145,6 +145,13 @@ true + + com.fasterxml.jackson.core + jackson-databind + ${jackson} + true + + org.slf4j jul-to-slf4j diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java new file mode 100644 index 000000000..e7e3879d4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java @@ -0,0 +1,336 @@ +/* + * 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.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.geo.GeoModule; +import org.springframework.data.geo.Point; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * @author Christoph Strobl + * @since 1.7 + */ +public class GeoJsonModule extends GeoModule { + + public GeoJsonModule() { + super(); + + addDeserializer(GeoJsonPoint.class, new GeoJsonPointDeserializer()); + addDeserializer(GeoJsonMultiPoint.class, new GeoJsonMultiPointDeserializer()); + addDeserializer(GeoJsonLineString.class, new GeoJsonLineStringDeserializer()); + addDeserializer(GeoJsonMultiLineString.class, new GeoJsonMultiLineStringDeserializer()); + addDeserializer(GeoJsonPolygon.class, new GeoJsonPolygonDeserializer()); + addDeserializer(GeoJsonMultiPolygon.class, new GeoJsonMultiPolygonDeserializer()); + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static abstract class GeoJsonDeserializer> extends JsonDeserializer { + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + */ + @Override + public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + + JsonNode node = jp.readValueAsTree(); + JsonNode coordinates = node.get("coordinates"); + + if (coordinates != null && coordinates.isArray()) { + return doDeserialize((ArrayNode) coordinates); + } + return null; + } + + /** + * Perform the actual deserialization given the {@literal coordinates} as {@link ArrayNode}. + * + * @param coordinates + * @return + */ + protected abstract T doDeserialize(ArrayNode coordinates); + + /** + * Get the {@link GeoJsonPoint} representation of given {@link ArrayNode} assuming {@code node.[0]} represents + * {@literal x - coordinate} and {@code node.[1]} is {@literal y}. + * + * @param node can be {@literal null}. + * @return {@literal null} when given a {@code null} value. + */ + protected GeoJsonPoint toGeoJsonPoint(ArrayNode node) { + + if (node == null) { + return null; + } + + return new GeoJsonPoint(node.get(0).asDouble(), node.get(1).asDouble()); + } + + /** + * Get the {@link Point} representation of given {@link ArrayNode} assuming {@code node.[0]} represents + * {@literal x - coordinate} and {@code node.[1]} is {@literal y}. + * + * @param node can be {@literal null}. + * @return {@literal null} when given a {@code null} value. + */ + protected Point toPoint(ArrayNode node) { + + if (node == null) { + return null; + } + + return new Point(node.get(0).asDouble(), node.get(1).asDouble()); + } + + /** + * Get the points nested within given {@link ArrayNode}. + * + * @param node can be {@literal null}. + * @return {@literal empty list} when given a {@code null} value. + */ + protected List toPoints(ArrayNode node) { + + if (node == null) { + return Collections.emptyList(); + } + + List points = new ArrayList(node.size()); + + for (JsonNode coordinatePair : node) { + if (coordinatePair.isArray()) { + points.add(toPoint((ArrayNode) coordinatePair)); + } + } + return points; + } + + protected GeoJsonLineString toLineString(ArrayNode node) { + return new GeoJsonLineString(toPoints((ArrayNode) node)); + } + } + + /** + * {@link JsonDeserializer} converting GeoJSON representation of {@literal Point}. + * + *
+	 * 
+	 * { "type": "Point", "coordinates": [10.0, 20.0] }
+	 * 
+	 * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ + static class GeoJsonPointDeserializer extends GeoJsonDeserializer { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJsonModule.GeoJsonDeserializer#doDeserialize(com.fasterxml.jackson.databind.node.ArrayNode) + */ + @Override + protected GeoJsonPoint doDeserialize(ArrayNode coordinates) { + return toGeoJsonPoint(coordinates); + } + } + + /** + * {@link JsonDeserializer} converting GeoJSON representation of {@literal LineString}. + * + *
+	 * 
+	 * { 
+	 *   "type": "LineString", 
+	 *   "coordinates": [ 
+	 *     [10.0, 20.0], [30.0, 40.0], [50.0, 60.0]
+	 *   ]
+	 * }
+	 * 
+	 * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ + static class GeoJsonLineStringDeserializer extends GeoJsonDeserializer { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJsonModule.GeoJsonDeserializer#doDeserialize(com.fasterxml.jackson.databind.node.ArrayNode) + */ + @Override + protected GeoJsonLineString doDeserialize(ArrayNode coordinates) { + return new GeoJsonLineString(toPoints(coordinates)); + } + } + + /** + * {@link JsonDeserializer} converting GeoJSON representation of {@literal MultiPoint}. + * + *
+	 * 
+	 * { 
+	 *   "type": "MultiPoint", 
+	 *   "coordinates": [ 
+	 *     [10.0, 20.0], [30.0, 40.0], [50.0, 60.0]
+	 *   ]
+	 * }
+	 * 
+	 * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ + static class GeoJsonMultiPointDeserializer extends GeoJsonDeserializer { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJsonModule.GeoJsonDeserializer#doDeserialize(com.fasterxml.jackson.databind.node.ArrayNode) + */ + @Override + protected GeoJsonMultiPoint doDeserialize(ArrayNode coordinates) { + return new GeoJsonMultiPoint(toPoints(coordinates)); + } + } + + /** + * {@link JsonDeserializer} converting GeoJSON representation of {@literal MultiLineString}. + * + *
+	 * 
+	 * { 
+	 *   "type": "MultiLineString", 
+	 *   "coordinates": [
+	 *     [ [10.0, 20.0], [30.0, 40.0] ], 
+	 *     [ [50.0, 60.0] , [70.0, 80.0] ]
+	 *   ]
+	 * }
+	 * 
+	 * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ + static class GeoJsonMultiLineStringDeserializer extends GeoJsonDeserializer { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJsonModule.GeoJsonDeserializer#doDeserialize(com.fasterxml.jackson.databind.node.ArrayNode) + */ + @Override + protected GeoJsonMultiLineString doDeserialize(ArrayNode coordinates) { + + List lines = new ArrayList(coordinates.size()); + + for (JsonNode lineString : coordinates) { + + if (lineString.isArray()) { + lines.add(toLineString((ArrayNode) lineString)); + } + } + + return new GeoJsonMultiLineString(lines); + } + } + + /** + * {@link JsonDeserializer} converting GeoJSON representation of {@literal Polygon}. + * + *
+	 * 
+	 * { 
+	 *   "type": "Polygon", 
+	 *   "coordinates": [ 
+	 *     [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] 
+	 *   ]
+	 * }
+	 * 
+	 * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ + static class GeoJsonPolygonDeserializer extends GeoJsonDeserializer { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJsonModule.GeoJsonDeserializer#doDeserialize(com.fasterxml.jackson.databind.node.ArrayNode) + */ + @Override + protected GeoJsonPolygon doDeserialize(ArrayNode coordinates) { + + for (JsonNode ring : coordinates) { + + // currently we do not support holes in polygons. + GeoJsonPolygon polygon = new GeoJsonPolygon(toPoints((ArrayNode) ring)); + return polygon; + } + return null; + } + } + + /** + * {@link JsonDeserializer} converting GeoJSON representation of {@literal MultiPolygon}. + * + *
+	 * 
+	 * { 
+	 *   "type": "MultiPolygon", 
+	 *   "coordinates": [
+	 *     [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]],
+	 *     [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],
+	 *     [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]
+	 *   ]
+	 * }
+	 * 
+	 * 
+ * + * @author Christoph Strobl + * @since 1.7 + */ + static class GeoJsonMultiPolygonDeserializer extends GeoJsonDeserializer { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.GeoJsonModule.GeoJsonDeserializer#doDeserialize(com.fasterxml.jackson.databind.node.ArrayNode) + */ + @Override + protected GeoJsonMultiPolygon doDeserialize(ArrayNode coordinates) { + + List polygones = new ArrayList(coordinates.size()); + + for (JsonNode polygon : coordinates) { + for (JsonNode ring : (ArrayNode) polygon) { + polygones.add(new GeoJsonPolygon(toPoints((ArrayNode) ring))); + } + } + + return new GeoJsonMultiPolygon(polygones); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonModuleUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonModuleUnitTests.java new file mode 100644 index 000000000..97b282317 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonModuleUnitTests.java @@ -0,0 +1,137 @@ +/* + * 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.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.geo.Point; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Christoph Strobl + */ +public class GeoJsonModuleUnitTests { + + ObjectMapper mapper; + + @Before + public void setUp() { + + mapper = new ObjectMapper(); + mapper.registerModule(new GeoJsonModule()); + } + + /** + * @see DATAMONGO-1181 + */ + @Test + public void shouldDeserializeJsonPointCorrectly() throws JsonParseException, JsonMappingException, IOException { + + String json = "{ \"type\": \"Point\", \"coordinates\": [10.0, 20.0] }"; + + assertThat(mapper.readValue(json, GeoJsonPoint.class), is(new GeoJsonPoint(10D, 20D))); + } + + /** + * @see DATAMONGO-1181 + */ + @Test + public void shouldDeserializeGeoJsonLineStringCorrectly() throws JsonParseException, JsonMappingException, + IOException { + + String json = "{ \"type\": \"LineString\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}"; + + assertThat(mapper.readValue(json, GeoJsonLineString.class), + is(new GeoJsonLineString(Arrays.asList(new Point(10, 20), new Point(30, 40), new Point(50, 60))))); + } + + /** + * @see DATAMONGO-1181 + */ + @Test + public void shouldDeserializeGeoJsonMultiPointCorrectly() throws JsonParseException, JsonMappingException, + IOException { + + String json = "{ \"type\": \"MultiPoint\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}"; + + assertThat(mapper.readValue(json, GeoJsonLineString.class), + is(new GeoJsonMultiPoint(Arrays.asList(new Point(10, 20), new Point(30, 40), new Point(50, 60))))); + } + + /** + * @see DATAMONGO-1181 + */ + @Test + @SuppressWarnings("unchecked") + public void shouldDeserializeGeoJsonMultiLineStringCorrectly() throws JsonParseException, JsonMappingException, + IOException { + + String json = "{ \"type\": \"MultiLineString\", \"coordinates\": [ [ [10.0, 20.0], [30.0, 40.0] ], [ [50.0, 60.0] , [70.0, 80.0] ] ]}"; + + assertThat( + mapper.readValue(json, GeoJsonMultiLineString.class), + is(new GeoJsonMultiLineString(Arrays.asList(new Point(10, 20), new Point(30, 40)), Arrays.asList(new Point(50, + 60), new Point(70, 80))))); + } + + /** + * @see DATAMONGO-1181 + */ + @Test + public void shouldDeserializeGeoJsonPolygonCorrectly() throws JsonParseException, JsonMappingException, IOException { + + String json = "{ \"type\": \"Polygon\", \"coordinates\": [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] ]}"; + + assertThat( + mapper.readValue(json, GeoJsonPolygon.class), + is(new GeoJsonPolygon(Arrays.asList(new Point(100, 0), new Point(101, 0), new Point(101, 1), new Point(100, 1), + new Point(100, 0))))); + } + + /** + * @see DATAMONGO-1181 + */ + @Test + public void shouldDeserializeGeoJsonMultiPolygonCorrectly() throws JsonParseException, JsonMappingException, + IOException { + + String json = "{ \"type\": \"Polygon\", \"coordinates\": [" + + "[[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]]," + + "[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]," + + "[[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]"// + + "]}"; + + assertThat( + mapper.readValue(json, GeoJsonMultiPolygon.class), + is(new GeoJsonMultiPolygon(Arrays.asList( + new GeoJsonPolygon(Arrays.asList(new Point(102, 2), new Point(103, 2), new Point(103, 3), + new Point(102, 3), new Point(102, 2))), + new GeoJsonPolygon(Arrays.asList(new Point(100, 0), new Point(101, 0), new Point(101, 1), + new Point(100, 1), new Point(100, 0))), + new GeoJsonPolygon(Arrays.asList(new Point(100.2, 0.2), new Point(100.8, 0.2), new Point(100.8, 0.8), + new Point(100.2, 0.8), new Point(100.2, 0.2))))))); + + } +} diff --git a/spring-data-mongodb/template.mf b/spring-data-mongodb/template.mf index 8071550e3..67179700b 100644 --- a/spring-data-mongodb/template.mf +++ b/spring-data-mongodb/template.mf @@ -7,6 +7,7 @@ Import-Package: Export-Template: org.springframework.data.mongodb.*;version="${project.version}" Import-Template: + com.fasterxml.jackson.*;version="${jackson:[=.=.=,+1.0.0)}";resolution:=optional, com.google.common.base.*;version="[11.0.0,14.0.0)";resolution:=optional, com.mongodb.*;version="${mongo.osgi:[=.=.=,+1.0.0)}", com.mysema.query.*;version="[2.1.1, 3.0.0)";resolution:=optional,