Skip to content

Commit 28ef92c

Browse files
Introduce Queryable annotation and add Schema derivation.
This commit decouples queryable encryption from explicit encryption and introduces the Queryable annotation to represent different query types like range and equality. Additionally it removes value conversion from range encryption and fixes update mapping of range encrypted fields. Original Pull Request: #4885
1 parent 156274e commit 28ef92c

27 files changed

+1784
-287
lines changed

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

+262-24
Large diffs are not rendered by default.

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.data.mapping.PropertyPath;
4040
import org.springframework.data.mapping.context.MappingContext;
4141
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
42+
import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
4243
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
4344
import org.springframework.data.mongodb.core.convert.MongoConverter;
4445
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
@@ -379,7 +380,11 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
379380
collectionOptions.getChangeStreamOptions().ifPresent(it -> result
380381
.changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages())));
381382

382-
collectionOptions.getEncryptedFields().ifPresent(result::encryptedFields);
383+
collectionOptions.getEncryptedFieldsOptions().map(EncryptedFieldsOptions::toDocument).ifPresent(encryptedFields -> {
384+
if (!encryptedFields.isEmpty()) {
385+
result.encryptedFields(encryptedFields);
386+
}
387+
});
383388
return result;
384389
}
385390

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java

+34-1
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,19 @@
3131
import org.springframework.data.mongodb.core.mapping.Field;
3232
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3333
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
34+
import org.springframework.data.mongodb.core.mapping.Queryable;
35+
import org.springframework.data.mongodb.core.mapping.RangeEncrypted;
3436
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
3537
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
3638
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
39+
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty;
3740
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
3841
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
3942
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
4043
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
4144
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
45+
import org.springframework.data.mongodb.core.schema.QueryCharacteristic;
46+
import org.springframework.data.mongodb.core.schema.QueryCharacteristics;
4247
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
4348
import org.springframework.data.util.TypeInformation;
4449
import org.springframework.util.Assert;
@@ -291,7 +296,35 @@ private JsonSchemaProperty applyEncryptionDataIfNecessary(MongoPersistentPropert
291296
if (!ObjectUtils.isEmpty(encrypted.keyId())) {
292297
enc = enc.keys(property.getEncryptionKeyIds());
293298
}
294-
return enc;
299+
300+
Queryable queryable = property.findAnnotation(Queryable.class);
301+
if (queryable == null || !StringUtils.hasText(queryable.queryType())) {
302+
return enc;
303+
}
304+
305+
QueryCharacteristic characteristic = new QueryCharacteristic() {
306+
307+
@Override
308+
public String queryType() {
309+
return queryable.queryType();
310+
}
311+
312+
@Override
313+
public Document toDocument() {
314+
315+
Document options = QueryCharacteristic.super.toDocument();
316+
317+
if (queryable.contentionFactor() >= 0) {
318+
options.put("contention", queryable.contentionFactor());
319+
}
320+
if (!queryable.queryAttributes().isEmpty()) {
321+
options.putAll(Document.parse(queryable.queryAttributes()));
322+
}
323+
324+
return options;
325+
}
326+
};
327+
return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(List.of(characteristic)));
295328
}
296329

297330
private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path,

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java

+90-8
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
package org.springframework.data.mongodb.core.convert;
1717

1818
import org.bson.conversions.Bson;
19-
2019
import org.springframework.data.convert.ValueConversionContext;
2120
import org.springframework.data.mapping.model.PropertyValueProvider;
2221
import org.springframework.data.mapping.model.SpELContext;
2322
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
2423
import org.springframework.data.util.TypeInformation;
24+
import org.springframework.lang.CheckReturnValue;
2525
import org.springframework.lang.Nullable;
2626

