7 changed files with 463 additions and 13 deletions
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright 2015-2025 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 |
||||
* |
||||
* https://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.config; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.data.mongodb.core.geo.GeoJsonJackson3Module; |
||||
import org.springframework.data.web.config.SpringDataJackson3Modules; |
||||
|
||||
/** |
||||
* Configuration class to expose {@link GeoJsonJackson3Module} as a Spring bean. |
||||
* |
||||
* @author Jens Schauder |
||||
*/ |
||||
public class GeoJsonJackson3Configuration implements SpringDataJackson3Modules { |
||||
|
||||
@Bean |
||||
public GeoJsonJackson3Module geoJsonModule() { |
||||
return new GeoJsonJackson3Module(); |
||||
} |
||||
} |
||||
@ -0,0 +1,296 @@
@@ -0,0 +1,296 @@
|
||||
package org.springframework.data.mongodb.core.geo; |
||||
|
||||
import tools.jackson.core.JacksonException; |
||||
import tools.jackson.core.JsonGenerator; |
||||
import tools.jackson.core.JsonParser; |
||||
import tools.jackson.core.Version; |
||||
import tools.jackson.databind.DeserializationContext; |
||||
import tools.jackson.databind.JacksonModule; |
||||
import tools.jackson.databind.JsonNode; |
||||
import tools.jackson.databind.SerializationContext; |
||||
import tools.jackson.databind.ValueDeserializer; |
||||
import tools.jackson.databind.ValueSerializer; |
||||
import tools.jackson.databind.module.SimpleDeserializers; |
||||
import tools.jackson.databind.module.SimpleSerializers; |
||||
import tools.jackson.databind.node.ArrayNode; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
import org.springframework.data.geo.Point; |
||||
import org.springframework.util.Assert; |
||||
|
||||
public class GeoJsonJackson3Module { |
||||
|
||||
private static Version version = new Version(3, 2, 0, null, "org.springframework.data", |
||||
"spring-data-mongodb-geojson"); |
||||
|
||||
public static class Serializers extends JacksonModule { |
||||
|
||||
@Override |
||||
public String getModuleName() { |
||||
return "Spring Data MongoDB GeoJson - Serializers"; |
||||
} |
||||
|
||||
@Override |
||||
public Version version() { |
||||
|
||||
return version; |
||||
} |
||||
|
||||
@Override |
||||
public void setupModule(SetupContext ctx) { |
||||
|
||||
final SimpleSerializers serializers = new SimpleSerializers(); |
||||
|
||||
serializers.addSerializer(GeoJsonPoint.class, new GeoJsonPointSerializer()); |
||||
serializers.addSerializer(GeoJsonMultiPoint.class, new GeoJsonMultiPointSerializer()); |
||||
serializers.addSerializer(GeoJsonLineString.class, new GeoJsonLineStringSerializer()); |
||||
serializers.addSerializer(GeoJsonMultiLineString.class, new GeoJsonMultiLineStringSerializer()); |
||||
serializers.addSerializer(GeoJsonPolygon.class, new GeoJsonPolygonSerializer()); |
||||
serializers.addSerializer(GeoJsonMultiPolygon.class, new GeoJsonMultiPolygonSerializer()); |
||||
|
||||
ctx.addSerializers(serializers); |
||||
} |
||||
} |
||||
|
||||
public static class Deserializers extends JacksonModule { |
||||
|
||||
@Override |
||||
public String getModuleName() { |
||||
return "Spring Data MongoDB GeoJson - Deserializers"; |
||||
} |
||||
|
||||
@Override |
||||
public Version version() { |
||||
return version; |
||||
} |
||||
|
||||
@Override |
||||
public void setupModule(SetupContext ctx) { |
||||
|
||||
final SimpleDeserializers deserializers = new SimpleDeserializers(); |
||||
|
||||
deserializers.addDeserializer(GeoJsonPoint.class, new GeoJsonPointDeserializer()); |
||||
deserializers.addDeserializer(GeoJsonMultiPoint.class, new GeoJsonMultiPointDeserializer()); |
||||
deserializers.addDeserializer(GeoJsonLineString.class, new GeoJsonLineStringDeserializer()); |
||||
deserializers.addDeserializer(GeoJsonMultiLineString.class, new GeoJsonMultiLineStringDeserializer()); |
||||
deserializers.addDeserializer(GeoJsonPolygon.class, new GeoJsonPolygonDeserializer()); |
||||
deserializers.addDeserializer(GeoJsonMultiPolygon.class, new GeoJsonMultiPolygonDeserializer()); |
||||
|
||||
ctx.addDeserializers(deserializers); |
||||
} |
||||
} |
||||
|
||||
private abstract static class GeoJsonDeserializer<T extends GeoJson<?>> extends ValueDeserializer<T> { |
||||
|
||||
public @Nullable T deserialize(JsonParser jp, @Nullable DeserializationContext context) throws JacksonException { |
||||
|
||||
JsonNode node = jp.readValueAsTree(); |
||||
JsonNode coordinates = node.get("coordinates"); |
||||
return coordinates != null && coordinates.isArray() ? this.doDeserialize((ArrayNode) coordinates) : null; |
||||
} |
||||
|
||||
protected abstract @Nullable T doDeserialize(ArrayNode coordinates); |
||||
|
||||
protected @Nullable GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { |
||||
return node == null ? null : new GeoJsonPoint(node.get(0).asDouble(), node.get(1).asDouble()); |
||||
} |
||||
|
||||
protected @Nullable Point toPoint(@Nullable ArrayNode node) { |
||||
return node == null ? null : new Point(node.get(0).asDouble(), node.get(1).asDouble()); |
||||
} |
||||
|
||||
protected List<Point> toPoints(@Nullable ArrayNode node) { |
||||
|
||||
if (node == null) { |
||||
return Collections.emptyList(); |
||||
} else { |
||||
List<Point> points = new ArrayList<>(node.size()); |
||||
|
||||
for (JsonNode coordinatePair : node) { |
||||
|
||||
if (coordinatePair.isArray()) { |
||||
|
||||
Point point = this.toPoint((ArrayNode) coordinatePair); |
||||
|
||||
Assert.notNull(point, "Point must not be null!"); |
||||
|
||||
points.add(point); |
||||
} |
||||
} |
||||
|
||||
return points; |
||||
} |
||||
} |
||||
|
||||
protected GeoJsonLineString toLineString(ArrayNode node) { |
||||
return new GeoJsonLineString(this.toPoints(node)); |
||||
} |
||||
} |
||||
|
||||
private static class GeoJsonPointDeserializer extends GeoJsonDeserializer<GeoJsonPoint> { |
||||
|
||||
protected @Nullable GeoJsonPoint doDeserialize(ArrayNode coordinates) { |
||||
return this.toGeoJsonPoint(coordinates); |
||||
} |
||||
} |
||||
|
||||
private static class GeoJsonLineStringDeserializer extends GeoJsonDeserializer<GeoJsonLineString> { |
||||
|
||||
protected GeoJsonLineString doDeserialize(ArrayNode coordinates) { |
||||
return new GeoJsonLineString(this.toPoints(coordinates)); |
||||
} |
||||
} |
||||
|
||||
private static class GeoJsonMultiPointDeserializer extends GeoJsonDeserializer<GeoJsonMultiPoint> { |
||||
|
||||
protected GeoJsonMultiPoint doDeserialize(ArrayNode coordinates) { |
||||
return new GeoJsonMultiPoint(this.toPoints(coordinates)); |
||||
} |
||||
} |
||||
|
||||
private static class GeoJsonMultiLineStringDeserializer extends GeoJsonDeserializer<GeoJsonMultiLineString> { |
||||
|
||||
protected GeoJsonMultiLineString doDeserialize(ArrayNode coordinates) { |
||||
List<GeoJsonLineString> lines = new ArrayList<>(coordinates.size()); |
||||
|
||||
for (JsonNode lineString : coordinates) { |
||||
if (lineString.isArray()) { |
||||
lines.add(this.toLineString((ArrayNode) lineString)); |
||||
} |
||||
} |
||||
|
||||
return new GeoJsonMultiLineString(lines); |
||||
} |
||||
} |
||||
|
||||
private static class GeoJsonPolygonDeserializer extends GeoJsonDeserializer<GeoJsonPolygon> { |
||||
|
||||
protected @Nullable GeoJsonPolygon doDeserialize(ArrayNode coordinates) { |
||||
|
||||
Iterator<JsonNode> coordinateIterator = coordinates.iterator(); |
||||
if (coordinateIterator.hasNext()) { |
||||
|
||||
JsonNode ring = coordinateIterator.next(); |
||||
return new GeoJsonPolygon(this.toPoints((ArrayNode) ring)); |
||||
|
||||
} else { |
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static class GeoJsonMultiPolygonDeserializer extends GeoJsonDeserializer<GeoJsonMultiPolygon> { |
||||
|
||||
protected GeoJsonMultiPolygon doDeserialize(ArrayNode coordinates) { |
||||
List<GeoJsonPolygon> polygons = new ArrayList<>(coordinates.size()); |
||||
|
||||
for (JsonNode polygon : coordinates) { |
||||
|
||||
for (JsonNode ring : polygon) { |
||||
polygons.add(new GeoJsonPolygon(this.toPoints((ArrayNode) ring))); |
||||
} |
||||
} |
||||
|
||||
return new GeoJsonMultiPolygon(polygons); |
||||
} |
||||
} |
||||
|
||||
private abstract static class GeoJsonSerializer<T extends GeoJson<? extends Iterable<?>>> extends ValueSerializer<T> { |
||||
|
||||
@Override |
||||
public void serialize(T shape, JsonGenerator jsonGenerator, SerializationContext context) { |
||||
|
||||
jsonGenerator.writeStartObject(); |
||||
jsonGenerator.writeStringProperty("type", shape.getType()); |
||||
jsonGenerator.writeArrayPropertyStart("coordinates"); |
||||
this.doSerialize(shape, jsonGenerator); |
||||
jsonGenerator.writeEndArray(); |
||||
jsonGenerator.writeEndObject(); |
||||
} |
||||
|
||||
protected abstract void doSerialize(T shape, JsonGenerator jsonGenerator); |
||||
|
||||
protected void writePoint(Point point, JsonGenerator jsonGenerator) { |
||||
|
||||
jsonGenerator.writeStartArray(); |
||||
this.writeRawCoordinates(point, jsonGenerator); |
||||
jsonGenerator.writeEndArray(); |
||||
} |
||||
|
||||
protected void writeRawCoordinates(Point point, JsonGenerator jsonGenerator) { |
||||
|
||||
jsonGenerator.writeNumber(point.getX()); |
||||
jsonGenerator.writeNumber(point.getY()); |
||||
} |
||||
|
||||
protected void writeLine(Iterable<Point> points, JsonGenerator jsonGenerator) { |
||||
|
||||
jsonGenerator.writeStartArray(); |
||||
this.writeRawLine(points, jsonGenerator); |
||||
jsonGenerator.writeEndArray(); |
||||
} |
||||
|
||||
protected void writeRawLine(Iterable<Point> points, JsonGenerator jsonGenerator) { |
||||
for (Point point : points) { |
||||
this.writePoint(point, jsonGenerator); |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
static class GeoJsonPointSerializer extends GeoJsonSerializer<GeoJsonPoint> { |
||||
protected void doSerialize(GeoJsonPoint value, JsonGenerator jsonGenerator) { |
||||
this.writeRawCoordinates(value, jsonGenerator); |
||||
} |
||||
} |
||||
|
||||
static class GeoJsonLineStringSerializer extends GeoJsonSerializer<GeoJsonLineString> { |
||||
protected void doSerialize(GeoJsonLineString value, JsonGenerator jsonGenerator) { |
||||
this.writeRawLine(value.getCoordinates(), jsonGenerator); |
||||
} |
||||
} |
||||
|
||||
static class GeoJsonMultiPointSerializer extends GeoJsonSerializer<GeoJsonMultiPoint> { |
||||
protected void doSerialize(GeoJsonMultiPoint value, JsonGenerator jsonGenerator) { |
||||
this.writeRawLine(value.getCoordinates(), jsonGenerator); |
||||
} |
||||
} |
||||
|
||||
static class GeoJsonMultiLineStringSerializer extends GeoJsonSerializer<GeoJsonMultiLineString> { |
||||
protected void doSerialize(GeoJsonMultiLineString value, JsonGenerator jsonGenerator) { |
||||
for (GeoJsonLineString lineString : value.getCoordinates()) { |
||||
this.writeLine(lineString.getCoordinates(), jsonGenerator); |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
static class GeoJsonPolygonSerializer extends GeoJsonSerializer<GeoJsonPolygon> { |
||||
protected void doSerialize(GeoJsonPolygon value, JsonGenerator jsonGenerator) throws JacksonException { |
||||
for (GeoJsonLineString lineString : value.getCoordinates()) { |
||||
this.writeLine(lineString.getCoordinates(), jsonGenerator); |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
static class GeoJsonMultiPolygonSerializer extends GeoJsonSerializer<GeoJsonMultiPolygon> { |
||||
protected void doSerialize(GeoJsonMultiPolygon value, JsonGenerator jsonGenerator) throws JacksonException { |
||||
for (GeoJsonPolygon polygon : value.getCoordinates()) { |
||||
jsonGenerator.writeStartArray(); |
||||
|
||||
for (GeoJsonLineString lineString : polygon.getCoordinates()) { |
||||
this.writeLine(lineString.getCoordinates(), jsonGenerator); |
||||
} |
||||
|
||||
jsonGenerator.writeEndArray(); |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
@ -1,2 +1,3 @@
@@ -1,2 +1,3 @@
|
||||
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.mongodb.config.GeoJsonConfiguration |
||||
org.springframework.data.web.config.SpringDataJackson3Modules=org.springframework.data.mongodb.config.GeoJsonJackson3Configuration |
||||
org.springframework.data.repository.core.support.RepositoryFactorySupport=org.springframework.data.mongodb.repository.support.MongoRepositoryFactory |
||||
|
||||
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright 2025 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 |
||||
* |
||||
* https://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.assertj.core.api.Assertions.*; |
||||
|
||||
import tools.jackson.databind.json.JsonMapper; |
||||
|
||||
import java.util.Arrays; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.data.geo.Point; |
||||
|
||||
/** |
||||
* Tests for the {@link GeoJsonModule}. |
||||
* |
||||
* @author Jens Schauder |
||||
*/ |
||||
class GeoJsonJackson3ModuleUnitTests { |
||||
|
||||
JsonMapper mapper; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
|
||||
mapper = JsonMapper.builder() |
||||
.addModule(new GeoJsonJackson3Module.Serializers()) |
||||
.addModule(new GeoJsonJackson3Module.Deserializers()) |
||||
.build(); |
||||
} |
||||
|
||||
@Test // GH-5100
|
||||
void shouldDeserializeJsonPointCorrectly() { |
||||
|
||||
String json = "{ \"type\": \"Point\", \"coordinates\": [10.0, 20.0] }"; |
||||
|
||||
assertThat(mapper.readValue(json, GeoJsonPoint.class)).isEqualTo(new GeoJsonPoint(10D, 20D)); |
||||
} |
||||
|
||||
@Test // GH-5100
|
||||
void shouldDeserializeGeoJsonLineStringCorrectly() { |
||||
|
||||
String json = "{ \"type\": \"LineString\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}"; |
||||
|
||||
assertThat(mapper.readValue(json, GeoJsonLineString.class)) |
||||
.isEqualTo(new GeoJsonLineString(Arrays.asList(new Point(10, 20), new Point(30, 40), new Point(50, 60)))); |
||||
} |
||||
|
||||
@Test // GH-5100
|
||||
void shouldDeserializeGeoJsonMultiPointCorrectly() { |
||||
|
||||
String json = "{ \"type\": \"MultiPoint\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}"; |
||||
|
||||
assertThat(mapper.readValue(json, GeoJsonLineString.class)) |
||||
.isEqualTo(new GeoJsonMultiPoint(Arrays.asList(new Point(10, 20), new Point(30, 40), new Point(50, 60)))); |
||||
} |
||||
|
||||
@Test // GH-5100
|
||||
@SuppressWarnings("unchecked") |
||||
void shouldDeserializeGeoJsonMultiLineStringCorrectly() { |
||||
|
||||
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)).isEqualTo(new GeoJsonMultiLineString( |
||||
Arrays.asList(new Point(10, 20), new Point(30, 40)), Arrays.asList(new Point(50, 60), new Point(70, 80)))); |
||||
} |
||||
|
||||
@Test // GH-5100
|
||||
void shouldDeserializeGeoJsonPolygonCorrectly() { |
||||
|
||||
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)).isEqualTo(new GeoJsonPolygon( |
||||
Arrays.asList(new Point(100, 0), new Point(101, 0), new Point(101, 1), new Point(100, 1), new Point(100, 0)))); |
||||
} |
||||
|
||||
@Test // GH-5100
|
||||
void shouldDeserializeGeoJsonMultiPolygonCorrectly() { |
||||
|
||||
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)).isEqualTo(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)))))); |
||||
|
||||
} |
||||
} |
||||
Loading…
Reference in new issue