Skip to content

Commit 37d6603

Browse files
christophstroblmp911de
authored andcommitted
Fix property value conversion in query mapper for nested values.
Closes #4510 Original pull request: #4517
1 parent f052256 commit 37d6603

File tree

4 files changed

+148
-53
lines changed

4 files changed

+148
-53
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

+63-53
Original file line numberDiff line numberDiff line change
@@ -449,65 +449,14 @@ protected Object getMappedValue(Field documentField, Object sourceValue) {
449449
if (documentField.getProperty() != null
450450
&& converter.getCustomConversions().hasValueConverter(documentField.getProperty())) {
451451

452-
MongoConversionContext conversionContext = new MongoConversionContext(new PropertyValueProvider<>() {
453-
@Override
454-
public <T> T getPropertyValue(MongoPersistentProperty property) {
455-
throw new IllegalStateException("No enclosing property available");
456-
}
457-
}, documentField.getProperty(), converter);
458452
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter = converter
459453
.getCustomConversions().getPropertyValueConversions().getValueConverter(documentField.getProperty());
460454

461-
/* might be an $in clause with multiple entries */
462-
if (!documentField.getProperty().isCollectionLike() && sourceValue instanceof Collection<?> collection) {
463-
return collection.stream().map(it -> valueConverter.write(it, conversionContext)).collect(Collectors.toList());
464-
}
465-
466-
return valueConverter.write(value, conversionContext);
455+
return convertValue(documentField, sourceValue, value, valueConverter);
467456
}
468457

469458
if (documentField.isIdField() && !documentField.isAssociation()) {
470-
471-
if (isDBObject(value)) {
472-
DBObject valueDbo = (DBObject) value;
473-
Document resultDbo = new Document(valueDbo.toMap());
474-
475-
if (valueDbo.containsField("$in") || valueDbo.containsField("$nin")) {
476-
String inKey = valueDbo.containsField("$in") ? "$in" : "$nin";
477-
List<Object> ids = new ArrayList<>();
478-
for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
479-
ids.add(convertId(id, getIdTypeForField(documentField)));
480-
}
481-
resultDbo.put(inKey, ids);
482-
} else if (valueDbo.containsField("$ne")) {
483-
resultDbo.put("$ne", convertId(valueDbo.get("$ne"), getIdTypeForField(documentField)));
484-
} else {
485-
return getMappedObject(resultDbo, Optional.empty());
486-
}
487-
return resultDbo;
488-
}
489-
490-
else if (isDocument(value)) {
491-
Document valueDbo = (Document) value;
492-
Document resultDbo = new Document(valueDbo);
493-
494-
if (valueDbo.containsKey("$in") || valueDbo.containsKey("$nin")) {
495-
String inKey = valueDbo.containsKey("$in") ? "$in" : "$nin";
496-
List<Object> ids = new ArrayList<>();
497-
for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
498-
ids.add(convertId(id, getIdTypeForField(documentField)));
499-
}
500-
resultDbo.put(inKey, ids);
501-
} else if (valueDbo.containsKey("$ne")) {
502-
resultDbo.put("$ne", convertId(valueDbo.get("$ne"), getIdTypeForField(documentField)));
503-
} else {
504-
return getMappedObject(resultDbo, Optional.empty());
505-
}
506-
return resultDbo;
507-
508-
} else {
509-
return convertId(value, getIdTypeForField(documentField));
510-
}
459+
return convertIdField(documentField, value);
511460
}
512461

513462
if (value == null) {
@@ -708,6 +657,67 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers
708657
return createReferenceFor(source, property);
709658
}
710659

