From cb3557a95473cc0a9cd107bb6477a0f774c310d0 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Sep 2025 11:09:59 +0200 Subject: [PATCH] Hacking: More intermediate types for path mapping fix issues with property path vs mongo path mapping by introducing segments that can represent various combinations --- .../mongodb/core/convert/QueryMapper.java | 13 +- .../data/mongodb/core/mapping/MongoPath.java | 371 +++++++++++++++--- .../data/mongodb/core/mapping/MongoPaths.java | 90 +++-- .../core/mapping/MongoPathsUnitTests.java | 262 +++++++++++++ 4 files changed, 635 insertions(+), 101 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPathsUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index fe5a57339..7b1ae9b9d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -36,7 +36,6 @@ import org.bson.BsonValue; import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; - import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Reference; @@ -57,14 +56,9 @@ import org.springframework.data.mongodb.core.aggregation.AggregationExpression; import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument; import org.springframework.data.mongodb.core.mapping.FieldName; -import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath; -import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath.MappedSegment; import org.springframework.data.mongodb.core.mapping.MongoPath; import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment; import org.springframework.data.mongodb.core.mapping.MongoPaths; -import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath; -import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath.Segment; -import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath.TargetType; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Query; @@ -1140,7 +1134,8 @@ public class QueryMapper { */ @Nullable public PropertyPath toPropertyPath( - MongoPath mongoPath, MongoPersistentEntity persistentEntity) { + + MongoPath mongoPath, MongoPersistentEntity persistentEntity) { StringBuilder path = new StringBuilder(); MongoPersistentEntity entity = persistentEntity; @@ -1184,7 +1179,6 @@ public class QueryMapper { return PropertyPath.from(path.toString(), persistentEntity.getType()); } - /** * Extension of {@link Field} to be backed with mapping metadata. * @@ -1331,7 +1325,6 @@ public class QueryMapper { return name; } - @Nullable protected PersistentPropertyPath getPath() { return propertyPath; @@ -1351,7 +1344,7 @@ public class QueryMapper { PropertyPath.from(Pattern.quote(sourceProperty.getName()), entity.getTypeInformation())); } - PropertyPath path = toPropertyPath(mongoPath, entity); + PropertyPath path = paths.mappedPath(mongoPath, entity.getTypeInformation()).propertyPath(); if (path == null || isPathToJavaLangClassProperty(path)) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPath.java index 95ce7fd12..6eaadc737 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPath.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPath.java @@ -20,10 +20,17 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment.PropertySegment; +import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath.Keyword; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ObjectUtils; @@ -50,6 +57,128 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp String segment(); + static PathSegment of(String segment) { + + Keyword keyword = Keyword.mapping.get(segment); + + if (keyword != null) { + return new KeywordSegment(keyword, new Segment(segment, false, true)); + } + + if (PositionSegment.POSITIONAL.matcher(segment).matches()) { + return new PositionSegment(new Segment(segment, true, false)); + } + + if (segment.startsWith("$")) { + return new KeywordSegment(null, new Segment(segment, false, true)); + } + + return new PropertySegment(new Segment(segment, false, false)); + } + + record Segment(String segment, boolean isNumeric, boolean isKeyword) implements PathSegment { + + } + + class KeywordSegment implements PathSegment { + + final @Nullable Keyword keyword; + final Segment segment; + + public KeywordSegment(@Nullable Keyword keyword, Segment segment) { + + this.keyword = keyword; + this.segment = segment; + } + + @Override + public boolean isNumeric() { + return false; + } + + @Override + public boolean isKeyword() { + return true; + } + + @Override + public String segment() { + return segment.segment(); + } + + @Override + public String toString() { + return segment(); + } + } + + class PositionSegment implements PathSegment { + + /** + * n numeric position
+ * $[] all positional operator for update operations,
+ * $[id] filtered positional operator for update operations,
+ * $ positional operator for update operations,
+ * $ projection operator when array index position is unknown
+ */ + private final static Pattern POSITIONAL = Pattern.compile("\\$\\[[a-zA-Z0-9]*]|\\$|\\d+"); + + final Segment segment; + + public PositionSegment(Segment segment) { + this.segment = segment; + } + + @Override + public boolean isNumeric() { + return true; + } + + @Override + public boolean isKeyword() { + return false; + } + + @Override + public String segment() { + return segment.segment(); + } + + @Override + public String toString() { + return segment(); + } + } + + class PropertySegment implements PathSegment { + + final Segment segment; + + public PropertySegment(Segment segment) { + this.segment = segment; + } + + @Override + public boolean isNumeric() { + return false; + } + + @Override + public boolean isKeyword() { + return false; + } + + @Override + public String segment() { + return segment.segment(); + } + + @Override + public String toString() { + return segment(); + } + } + } /** @@ -65,13 +194,13 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp RawMongoPath::new); private final String path; - private final List segments; + private final List segments; private RawMongoPath(String path) { this(path, segmentsOf(path)); } - RawMongoPath(String path, List segments) { + RawMongoPath(String path, List segments) { this.path = path; this.segments = List.copyOf(segments); @@ -89,24 +218,24 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp return CACHE.get(path); } - private static List segmentsOf(String path) { + private static List segmentsOf(String path) { return segmentsOf(path.split("\\.")); } - private static List segmentsOf(String[] rawSegments) { + private static List segmentsOf(String[] rawSegments) { - List segments = new ArrayList<>(rawSegments.length); + List segments = new ArrayList<>(rawSegments.length); for (String segment : rawSegments) { - segments.add(Segment.of(segment)); + segments.add(PathSegment.of(segment)); } return segments; } - public List getSegments() { + public List getSegments() { return this.segments; } - public List segments() { + public List segments() { return this.segments; } @@ -135,55 +264,8 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp return StringUtils.collectionToDelimitedString(segments, "."); } - public record Segment(String segment, boolean keyword, boolean numeric, - TargetType targetType) implements PathSegment { - - private final static Pattern POSITIONAL = Pattern.compile("\\$\\[\\d+]"); - - static Segment of(String segment) { - - Keyword keyword = Keyword.mapping.get(segment); + public enum Keyword { - if (keyword != null) { - return new Segment(segment, true, false, keyword.getType()); - } - - if (POSITIONAL.matcher(segment).matches()) { - return new Segment(segment, true, false, RawMongoPath.Keyword.$POSITIONAL.getType()); - } - - try { - // positional paths - Integer.decode(segment); - return new Segment(segment, false, true, RawMongoPath.TargetType.PROPERTY); - } catch (NumberFormatException e) { - - } - - return new Segment(segment, segment.startsWith("$"), false, RawMongoPath.TargetType.PROPERTY); - } - - @Override - public String toString() { - return segment; - } - - @Override - public boolean isNumeric() { - return numeric; - } - - @Override - public boolean isKeyword() { - return keyword; - } - } - - enum Keyword { - - $PROJECTION("$", TargetType.PROPERTY), // - $POSITIONAL("$[n]", TargetType.PROPERTY), // - $ALL_POSITIONAL("$[]", TargetType.PROPERTY), // $IN(TargetType.COLLECTION), // $NIN(TargetType.COLLECTION), // $EXISTS(TargetType.BOOLEAN), // @@ -249,16 +331,90 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp final class MappedMongoPath implements MongoPath { private final RawMongoPath source; - private final List mappedSegments; + private final TypeInformation type; + private final List segments; + private final Lazy propertyPath = Lazy.of(this::assemblePropertyPath); + private final Lazy mappedPath = Lazy.of(this::assembleMappedPath); - public MappedMongoPath(RawMongoPath source, List segments) { + public MappedMongoPath(RawMongoPath source, TypeInformation type, List segments) { this.source = source; - this.mappedSegments = segments; + this.type = type; + this.segments = segments; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MappedMongoPath that = (MappedMongoPath) o; + return source.equals(that.source) && type.equals(that.type); + } + + @Override + public int hashCode() { + return Objects.hash(source, type); + } + + public static MappedMongoPath just(RawMongoPath source) { + return new MappedMongoPath(source, TypeInformation.OBJECT, + source.segments().stream().map(it -> new MappedPropertySegment(it.segment(), it, null)).toList()); + } + + public @Nullable PropertyPath propertyPath() { + return this.propertyPath.getNullable(); + } + + private String assembleMappedPath() { + return segments.stream().map(PathSegment::segment).filter(StringUtils::hasText).collect(Collectors.joining(".")); + } + + private @Nullable PropertyPath assemblePropertyPath() { + + StringBuilder path = new StringBuilder(); + + for (PathSegment segment : segments) { + + if (segment instanceof PropertySegment) { + return null; + } + + if (segment.isKeyword() || segment.isNumeric()) { + continue; + } + + String name = segment.segment(); + if (segment instanceof MappedPropertySegment mappedSegment) { + name = mappedSegment.getSource().segment(); + } else if (segment instanceof WrappedSegment wrappedSegment) { + if (wrappedSegment.getInner() != null) { + name = wrappedSegment.getOuter().getProperty().getName() + "." + + wrappedSegment.getInner().getProperty().getName(); + } else { + name = wrappedSegment.getOuter().getProperty().getName(); + } + } + + if (!path.isEmpty()) { + path.append("."); + } + + path.append(Pattern.quote(name)); + } + + if (path.isEmpty()) { + return null; + } + + return PropertyPath.from(path.toString(), type); } @Override public String path() { - return StringUtils.collectionToDelimitedString(mappedSegments, "."); + return mappedPath.get(); } public String sourcePath() { @@ -266,15 +422,76 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp } @Override - public List segments() { - return mappedSegments; + @SuppressWarnings("unchecked") + public List segments() { + return (List) segments; } public String toString() { return path(); } - public record MappedSegment(PathSegment source, String mappedName) implements PathSegment { + public static class AssociationSegment extends MappedPropertySegment { + + public AssociationSegment(String mappedName, PathSegment source, MongoPersistentProperty property) { + super(mappedName, source, property); + } + } + + public static class WrappedSegment implements PathSegment { + + private final String mappedName; + private final MappedPropertySegment outer; + private final MappedPropertySegment inner; + + public WrappedSegment(String mappedName, MappedPropertySegment outer, MappedPropertySegment inner) { + this.mappedName = mappedName; + this.outer = outer; + this.inner = inner; + } + + public MappedPropertySegment getInner() { + return inner; + } + + public MappedPropertySegment getOuter() { + return outer; + } + + @Override + public boolean isNumeric() { + return false; + } + + @Override + public boolean isKeyword() { + return false; + } + + @Override + public String segment() { + return mappedName; + } + + @Override + public String toString() { + return segment(); + } + + + } + + public static class MappedPropertySegment implements PathSegment { + + PathSegment source; + String mappedName; + MongoPersistentProperty property; + + public MappedPropertySegment(String mappedName, PathSegment source, MongoPersistentProperty property) { + this.source = source; + this.mappedName = mappedName; + this.property = property; + } @Override public boolean isNumeric() { @@ -291,11 +508,39 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp return mappedName; } + public boolean isMappedToProperty() { + return property != null; + } + @NonNull @Override public String toString() { return mappedName; } + + public PathSegment getSource() { + return source; + } + + public void setSource(PathSegment source) { + this.source = source; + } + + public String getMappedName() { + return mappedName; + } + + public void setMappedName(String mappedName) { + this.mappedName = mappedName; + } + + public MongoPersistentProperty getProperty() { + return property; + } + + public void setProperty(MongoPersistentProperty property) { + this.property = property; + } } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPaths.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPaths.java index 07282520e..de9a3df83 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPaths.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPaths.java @@ -19,9 +19,11 @@ import java.util.ArrayList; import java.util.List; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath.MappedSegment; -import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath.Segment; -import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath.TargetType; +import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath; +import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath.MappedPropertySegment; +import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath.WrappedSegment; +import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment; +import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath; import org.springframework.data.util.TypeInformation; import org.springframework.util.ConcurrentLruCache; @@ -31,7 +33,8 @@ import org.springframework.util.ConcurrentLruCache; */ public class MongoPaths { - private final ConcurrentLruCache CACHE = new ConcurrentLruCache<>(128, this::mapFieldNames); + private final ConcurrentLruCache CACHE = new ConcurrentLruCache<>(128, + this::mapFieldNames); private final MappingContext, MongoPersistentProperty> mappingContext; public MongoPaths(MappingContext, MongoPersistentProperty> mappingContext) { @@ -42,15 +45,23 @@ public class MongoPaths { return MongoPath.RawMongoPath.parse(path); } - public MongoPath mappedPath(MongoPath path, TypeInformation type) { + public MappedMongoPath mappedPath(MongoPath path, Class type) { + return mappedPath(path, TypeInformation.of(type)); + } + + public MappedMongoPath mappedPath(MongoPath path, TypeInformation type) { - if (!(path instanceof MongoPath.RawMongoPath rawMongoPath)) { - return path; + if (path instanceof MappedMongoPath mappedPath) { + return mappedPath; } - if (!mappingContext.hasPersistentEntityFor(type.getType())) { - return path; + MongoPath.RawMongoPath rawMongoPath = (RawMongoPath) path; + + MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(type); + if (persistentEntity == null) { + return MappedMongoPath.just(rawMongoPath); } + return CACHE.get(new PathAndType(rawMongoPath, type)); } @@ -60,38 +71,61 @@ public class MongoPaths { MongoPath.MappedMongoPath mapFieldNames(PathAndType cacheKey) { MongoPath.RawMongoPath mongoPath = cacheKey.path(); - MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(cacheKey.type()); + MongoPersistentEntity root = mappingContext.getPersistentEntity(cacheKey.type()); + MongoPersistentEntity persistentEntity = root; - List segments = new ArrayList<>(mongoPath.getSegments().size()); + List segments = new ArrayList<>(mongoPath.getSegments().size()); - for (Segment segment : mongoPath.getSegments()) { + for (int i = 0; i < mongoPath.getSegments().size(); i++) { - if (persistentEntity != null && !segment.keyword() - && (segment.targetType() == TargetType.ANY || segment.targetType() == TargetType.PROPERTY)) { + EntityIndexSegment eis = segment(i, mongoPath.getSegments(), persistentEntity); + segments.add(eis.segment()); + persistentEntity = eis.entity(); + i = eis.index(); + } - MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(segment.toString()); + return new MongoPath.MappedMongoPath(mongoPath, root.getTypeInformation(), segments); + } - String name = segment.segment(); + EntityIndexSegment segment(int index, List segments, MongoPersistentEntity currentEntity) { - if (persistentProperty != null) { + PathSegment segment = segments.get(index); + MongoPersistentEntity entity = currentEntity; - if (persistentProperty.isEntity()) { - persistentEntity = mappingContext.getPersistentEntity(persistentProperty); - } + if (entity != null && !segment.isKeyword()) { - if (persistentProperty.isUnwrapped()) { - continue; - } + MongoPersistentProperty persistentProperty = entity.getPersistentProperty(segment.segment()); + + if (persistentProperty != null) { + +// if(persistentProperty.isEntity()) { + entity = mappingContext.getPersistentEntity(persistentProperty); +// } - name = persistentProperty.getFieldName(); + if (persistentProperty.isUnwrapped()) { + + if (segments.size() > index + 1) { + EntityIndexSegment inner = segment(index + 1, segments, entity); + if (inner.segment() instanceof MappedPropertySegment mappedInnerSegment) { + return new EntityIndexSegment(inner.entity(), inner.index(), + new WrappedSegment(mappedInnerSegment.getMappedName(), + new MappedPropertySegment(persistentProperty.findAnnotation(Unwrapped.class).prefix(), segment, + persistentProperty), + mappedInnerSegment)); + } + } else { + return new EntityIndexSegment(entity, index, new WrappedSegment("", new MappedPropertySegment( + persistentProperty.findAnnotation(Unwrapped.class).prefix(), segment, persistentProperty), null)); + } } - segments.add(new MappedSegment(segment, name)); - } else { - segments.add(new MappedSegment(segment, segment.segment())); + return new EntityIndexSegment(entity, index, + new MappedPropertySegment(persistentProperty.getFieldName(), segment, persistentProperty)); } } + return new EntityIndexSegment(entity, index, segment); + } - return new MongoPath.MappedMongoPath(mongoPath, segments); + record EntityIndexSegment(MongoPersistentEntity entity, int index, PathSegment segment) { } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPathsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPathsUnitTests.java new file mode 100644 index 000000000..d93ea427d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPathsUnitTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2025-present 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.mapping; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath; +import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath.MappedPropertySegment; +import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment; +import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment.PositionSegment; +import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment.PropertySegment; +import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty; +import org.springframework.data.mongodb.test.util.MongoTestMappingContext; + +/** + * Unit tests for {@link MongoPaths} + * + * @author Christoph Strobl + */ +class MongoPathsUnitTests { + + MongoPaths paths; + MongoTestMappingContext mappingContext; + + @BeforeEach + void beforeEach() { + + mappingContext = MongoTestMappingContext.newTestContext(); + paths = new MongoPaths(mappingContext); + } + + @Test // GH-4516 + void rawPathCaching() { + + MongoPath sourcePath = paths.create("inner.value.num"); + MongoPath samePathAgain = paths.create("inner.value.num"); + + assertThat(sourcePath).isSameAs(samePathAgain); + } + + @Test // GH-4516 + void mappedPathCaching() { + + MongoPath sourcePath = paths.create("inner.value.num"); + + MappedMongoPath mappedPath = paths.mappedPath(sourcePath, Outer.class); + MappedMongoPath pathMappedAgain = paths.mappedPath(sourcePath, Outer.class); + assertThat(mappedPath).isSameAs(pathMappedAgain) // + .isNotEqualTo(paths.mappedPath(sourcePath, Inner.class)); + } + + @Test // GH-4516 + void simplePath() { + + MongoPath mongoPath = paths.create("inner.value.num"); + + assertThat(mongoPath.segments()).hasOnlyElementsOfType(PathSegment.PropertySegment.class); + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + + assertThat(mappedMongoPath.path()).isEqualTo("inner.val.f_val"); + assertThat(mappedMongoPath.segments()).hasOnlyElementsOfType(MappedPropertySegment.class); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.value.num", Outer.class)); + } + + @Test // GH-4516 + void mappedPathWithArrayPosition() { + + MongoPath mongoPath = paths.create("inner.valueList.0.num"); + + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PositionSegment.class, PropertySegment.class); + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + + assertThat(mappedMongoPath.path()).isEqualTo("inner.valueList.0.f_val"); + assertThat(mappedMongoPath.segments()).hasExactlyElementsOfTypes(MappedPropertySegment.class, + MappedPropertySegment.class, PositionSegment.class, MappedPropertySegment.class); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.valueList.num", Outer.class)); + } + + @Test // GH-4516 + void mappedPathWithReferenceToNonDomainTypeField() { + + MongoPath mongoPath = paths.create("inner.valueList.0.xxx"); + + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PositionSegment.class, PropertySegment.class); + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + + assertThat(mappedMongoPath.path()).isEqualTo("inner.valueList.0.xxx"); + assertThat(mappedMongoPath.segments()).hasExactlyElementsOfTypes(MappedPropertySegment.class, + MappedPropertySegment.class, PositionSegment.class, PropertySegment.class); + assertThat(mappedMongoPath.propertyPath()).isNull(); + } + + @Test // GH-4516 + void mappedPathToPropertyWithinUnwrappedUnwrappedProperty() { + + MongoPath mongoPath = paths.create("inner.wrapper.v1"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.pre-fix-v_1"); + + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.wrapper.v1", Outer.class)); + } + + @Test // GH-4516 + void mappedPathToUnwrappedProperty() { // eg. for update mapping + + MongoPath mongoPath = paths.create("inner.wrapper"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner"); + + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.wrapper", Outer.class)); + } + + @Test // GH-4516 + void justPropertySegments() { + + MongoPath mongoPath = paths.create("inner.value"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.val"); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.value", Outer.class)); + } + + @Test // GH-4516 + void withPositionalOperatorForUpdates() { + + MongoPath mongoPath = paths.create("inner.value.$"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PositionSegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.val.$"); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.value", Outer.class)); + } + + @Test // GH-4516 + void withProjectionOperatorForArray() { + + MongoPath mongoPath = paths.create("inner.value.$.num"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PositionSegment.class, PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.val.$.f_val"); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.value.num", Outer.class)); + } + + @Test // GH-4516 + void withAllPositionalOperatorForUpdates() { + + MongoPath mongoPath = paths.create("inner.value.$[].num"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PositionSegment.class, PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.val.$[].f_val"); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.value.num", Outer.class)); + } + + @Test // GH-4516 + void withNumericFilteredPositionalOperatorForUpdates() { + + MongoPath mongoPath = paths.create("inner.value.$[1].num"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PositionSegment.class, PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.val.$[1].f_val"); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.value.num", Outer.class)); + } + + @Test // GH-4516 + void withFilteredPositionalOperatorForUpdates() { + + MongoPath mongoPath = paths.create("inner.value.$[elem].num"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PositionSegment.class, PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.val.$[elem].f_val"); + assertThat(mappedMongoPath.propertyPath()).isEqualTo(PropertyPath.from("inner.value.num", Outer.class)); + } + + @Test // GH-4516 + void unwrappedWithNonDomainTypeAndPathThatPointsToPropertyOfUnwrappedType() { + + MongoPath mongoPath = paths.create("inner.wrapper.document.v2"); + assertThat(mongoPath.segments()).hasExactlyElementsOfTypes(PropertySegment.class, PropertySegment.class, + PropertySegment.class, PropertySegment.class); + + MappedMongoPath mappedMongoPath = paths.mappedPath(mongoPath, Outer.class); + assertThat(mappedMongoPath.path()).isEqualTo("inner.pre-fix-document.v2"); + assertThat(mappedMongoPath.propertyPath()).isNull(); + } + + static class Outer { + + String id; + Inner inner; + + @DBRef // + Referenced ref; + + } + + static class Inner { + + @Field("val") // + Value value; + + @Unwrapped(prefix = "pre-fix-", onEmpty = OnEmpty.USE_NULL) // + Wrapper wrapper; + + List valueList; + } + + static class Referenced { + + @Id String id; + String value; + } + + static class Wrapper { + + @Field("v_1") String v1; + String v2; + org.bson.Document document; + } + + static class Value { + + String s_val; + + @Field("f_val") Float num; + } +}