2727
/**
@@ -38,7 +38,7 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
3838

3939
@Nullable private final MongoPersistentProperty persistentProperty;
4040
@Nullable private final SpELContext spELContext;
41-
@Nullable private final String fieldNameAndQueryOperator;
41+
@Nullable private final OperatorContext operatorContext;
4242

4343
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
4444
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
@@ -53,19 +53,19 @@ public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> acc
5353

5454
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
5555
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
56-
@Nullable String fieldNameAndQueryOperator) {
57-
this(accessor, persistentProperty, mongoConverter, null, fieldNameAndQueryOperator);
56+
@Nullable OperatorContext operatorContext) {
57+
this(accessor, persistentProperty, mongoConverter, null, operatorContext);
5858
}
5959

6060
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
6161
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
62-
@Nullable SpELContext spELContext, @Nullable String fieldNameAndQueryOperator) {
62+
@Nullable SpELContext spELContext, @Nullable OperatorContext operatorContext) {
6363

6464
this.accessor = accessor;
6565
this.persistentProperty = persistentProperty;
6666
this.mongoConverter = mongoConverter;
6767
this.spELContext = spELContext;
68-
this.fieldNameAndQueryOperator = fieldNameAndQueryOperator;
68+
this.operatorContext = operatorContext;
6969
}
7070

7171
@Override
@@ -78,6 +78,17 @@ public MongoPersistentProperty getProperty() {
7878
return persistentProperty;
7979
}
8080

81+
/**
82+
*
83+
* @param operatorContext
84+
* @return new instance of {@link MongoConversionContext}.
85+
* @since 4.5
86+
*/
87+
@CheckReturnValue
88+
public MongoConversionContext forOperator(@Nullable OperatorContext operatorContext) {
89+
return new MongoConversionContext(accessor, persistentProperty, mongoConverter, spELContext, operatorContext);
90+
}
91+
8192
@Nullable
8293
public Object getValue(String propertyPath) {
8394
return accessor.getPropertyValue(getProperty().getOwner().getRequiredPersistentProperty(propertyPath));
@@ -101,7 +112,78 @@ public SpELContext getSpELContext() {
101112
}
102113

103114
@Nullable
104-
public String getFieldNameAndQueryOperator() {
105-
return fieldNameAndQueryOperator;
115+
public OperatorContext getOperatorContext() {
116+
return operatorContext;
117+
}
118+
119+
/**
120+
* The {@link OperatorContext} provides access to the actual conversion intent like a write operation or a query
121+
* operator such as {@literal $gte}.
122+
*
123+
* @since 4.5
124+
*/
125+
public interface OperatorContext {
126+
127+
/**
128+
* The operator the conversion is used in.
129+
* @return {@literal write} for simple write operations during save, or a query operator.
130+
*/
131+
String getOperator();
132+
133+
/**
134+
* The context path the operator is used in.
135+
* @return never {@literal null}.
136+
*/
137+
String getPath();
138+
139+
boolean isWriteOperation();
140+
}
141+
142+
public static class WriteOperatorContext implements OperatorContext {
143+
144+
private final String path;
145+
146+
public WriteOperatorContext(String path) {
147+
this.path = path;
148+
}
149+
150+
@Override
151+
public String getOperator() {
152+
return "write";
153+
}
154+
155+
@Override
156+
public String getPath() {
157+
return path;
158+
}
159+
160+
@Override
161+
public boolean isWriteOperation() {
162+
return true;
163+
}
164+
}
165+
166+
public static class QueryOperatorContext implements OperatorContext {
167+
168+
private final String operator;
169+
private final String path;
170+
171+
public QueryOperatorContext(@Nullable String operator, String path) {
172+
this.operator = operator != null ? operator : "$eq";
173+
this.path = path;
174+
}
175+
176+
public String getOperator() {
177+
return operator;
178+
}
179+
180+
public String getPath() {
181+
return path;
182+
}
183+
184+
@Override
185+
public boolean isWriteOperation() {
186+
return false;
187+
}
106188
}
107189
}

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

+14-13
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.bson.Document;
3838
import org.bson.conversions.Bson;
3939
import org.bson.types.ObjectId;
40-
4140
import org.springframework.core.convert.ConversionService;
4241
import org.springframework.core.convert.converter.Converter;
4342
import org.springframework.data.annotation.Reference;
@@ -58,6 +57,8 @@
5857
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
5958
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
6059
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
60+
import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext;
61+
import org.springframework.data.mongodb.core.convert.MongoConversionContext.QueryOperatorContext;
6162
import org.springframework.data.mongodb.core.mapping.FieldName;
6263
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
6364
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -672,18 +673,21 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu
672673

673674
MongoPersistentProperty property = documentField.getProperty();
674675

675-
String fieldNameAndQueryOperator = property != null && !property.getFieldName().equals(documentField.name)
676-
? property.getFieldName() + "." + documentField.name
677-
: documentField.name;
678-
679-
MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE,
680-
property, converter, fieldNameAndQueryOperator);
676+
OperatorContext criteriaContext = new QueryOperatorContext(
677+
isKeyword(documentField.name) ? documentField.name : "$eq", property.getFieldName());
678+
MongoConversionContext conversionContext;
679+
if (valueConverter instanceof MongoConversionContext mcc) {
680+
conversionContext = mcc.forOperator(criteriaContext);
681+
} else {
682+
conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE, property, converter,
683+
criteriaContext);
684+
}
681685

