Browse Source

Hacking: More intermediate types for path mapping

fix issues with property path vs mongo path mapping by introducing segments that can represent various combinations
issue/4516
Christoph Strobl 3 months ago
parent
commit
cb3557a954
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 13
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  2. 371
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPath.java
  3. 90
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPaths.java
  4. 262
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPathsUnitTests.java

13
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.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Reference; 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.aggregation.RelaxedTypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument; import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
import org.springframework.data.mongodb.core.mapping.FieldName; 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;
import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment; import org.springframework.data.mongodb.core.mapping.MongoPath.PathSegment;
import org.springframework.data.mongodb.core.mapping.MongoPaths; 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.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
@ -1140,7 +1134,8 @@ public class QueryMapper {
*/ */
@Nullable @Nullable
public PropertyPath toPropertyPath( public PropertyPath toPropertyPath(
MongoPath mongoPath, MongoPersistentEntity<?> persistentEntity) {
MongoPath mongoPath, MongoPersistentEntity<?> persistentEntity) {
StringBuilder path = new StringBuilder(); StringBuilder path = new StringBuilder();
MongoPersistentEntity<?> entity = persistentEntity; MongoPersistentEntity<?> entity = persistentEntity;
@ -1184,7 +1179,6 @@ public class QueryMapper {
return PropertyPath.from(path.toString(), persistentEntity.getType()); return PropertyPath.from(path.toString(), persistentEntity.getType());
} }
/** /**
* Extension of {@link Field} to be backed with mapping metadata. * Extension of {@link Field} to be backed with mapping metadata.
* *
@ -1331,7 +1325,6 @@ public class QueryMapper {
return name; return name;
} }
@Nullable @Nullable
protected PersistentPropertyPath<MongoPersistentProperty> getPath() { protected PersistentPropertyPath<MongoPersistentProperty> getPath() {
return propertyPath; return propertyPath;
@ -1351,7 +1344,7 @@ public class QueryMapper {
PropertyPath.from(Pattern.quote(sourceProperty.getName()), entity.getTypeInformation())); 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)) { if (path == null || isPathToJavaLangClassProperty(path)) {
return null; return null;

371
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.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.data.mapping.PropertyPath; 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.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@ -50,6 +57,128 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp
String segment(); 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 <br />
* $[] all positional operator for update operations, <br />
* $[id] filtered positional operator for update operations, <br />
* $ positional operator for update operations, <br />
* $ projection operator when array index position is unknown <br />
*/
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); RawMongoPath::new);
private final String path; private final String path;
private final List<Segment> segments; private final List<PathSegment> segments;
private RawMongoPath(String path) { private RawMongoPath(String path) {
this(path, segmentsOf(path)); this(path, segmentsOf(path));
} }
RawMongoPath(String path, List<Segment> segments) { RawMongoPath(String path, List<PathSegment> segments) {
this.path = path; this.path = path;
this.segments = List.copyOf(segments); this.segments = List.copyOf(segments);
@ -89,24 +218,24 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp
return CACHE.get(path); return CACHE.get(path);
} }
private static List<Segment> segmentsOf(String path) { private static List<PathSegment> segmentsOf(String path) {
return segmentsOf(path.split("\\.")); return segmentsOf(path.split("\\."));
} }
private static List<Segment> segmentsOf(String[] rawSegments) { private static List<PathSegment> segmentsOf(String[] rawSegments) {
List<Segment> segments = new ArrayList<>(rawSegments.length); List<PathSegment> segments = new ArrayList<>(rawSegments.length);
for (String segment : rawSegments) { for (String segment : rawSegments) {
segments.add(Segment.of(segment)); segments.add(PathSegment.of(segment));
} }
return segments; return segments;
} }
public List<Segment> getSegments() { public List<PathSegment> getSegments() {
return this.segments; return this.segments;
} }
public List<Segment> segments() { public List<PathSegment> segments() {
return this.segments; return this.segments;
} }
@ -135,55 +264,8 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp
return StringUtils.collectionToDelimitedString(segments, "."); return StringUtils.collectionToDelimitedString(segments, ".");
} }
public record Segment(String segment, boolean keyword, boolean numeric, public enum Keyword {
TargetType targetType) implements PathSegment {
private final static Pattern POSITIONAL = Pattern.compile("\\$\\[\\d+]");
static Segment of(String segment) {
Keyword keyword = Keyword.mapping.get(segment);
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), // $IN(TargetType.COLLECTION), //
$NIN(TargetType.COLLECTION), // $NIN(TargetType.COLLECTION), //
$EXISTS(TargetType.BOOLEAN), // $EXISTS(TargetType.BOOLEAN), //
@ -249,16 +331,90 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp
final class MappedMongoPath implements MongoPath { final class MappedMongoPath implements MongoPath {
private final RawMongoPath source; private final RawMongoPath source;
private final List<MappedSegment> mappedSegments; private final TypeInformation<?> type;
private final List<? extends PathSegment> segments;
private final Lazy<PropertyPath> propertyPath = Lazy.of(this::assemblePropertyPath);
private final Lazy<String> mappedPath = Lazy.of(this::assembleMappedPath);
public MappedMongoPath(RawMongoPath source, List<MappedSegment> segments) { public MappedMongoPath(RawMongoPath source, TypeInformation<?> type, List<? extends PathSegment> segments) {
this.source = source; 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 @Override
public String path() { public String path() {
return StringUtils.collectionToDelimitedString(mappedSegments, "."); return mappedPath.get();
} }
public String sourcePath() { public String sourcePath() {
@ -266,15 +422,76 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp
} }
@Override @Override
public List<MappedSegment> segments() { @SuppressWarnings("unchecked")
return mappedSegments; public List<PathSegment> segments() {
return (List<PathSegment>) segments;
} }
public String toString() { public String toString() {
return path(); 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 @Override
public boolean isNumeric() { public boolean isNumeric() {
@ -291,11 +508,39 @@ public sealed interface MongoPath permits MongoPath.RawMongoPath, MongoPath.Mapp
return mappedName; return mappedName;
} }
public boolean isMappedToProperty() {
return property != null;
}
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
return mappedName; 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;
}
} }
} }
} }

90
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 java.util.List;
import org.springframework.data.mapping.context.MappingContext; 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.MappedMongoPath;
import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath.Segment; import org.springframework.data.mongodb.core.mapping.MongoPath.MappedMongoPath.MappedPropertySegment;
import org.springframework.data.mongodb.core.mapping.MongoPath.RawMongoPath.TargetType; 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.data.util.TypeInformation;
import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ConcurrentLruCache;
@ -31,7 +33,8 @@ import org.springframework.util.ConcurrentLruCache;
*/ */
public class MongoPaths { public class MongoPaths {
private final ConcurrentLruCache<PathAndType, MongoPath.MappedMongoPath> CACHE = new ConcurrentLruCache<>(128, this::mapFieldNames); private final ConcurrentLruCache<PathAndType, MongoPath.MappedMongoPath> CACHE = new ConcurrentLruCache<>(128,
this::mapFieldNames);
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext; private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
public MongoPaths(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) { public MongoPaths(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
@ -42,15 +45,23 @@ public class MongoPaths {
return MongoPath.RawMongoPath.parse(path); 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)) { if (path instanceof MappedMongoPath mappedPath) {
return path; return mappedPath;
} }
if (!mappingContext.hasPersistentEntityFor(type.getType())) { MongoPath.RawMongoPath rawMongoPath = (RawMongoPath) path;
return path;
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(type);
if (persistentEntity == null) {
return MappedMongoPath.just(rawMongoPath);
} }
return CACHE.get(new PathAndType(rawMongoPath, type)); return CACHE.get(new PathAndType(rawMongoPath, type));
} }
@ -60,38 +71,61 @@ public class MongoPaths {
MongoPath.MappedMongoPath mapFieldNames(PathAndType cacheKey) { MongoPath.MappedMongoPath mapFieldNames(PathAndType cacheKey) {
MongoPath.RawMongoPath mongoPath = cacheKey.path(); MongoPath.RawMongoPath mongoPath = cacheKey.path();
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(cacheKey.type()); MongoPersistentEntity<?> root = mappingContext.getPersistentEntity(cacheKey.type());
MongoPersistentEntity<?> persistentEntity = root;
List<MappedSegment> segments = new ArrayList<>(mongoPath.getSegments().size()); List<PathSegment> 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() EntityIndexSegment eis = segment(i, mongoPath.getSegments(), persistentEntity);
&& (segment.targetType() == TargetType.ANY || segment.targetType() == TargetType.PROPERTY)) { 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<PathSegment> segments, MongoPersistentEntity<?> currentEntity) {
if (persistentProperty != null) { PathSegment segment = segments.get(index);
MongoPersistentEntity<?> entity = currentEntity;
if (persistentProperty.isEntity()) { if (entity != null && !segment.isKeyword()) {
persistentEntity = mappingContext.getPersistentEntity(persistentProperty);
}
if (persistentProperty.isUnwrapped()) { MongoPersistentProperty persistentProperty = entity.getPersistentProperty(segment.segment());
continue;
} 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)); return new EntityIndexSegment(entity, index,
} else { new MappedPropertySegment(persistentProperty.getFieldName(), segment, persistentProperty));
segments.add(new MappedSegment(segment, segment.segment()));
} }
} }
return new EntityIndexSegment(entity, index, segment);
}
return new MongoPath.MappedMongoPath(mongoPath, segments); record EntityIndexSegment(MongoPersistentEntity<?> entity, int index, PathSegment segment) {
} }
} }

262
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<Value> 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;
}
}
Loading…
Cancel
Save