From 603d0e871f748f37415c54e797887bda84b43a33 Mon Sep 17 00:00:00 2001 From: dragonfsky Date: Tue, 5 May 2026 23:24:38 +0800 Subject: [PATCH] Support array-backed Point mapping. Closes #4997 Signed-off-by: dragonfsky --- .../mongodb/core/convert/GeoConverters.java | 51 +++++++++++++++++++ .../core/convert/MappingMongoConverter.java | 10 ++++ .../mongodb/core/convert/QueryMapper.java | 26 ++++++++++ .../mongodb/core/convert/UpdateMapper.java | 7 +++ .../core/convert/GeoConvertersUnitTests.java | 8 +++ .../MappingMongoConverterUnitTests.java | 45 ++++++++++++++++ .../core/convert/QueryMapperUnitTests.java | 25 +++++++++ .../core/convert/UpdateMapperUnitTests.java | 19 +++++++ 8 files changed, 191 insertions(+) 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 bd9a838a32..5f839cd229 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 @@ -45,7 +45,10 @@ 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.mapping.FieldType; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.GeoCommand; +import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -60,6 +63,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Thiago Diniz da Silveira + * @author dragonfsky * @since 1.5 */ @SuppressWarnings("ConstantConditions") @@ -149,6 +153,31 @@ public Point convert(Document source) { } } + /** + * Converts a {@link List} of coordinates into a {@link Point}. + * + * @author dragonfsky + * @since 5.1 + */ + @ReadingConverter + enum ListToPointConverter implements Converter, @Nullable Point> { + + INSTANCE; + + @Override + @SuppressWarnings("NullAway") + public Point convert(List source) { + + if (ObjectUtils.isEmpty(source)) { + return null; + } + + Assert.isTrue(source.size() == 2, "Source must contain 2 elements"); + + return new Point(toPrimitiveDoubleValue(source.get(0)), toPrimitiveDoubleValue(source.get(1))); + } + } + /** * Converts a {@link Point} into a {@link List} of {@link Double}s. * @@ -728,6 +757,28 @@ static List toList(Point point) { return Arrays.asList(point.getX(), point.getY()); } + static boolean isArrayBackedPoint(MongoPersistentProperty property, @Nullable Object value) { + return value instanceof Point && isArrayBackedPointProperty(property); + } + + static boolean isArrayBackedPointProperty(MongoPersistentProperty property) { + return property.hasExplicitWriteTarget() && property.getMongoField().getFieldType() == FieldType.ARRAY + && Point.class.isAssignableFrom(property.getType()); + } + + static List writeArrayBackedPoint(Point point) { + return toList(point); + } + + @SuppressWarnings("unchecked") + static @Nullable Point readArrayBackedPoint(Object source) { + + Collection coordinates = BsonUtils.asCollection(source); + List coordinateList = coordinates instanceof List list ? list : new ArrayList<>(coordinates); + + return ListToPointConverter.INSTANCE.convert((List) coordinateList); + } + /** * Converts a coordinate pairs nested in {@link List} into {@link GeoJsonPoint}s. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index e7e43d9e0f..2aa9f0c4e2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -60,6 +60,7 @@ import org.springframework.data.convert.TypeMapper; import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.core.TypeInformation; +import org.springframework.data.geo.Point; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.InstanceCreatorMetadata; import org.springframework.data.mapping.MappingException; @@ -1339,6 +1340,11 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist return; } + if (GeoConverters.isArrayBackedPoint(property, value)) { + accessor.put(property, GeoConverters.writeArrayBackedPoint((Point) value)); + return; + } + accessor.put(property, getPotentiallyConvertedSimpleWrite(value, property.hasExplicitWriteTarget() ? property.getFieldType() : Object.class)); } @@ -2008,6 +2014,10 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider source) { + + List converted = new ArrayList<>(source.size()); + boolean hasPoint = false; + + for (Object candidate : source) { + + if (candidate instanceof Point point) { + converted.add(GeoConverters.writeArrayBackedPoint(point)); + hasPoint = true; + } else { + converted.add(candidate); + } + } + + if (hasPoint) { + return converted; + } + } + if (!conversionService.canConvert(value.getClass(), documentField.getProperty().getFieldType())) { return value; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java index d9d17b2929..34e58764fa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java @@ -31,6 +31,7 @@ import org.springframework.data.core.TypeInformation; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.geo.Point; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConversionContext.WriteOperatorContext; @@ -236,6 +237,12 @@ private boolean isQuery(@Nullable Object value) { } TypeInformation typeHint = field == null ? TypeInformation.OBJECT : field.getTypeHint(); + + if (field != null && field.getProperty() != null + && GeoConverters.isArrayBackedPoint(field.getProperty(), value)) { + return GeoConverters.writeArrayBackedPoint((Point) value); + } + return converter.convertToMongoType(value, typeHint); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java index bb0d43ddba..52992dfa11 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java @@ -39,6 +39,7 @@ * @author Thomas Darimont * @author Oliver Gierke * @author Christoph Strobl + * @author dragonfsky * @since 1.5 */ public class GeoConvertersUnitTests { @@ -152,6 +153,13 @@ public void convertsPointCorrectlyWhenUsingNonDoubleForCoordinates() { .isEqualTo(new Point(1, 2)); } + @Test // GH-4997 + public void convertsCoordinateListToPointCorrectly() { + + assertThat(ListToPointConverter.INSTANCE.convert(Arrays.asList(-73.99171, 40.738868))) + .isEqualTo(new Point(-73.99171, 40.738868)); + } + @Test // DATAMONGO-1607 public void convertsCircleCorrectlyWhenUsingNonDoubleForCoordinates() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 931b9a6cea..6dc78e5d7a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -126,6 +126,7 @@ * @author Roman Puchkovskiy * @author Heesu Jung * @author Julia Lee + * @author dragonfsky */ @ExtendWith(MockitoExtension.class) class MappingMongoConverterUnitTests { @@ -1552,6 +1553,40 @@ void shouldReadEntityWithGeoBoxCorrectly() { assertThat(result.box).isEqualTo(object.box); } + @Test // GH-4997 + void shouldReadEntityWithGeoPointFromArrayCoordinates() { + + org.bson.Document document = new org.bson.Document("point", Arrays.asList(-73.99171, 40.738868)); + + ClassWithArrayBackedGeoPoint result = converter.read(ClassWithArrayBackedGeoPoint.class, document); + + assertThat(result.point).isEqualTo(new Point(-73.99171, 40.738868)); + } + + @Test // GH-4997 + void shouldWriteArrayBackedGeoPointAsArrayCoordinates() { + + ClassWithArrayBackedGeoPoint object = new ClassWithArrayBackedGeoPoint(); + object.point = new Point(-73.99171, 40.738868); + + org.bson.Document document = new org.bson.Document(); + converter.write(object, document); + + assertThat(document.get("point")).isEqualTo(Arrays.asList(-73.99171, 40.738868)); + } + + @Test // GH-4997 + void shouldKeepDefaultGeoPointRepresentation() { + + ClassWithGeoPoint object = new ClassWithGeoPoint(); + object.point = new Point(-73.99171, 40.738868); + + org.bson.Document document = new org.bson.Document(); + converter.write(object, document); + + assertThat(document.get("point")).isEqualTo(toDocument(object.point)); + } + @Test // DATAMONGO-858 void shouldWriteEntityWithGeoPolygonCorrectly() { @@ -4059,6 +4094,16 @@ class ClassWithGeoBox { Box box; } + class ClassWithGeoPoint { + + Point point; + } + + class ClassWithArrayBackedGeoPoint { + + @Field(targetType = FieldType.ARRAY) Point point; + } + class ClassWithGeoCircle { Circle circle; 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 7e7a3c607f..677fbb8055 100755 --- 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 @@ -1000,6 +1000,30 @@ void exampleShouldBeMappedCorrectlyWhenContainingLegacyPoint() { assertThat(document).containsEntry("legacyPoint.y", 20D); } + @Test // GH-4997 + void shouldMapArrayBackedPointQueryToCoordinateArray() { + + Query query = query(where("arrayBackedPoint").is(new Point(-73.99171, 40.738868))); + + org.bson.Document document = mapper.getMappedObject(query.getQueryObject(), + context.getPersistentEntity(ClassWithGeoTypes.class)); + + assertThat(document).containsEntry("arrayBackedPoint", Arrays.asList(-73.99171, 40.738868)); + } + + @Test // GH-4997 + void shouldMapArrayBackedPointInQueryToCoordinateArrays() { + + Query query = query(where("arrayBackedPoint").in(new Point(-73.99171, 40.738868), new Point(10D, 20D))); + + org.bson.Document document = mapper.getMappedObject(query.getQueryObject(), + context.getPersistentEntity(ClassWithGeoTypes.class)); + + List> expected = Arrays.asList(Arrays.asList(-73.99171, 40.738868), + Arrays.asList(10D, 20D)); + assertThat(getAsDocument(document, "arrayBackedPoint").get("$in")).isEqualTo(expected); + } + @Test // GH-3544 void exampleWithCombinedCriteriaShouldBeMappedCorrectly() { @@ -1902,6 +1926,7 @@ static class ClassWithGeoTypes { double[] justAnArray; Point legacyPoint; + @Field(targetType = FieldType.ARRAY) Point arrayBackedPoint; GeoJsonPoint geoJsonPoint; @Field("geoJsonPointWithNameViaFieldAnnotation") GeoJsonPoint namedGeoJsonPoint; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java index 680a1b80de..9c6ead1597 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java @@ -44,10 +44,12 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.geo.Point; import org.springframework.data.mapping.MappingException; import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.Unwrapped; import org.springframework.data.mongodb.core.query.Criteria; @@ -149,6 +151,18 @@ void updateMapperShouldNotPersistTypeInformationForNullValues() { assertThat(set.get("_class")).isNull(); } + @Test // GH-4997 + void updateMapperShouldMapArrayBackedPointToCoordinateArray() { + + Update update = Update.update("point", new Point(-73.99171, 40.738868)); + + Document mappedObject = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(ClassWithArrayBackedGeoPoint.class)); + + Document set = getAsDocument(mappedObject, "$set"); + assertThat(set.get("point")).isEqualTo(Arrays.asList(-73.99171, 40.738868)); + } + @Test // DATAMONGO-407 void updateMapperShouldRetainTypeInformationForNestedCollectionElements() { @@ -1830,4 +1844,9 @@ static class WithPropertyValueConverter { @ValueConverter(ReversingValueConverter.class) String text; } + + static class ClassWithArrayBackedGeoPoint { + + @Field(targetType = FieldType.ARRAY) Point point; + } }