682686
return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext);
683687
}
684688

685689
@Nullable
686-
private Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value,
690+
protected Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value,
687691
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter,
688692
MongoConversionContext conversionContext) {
689693

@@ -707,10 +711,7 @@ private Object convertValueWithConversionContext(Field documentField, Object sou
707711

708712
return BsonUtils.mapValues(document, (key, val) -> {
709713
if (isKeyword(key)) {
710-
MongoConversionContext fieldConversionContext = new MongoConversionContext(
711-
NoPropertyPropertyValueProvider.INSTANCE, property, converter,
712-
conversionContext.getFieldNameAndQueryOperator() + "." + key);
713-
return convertValueWithConversionContext(documentField, val, val, valueConverter, fieldConversionContext);
714+
return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext.forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().getPath())));
714715
}
715716
return val;
716717
});
@@ -1624,7 +1625,7 @@ public MongoConverter getConverter() {
16241625
return converter;
16251626
}
16261627

1627-
private enum NoPropertyPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> {
1628+
enum NoPropertyPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> {
16281629

16291630
INSTANCE;
16301631

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java

+10
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@
2424
import org.bson.Document;
2525
import org.bson.conversions.Bson;
2626
import org.springframework.core.convert.converter.Converter;
27+
import org.springframework.data.convert.PropertyValueConverter;
28+
import org.springframework.data.convert.ValueConversionContext;
2729
import org.springframework.data.domain.Sort;
2830
import org.springframework.data.domain.Sort.Order;
2931
import org.springframework.data.mapping.Association;
3032
import org.springframework.data.mapping.context.MappingContext;
33+
import org.springframework.data.mongodb.core.convert.MongoConversionContext.WriteOperatorContext;
3134
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3235
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
3336
import org.springframework.data.mongodb.core.query.Query;
@@ -160,6 +163,13 @@ protected Entry<String, Object> getMappedObjectForField(Field field, Object rawV
160163
return super.getMappedObjectForField(field, rawValue);
161164
}
162165

166+
protected Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value,
167+
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter,
168+
MongoConversionContext conversionContext) {
169+
170+
return super.convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext.forOperator(new WriteOperatorContext(documentField.name)));
171+
}
172+
163173
private Entry<String, Object> getMappedUpdateModifier(Field field, Object rawValue) {
164174
Object value;
165175

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.mongodb.core.convert.encryption;
1717

1818
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
19+
import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext;
1920
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
2021
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
2122
import org.springframework.data.util.TypeInformation;
@@ -70,7 +71,7 @@ public <T> T write(@Nullable Object value, TypeInformation<T> target) {
7071

7172
@Override
7273
@Nullable
73-
public String getFieldNameAndQueryOperator() {
74-
return conversionContext.getFieldNameAndQueryOperator();
74+
public OperatorContext getOperatorContext() {
75+
return conversionContext.getOperatorContext();
7576
}
7677
}

0 commit comments

Comments
 (0)