Browse Source

Initial polishing.

issue/4185-light
Mark Paluch 8 months ago
parent
commit
d0f3668fb0
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 52
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
  2. 17
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
  3. 45
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
  4. 39
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
  5. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  6. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
  7. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
  8. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java
  9. 11
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java
  10. 20
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java
  11. 10
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
  12. 37
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java

52
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

@ -24,12 +24,12 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import org.bson.BsonBinary; import org.bson.BsonBinary;
import org.bson.BsonBinarySubType; import org.bson.BsonBinarySubType;
import org.bson.BsonNull; import org.bson.BsonNull;
import org.bson.Document; import org.bson.Document;
import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty;
@ -43,7 +43,6 @@ import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.util.Optionals; import org.springframework.data.util.Optionals;
import org.springframework.lang.CheckReturnValue; import org.springframework.lang.CheckReturnValue;
import org.springframework.lang.Contract; import org.springframework.lang.Contract;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@ -241,11 +240,11 @@ public class CollectionOptions {
* Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given * Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given
* {@link MongoJsonSchema}. * {@link MongoJsonSchema}.
* *
* @param schema can be {@literal null}. * @param schema must not be {@literal null}.
* @return new {@link CollectionOptions}. * @return new {@link CollectionOptions}.
* @since 2.1 * @since 2.1
*/ */
public CollectionOptions schema(@Nullable MongoJsonSchema schema) { public CollectionOptions schema(MongoJsonSchema schema) {
return validator(Validator.schema(schema)); return validator(Validator.schema(schema));
} }
@ -473,7 +472,7 @@ public class CollectionOptions {
* Get the {@code encryptedFields} if available. * Get the {@code encryptedFields} if available.
* *
* @return {@link Optional#empty()} if not specified. * @return {@link Optional#empty()} if not specified.
* @since 4.5.0 * @since 4.5
*/ */
public Optional<EncryptedFieldsOptions> getEncryptedFieldsOptions() { public Optional<EncryptedFieldsOptions> getEncryptedFieldsOptions() {
return Optional.ofNullable(encryptedFieldsOptions); return Optional.ofNullable(encryptedFieldsOptions);
@ -552,7 +551,8 @@ public class CollectionOptions {
private final @Nullable ValidationLevel validationLevel; private final @Nullable ValidationLevel validationLevel;
private final @Nullable ValidationAction validationAction; private final @Nullable ValidationAction validationAction;
public ValidationOptions(Validator validator, ValidationLevel validationLevel, ValidationAction validationAction) { public ValidationOptions(@Nullable Validator validator, @Nullable ValidationLevel validationLevel,
@Nullable ValidationAction validationAction) {
this.validator = validator; this.validator = validator;
this.validationLevel = validationLevel; this.validationLevel = validationLevel;
@ -669,7 +669,7 @@ public class CollectionOptions {
/** /**
* Encapsulation of Encryption options for collections. * Encapsulation of Encryption options for collections.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @since 4.5 * @since 4.5
*/ */
@ -677,8 +677,19 @@ public class CollectionOptions {
private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions(); private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions();
private @Nullable MongoJsonSchema schema; private final @Nullable MongoJsonSchema schema;
private List<QueryableJsonSchemaProperty> queryableProperties; private final List<QueryableJsonSchemaProperty> queryableProperties;
EncryptedFieldsOptions() {
this(null, List.of());
}
private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema,
List<QueryableJsonSchemaProperty> queryableProperties) {
this.schema = schema;
this.queryableProperties = queryableProperties;
}
/** /**
* @return {@link EncryptedFieldsOptions#NONE} * @return {@link EncryptedFieldsOptions#NONE}
@ -701,17 +712,6 @@ public class CollectionOptions {
return new EncryptedFieldsOptions(null, List.copyOf(properties)); return new EncryptedFieldsOptions(null, List.copyOf(properties));
} }
EncryptedFieldsOptions() {
this(null, List.of());
}
private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema,
List<QueryableJsonSchemaProperty> queryableProperties) {
this.schema = schema;
this.queryableProperties = queryableProperties;
}
/** /**
* Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property. * Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property.
* <p> * <p>
@ -739,7 +739,6 @@ public class CollectionOptions {
return new Document("fields", selectPaths()); return new Document("fields", selectPaths());
} }
@NonNull
private List<Document> selectPaths() { private List<Document> selectPaths() {
Map<String, Document> fields = new LinkedHashMap<>(); Map<String, Document> fields = new LinkedHashMap<>();
@ -760,10 +759,13 @@ public class CollectionOptions {
List<Document> converted = new ArrayList<>(queryableProperties.size()); List<Document> converted = new ArrayList<>(queryableProperties.size());
for (QueryableJsonSchemaProperty property : queryableProperties) { for (QueryableJsonSchemaProperty property : queryableProperties) {
Document field = new Document("path", property.getIdentifier()); Document field = new Document("path", property.getIdentifier());
if (!property.getTypes().isEmpty()) { if (!property.getTypes().isEmpty()) {
field.append("bsonType", property.getTypes().iterator().next().toBsonType().value()); field.append("bsonType", property.getTypes().iterator().next().toBsonType().value());
} }
if (property if (property
.getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) { .getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) {
if (encrypted.getKeyId() != null) { if (encrypted.getKeyId() != null) {
@ -775,11 +777,13 @@ public class CollectionOptions {
} }
} }
} }
field.append("queries", property.getCharacteristics().getCharacteristics().stream()
.map(QueryCharacteristic::toDocument).collect(Collectors.toList())); field.append("queries", property.getCharacteristics().map(QueryCharacteristic::toDocument).toList());
if (!field.containsKey("keyId")) { if (!field.containsKey("keyId")) {
field.append("keyId", BsonNull.VALUE); field.append("keyId", BsonNull.VALUE);
} }
converted.add(field); converted.add(field);
} }
return converted; return converted;
@ -813,7 +817,7 @@ public class CollectionOptions {
} }
} }
private static void collectPaths(Document document, String currentPath, Map<String, Document> paths) { private static void collectPaths(Document document, @Nullable String currentPath, Map<String, Document> paths) {
if (document.containsKey("type") && document.get("type").equals("object")) { if (document.containsKey("type") && document.get("type").equals("object")) {
Object o = document.get("properties"); Object o = document.get("properties");

17
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

@ -22,6 +22,7 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import org.bson.BsonNull; import org.bson.BsonNull;
import org.bson.Document; import org.bson.Document;
@ -377,14 +378,16 @@ class EntityOperations {
result.timeSeriesOptions(options); result.timeSeriesOptions(options);
}); });
collectionOptions.getChangeStreamOptions().ifPresent(it -> result collectionOptions.getChangeStreamOptions() //
.changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages()))); .map(CollectionOptions.CollectionChangeStreamOptions::getPreAndPostImages) //
.map(ChangeStreamPreAndPostImagesOptions::new) //
.ifPresent(result::changeStreamPreAndPostImagesOptions);
collectionOptions.getEncryptedFieldsOptions() //
.map(EncryptedFieldsOptions::toDocument) //
.filter(Predicate.not(Document::isEmpty)) //
.ifPresent(result::encryptedFields);
collectionOptions.getEncryptedFieldsOptions().map(EncryptedFieldsOptions::toDocument).ifPresent(encryptedFields -> {
if (!encryptedFields.isEmpty()) {
result.encryptedFields(encryptedFields);
}
});
return result; return result;
} }

45
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java

@ -24,6 +24,7 @@ import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.bson.Document; import org.bson.Document;
import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter;
@ -32,7 +33,6 @@ import org.springframework.data.mongodb.core.mapping.Field;
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.mapping.Queryable; import org.springframework.data.mongodb.core.mapping.Queryable;
import org.springframework.data.mongodb.core.mapping.RangeEncrypted;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
@ -126,29 +126,31 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type); MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder(); MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
{ Encrypted encrypted = entity.findAnnotation(Encrypted.class);
Encrypted encrypted = entity.findAnnotation(Encrypted.class); if (encrypted != null) {
if (encrypted != null) { schemaBuilder.encryptionMetadata(getEncryptionMetadata(entity, encrypted));
}
Document encryptionMetadata = new Document(); List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
Collection<Object> encryptionKeyIds = entity.getEncryptionKeyIds(); return schemaBuilder.build();
if (!CollectionUtils.isEmpty(encryptionKeyIds)) { }
encryptionMetadata.append("keyId", encryptionKeyIds);
}
if (StringUtils.hasText(encrypted.algorithm())) { private static Document getEncryptionMetadata(MongoPersistentEntity<?> entity, Encrypted encrypted) {
encryptionMetadata.append("algorithm", encrypted.algorithm());
}
schemaBuilder.encryptionMetadata(encryptionMetadata); Document encryptionMetadata = new Document();
}
Collection<Object> encryptionKeyIds = entity.getEncryptionKeyIds();
if (!CollectionUtils.isEmpty(encryptionKeyIds)) {
encryptionMetadata.append("keyId", encryptionKeyIds);
} }
List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity); if (StringUtils.hasText(encrypted.algorithm())) {
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0])); encryptionMetadata.append("algorithm", encrypted.algorithm());
}
return schemaBuilder.build(); return encryptionMetadata;
} }
private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path, private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path,
@ -190,8 +192,8 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
Class<?> rawTargetType = computeTargetType(property); // target type before conversion Class<?> rawTargetType = computeTargetType(property); // target type before conversion
Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type
if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class
if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class || ClassUtils.isAssignable(targetType, rawTargetType) ) { || ClassUtils.isAssignable(targetType, rawTargetType)) {
targetType = rawTargetType; targetType = rawTargetType;
} }
@ -317,14 +319,15 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
if (queryable.contentionFactor() >= 0) { if (queryable.contentionFactor() >= 0) {
options.put("contention", queryable.contentionFactor()); options.put("contention", queryable.contentionFactor());
} }
if (!queryable.queryAttributes().isEmpty()) {
if (StringUtils.hasText(queryable.queryAttributes())) {
options.putAll(Document.parse(queryable.queryAttributes())); options.putAll(Document.parse(queryable.queryAttributes()));
} }
return options; return options;
} }
}; };
return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(List.of(characteristic))); return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(characteristic));
} }
private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path, private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path,

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

@ -79,7 +79,6 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
} }
/** /**
*
* @param operatorContext * @param operatorContext
* @return new instance of {@link MongoConversionContext}. * @return new instance of {@link MongoConversionContext}.
* @since 4.5 * @since 4.5
@ -119,71 +118,53 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
/** /**
* The {@link OperatorContext} provides access to the actual conversion intent like a write operation or a query * The {@link OperatorContext} provides access to the actual conversion intent like a write operation or a query
* operator such as {@literal $gte}. * operator such as {@literal $gte}.
* *
* @since 4.5 * @since 4.5
*/ */
public interface OperatorContext { public interface OperatorContext {
/** /**
* The operator the conversion is used in. * The operator the conversion is used in.
*
* @return {@literal write} for simple write operations during save, or a query operator. * @return {@literal write} for simple write operations during save, or a query operator.
*/ */
String getOperator(); String operator();
/** /**
* The context path the operator is used in. * The context path the operator is used in.
*
* @return never {@literal null}. * @return never {@literal null}.
*/ */
String getPath(); String path();
boolean isWriteOperation(); boolean isWriteOperation();
}
public static class WriteOperatorContext implements OperatorContext {
private final String path; }
public WriteOperatorContext(String path) { record WriteOperatorContext(String path) implements OperatorContext {
this.path = path;
}
@Override @Override
public String getOperator() { public String operator() {
return "write"; return "write";
} }
@Override
public String getPath() {
return path;
}
@Override @Override
public boolean isWriteOperation() { public boolean isWriteOperation() {
return true; return true;
} }
} }
public static class QueryOperatorContext implements OperatorContext { record QueryOperatorContext(String operator, String path) implements OperatorContext {
private final String operator;
private final String path;
public QueryOperatorContext(@Nullable String operator, String path) { public QueryOperatorContext(@Nullable String operator, String path) {
this.operator = operator != null ? operator : "$eq"; this.operator = operator != null ? operator : "$eq";
this.path = path; this.path = path;
} }
public String getOperator() {
return operator;
}
public String getPath() {
return path;
}
@Override @Override
public boolean isWriteOperation() { public boolean isWriteOperation() {
return false; return false;
} }
} }
} }

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

@ -711,7 +711,8 @@ public class QueryMapper {
return BsonUtils.mapValues(document, (key, val) -> { return BsonUtils.mapValues(document, (key, val) -> {
if (isKeyword(key)) { if (isKeyword(key)) {
return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext.forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().getPath()))); return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext
.forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().path())));
} }
return val; return val;
}); });

27
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java

@ -15,9 +15,9 @@
*/ */
package org.springframework.data.mongodb.core.convert.encryption; package org.springframework.data.mongodb.core.convert.encryption;
import static java.util.Arrays.asList; import static java.util.Arrays.*;
import static java.util.Collections.singletonList; import static java.util.Collections.*;
import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions; import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -32,6 +32,7 @@ import org.bson.BsonDocument;
import org.bson.BsonValue; import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
import org.bson.types.Binary; import org.bson.types.Binary;
import org.springframework.core.CollectionFactory; import org.springframework.core.CollectionFactory;
import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoConversionContext;
import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext;
@ -59,8 +60,8 @@ import org.springframework.util.StringUtils;
public class MongoEncryptionConverter implements EncryptingConverter<Object, Object> { public class MongoEncryptionConverter implements EncryptingConverter<Object, Object> {
private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class); private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class);
private static final String EQUALITY_OPERATOR = "$eq";
private static final List<String> RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte"); private static final List<String> RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte");
public static final String AND_OPERATOR = "$and";
private final Encryption<BsonValue, BsonBinary> encryption; private final Encryption<BsonValue, BsonBinary> encryption;
private final EncryptionKeyResolver keyResolver; private final EncryptionKeyResolver keyResolver;
@ -189,7 +190,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
} }
private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty, private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty,
OperatorContext operatorContext) { @Nullable OperatorContext operatorContext) {
Queryable queryableAnnotation = persistentProperty.findAnnotation(Queryable.class); Queryable queryableAnnotation = persistentProperty.findAnnotation(Queryable.class);
if (queryableAnnotation == null || !StringUtils.hasText(queryableAnnotation.queryType())) { if (queryableAnnotation == null || !StringUtils.hasText(queryableAnnotation.queryType())) {
@ -248,29 +249,29 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
* The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method ensures these * The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method ensures these
* requirements are met and then picks out and returns just the value for use with a range query. * requirements are met and then picks out and returns just the value for use with a range query.
* *
* @param fieldNameAndQueryOperator field name and query operator * @param operatorContext field name and query operator.
* @param value the value of the expression to be encrypted * @param value the value of the expression to be encrypted.
* @param encryptionOptions the options * @param encryptionOptions the options.
* @return the encrypted range value for use in a range query * @return the encrypted range value for use in a range query.
*/ */
private BsonValue encryptExpression(OperatorContext operatorContext, Object value, private BsonValue encryptExpression(OperatorContext operatorContext, Object value,
EncryptionOptions encryptionOptions) { EncryptionOptions encryptionOptions) {
BsonValue doc = BsonUtils.simpleToBsonValue(value); BsonValue doc = BsonUtils.simpleToBsonValue(value);
String fieldName = operatorContext.getPath(); String fieldName = operatorContext.path();
String queryOperator = operatorContext.getOperator(); String queryOperator = operatorContext.operator();
if (!RANGE_OPERATORS.contains(queryOperator)) { if (!RANGE_OPERATORS.contains(queryOperator)) {
throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the " throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the "
+ "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName)); + "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName));
} }
BsonDocument encryptExpression = new BsonDocument("$and", BsonDocument encryptExpression = new BsonDocument(AND_OPERATOR,
new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc))))); new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc)))));
BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions); BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions);
return result.getArray("$and").get(0).asDocument().getDocument(fieldName).getBinary(queryOperator); return result.getArray(AND_OPERATOR).get(0).asDocument().getDocument(fieldName).getBinary(queryOperator);
} }
private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,

