properties) {
+ return new EncryptedFieldsOptions(null, List.copyOf(properties));
+ }
+
+ /**
+ * Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property.
+ *
+ * Please note that, a given {@link JsonSchemaProperty} may override options from a given {@link MongoJsonSchema} if
+ * set.
+ *
+ * @param property the queryable source - typically
+ * {@link org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty
+ * encrypted}.
+ * @param characteristics the query options to set.
+ * @return new instance of {@link EncryptedFieldsOptions}.
+ */
+ @Contract("_, _ -> new")
+ @CheckReturnValue
+ public EncryptedFieldsOptions queryable(JsonSchemaProperty property, QueryCharacteristic... characteristics) {
+
+ List targetPropertyList = new ArrayList<>(queryableProperties.size() + 1);
+ targetPropertyList.addAll(queryableProperties);
+ targetPropertyList.add(JsonSchemaProperty.queryable(property, List.of(characteristics)));
+
+ return new EncryptedFieldsOptions(schema, targetPropertyList);
+ }
+
+ public Document toDocument() {
+ return new Document("fields", selectPaths());
+ }
+
+ private List selectPaths() {
+
+ Map fields = new LinkedHashMap<>();
+ for (Document field : fromSchema()) {
+ fields.put(field.get("path", String.class), field);
+ }
+ for (Document field : fromProperties()) {
+ fields.put(field.get("path", String.class), field);
+ }
+ return List.copyOf(fields.values());
+ }
+
+ private List fromProperties() {
+
+ if (queryableProperties.isEmpty()) {
+ return List.of();
+ }
+
+ List converted = new ArrayList<>(queryableProperties.size());
+ for (QueryableJsonSchemaProperty property : queryableProperties) {
+
+ Document field = new Document("path", property.getIdentifier());
+
+ if (!property.getTypes().isEmpty()) {
+ field.append("bsonType", property.getTypes().iterator().next().toBsonType().value());
+ }
+
+ if (property
+ .getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) {
+ if (encrypted.getKeyId() != null) {
+ if (encrypted.getKeyId() instanceof String stringKey) {
+ field.append("keyId",
+ new BsonBinary(BsonBinarySubType.UUID_STANDARD, stringKey.getBytes(StandardCharsets.UTF_8)));
+ } else {
+ field.append("keyId", encrypted.getKeyId());
+ }
+ }
+ }
+
+ field.append("queries", StreamSupport.stream(property.getCharacteristics().spliterator(), false)
+ .map(QueryCharacteristic::toDocument).toList());
+
+ if (!field.containsKey("keyId")) {
+ field.append("keyId", BsonNull.VALUE);
+ }
+
+ converted.add(field);
+ }
+ return converted;
+ }
+
+ private List fromSchema() {
+
+ if (schema == null) {
+ return List.of();
+ }
+
+ Document root = schema.schemaDocument();
+ Map paths = new LinkedHashMap<>();
+ collectPaths(root, null, paths);
+
+ List fields = new ArrayList<>();
+ if (!paths.isEmpty()) {
+
+ for (Entry entry : paths.entrySet()) {
+ Document field = new Document("path", entry.getKey());
+ field.append("keyId", entry.getValue().getOrDefault("keyId", BsonNull.VALUE));
+ if (entry.getValue().containsKey("bsonType")) {
+ field.append("bsonType", entry.getValue().get("bsonType"));
+ }
+ field.put("queries", entry.getValue().get("queries"));
+ fields.add(field);
+ }
+ }
+
+ return fields;
+ }
+ }
+
+ private static void collectPaths(Document document, @Nullable String currentPath, Map paths) {
+
+ if (document.containsKey("type") && document.get("type").equals("object")) {
+ Object o = document.get("properties");
+ if (o == null) {
+ return;
+ }
+
+ if (o instanceof Document properties) {
+ for (Entry entry : properties.entrySet()) {
+ if (entry.getValue() instanceof Document nested) {
+
+ String path = currentPath == null ? entry.getKey() : (currentPath + "." + entry.getKey());
+ if (nested.containsKey("encrypt")) {
+ Document target = new Document(nested.get("encrypt", Document.class));
+ if (nested.containsKey("queries")) {
+ List> queries = nested.get("queries", List.class);
+ if (!queries.isEmpty() && queries.iterator().next() instanceof Document qd) {
+ target.putAll(qd);
+ }
+ }
+ paths.put(path, target);
+ } else {
+ collectPaths(nested, path, paths);
+ }
+ }
+ }
+ }
+ }
+ }
+
/**
* Encapsulation of options applied to define collections change stream behaviour.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
index f64391e8cd..601b6898b8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
@@ -19,11 +19,13 @@
* Encryption algorithms supported by MongoDB Client Side Field Level Encryption.
*
* @author Christoph Strobl
+ * @author Ross Lawley
* @since 3.3
*/
public final class EncryptionAlgorithms {
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
+ public static final String RANGE = "Range";
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
index 65a5131dd1..38269787cb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
@@ -22,6 +22,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
import org.bson.BsonNull;
import org.bson.Document;
@@ -39,6 +40,7 @@
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
+import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
@@ -83,6 +85,7 @@
* @author Mark Paluch
* @author Christoph Strobl
* @author Ben Foster
+ * @author Ross Lawley
* @since 2.1
* @see MongoTemplate
* @see ReactiveMongoTemplate
@@ -375,8 +378,15 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
result.timeSeriesOptions(options);
});
- collectionOptions.getChangeStreamOptions().ifPresent(it -> result
- .changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages())));
+ collectionOptions.getChangeStreamOptions() //
+ .map(CollectionOptions.CollectionChangeStreamOptions::getPreAndPostImages) //
+ .map(ChangeStreamPreAndPostImagesOptions::new) //
+ .ifPresent(result::changeStreamPreAndPostImagesOptions);
+
+ collectionOptions.getEncryptedFieldsOptions() //
+ .map(EncryptedFieldsOptions::toDocument) //
+ .filter(Predicate.not(Document::isEmpty)) //
+ .ifPresent(result::encryptedFields);
return result;
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java
new file mode 100644
index 0000000000..c04ae9d603
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 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;
+
+import org.bson.Document;
+
+enum EntityResultConverter implements QueryResultConverter