660+
@Nullable
661+
private Object convertValue(Field documentField, Object sourceValue, Object value,
662+
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter) {
663+
664+
MongoConversionContext conversionContext = new MongoConversionContext(new PropertyValueProvider<>() {
665+
@Override
666+
public <T> T getPropertyValue(MongoPersistentProperty property) {
667+
throw new IllegalStateException("No enclosing property available");
668+
}
669+
}, documentField.getProperty(), converter);
670+
671+
/* might be an $in clause with multiple entries */
672+
if (!documentField.getProperty().isCollectionLike() && sourceValue instanceof Collection<?> collection) {
673+
return collection.stream().map(it -> valueConverter.write(it, conversionContext)).collect(Collectors.toList());
674+
}
675+
676+
if (!documentField.getProperty().isMap() && sourceValue instanceof Document document) {
677+
678+
return BsonUtils.mapValues(document, (key, val) -> {
679+
if (isKeyword(key)) {
680+
return getMappedValue(documentField, val);
681+
}
682+
return val;
683+
});
684+
}
685+
686+
return valueConverter.write(value, conversionContext);
687+
}
688+
689+
@Nullable
690+
private Object convertIdField(Field documentField, Object source) {
691+
692+
Object value = source;
693+
if (isDBObject(source)) {
694+
DBObject valueDbo = (DBObject) source;
695+
value = new Document(valueDbo.toMap());
696+
}
697+
698+
if (!isDocument(value)) {
699+
return convertId(value, getIdTypeForField(documentField));
700+
}
701+
702+
Document valueDbo = (Document) value;
703+
Document resultDbo = new Document(valueDbo);
704+
705+
if (valueDbo.containsKey("$in") || valueDbo.containsKey("$nin")) {
706+
String inKey = valueDbo.containsKey("$in") ? "$in" : "$nin";
707+
List<Object> ids = new ArrayList<>();
708+
for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
709+
ids.add(convertId(id, getIdTypeForField(documentField)));
710+
}
711+
resultDbo.put(inKey, ids);
712+
} else if (valueDbo.containsKey("$ne")) {
713+
resultDbo.put("$ne", convertId(valueDbo.get("$ne"), getIdTypeForField(documentField)));
714+
} else {
715+
return getMappedObject(resultDbo, Optional.empty());
716+
}
717+
return resultDbo;
718+
719+
}
720+
711721
/**
712722
* Checks whether the given value is a {@link Document}.
713723
*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java

+20
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
import java.util.Collection;
2121
import java.util.Collections;
2222
import java.util.Date;
23+
import java.util.LinkedHashMap;
2324
import java.util.List;
2425
import java.util.Map;
26+
import java.util.Map.Entry;
2527
import java.util.StringJoiner;
28+
import java.util.function.BiFunction;
2629
import java.util.function.Function;
2730
import java.util.stream.StreamSupport;
2831

@@ -716,6 +719,23 @@ public static Collection<?> asCollection(Object source) {
716719
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
717720
}
718721

722+
public static Document mapValues(Document source, BiFunction<String, Object, Object> valueMapper) {
723+
return mapEntries(source, Entry::getKey, entry -> valueMapper.apply(entry.getKey(), entry.getValue()));
724+
}
725+
726+
public static Document mapEntries(Document source, Function<Entry<String,Object>,String> keyMapper, Function<Entry<String,Object>,Object> valueMapper) {
727+
728+
if(source.isEmpty()) {
729+
return source;
730+
}
731+
732+
Map<String, Object> target = new LinkedHashMap<>(source.size(), 1f);
733+
for(Entry<String,Object> entry : source.entrySet()) {
734+
target.put(keyMapper.apply(entry), valueMapper.apply(entry));
735+
}
736+
return new Document(target);
737+
}
738+
719739
@Nullable
720740
private static String toJson(@Nullable Object value) {
721741

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

+18
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,24 @@ void mappingShouldRetainMapKeyOrder() {
15281528
assertThat(target.get("simpleMap", Map.class)).containsExactlyEntriesOf(sourceMap);
15291529
}
15301530

1531+
@Test // GH-4510
1532+
void convertsNestedOperatorValueForPropertyThatHasValueConverter() {
1533+
1534+
org.bson.Document mappedObject = mapper.getMappedObject(query(where("text").gt("spring").lt( "data")).getQueryObject(),
1535+
context.getPersistentEntity(WithPropertyValueConverter.class));
1536+
1537+
assertThat(mappedObject).isEqualTo("{ 'text' : { $gt : 'gnirps', $lt : 'atad' } }");
1538+
}
1539+
1540+
@Test // GH-4510
1541+
void convertsNestedOperatorValueForPropertyContainingListThatHasValueConverter() {
1542+
1543+
org.bson.Document mappedObject = mapper.getMappedObject(query(where("text").gt("spring").in( "data")).getQueryObject(),
1544+
context.getPersistentEntity(WithPropertyValueConverter.class));
1545+
1546+
assertThat(mappedObject).isEqualTo("{ 'text' : { $gt : 'gnirps', $in : [ 'atad' ] } }");
1547+
}
1548+
15311549
class WithSimpleMap {
15321550
Map<String, String> simpleMap;
15331551
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/BsonUtilsTest.java

+47
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.LinkedHashMap;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.Map.Entry;
3233
import java.util.stream.Stream;
3334

3435
import org.bson.BsonArray;
@@ -180,6 +181,52 @@ void resolveValueForField(FieldName fieldName, boolean exists) {
180181
}
181182
}
182183

184+
@Test
185+
void retainsOrderWhenMappingValues() {
186+
187+
Document source = new Document();
188+
source.append("z", "first-entry");
189+
source.append("a", "second-entry");
190+
source.append("0", "third-entry");
191+
source.append("9", "fourth-entry");
192+
193+
Document target = BsonUtils.mapValues(source, (key, value) -> value);
194+
assertThat(source).isNotSameAs(target).containsExactlyEntriesOf(source);
195+
}
196+
197+
@Test
198+
void retainsOrderWhenMappingKeys() {
199+
200+
Document source = new Document();
201+
source.append("z", "first-entry");
202+
source.append("a", "second-entry");
203+
204+
Document target = BsonUtils.mapEntries(source, entry -> entry.getKey().toUpperCase(), Entry::getValue);
205+
assertThat(target).containsExactly(Map.entry("Z", "first-entry"), Map.entry("A", "second-entry"));
206+
}
207+
208+
@Test
209+
void appliesValueMapping() {
210+
Document source = new Document();
211+
source.append("z", "first-entry");
212+
source.append("a", "second-entry");
213+
214+
Document target = BsonUtils.mapValues(source,
215+
(key, value) -> new StringBuilder(value.toString()).reverse().toString());
216+
assertThat(target).containsValues("yrtne-tsrif", "yrtne-dnoces");
217+
}
218+
219+
@Test
220+
void appliesKeyMapping() {
221+
222+
Document source = new Document();
223+
source.append("z", "first-entry");
224+
source.append("a", "second-entry");
225+
226+
Document target = BsonUtils.mapEntries(source, entry -> entry.getKey().toUpperCase(), Entry::getValue);
227+
assertThat(target).containsKeys("Z", "A");
228+
}
229+
183230
static Stream<Arguments> fieldNames() {
184231
return Stream.of(//
185232
Arguments.of(FieldName.path("a"), true), //

0 commit comments

Comments
 (0)