8
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java

@ -20,11 +20,13 @@ import org.bson.BsonDocument;
/** /**
* Component responsible for encrypting and decrypting values. * Component responsible for encrypting and decrypting values.
* *
* @param <P> plaintext type.
* @param <C> ciphertext type.
* @author Christoph Strobl * @author Christoph Strobl
* @author Ross Lawley * @author Ross Lawley
* @since 4.1 * @since 4.1
*/ */
public interface Encryption<S, T> { public interface Encryption<P, C> {
/** /**
* Encrypt the given value. * Encrypt the given value.
@ -33,7 +35,7 @@ public interface Encryption<S, T> {
* @param options must not be {@literal null}. * @param options must not be {@literal null}.
* @return the encrypted value. * @return the encrypted value.
*/ */
T encrypt(S value, EncryptionOptions options); C encrypt(P value, EncryptionOptions options);
/** /**
* Decrypt the given value. * Decrypt the given value.
@ -41,7 +43,7 @@ public interface Encryption<S, T> {
* @param value must not be {@literal null}. * @param value must not be {@literal null}.
* @return the decrypted value. * @return the decrypted value.
*/ */
S decrypt(T value); P decrypt(C value);
/** /**
* Encrypt the given expression. * Encrypt the given expression.

5
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2025. the original author or authors. * Copyright 2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,13 +30,11 @@ public @interface Queryable {
/** /**
* @return empty {@link String} if not set. * @return empty {@link String} if not set.
* @since 4.5
*/ */
String queryType() default ""; String queryType() default "";
/** /**
* @return empty {@link String} if not set. * @return empty {@link String} if not set.
* @since 4.5
*/ */
String queryAttributes() default ""; String queryAttributes() default "";
@ -46,4 +44,5 @@ public @interface Queryable {
* @return the contention factor * @return the contention factor
*/ */
long contentionFactor() default -1; long contentionFactor() default -1;
} }

11
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java

@ -34,7 +34,7 @@ import org.springframework.core.annotation.AliasFor;
public @interface RangeEncrypted { public @interface RangeEncrypted {
/** /**
* Set the contention factor * Set the contention factor.
* *
* @return the contention factor * @return the contention factor
*/ */
@ -42,15 +42,16 @@ public @interface RangeEncrypted {
long contentionFactor() default -1; long contentionFactor() default -1;
/** /**
* Set the {@literal range} options * Set the {@literal range} options.
* <p> * <p>
* Should be valid extended json representing the range options and including the following values: {@code min}, * Should be valid extended {@link org.bson.Document#parse(String) JSON} representing the range options and including
* {@code max}, {@code trimFactor} and {@code sparsity}. * the following values: {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}.
* <p> * <p>
* Please note that values are data type sensitive and may require proper identification via eg. {@code $numberLong}. * Please note that values are data type sensitive and may require proper identification via eg. {@code $numberLong}.
* *
* @return the json representation of range options * @return the {@link org.bson.Document#parse(String) JSON} representation of range options.
*/ */
@AliasFor(annotation = Queryable.class, value = "queryAttributes") @AliasFor(annotation = Queryable.class, value = "queryAttributes")
String rangeOptions() default ""; String rangeOptions() default "";
} }

20
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java

@ -16,21 +16,25 @@
package org.springframework.data.mongodb.core.schema; package org.springframework.data.mongodb.core.schema;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List; import java.util.List;
import org.bson.BsonNull; import org.bson.BsonNull;
import org.bson.Document; import org.bson.Document;
import org.springframework.data.domain.Range; import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Range.Bound;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
* @since 4.5 * @since 4.5
*/ */
public class QueryCharacteristics { public class QueryCharacteristics implements Streamable<QueryCharacteristic> {
private static final QueryCharacteristics NONE = new QueryCharacteristics(List.of()); private static final QueryCharacteristics NONE = new QueryCharacteristics(Collections.emptyList());
private final List<QueryCharacteristic> characteristics; private final List<QueryCharacteristic> characteristics;
@ -46,14 +50,19 @@ public class QueryCharacteristics {
return new QueryCharacteristics(List.copyOf(characteristics)); return new QueryCharacteristics(List.copyOf(characteristics));
} }
QueryCharacteristics(QueryCharacteristic... characteristics) { public static QueryCharacteristics of(QueryCharacteristic... characteristics) {
this.characteristics = Arrays.asList(characteristics); return new QueryCharacteristics(Arrays.asList(characteristics));
} }
public List<QueryCharacteristic> getCharacteristics() { public List<QueryCharacteristic> getCharacteristics() {
return characteristics; return characteristics;
} }
@Override
public Iterator<QueryCharacteristic> iterator() {
return this.characteristics.iterator();
}
public static <T> RangeQuery<T> range() { public static <T> RangeQuery<T> range() {
return new RangeQuery<>(); return new RangeQuery<>();
} }
@ -96,7 +105,8 @@ public class QueryCharacteristics {
this(Range.unbounded(), null, null, null); this(Range.unbounded(), null, null, null);
} }
public RangeQuery(Range<T> valueRange, Integer trimFactor, Long sparsity, Long contention) { public RangeQuery(@Nullable Range<T> valueRange, @Nullable Integer trimFactor, @Nullable Long sparsity,
@Nullable Long contention) {
this.valueRange = valueRange; this.valueRange = valueRange;
this.trimFactor = trimFactor; this.trimFactor = trimFactor;
this.sparsity = sparsity; this.sparsity = sparsity;

10
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2023-2024 the original author or authors. * Copyright 2024-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,9 +15,8 @@
*/ */
package org.springframework.data.mongodb.core.encryption; package org.springframework.data.mongodb.core.encryption;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -35,6 +34,7 @@ import org.bson.Document;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@ -359,7 +359,7 @@ class RangeEncryptionTests {
path = StringUtils.arrayToDelimitedString(ctx.getProperty().getMongoField().getName().parts(), "."); path = StringUtils.arrayToDelimitedString(ctx.getProperty().getMongoField().getName().parts(), ".");
} }
if (ctx.getOperatorContext() != null) { if (ctx.getOperatorContext() != null) {
path = ctx.getOperatorContext().getPath(); path = ctx.getOperatorContext().path();
} }
return EncryptionKey.keyId(keyHolder.getEncryptionKey(path)); return EncryptionKey.keyId(keyHolder.getEncryptionKey(path));
})); }));

37
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java

@ -15,13 +15,10 @@
*/ */
package org.springframework.data.mongodb.core.schema; package org.springframework.data.mongodb.core.schema;
import static org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty.rangeEncrypted; import static org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty.*;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted; import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.number; import static org.springframework.data.mongodb.test.util.Assertions.*;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.string;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import static org.springframework.data.mongodb.test.util.Assertions.assertThatIllegalArgumentException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -119,13 +116,27 @@ class MongoJsonSchemaUnitTests {
List.of(QueryCharacteristics.range().contention(0).trimFactor(1).sparsity(1).min(0).max(200)))) List.of(QueryCharacteristics.range().contention(0).trimFactor(1).sparsity(1).min(0).max(200))))
.build(); .build();
assertThat(schema.toDocument()).isEqualTo(new Document("$jsonSchema", assertThat(schema.toDocument().get("$jsonSchema", Document.class)).isEqualTo("""
new Document("type", "object").append("properties", {
new Document("ssn", "type": "object",
new Document("encrypt", "properties": {
new Document("bsonType", "long").append("algorithm", "Range").append("queries", "ssn": {
List.of(new Document("contention", 0L).append("trimFactor", 1).append("sparsity", 1L) "encrypt": {
.append("queryType", "range").append("min", 0).append("max", 200)))))))); "bsonType": "long",
"algorithm": "Range",
"queries": [{
"queryType": "range",
"contention": {$numberLong: "0"},
"trimFactor": 1,
"sparsity": {$numberLong: "1"},
"min": 0,
"max": 200
}]
}
}
}
}
""");
} }
@Test // DATAMONGO-1835 @Test // DATAMONGO-1835

Loading…
Cancel
Save