Compare commits

..

32 Commits
4.1.1 ... 4.1.x

Author SHA1 Message Date
Julia
f0858fba10 Fix #self @DocumentReference resolution when used in constructor.
This commit enables document reference lookup to use `DocumentReferenceSource` to properly instantiate an entity containig a @DocumentReference `#self` property.

Closes #4484
Original Pull Request: #4486
2023-08-31 09:21:36 +02:00
Mark Paluch
0b3d1b823f Polishing.
Refactor duplicate code into callback.

See #4481
2023-08-24 14:03:06 +02:00
Mark Paluch
0be1e52232 Guard command completion listener against unsupported observation context.
We now no longer attempt to complete the Observation if the context is not a MongoDB one. For commands that target the admin database and run within a parent observation, we still might have an Observation but that one points to the parent invocation and not the MongoDB one as we do not record commands for the admin database.

Closes #4481
2023-08-24 14:03:06 +02:00
Mark Paluch
31da8b11d2 After release cleanups.
See #4451
2023-08-18 14:07:35 +02:00
Mark Paluch
1a002e2159 Prepare next development iteration.
See #4451
2023-08-18 14:07:33 +02:00
Mark Paluch
c8432ef71a Release version 4.1.3 (2023.0.3).
See #4451
2023-08-18 14:04:04 +02:00
Mark Paluch
ad3cfda587 Prepare 4.1.3 (2023.0.3).
See #4451
2023-08-18 14:03:48 +02:00
Mark Paluch
9a68ee4e7f Polishing.
Update since tags. Add missing Override annotation.

See #4070
Original pull request: #4242
2023-08-17 14:18:07 +02:00
Christoph Strobl
7517527bda Introduce AggregationVariable type.
This commit introduces a new AggregationVariable type that is intended to better identify variables within a pipeline to avoid mapping failures caused by invalid field names.

Closes #4070
Original pull request: #4242
2023-08-17 14:18:07 +02:00
Christoph Strobl
0bfb32a109 Update documentation regarding java.time type conversion.
Closes #3482
Original pull request: #4460
2023-08-17 10:45:20 +02:00
Julia Lee
ca44302a9f Update CI properties.
See #4451
2023-08-14 11:25:43 -04:00
Julia Lee
9ebbbde47e Upgrade to Maven Wrapper 3.9.4.
See #4469
2023-08-14 07:55:26 -04:00
Julia
24b0119f10 Polishing for formatting
Original Pull Request: #4455
2023-08-07 11:29:44 -04:00
Julia
8a97ea5a5e Add integration test to ensure schema validation fails when domain type property values are not encrypted as expected.
Closes #4454
Original Pull Request: #4455
2023-08-07 11:23:51 -04:00
Christoph Strobl
d7a8206000 Fix schema generation for encrypted fields that are considered domain entities.
This commit makes sure to consider the encrypted annotation on fields that are considered domain type property values, encrypting the entire object if necessary.
2023-08-07 11:23:50 -04:00
Oliver Christen
5c99c20ee2 Correct misspellings in documentation
Closes: #4461
2023-07-31 14:15:35 -04:00
Christoph Strobl
7946e2b7c9 Polishing.
Use previous context instead of root for mapping objects within an Inheriting context. This avoids accidental mapping of fields against the root entity after eg. a projection stage.
Add missing tests for AggregationOperationRenderer to ensure intended context propagation.

Original Pull Request: #4459
2023-07-28 07:45:41 +02:00
Julia Lee
adea4ba0a9 Fix mapping custom field names in downstream stages in TypedAggregation pipelines.
Use the root AggregationOperationContext in nested ExposedFieldsAggregationOperationContext to properly apply mapping for domain properties that use @Field.

Closes #4443
Original Pull Request: #4459
2023-07-28 07:45:22 +02:00
Julia Lee
529a0c7b15 Fix test setup so that temporal conversions use symmetric timezone setting.
Closes: #4446
Original Pull Request: #4447
2023-07-17 04:25:57 -04:00
Mark Paluch
77a96229c8 After release cleanups.
See #4420
2023-07-14 13:57:59 +02:00
Mark Paluch
4077224a04 Prepare next development iteration.
See #4420
2023-07-14 13:57:57 +02:00
Mark Paluch
fd175af09c Release version 4.1.2 (2023.0.2).
See #4420
2023-07-14 13:54:07 +02:00
Mark Paluch
a6372e6629 Prepare 4.1.2 (2023.0.2).
See #4420
2023-07-14 13:52:53 +02:00
Mark Paluch
0b6c9978e6 Polishing.
Reformat code, replace known unsupported constructor with UnsupportedOperationException.

See #4432
Original pull request: #4439
2023-07-10 11:03:30 +02:00
Christoph Strobl
edeb423b0e Fix encryption of java.time types.
This commit makes sure to convert java.time types into their BsonValue representation before encrypting.

See #4432
Original pull request: #4439
2023-07-10 10:49:23 +02:00
Christoph Strobl
6cee61d807 Fix decryption when client is using AutoEncryptionSettings#isBypassAutoEncryption().
This commit makes sure to convert already decrypted entries returned by the driver in case the client is configured with encryption settings.

Closes #4432
Original pull request: #4439
2023-07-10 10:49:21 +02:00
Mark Paluch
70530d6ee4 Update CI properties.
See #4420
2023-07-03 09:48:57 +02:00
Mark Paluch
9cfc46e589 Upgrade to Maven Wrapper 3.9.3.
See #4435
2023-07-03 09:48:21 +02:00
Christoph Strobl
7cb27c7465 Polishing.
Update tests to make use of ValueSource.
Replace regex based path inspection with segment by segment analysis.

Original Pull Request: #4427
2023-06-28 13:30:50 +02:00
lijixue
0cd082e7fa Fix QueryMapper property path resolution for nested paths containing numeric values.
Prior to this fix a path that contains numeric values used as position parameters would have been stripped in a way that left out the last digit. This could lead to wrong path resolution if the incorrectly constructed property name accidentally matched an existing one.

Closes: #4426
Original Pull Request: #4427
2023-06-28 13:30:40 +02:00
John Blum
56e763c9c0 After release cleanups.
See #4386
2023-06-16 08:13:30 -07:00
John Blum
489f593395 Prepare next development iteration.
See #4386
2023-06-16 08:13:28 -07:00
40 changed files with 1737 additions and 565 deletions

View File

@@ -1,2 +1,2 @@
#Tue Jun 13 08:53:53 CEST 2023
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip
#Mon Aug 14 07:55:26 EDT 2023
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip

View File

@@ -1,5 +1,5 @@
# Java versions
java.main.tag=17.0.6_10-jdk-focal
java.main.tag=17.0.8_7-jdk-focal
java.next.tag=20-jdk-jammy
# Docker container images - standard
@@ -7,15 +7,15 @@ docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/ecli
docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag}
# Supported versions of MongoDB
docker.mongodb.4.4.version=4.4.18
docker.mongodb.5.0.version=5.0.14
docker.mongodb.6.0.version=6.0.4
docker.mongodb.4.4.version=4.4.23
docker.mongodb.5.0.version=5.0.19
docker.mongodb.6.0.version=6.0.8
# Supported versions of Redis
docker.redis.6.version=6.2.10
docker.redis.6.version=6.2.13
# Supported versions of Cassandra
docker.cassandra.3.version=3.11.14
docker.cassandra.3.version=3.11.15
# Docker environment settings
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home

22
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.1</version>
<version>4.1.4-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.1.1</version>
<version>3.1.4-SNAPSHOT</version>
</parent>
<modules>
@@ -26,7 +26,7 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>3.1.1</springdata.commons>
<springdata.commons>3.1.4-SNAPSHOT</springdata.commons>
<mongo>4.9.1</mongo>
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>
@@ -144,8 +144,20 @@
</dependencies>
<repositories>
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.1</version>
<version>4.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.1</version>
<version>4.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.1</version>
<version>4.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -203,8 +203,9 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
target.properties(nestedProperties.toArray(new JsonSchemaProperty[0])), required));
}
}
return targetProperties.size() == 1 ? targetProperties.iterator().next()
JsonSchemaProperty schemaProperty = targetProperties.size() == 1 ? targetProperties.iterator().next()
: JsonSchemaProperty.merged(targetProperties);
return applyEncryptionDataIfNecessary(property, schemaProperty);
}
}

View File

@@ -63,7 +63,7 @@ class AggregationOperationRenderer {
contextToUse = new InheritingExposedFieldsAggregationOperationContext(fields, contextToUse);
} else {
contextToUse = fields.exposesNoFields() ? DEFAULT_CONTEXT
: new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), contextToUse);
: new ExposedFieldsAggregationOperationContext(fields, contextToUse);
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2022-2024 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.aggregation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* A special field that points to a variable {@code $$} expression.
*
* @author Christoph Strobl
* @since 4.1.3
*/
public interface AggregationVariable extends Field {
String PREFIX = "$$";
/**
* @return {@literal true} if the fields {@link #getName() name} does not match the defined {@link #getTarget()
* target}.
*/
@Override
default boolean isAliased() {
return !ObjectUtils.nullSafeEquals(getName(), getTarget());
}
@Override
default String getName() {
return getTarget();
}
@Override
default boolean isInternal() {
return false;
}
/**
* Create a new {@link AggregationVariable} for the given name.
* <p>
* Variables start with {@code $$}. If not, the given value gets prefixed with {@code $$}.
*
* @param value must not be {@literal null}.
* @return new instance of {@link AggregationVariable}.
* @throws IllegalArgumentException if given value is {@literal null}.
*/
static AggregationVariable variable(String value) {
Assert.notNull(value, "Value must not be null");
return new AggregationVariable() {
private final String val = AggregationVariable.prefixVariable(value);
@Override
public String getTarget() {
return val;
}
};
}
/**
* Create a new {@link #isInternal() local} {@link AggregationVariable} for the given name.
* <p>
* Variables start with {@code $$}. If not, the given value gets prefixed with {@code $$}.
*
* @param value must not be {@literal null}.
* @return new instance of {@link AggregationVariable}.
* @throws IllegalArgumentException if given value is {@literal null}.
*/
static AggregationVariable localVariable(String value) {
Assert.notNull(value, "Value must not be null");
return new AggregationVariable() {
private final String val = AggregationVariable.prefixVariable(value);
@Override
public String getTarget() {
return val;
}
@Override
public boolean isInternal() {
return true;
}
};
}
/**
* Check if the given field name reference may be variable.
*
* @param fieldRef can be {@literal null}.
* @return true if given value matches the variable identification pattern.
*/
static boolean isVariable(@Nullable String fieldRef) {
return fieldRef != null && fieldRef.stripLeading().matches("^\\$\\$\\w.*");
}
/**
* Check if the given field may be variable.
*
* @param field can be {@literal null}.
* @return true if given {@link Field field} is an {@link AggregationVariable} or if its value is a
* {@link #isVariable(String) variable}.
*/
static boolean isVariable(Field field) {
if (field instanceof AggregationVariable) {
return true;
}
return isVariable(field.getTarget());
}
private static String prefixVariable(String variable) {
var trimmed = variable.stripLeading();
return trimmed.startsWith(PREFIX) ? trimmed : (PREFIX + trimmed);
}
}

View File

@@ -1515,24 +1515,15 @@ public class ArrayOperators {
}
}
public enum Variable implements Field {
public enum Variable implements AggregationVariable {
THIS {
@Override
public String getName() {
return "$$this";
}
@Override
public String getTarget() {
return "$$this";
}
@Override
public boolean isAliased() {
return false;
}
@Override
public String toString() {
return getName();
@@ -1540,27 +1531,23 @@ public class ArrayOperators {
},
VALUE {
@Override
public String getName() {
return "$$value";
}
@Override
public String getTarget() {
return "$$value";
}
@Override
public boolean isAliased() {
return false;
}
@Override
public String toString() {
return getName();
}
};
@Override
public boolean isInternal() {
return true;
}
/**
* Create a {@link Field} reference to a given {@literal property} prefixed with the {@link Variable} identifier.
* eg. {@code $$value.product}
@@ -1592,6 +1579,16 @@ public class ArrayOperators {
}
};
}
public static boolean isVariable(Field field) {
for (Variable var : values()) {
if (field.getTarget().startsWith(var.getTarget())) {
return true;
}
}
return false;
}
}
}

View File

@@ -67,7 +67,7 @@ public final class Fields implements Iterable<Field> {
Assert.notNull(names, "Field names must not be null");
List<Field> fields = new ArrayList<Field>();
List<Field> fields = new ArrayList<>();
for (String name : names) {
fields.add(field(name));
@@ -114,7 +114,7 @@ public final class Fields implements Iterable<Field> {
private static List<Field> verify(List<Field> fields) {
Map<String, Field> reference = new HashMap<String, Field>();
Map<String, Field> reference = new HashMap<>();
for (Field field : fields) {
@@ -133,7 +133,7 @@ public final class Fields implements Iterable<Field> {
private Fields(Fields existing, Field tail) {
this.fields = new ArrayList<Field>(existing.fields.size() + 1);
this.fields = new ArrayList<>(existing.fields.size() + 1);
this.fields.addAll(existing.fields);
this.fields.add(tail);
}
@@ -245,7 +245,7 @@ public final class Fields implements Iterable<Field> {
private static String cleanUp(String source) {
if (SystemVariable.isReferingToSystemVariable(source)) {
if (AggregationVariable.isVariable(source)) {
return source;
}
@@ -253,10 +253,12 @@ public final class Fields implements Iterable<Field> {
return dollarIndex == -1 ? source : source.substring(dollarIndex + 1);
}
@Override
public String getName() {
return name;
}
@Override
public String getTarget() {
if (isLocalVar() || pointsToDBRefId()) {

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
/**
@@ -22,6 +23,7 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldRefe
* {@link AggregationOperationContext}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 1.9
*/
class InheritingExposedFieldsAggregationOperationContext extends ExposedFieldsAggregationOperationContext {
@@ -43,6 +45,11 @@ class InheritingExposedFieldsAggregationOperationContext extends ExposedFieldsAg
this.previousContext = previousContext;
}
@Override
public Document getMappedObject(Document document) {
return previousContext.getMappedObject(document);
}
@Override
protected FieldReference resolveExposedField(Field field, String name) {

View File

@@ -21,7 +21,6 @@ import java.util.Collections;
import java.util.List;
import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.expression.spel.ast.Projection;
import org.springframework.util.Assert;
@@ -431,6 +430,7 @@ public class ReplaceRootOperation implements FieldsExposingAggregationOperation
* @param context will never be {@literal null}.
* @return never {@literal null}.
*/
@Override
Document toDocument(AggregationOperationContext context);
}

View File

@@ -24,7 +24,7 @@ import org.springframework.lang.Nullable;
* @author Christoph Strobl
* @see <a href="https://docs.mongodb.com/manual/reference/aggregation-variables">Aggregation Variables</a>.
*/
public enum SystemVariable {
public enum SystemVariable implements AggregationVariable {
/**
* Variable for the current datetime.
@@ -82,8 +82,6 @@ public enum SystemVariable {
*/
SEARCH_META;
private static final String PREFIX = "$$";
/**
* Return {@literal true} if the given {@code fieldRef} denotes a well-known system variable, {@literal false}
* otherwise.
@@ -93,13 +91,12 @@ public enum SystemVariable {
*/
public static boolean isReferingToSystemVariable(@Nullable String fieldRef) {
if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
String candidate = variableNameFrom(fieldRef);
if (candidate == null) {
return false;
}
int indexOfFirstDot = fieldRef.indexOf('.');
String candidate = fieldRef.substring(2, indexOfFirstDot == -1 ? fieldRef.length() : indexOfFirstDot);
candidate = candidate.startsWith(PREFIX) ? candidate.substring(2) : candidate;
for (SystemVariable value : values()) {
if (value.name().equals(candidate)) {
return true;
@@ -113,4 +110,20 @@ public enum SystemVariable {
public String toString() {
return PREFIX.concat(name());
}
@Override
public String getTarget() {
return toString();
}
@Nullable
static String variableNameFrom(@Nullable String fieldRef) {
if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
return null;
}
int indexOfFirstDot = fieldRef.indexOf('.');
return indexOfFirstDot == -1 ? fieldRef : fieldRef.substring(2, indexOfFirstDot);
}
}

View File

@@ -133,7 +133,7 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
protected FieldReference getReferenceFor(Field field) {
if(entity.getNullable() == null) {
if(entity.getNullable() == null || AggregationVariable.isVariable(field)) {
return new DirectFieldReference(new ExposedField(field, true));
}

View File

@@ -98,6 +98,7 @@ import com.mongodb.DBRef;
* @author Roman Puchkovskiy
* @author Heesu Jung
* @author Divya Srivastava
* @author Julia Lee
*/
public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware {
@@ -1976,8 +1977,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (property.isDocumentReference()) {
return (T) dbRefResolver.resolveReference(property, accessor.get(property), referenceLookupDelegate,
context::convert);
return (T) dbRefResolver.resolveReference(property,
new DocumentReferenceSource(accessor.getDocument(), accessor.get(property)),
referenceLookupDelegate, context::convert);
}
return super.getPropertyValue(property);

View File

@@ -1089,7 +1089,7 @@ public class QueryMapper {
protected static class MetadataBackedField extends Field {
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
private static final Pattern DOT_POSITIONAL_PATTERN = Pattern.compile("\\.\\d+(?!$)");
private static final Pattern NUMERIC_SEGMENT = Pattern.compile("\\d+");
private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s; Associations can only be pointed to directly or via their id property";
private final MongoPersistentEntity<?> entity;
@@ -1231,14 +1231,13 @@ public class QueryMapper {
private PersistentPropertyPath<MongoPersistentProperty> getPath(String pathExpression,
@Nullable MongoPersistentProperty sourceProperty) {
String rawPath = removePlaceholders(POSITIONAL_OPERATOR,
removePlaceholders(DOT_POSITIONAL_PATTERN, pathExpression));
if (sourceProperty != null && sourceProperty.getOwner().equals(entity)) {
return mappingContext.getPersistentPropertyPath(
PropertyPath.from(Pattern.quote(sourceProperty.getName()), entity.getTypeInformation()));
}
String rawPath = resolvePath(pathExpression);
PropertyPath path = forName(rawPath);
if (path == null || isPathToJavaLangClassProperty(path)) {
return null;
@@ -1333,6 +1332,38 @@ public class QueryMapper {
return false;
}
private static String resolvePath(String source) {
String[] segments = source.split("\\.");
if (segments.length == 1) {
return source;
}
List<String> path = new ArrayList<>(segments.length);
/* always start from a property, so we can skip the first segment.
from there remove any position placeholder */
for(int i=1; i < segments.length; i++) {
String segment = segments[i];
if (segment.startsWith("[") && segment.endsWith("]")) {
continue;
}
if (NUMERIC_SEGMENT.matcher(segment).matches()) {
continue;
}
path.add(segment);
}
// when property is followed only by placeholders eg. 'values.0.3.90'
// or when there is no difference in the number of segments
if (path.isEmpty() || segments.length == path.size() + 1) {
return source;
}
path.add(0, segments[0]);
return StringUtils.collectionToDelimitedString(path, ".");
}
/**
* Return the {@link Converter} to be used to created the mapped key. Default implementation will use
* {@link PropertyToFieldNameConverter}.

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.convert.encryption;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -63,7 +64,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
public Object read(Object value, MongoConversionContext context) {
Object decrypted = EncryptingConverter.super.read(value, context);
return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted;
return decrypted instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : decrypted;
}
@Override
@@ -87,36 +88,56 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
}
MongoPersistentProperty persistentProperty = getProperty(context);
if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable<?> iterable) {
int size = iterable instanceof Collection<?> c ? c.size() : 10;
if (!persistentProperty.isEntity()) {
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it)));
iterable.forEach(it -> {
if (it instanceof BsonValue bsonValue) {
collection.add(BsonUtils.toJavaType(bsonValue));
} else {
collection.add(context.read(it, persistentProperty.getActualType()));
}
});
return collection;
} else {
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
iterable.forEach(it -> {
collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType()));
if (it instanceof BsonValue bsonValue) {
collection.add(context.read(BsonUtils.toJavaType(bsonValue), persistentProperty.getActualType()));
} else {
collection.add(context.read(it, persistentProperty.getActualType()));
}
});
return collection;
}
}
if (!persistentProperty.isEntity() && decryptedValue instanceof BsonValue bsonValue) {
if (persistentProperty.isMap() && persistentProperty.getType() != Document.class) {
return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue));
if (!persistentProperty.isEntity() && persistentProperty.isMap()) {
if (persistentProperty.getType() != Document.class) {
if (decryptedValue instanceof BsonValue bsonValue) {
return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue));
}
if (decryptedValue instanceof Document document) {
return new LinkedHashMap<>(document);
}
if (decryptedValue instanceof Map map) {
return map;
}
}
return BsonUtils.toJavaType(bsonValue);
}
if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) {
return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation().getType());
}
if (persistentProperty.isEntity() && decryptedValue instanceof Document document) {
return context.read(document, persistentProperty.getTypeInformation().getType());
}
return decryptedValue;
}

View File

@@ -36,6 +36,7 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.Timest
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link JsonSchemaProperty} implementation.
@@ -1139,7 +1140,9 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
enc.append("bsonType", type.toBsonType().value()); // TODO: no samples with type -> is it bson type all the way?
}
enc.append("algorithm", algorithm);
if (StringUtils.hasText(algorithm)) {
enc.append("algorithm", algorithm);
}
propertySpecification.append("encrypt", enc);

View File

@@ -15,6 +15,12 @@
*/
package org.springframework.data.mongodb.observability;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import java.util.function.BiConsumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
@@ -27,10 +33,6 @@ import com.mongodb.event.CommandListener;
import com.mongodb.event.CommandStartedEvent;
import com.mongodb.event.CommandSucceededEvent;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
/**
* Implement MongoDB's {@link CommandListener} using Micrometer's {@link Observation} API.
*
@@ -126,50 +128,54 @@ public class MongoObservationCommandListener implements CommandListener {
@Override
public void commandSucceeded(CommandSucceededEvent event) {
RequestContext requestContext = event.getRequestContext();
doInObservation(event.getRequestContext(), (observation, context) -> {
if (requestContext == null) {
return;
}
context.setCommandSucceededEvent(event);
Observation observation = requestContext.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
if (observation == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Command succeeded - will stop observation [" + observation + "]");
}
MongoHandlerContext context = (MongoHandlerContext) observation.getContext();
context.setCommandSucceededEvent(event);
if (log.isDebugEnabled()) {
log.debug("Command succeeded - will stop observation [" + observation + "]");
}
observation.stop();
observation.stop();
});
}
@Override
public void commandFailed(CommandFailedEvent event) {
RequestContext requestContext = event.getRequestContext();
doInObservation(event.getRequestContext(), (observation, context) -> {
context.setCommandFailedEvent(event);
if (log.isDebugEnabled()) {
log.debug("Command failed - will stop observation [" + observation + "]");
}
observation.error(event.getThrowable());
observation.stop();
});
}
/**
* Performs the given action for the {@link Observation} and {@link MongoHandlerContext} if there is an ongoing Mongo
* Observation. Exceptions thrown by the action are relayed to the caller.
*
* @param requestContext the context to extract the Observation from.
* @param action the action to invoke.
*/
private void doInObservation(@Nullable RequestContext requestContext,
BiConsumer<Observation, MongoHandlerContext> action) {
if (requestContext == null) {
return;
}
Observation observation = requestContext.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
if (observation == null) {
if (observation == null || !(observation.getContext()instanceof MongoHandlerContext context)) {
return;
}
MongoHandlerContext context = (MongoHandlerContext) observation.getContext();
context.setCommandFailedEvent(event);
if (log.isDebugEnabled()) {
log.debug("Command failed - will stop observation [" + observation + "]");
}
observation.error(event.getThrowable());
observation.stop();
action.accept(observation, context);
}
/**

View File

@@ -15,6 +15,12 @@
*/
package org.springframework.data.mongodb.util;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -280,36 +286,22 @@ public class BsonUtils {
*/
public static Object toJavaType(BsonValue value) {
switch (value.getBsonType()) {
case INT32:
return value.asInt32().getValue();
case INT64:
return value.asInt64().getValue();
case STRING:
return value.asString().getValue();
case DECIMAL128:
return value.asDecimal128().doubleValue();
case DOUBLE:
return value.asDouble().getValue();
case BOOLEAN:
return value.asBoolean().getValue();
case OBJECT_ID:
return value.asObjectId().getValue();
case DB_POINTER:
return new DBRef(value.asDBPointer().getNamespace(), value.asDBPointer().getId());
case BINARY:
return value.asBinary().getData();
case DATE_TIME:
return new Date(value.asDateTime().getValue());
case SYMBOL:
return value.asSymbol().getSymbol();
case ARRAY:
return value.asArray().toArray();
case DOCUMENT:
return Document.parse(value.asDocument().toJson());
default:
return value;
}
return switch (value.getBsonType()) {
case INT32 -> value.asInt32().getValue();
case INT64 -> value.asInt64().getValue();
case STRING -> value.asString().getValue();
case DECIMAL128 -> value.asDecimal128().doubleValue();
case DOUBLE -> value.asDouble().getValue();
case BOOLEAN -> value.asBoolean().getValue();
case OBJECT_ID -> value.asObjectId().getValue();
case DB_POINTER -> new DBRef(value.asDBPointer().getNamespace(), value.asDBPointer().getId());
case BINARY -> value.asBinary().getData();
case DATE_TIME -> new Date(value.asDateTime().getValue());
case SYMBOL -> value.asSymbol().getSymbol();
case ARRAY -> value.asArray().toArray();
case DOCUMENT -> Document.parse(value.asDocument().toJson());
default -> value;
};
}
/**
@@ -358,17 +350,36 @@ public class BsonUtils {
return new BsonDouble(floatValue);
}
if(source instanceof Binary binary) {
if (source instanceof Binary binary) {
return new BsonBinary(binary.getType(), binary.getData());
}
if (source instanceof Temporal) {
if (source instanceof Instant value) {
return new BsonDateTime(value.toEpochMilli());
}
if (source instanceof LocalDateTime value) {
return new BsonDateTime(value.toInstant(ZoneOffset.UTC).toEpochMilli());
}
if (source instanceof LocalDate value) {
return new BsonDateTime(value.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli());
}
if (source instanceof LocalTime value) {
return new BsonDateTime(value.atDate(LocalDate.ofEpochDay(0L)).toInstant(ZoneOffset.UTC).toEpochMilli());
}
}
if (source instanceof Date date) {
new BsonDateTime(date.getTime());
}
throw new IllegalArgumentException(String.format("Unable to convert %s (%s) to BsonValue.", source,
source != null ? source.getClass().getName() : "null"));
}
/**
* Merge the given {@link Document documents} into on in the given order. Keys contained within multiple documents are
* overwritten by their follow ups.
* overwritten by their follow-ups.
*
* @param documents must not be {@literal null}. Can be empty.
* @return the document containing all key value pairs.
@@ -669,7 +680,7 @@ public class BsonUtils {
if (value instanceof Collection<?> collection) {
return toString(collection);
} else if (value instanceof Map<?,?> map) {
} else if (value instanceof Map<?, ?> map) {
return toString(map);
} else if (ObjectUtils.isArray(value)) {
return toString(Arrays.asList(ObjectUtils.toObjectArray(value)));
@@ -691,8 +702,9 @@ public class BsonUtils {
private static String toString(Map<?, ?> source) {
// Avoid String.format for performance
return iterableToDelimitedString(source.entrySet(), "{ ", " }",
entry -> String.format("\"%s\" : %s", entry.getKey(), toJson(entry.getValue())));
entry -> "\"" + entry.getKey() + "\" : " + toJson(entry.getValue()));
}
private static String toString(Collection<?> source) {

View File

@@ -271,6 +271,17 @@ class MappingMongoJsonSchemaCreatorUnitTests {
.containsEntry("properties.value", new Document("type", "string"));
}
@Test // GH-4454
void wrapEncryptedEntityTypeLikeProperty() {
MongoJsonSchema schema = MongoJsonSchemaCreator.create() //
.filter(MongoJsonSchemaCreator.encryptedOnly()) // filter non encrypted fields
.createSchemaFor(WithEncryptedEntityLikeProperty.class);
assertThat(schema.schemaDocument()) //
.containsEntry("properties.domainTypeValue", Document.parse("{'encrypt': {'bsonType': 'object' } }"));
}
// --> TYPES AND JSON
// --> ENUM
@@ -676,4 +687,9 @@ class MappingMongoJsonSchemaCreatorUnitTests {
static class PropertyClashWithA {
Integer aNonEncrypted;
}
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
static class WithEncryptedEntityLikeProperty {
@Encrypted SomeDomainType domainTypeValue;
}
}

View File

@@ -62,6 +62,7 @@ import com.mongodb.client.model.Filters;
* {@link DocumentReference} related integration tests for {@link MongoTemplate}.
*
* @author Christoph Strobl
* @author Julia Lee
*/
@ExtendWith(MongoClientExtension.class)
public class MongoTemplateDocumentReferenceTests {
@@ -1270,6 +1271,32 @@ public class MongoTemplateDocumentReferenceTests {
.isEqualTo(new ObjectRefHavingStringIdTargetType(id.toHexString(), "me-the-referenced-object"));
}
@Test // GH-4484
void resolveReferenceForOneToManyLookupWithSelfVariableWhenUsedInCtorArgument() {
OneToManyStylePublisherWithRequiredArgsCtor publisher = new OneToManyStylePublisherWithRequiredArgsCtor("p-100", null);
template.save(publisher);
OneToManyStyleBook book1 = new OneToManyStyleBook();
book1.id = "id-1";
book1.publisherId = publisher.id;
OneToManyStyleBook book2 = new OneToManyStyleBook();
book2.id = "id-2";
book2.publisherId = "p-200";
OneToManyStyleBook book3 = new OneToManyStyleBook();
book3.id = "id-3";
book3.publisherId = publisher.id;
template.save(book1);
template.save(book2);
template.save(book3);
OneToManyStylePublisherWithRequiredArgsCtor target = template.findOne(query(where("id").is(publisher.id)), OneToManyStylePublisherWithRequiredArgsCtor.class);
assertThat(target.books).containsExactlyInAnyOrder(book1, book3);
}
@Data
static class SingleRefRoot {
@@ -1614,4 +1641,40 @@ public class MongoTemplateDocumentReferenceTests {
@DocumentReference private List<WithListOfRefs> refs;
}
static class OneToManyStylePublisherWithRequiredArgsCtor {
@Id
String id;
@ReadOnlyProperty
@DocumentReference(lookup="{'publisherId':?#{#self._id} }")
List<OneToManyStyleBook> books;
public OneToManyStylePublisherWithRequiredArgsCtor(String id, List<OneToManyStyleBook> books) {
this.id = id;
this.books = books;
}
public String getId() {
return this.id;
}
public List<OneToManyStyleBook> getBooks() {
return this.books;
}
public void setId(String id) {
this.id = id;
}
public void setBooks(List<OneToManyStyleBook> books) {
this.books = books;
}
public String toString() {
return "MongoTemplateDocumentReferenceTests.OneToManyStylePublisherWithRequiredArgsCtor(id=" + this.getId() + ", book="
+ this.getBooks() + ")";
}
}
}

View File

@@ -35,8 +35,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.CollectionOptions.ValidationOptions;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import org.springframework.lang.Nullable;
@@ -48,11 +50,13 @@ import com.mongodb.client.model.ValidationLevel;
/**
* Integration tests for {@link CollectionOptions#validation(ValidationOptions)} using
* {@link org.springframework.data.mongodb.core.validation.CriteriaValidator} and
* {@link org.springframework.data.mongodb.core.validation.DocumentValidator}.
* {@link org.springframework.data.mongodb.core.validation.CriteriaValidator},
* {@link org.springframework.data.mongodb.core.validation.DocumentValidator} and
* {@link org.springframework.data.mongodb.core.validation.JsonSchemaValidator}.
*
* @author Andreas Zink
* @author Christoph Strobl
* @author Julia Lee
*/
@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
public class MongoTemplateValidationTests {
@@ -188,6 +192,20 @@ public class MongoTemplateValidationTests {
assertThat(getValidatorInfo(COLLECTION_NAME)).isEqualTo(new Document("customName", new Document("$type", "bool")));
}
@Test // GH-4454
public void failsJsonSchemaValidationForEncryptedDomainEntityProperty() {
MongoJsonSchema schema = MongoJsonSchemaCreator.create().createSchemaFor(BeanWithEncryptedDomainEntity.class);
template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schema(schema));
BeanWithEncryptedDomainEntity person = new BeanWithEncryptedDomainEntity();
person.encryptedDomainEntity = new SimpleBean("some string", 100, null);
assertThatExceptionOfType(DataIntegrityViolationException.class)
.isThrownBy(() -> template.save(person))
.withMessageContaining("Document failed validation");
}
private Document getCollectionOptions(String collectionName) {
return getCollectionInfo(collectionName).get("options", Document.class);
}
@@ -222,4 +240,10 @@ public class MongoTemplateValidationTests {
private @Nullable Integer rangedInteger;
private @Field("customName") Object customFieldName;
}
@org.springframework.data.mongodb.core.mapping.Document(collection = COLLECTION_NAME)
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
static class BeanWithEncryptedDomainEntity {
@Encrypted SimpleBean encryptedDomainEntity;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2023 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.aggregation;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.List;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
/**
* @author Christoph Strobl
*/
public class AggregationOperationRendererUnitTests {
@Test // GH-4443
void nonFieldsExposingAggregationOperationContinuesWithSameContextForNextStage() {
AggregationOperationContext rootContext = mock(AggregationOperationContext.class);
AggregationOperation stage1 = mock(AggregationOperation.class);
AggregationOperation stage2 = mock(AggregationOperation.class);
AggregationOperationRenderer.toDocument(List.of(stage1, stage2), rootContext);
verify(stage1).toPipelineStages(eq(rootContext));
verify(stage2).toPipelineStages(eq(rootContext));
}
@Test // GH-4443
void fieldsExposingAggregationOperationNotExposingFieldsForcesUseOfDefaultContextForNextStage() {
AggregationOperationContext rootContext = mock(AggregationOperationContext.class);
FieldsExposingAggregationOperation stage1 = mock(FieldsExposingAggregationOperation.class);
ExposedFields stage1fields = mock(ExposedFields.class);
AggregationOperation stage2 = mock(AggregationOperation.class);
when(stage1.getFields()).thenReturn(stage1fields);
when(stage1fields.exposesNoFields()).thenReturn(true);
AggregationOperationRenderer.toDocument(List.of(stage1, stage2), rootContext);
verify(stage1).toPipelineStages(eq(rootContext));
verify(stage2).toPipelineStages(eq(AggregationOperationRenderer.DEFAULT_CONTEXT));
}
@Test // GH-4443
void fieldsExposingAggregationOperationForcesNewContextForNextStage() {
AggregationOperationContext rootContext = mock(AggregationOperationContext.class);
FieldsExposingAggregationOperation stage1 = mock(FieldsExposingAggregationOperation.class);
ExposedFields stage1fields = mock(ExposedFields.class);
AggregationOperation stage2 = mock(AggregationOperation.class);
when(stage1.getFields()).thenReturn(stage1fields);
when(stage1fields.exposesNoFields()).thenReturn(false);
ArgumentCaptor<AggregationOperationContext> captor = ArgumentCaptor.forClass(AggregationOperationContext.class);
AggregationOperationRenderer.toDocument(List.of(stage1, stage2), rootContext);
verify(stage1).toPipelineStages(eq(rootContext));
verify(stage2).toPipelineStages(captor.capture());
assertThat(captor.getValue()).isInstanceOf(ExposedFieldsAggregationOperationContext.class)
.isNotInstanceOf(InheritingExposedFieldsAggregationOperationContext.class);
}
@Test // GH-4443
void inheritingFieldsExposingAggregationOperationForcesNewContextForNextStageKeepingReferenceToPreviousContext() {
AggregationOperationContext rootContext = mock(AggregationOperationContext.class);
InheritsFieldsAggregationOperation stage1 = mock(InheritsFieldsAggregationOperation.class);
InheritsFieldsAggregationOperation stage2 = mock(InheritsFieldsAggregationOperation.class);
InheritsFieldsAggregationOperation stage3 = mock(InheritsFieldsAggregationOperation.class);
ExposedFields exposedFields = mock(ExposedFields.class);
when(exposedFields.exposesNoFields()).thenReturn(false);
when(stage1.getFields()).thenReturn(exposedFields);
when(stage2.getFields()).thenReturn(exposedFields);
when(stage3.getFields()).thenReturn(exposedFields);
ArgumentCaptor<AggregationOperationContext> captor = ArgumentCaptor.forClass(AggregationOperationContext.class);
AggregationOperationRenderer.toDocument(List.of(stage1, stage2, stage3), rootContext);
verify(stage1).toPipelineStages(captor.capture());
verify(stage2).toPipelineStages(captor.capture());
verify(stage3).toPipelineStages(captor.capture());
assertThat(captor.getAllValues().get(0)).isEqualTo(rootContext);
assertThat(captor.getAllValues().get(1))
.asInstanceOf(InstanceOfAssertFactories.type(InheritingExposedFieldsAggregationOperationContext.class))
.extracting("previousContext").isSameAs(captor.getAllValues().get(0));
assertThat(captor.getAllValues().get(2))
.asInstanceOf(InstanceOfAssertFactories.type(InheritingExposedFieldsAggregationOperationContext.class))
.extracting("previousContext").isSameAs(captor.getAllValues().get(1));
}
}

View File

@@ -91,6 +91,7 @@ import com.mongodb.client.MongoCollection;
* @author Sergey Shcherbakov
* @author Minsu Kim
* @author Sangyong Choi
* @author Julia Lee
*/
@ExtendWith(MongoTemplateExtension.class)
public class AggregationTests {
@@ -119,7 +120,7 @@ public class AggregationTests {
mongoTemplate.flush(Product.class, UserWithLikes.class, DATAMONGO753.class, Data.class, DATAMONGO788.class,
User.class, Person.class, Reservation.class, Venue.class, MeterData.class, LineItem.class, InventoryItem.class,
Sales.class, Sales2.class, Employee.class, Art.class, Venue.class);
Sales.class, Sales2.class, Employee.class, Art.class, Venue.class, Item.class);
mongoTemplate.dropCollection(INPUT_COLLECTION);
mongoTemplate.dropCollection("personQueryTemp");
@@ -1992,6 +1993,42 @@ public class AggregationTests {
assertThat(aggregate.getMappedResults()).contains(widget);
}
@Test // GH-4443
void shouldHonorFieldAliasesForFieldReferencesUsingFieldExposingOperation() {
Item item1 = Item.builder().itemId("1").tags(Arrays.asList("a", "b")).build();
Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build();
mongoTemplate.insert(Arrays.asList(item1, item2), Item.class);
TypedAggregation<Item> aggregation = newAggregation(Item.class,
match(where("itemId").is("1")),
unwind("tags"),
match(where("itemId").is("1").and("tags").is("c")));
AggregationResults<Document> results = mongoTemplate.aggregate(aggregation, Document.class);
List<Document> mappedResults = results.getMappedResults();
assertThat(mappedResults).hasSize(1);
assertThat(mappedResults.get(0)).containsEntry("item_id", "1");
}
@Test // GH-4443
void projectShouldResetContextToAvoidMappingFieldsAgainstANoLongerExistingTarget() {
Item item1 = Item.builder().itemId("1").tags(Arrays.asList("a", "b")).build();
Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build();
mongoTemplate.insert(Arrays.asList(item1, item2), Item.class);
TypedAggregation<Item> aggregation = newAggregation(Item.class,
match(where("itemId").is("1")),
unwind("tags"),
project().and("itemId").as("itemId").and("tags").as("tags"),
match(where("itemId").is("1").and("tags").is("c")));
AggregationResults<Document> results = mongoTemplate.aggregate(aggregation, Document.class);
List<Document> mappedResults = results.getMappedResults();
assertThat(mappedResults).hasSize(1);
assertThat(mappedResults.get(0)).containsEntry("itemId", "1");
}
private void createUsersWithReferencedPersons() {
mongoTemplate.dropCollection(User.class);
@@ -2244,7 +2281,7 @@ public class AggregationTests {
List<Item> items;
}
// DATAMONGO-1491
// DATAMONGO-1491, GH-4443
@lombok.Data
@Builder
static class Item {
@@ -2253,6 +2290,7 @@ public class AggregationTests {
String itemId;
Integer quantity;
Long price;
List<String> tags = new ArrayList<>();
}
// DATAMONGO-1538

View File

@@ -49,6 +49,7 @@ import com.mongodb.client.model.Projections;
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Julia Lee
*/
public class AggregationUnitTests {
@@ -612,7 +613,7 @@ public class AggregationUnitTests {
WithRetypedIdField.class, mappingContext,
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)));
Document document = project(WithRetypedIdField.class).toDocument(context);
assertThat(document).isEqualTo(new Document("$project", new Document("_id", 1).append("renamed-field", 1)));
assertThat(document).isEqualTo(new Document("$project", new Document("_id", 1).append("renamed-field", 1).append("entries", 1)));
}
@Test // GH-4038
@@ -653,6 +654,22 @@ public class AggregationUnitTests {
assertThat(documents.get(2)).isEqualTo("{ $sort : { 'serial_number' : -1, 'label_name' : -1 } }");
}
@Test // GH-4443
void fieldsExposingContextShouldUseCustomFieldNameFromRelaxedRootContext() {
MongoMappingContext mappingContext = new MongoMappingContext();
RelaxedTypeBasedAggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(
WithRetypedIdField.class, mappingContext,
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)));
TypedAggregation<WithRetypedIdField> agg = newAggregation(WithRetypedIdField.class,
unwind("entries"), match(where("foo").is("value 2")));
List<Document> pipeline = agg.toPipeline(context);
Document fields = getAsDocument(pipeline.get(1), "$match");
assertThat(fields.get("renamed-field")).isEqualTo("value 2");
}
private Document extractPipelineElement(Document agg, int index, String operation) {
List<Document> pipeline = (List<Document>) agg.get("pipeline");
@@ -672,5 +689,7 @@ public class AggregationUnitTests {
@org.springframework.data.mongodb.core.mapping.Field("renamed-field") private String foo;
private List<String> entries = new ArrayList<>();
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2022-2023 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.aggregation;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
/**
* Unit tests for {@link AggregationVariable}.
*
* @author Christoph Strobl
*/
class AggregationVariableUnitTests {
@Test // GH-4070
void variableErrorsOnNullValue() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> AggregationVariable.variable(null));
}
@Test // GH-4070
void createsVariable() {
var variable = AggregationVariable.variable("$$now");
assertThat(variable.getTarget()).isEqualTo("$$now");
assertThat(variable.isInternal()).isFalse();
}
@Test // GH-4070
void prefixesVariableIfNeeded() {
var variable = AggregationVariable.variable("this");
assertThat(variable.getTarget()).isEqualTo("$$this");
}
@Test // GH-4070
void localVariableErrorsOnNullValue() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> AggregationVariable.localVariable(null));
}
@Test // GH-4070
void localVariable() {
var variable = AggregationVariable.localVariable("$$this");
assertThat(variable.getTarget()).isEqualTo("$$this");
assertThat(variable.isInternal()).isTrue();
}
@Test // GH-4070
void prefixesLocalVariableIfNeeded() {
var variable = AggregationVariable.localVariable("this");
assertThat(variable.getTarget()).isEqualTo("$$this");
}
@Test // GH-4070
void isVariableReturnsTrueForAggregationVariableTypes() {
var variable = Mockito.mock(AggregationVariable.class);
assertThat(AggregationVariable.isVariable(variable)).isTrue();
}
@Test // GH-4070
void isVariableReturnsTrueForFieldThatTargetsVariable() {
var variable = Fields.field("value", "$$this");
assertThat(AggregationVariable.isVariable(variable)).isTrue();
}
@Test // GH-4070
void isVariableReturnsFalseForFieldThatDontTargetsVariable() {
var variable = Fields.field("value", "$this");
assertThat(AggregationVariable.isVariable(variable)).isFalse();
}
}

View File

@@ -20,8 +20,6 @@ import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.aggregation.Fields.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import lombok.AllArgsConstructor;
import java.util.Arrays;
import java.util.List;
@@ -39,8 +37,11 @@ import org.springframework.data.convert.CustomConversions;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce;
import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce.Variable;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.SetOperators.SetUnion;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
@@ -455,14 +456,44 @@ public class TypeBasedAggregationOperationContextUnitTests {
.isEqualTo(new Document("val", "$withUnwrapped.prefix-with-at-field-annotation"));
}
@Test // GH-4070
void rendersLocalVariables() {
AggregationOperationContext context = getContext(WithLists.class);
Document agg = newAggregation(WithLists.class,
project()
.and(Reduce.arrayOf("listOfListOfString").withInitialValue(field("listOfString"))
.reduce(SetUnion.arrayAsSet(Variable.VALUE.getTarget()).union(Variable.THIS.getTarget())))
.as("listOfString")).toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")).isEqualTo(Document.parse("""
{
"listOfString" : {
"$reduce" : {
"in" : { "$setUnion" : ["$$value", "$$this"] },
"initialValue" : "$listOfString",
"input" : "$listOfListOfString"
}
}
}
"""));
}
@org.springframework.data.mongodb.core.mapping.Document(collection = "person")
@AllArgsConstructor
public static class FooPerson {
final ObjectId id;
final String name;
@org.springframework.data.mongodb.core.mapping.Field("last_name") final String lastName;
final Age age;
public FooPerson(ObjectId id, String name, String lastName, Age age) {
this.id = id;
this.name = name;
this.lastName = lastName;
this.age = age;
}
}
public static class Age {
@@ -553,4 +584,9 @@ public class TypeBasedAggregationOperationContextUnitTests {
@org.springframework.data.mongodb.core.mapping.Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
}
static class WithLists {
public List<String> listOfString;
public List<List<String>> listOfListOfString;
}
}

View File

@@ -30,7 +30,6 @@ import java.math.BigInteger;
import java.net.URL;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.*;
@@ -110,6 +109,7 @@ import com.mongodb.DBRef;
* @author Mark Paluch
* @author Roman Puchkovskiy
* @author Heesu Jung
* @author Julia Lee
*/
@ExtendWith(MockitoExtension.class)
class MappingMongoConverterUnitTests {
@@ -2623,7 +2623,7 @@ class MappingMongoConverterUnitTests {
void projectShouldReadSimpleInterfaceProjection() {
org.bson.Document source = new org.bson.Document("birthDate",
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay().toInstant(ZoneOffset.UTC))).append("foo", "Walter");
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay(systemDefault()).toInstant())).append("foo", "Walter");
EntityProjectionIntrospector discoverer = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
@@ -2641,7 +2641,7 @@ class MappingMongoConverterUnitTests {
void projectShouldReadSimpleDtoProjection() {
org.bson.Document source = new org.bson.Document("birthDate",
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay().toInstant(ZoneOffset.UTC))).append("foo", "Walter");
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay(systemDefault()).toInstant())).append("foo", "Walter");
EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()

View File

@@ -35,6 +35,8 @@ import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.convert.converter.Converter;
@@ -1213,24 +1215,26 @@ class UpdateMapperUnitTests {
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.a.b.d", "e")));
}
@Test // GH-3775
void mapNestedIntegerFieldCorrectly() {
@ParameterizedTest // GH-3775, GH-4426
@ValueSource(strings = {"levelOne.0.1.3", "levelOne.0.1.32", "levelOne2.0.1.32", "levelOne2.0.1.320"})
void mapNestedIntegerFieldCorrectly(String path) {
Update update = new Update().set("levelOne.0.1.3", "4");
Update update = new Update().set(path, "4");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(EntityWithNestedMap.class));
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.0.1.3", "4")));
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document(path, "4")));
}
@Test // GH-3775
void mapNestedMixedStringIntegerFieldCorrectly() {
@ParameterizedTest // GH-3775, GH-4426
@ValueSource(strings = {"levelOne.0.1.c", "levelOne.0.1.c.32", "levelOne2.0.1.32.c", "levelOne2.0.1.c.320"})
void mapNestedMixedStringIntegerFieldCorrectly(String path) {
Update update = new Update().set("levelOne.0.1.c", "4");
Update update = new Update().set(path, "4");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(EntityWithNestedMap.class));
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.0.1.c", "4")));
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document(path, "4")));
}
@Test // GH-3775
@@ -1720,6 +1724,7 @@ class UpdateMapperUnitTests {
static class EntityWithNestedMap {
Map<String, Map<String, Map<String, Object>>> levelOne;
Map<String, Map<String, Map<String, Object>>> levelOne2;
}
static class Customer {

View File

@@ -0,0 +1,756 @@
/*
* Copyright 2023 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.encryption;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import java.security.SecureRandom;
import java.time.LocalDate;
import java.time.Month;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.assertj.core.api.Assertions;
import org.bson.BsonBinary;
import org.bson.Document;
import org.bson.types.Binary;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.Lazy;
import com.mongodb.ClientEncryptionSettings;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoNamespace;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.client.model.vault.DataKeyOptions;
import com.mongodb.client.vault.ClientEncryption;
import com.mongodb.client.vault.ClientEncryptions;
/**
* @author Christoph Strobl
* @author Julia Lee
*/
public abstract class AbstractEncryptionTestBase {
@Autowired MongoTemplate template;
@Test // GH-4284
void encryptAndDecryptSimpleValue() {
Person source = new Person();
source.id = "id-1";
source.ssn = "mySecretSSN";
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4432
void encryptAndDecryptJavaTime() {
Person source = new Person();
source.id = "id-1";
source.today = LocalDate.of(1979, Month.SEPTEMBER, 18);
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("today")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptComplexValue() {
Person source = new Person();
source.id = "id-1";
source.address = new Address();
source.address.city = "NYC";
source.address.street = "4th Ave.";
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptValueWithinComplexOne() {
Person source = new Person();
source.id = "id-1";
source.encryptedZip = new AddressWithEncryptedZip();
source.encryptedZip.city = "Boston";
source.encryptedZip.street = "central square";
source.encryptedZip.zip = "1234567890";
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> {
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
}) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptListOfSimpleValue() {
Person source = new Person();
source.id = "id-1";
source.listOfString = Arrays.asList("spring", "data", "mongodb");
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptListOfComplexValue() {
Person source = new Person();
source.id = "id-1";
Address address = new Address();
address.city = "SFO";
address.street = "---";
source.listOfComplex = Collections.singletonList(address);
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("listOfComplex")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptMapOfSimpleValues() {
Person source = new Person();
source.id = "id-1";
source.mapOfString = Map.of("k1", "v1", "k2", "v2");
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("mapOfString")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptMapOfComplexValues() {
Person source = new Person();
source.id = "id-1";
Address address1 = new Address();
address1.city = "SFO";
address1.street = "---";
Address address2 = new Address();
address2.city = "NYC";
address2.street = "---";
source.mapOfComplex = Map.of("a1", address1, "a2", address2);
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("mapOfComplex")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void canQueryDeterministicallyEncrypted() {
Person source = new Person();
source.id = "id-1";
source.ssn = "mySecretSSN";
template.save(source);
Person loaded = template.query(Person.class).matching(where("ssn").is(source.ssn)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test // GH-4284
void cannotQueryRandomlyEncrypted() {
Person source = new Person();
source.id = "id-1";
source.wallet = "secret-wallet-id";
template.save(source);
Person loaded = template.query(Person.class).matching(where("wallet").is(source.wallet)).firstValue();
assertThat(loaded).isNull();
}
@Test // GH-4284
void updateSimpleTypeEncryptedFieldWithNewValue() {
Person source = new Person();
source.id = "id-1";
template.save(source);
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value"))
.first();
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
.loadedMatches(it -> assertThat(it.getSsn()).isEqualTo("secret-value"));
}
@Test // GH-4284
void updateComplexTypeEncryptedFieldWithNewValue() {
Person source = new Person();
source.id = "id-1";
template.save(source);
Address address = new Address();
address.city = "SFO";
address.street = "---";
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("address", address)).first();
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
.loadedMatches(it -> assertThat(it.getAddress()).isEqualTo(address));
}
@Test // GH-4284
void updateEncryptedFieldInNestedElementWithNewValue() {
Person source = new Person();
source.id = "id-1";
source.encryptedZip = new AddressWithEncryptedZip();
source.encryptedZip.city = "Boston";
source.encryptedZip.street = "central square";
template.save(source);
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("encryptedZip.zip", "179"))
.first();
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> {
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
}) //
.loadedMatches(it -> assertThat(it.getEncryptedZip().getZip()).isEqualTo("179"));
}
@Test
void aggregationWithMatch() {
Person person = new Person();
person.id = "id-1";
person.name = "p1-name";
person.ssn = "mySecretSSN";
template.save(person);
AggregationResults<Person> aggregationResults = template.aggregateAndReturn(Person.class)
.by(newAggregation(Person.class, Aggregation.match(where("ssn").is(person.ssn)))).all();
assertThat(aggregationResults.getMappedResults()).containsExactly(person);
}
@Test
void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local",
new DataKeyOptions().keyAltNames(Collections.singletonList("user-1")));
BsonBinary user2key = mongoClientEncryption.getClientEncryption().createDataKey("local",
new DataKeyOptions().keyAltNames(Collections.singletonList("user-2")));
Person p1 = new Person();
p1.id = "id-1";
p1.name = "user-1";
p1.ssn = "ssn";
p1.viaAltKeyNameField = "value-1";
Person p2 = new Person();
p2.id = "id-2";
p2.name = "user-2";
p2.viaAltKeyNameField = "value-1";
Person p3 = new Person();
p3.id = "id-3";
p3.name = "user-1";
p3.viaAltKeyNameField = "value-1";
template.save(p1);
template.save(p2);
template.save(p3);
template.execute(Person.class, collection -> {
collection.find(new Document()).forEach(it -> System.out.println(it.toJson()));
return null;
});
// remove the key and invalidate encrypted data
mongoClientEncryption.getClientEncryption().deleteKey(user2key);
// clear the 60 second key cache within the mongo client
mongoClientEncryption.destroy();
assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1);
assertThatExceptionOfType(PermissionDeniedDataAccessException.class)
.isThrownBy(() -> template.query(Person.class).matching(where("id").is(p2.id)).firstValue());
}
<T> SaveAndLoadAssert<T> verifyThat(T source) {
return new SaveAndLoadAssert<>(source);
}
class SaveAndLoadAssert<T> {
T source;
Function<T, ?> idProvider;
SaveAndLoadAssert(T source) {
this.source = source;
}
SaveAndLoadAssert<T> identifiedBy(Function<T, ?> idProvider) {
this.idProvider = idProvider;
return this;
}
SaveAndLoadAssert<T> wasSavedAs(Document expected) {
return wasSavedMatching(it -> Assertions.assertThat(it).isEqualTo(expected));
}
SaveAndLoadAssert<T> wasSavedMatching(Consumer<Document> saved) {
AbstractEncryptionTestBase.this.assertSaved(source, idProvider, saved);
return this;
}
SaveAndLoadAssert<T> loadedMatches(Consumer<T> expected) {
AbstractEncryptionTestBase.this.assertLoaded(source, idProvider, expected);
return this;
}
SaveAndLoadAssert<T> loadedIsEqualToSource() {
return loadedIsEqualTo(source);
}
SaveAndLoadAssert<T> loadedIsEqualTo(T expected) {
return loadedMatches(it -> Assertions.assertThat(it).isEqualTo(expected));
}
}
<T> void assertSaved(T source, Function<T, ?> idProvider, Consumer<Document> dbValue) {
Document savedDocument = template.execute(Person.class, collection -> {
MongoNamespace namespace = collection.getNamespace();
try (MongoClient rawClient = MongoClients.create()) {
return rawClient.getDatabase(namespace.getDatabaseName()).getCollection(namespace.getCollectionName())
.find(new Document("_id", idProvider.apply(source))).first();
}
});
dbValue.accept(savedDocument);
}
<T> void assertLoaded(T source, Function<T, ?> idProvider, Consumer<T> loadedValue) {
T loaded = template.query((Class<T>) source.getClass()).matching(where("id").is(idProvider.apply(source)))
.firstValue();
loadedValue.accept(loaded);
}
protected static class EncryptionConfig extends AbstractMongoClientConfiguration {
@Autowired ApplicationContext applicationContext;
@Override
protected String getDatabaseName() {
return "fle-test";
}
@Bean
public MongoClient mongoClient() {
return super.mongoClient();
}
@Override
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
converterConfigurationAdapter
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext))
.useNativeDriverJavaTimeCodecs();
}
@Bean
MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) {
Lazy<BsonBinary> dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local",
new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))));
return new MongoEncryptionConverter(mongoClientEncryption,
EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get())));
}
@Bean
CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
}
@Bean
ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) {
MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault");
MongoCollection<Document> keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName())
.getCollection(keyVaultNamespace.getCollectionName());
keyVaultCollection.drop();
// Ensure that two data keys cannot share the same keyAltName.
keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"),
new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames")));
MongoCollection<Document> collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test");
collection.drop(); // Clear old data
byte[] localMasterKey = new byte[96];
new SecureRandom().nextBytes(localMasterKey);
Map<String, Map<String, Object>> kmsProviders = Map.of("local", Map.of("key", localMasterKey));
// Create the ClientEncryption instance
return ClientEncryptionSettings.builder() //
.keyVaultMongoClientSettings(
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) //
.keyVaultNamespace(keyVaultNamespace.getFullName()) //
.kmsProviders(kmsProviders) //
.build();
}
}
static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
CachingMongoClientEncryption(Supplier<ClientEncryption> source) {
super(() -> {
if (cache.get() != null) {
return cache.get();
}
ClientEncryption clientEncryption = source.get();
cache.set(clientEncryption);
return clientEncryption;
});
}
@Override
public void destroy() {
ClientEncryption clientEncryption = cache.get();
if (clientEncryption != null) {
clientEncryption.close();
cache.set(null);
}
}
}
@org.springframework.data.mongodb.core.mapping.Document("test")
static class Person {
String id;
String name;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
String ssn;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "mySuperSecretKey") //
String wallet;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random
Address address;
AddressWithEncryptedZip encryptedZip;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
List<String> listOfString;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
List<Address> listOfComplex;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/name") //
String viaAltKeyNameField;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
Map<String, String> mapOfString;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
Map<String, Address> mapOfComplex;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
LocalDate today;
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getSsn() {
return this.ssn;
}
public String getWallet() {
return this.wallet;
}
public Address getAddress() {
return this.address;
}
public AddressWithEncryptedZip getEncryptedZip() {
return this.encryptedZip;
}
public List<String> getListOfString() {
return this.listOfString;
}
public List<Address> getListOfComplex() {
return this.listOfComplex;
}
public String getViaAltKeyNameField() {
return this.viaAltKeyNameField;
}
public Map<String, String> getMapOfString() {
return this.mapOfString;
}
public Map<String, Address> getMapOfComplex() {
return this.mapOfComplex;
}
public LocalDate getToday() {
return today;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setSsn(String ssn) {
this.ssn = ssn;
}
public void setWallet(String wallet) {
this.wallet = wallet;
}
public void setAddress(Address address) {
this.address = address;
}
public void setEncryptedZip(AddressWithEncryptedZip encryptedZip) {
this.encryptedZip = encryptedZip;
}
public void setListOfString(List<String> listOfString) {
this.listOfString = listOfString;
}
public void setListOfComplex(List<Address> listOfComplex) {
this.listOfComplex = listOfComplex;
}
public void setViaAltKeyNameField(String viaAltKeyNameField) {
this.viaAltKeyNameField = viaAltKeyNameField;
}
public void setMapOfString(Map<String, String> mapOfString) {
this.mapOfString = mapOfString;
}
public void setMapOfComplex(Map<String, Address> mapOfComplex) {
this.mapOfComplex = mapOfComplex;
}
public void setToday(LocalDate today) {
this.today = today;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return Objects.equals(id, person.id) && Objects.equals(name, person.name) && Objects.equals(ssn, person.ssn)
&& Objects.equals(wallet, person.wallet) && Objects.equals(address, person.address)
&& Objects.equals(encryptedZip, person.encryptedZip) && Objects.equals(listOfString, person.listOfString)
&& Objects.equals(listOfComplex, person.listOfComplex)
&& Objects.equals(viaAltKeyNameField, person.viaAltKeyNameField)
&& Objects.equals(mapOfString, person.mapOfString) && Objects.equals(mapOfComplex, person.mapOfComplex)
&& Objects.equals(today, person.today);
}
@Override
public int hashCode() {
return Objects.hash(id, name, ssn, wallet, address, encryptedZip, listOfString, listOfComplex, viaAltKeyNameField,
mapOfString, mapOfComplex, today);
}
public String toString() {
return "EncryptionTests.Person(id=" + this.getId() + ", name=" + this.getName() + ", ssn=" + this.getSsn()
+ ", wallet=" + this.getWallet() + ", address=" + this.getAddress() + ", encryptedZip="
+ this.getEncryptedZip() + ", listOfString=" + this.getListOfString() + ", listOfComplex="
+ this.getListOfComplex() + ", viaAltKeyNameField=" + this.getViaAltKeyNameField() + ", mapOfString="
+ this.getMapOfString() + ", mapOfComplex=" + this.getMapOfComplex() + ", today=" + this.getToday() + ")";
}
}
static class Address {
String city;
String street;
public Address() {}
public String getCity() {
return this.city;
}
public String getStreet() {
return this.street;
}
public void setCity(String city) {
this.city = city;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Address address = (Address) o;
return Objects.equals(city, address.city) && Objects.equals(street, address.street);
}
@Override
public int hashCode() {
return Objects.hash(city, street);
}
public String toString() {
return "EncryptionTests.Address(city=" + this.getCity() + ", street=" + this.getStreet() + ")";
}
}
static class AddressWithEncryptedZip extends Address {
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip;
@Override
public String toString() {
return "AddressWithEncryptedZip{" + "zip='" + zip + '\'' + ", city='" + getCity() + '\'' + ", street='"
+ getStreet() + '\'' + '}';
}
public String getZip() {
return this.zip;
}
public void setZip(String zip) {
this.zip = zip;
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2023 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.encryption;
import java.util.Collections;
import org.bson.BsonBinary;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
import org.springframework.data.mongodb.core.encryption.BypassAutoEncryptionTest.Config;
import org.springframework.data.util.Lazy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.mongodb.AutoEncryptionSettings;
import com.mongodb.ClientEncryptionSettings;
import com.mongodb.MongoClientSettings.Builder;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.model.vault.DataKeyOptions;
import com.mongodb.client.vault.ClientEncryptions;
/**
* Encryption tests for client having {@link AutoEncryptionSettings#isBypassAutoEncryption()}.
*
* @author Christoph Strobl
* @author Julia Lee
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Config.class)
public class BypassAutoEncryptionTest extends AbstractEncryptionTestBase {
@Disabled
@Override
void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
super.altKeyDetection(mongoClientEncryption);
}
@Configuration
static class Config extends EncryptionConfig {
@Autowired ApplicationContext applicationContext;
@Override
protected void configureClientSettings(Builder builder) {
MongoClient mongoClient = MongoClients.create();
ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(mongoClient);
mongoClient.close();
builder.autoEncryptionSettings(AutoEncryptionSettings.builder() //
.kmsProviders(clientEncryptionSettings.getKmsProviders()) //
.keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()) //
.bypassAutoEncryption(true).build());
}
@Override
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
converterConfigurationAdapter
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext))
.useNativeDriverJavaTimeCodecs();
}
@Bean
@Override
MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) {
Lazy<BsonBinary> dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local",
new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))));
return new MongoEncryptionConverter(mongoClientEncryption,
EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get())));
}
@Bean
@Override
CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
}
}
}

View File

@@ -15,48 +15,30 @@
*/
package org.springframework.data.mongodb.core.encryption;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.assertj.core.api.Assertions;
import org.bson.BsonBinary;
import org.bson.Document;
import org.bson.types.Binary;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config;
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.Lazy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -71,353 +53,15 @@ import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.client.model.vault.DataKeyOptions;
import com.mongodb.client.vault.ClientEncryption;
import com.mongodb.client.vault.ClientEncryptions;
/**
* @author Christoph Strobl
* @author Julia Lee
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Config.class)
public class EncryptionTests {
@Autowired MongoTemplate template;
@Test // GH-4284
void encryptAndDecryptSimpleValue() {
Person source = new Person();
source.id = "id-1";
source.ssn = "mySecretSSN";
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptComplexValue() {
Person source = new Person();
source.id = "id-1";
source.address = new Address();
source.address.city = "NYC";
source.address.street = "4th Ave.";
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptValueWithinComplexOne() {
Person source = new Person();
source.id = "id-1";
source.encryptedZip = new AddressWithEncryptedZip();
source.encryptedZip.city = "Boston";
source.encryptedZip.street = "central square";
source.encryptedZip.zip = "1234567890";
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> {
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
}) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptListOfSimpleValue() {
Person source = new Person();
source.id = "id-1";
source.listOfString = Arrays.asList("spring", "data", "mongodb");
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptListOfComplexValue() {
Person source = new Person();
source.id = "id-1";
Address address = new Address();
address.city = "SFO";
address.street = "---";
source.listOfComplex = Collections.singletonList(address);
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("listOfComplex")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptMapOfSimpleValues() {
Person source = new Person();
source.id = "id-1";
source.mapOfString = Map.of("k1", "v1", "k2", "v2");
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("mapOfString")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void encryptAndDecryptMapOfComplexValues() {
Person source = new Person();
source.id = "id-1";
Address address1 = new Address();
address1.city = "SFO";
address1.street = "---";
Address address2 = new Address();
address2.city = "NYC";
address2.street = "---";
source.mapOfComplex = Map.of("a1", address1, "a2", address2);
template.save(source);
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("mapOfComplex")).isInstanceOf(Binary.class)) //
.loadedIsEqualToSource();
}
@Test // GH-4284
void canQueryDeterministicallyEncrypted() {
Person source = new Person();
source.id = "id-1";
source.ssn = "mySecretSSN";
template.save(source);
Person loaded = template.query(Person.class).matching(where("ssn").is(source.ssn)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test // GH-4284
void cannotQueryRandomlyEncrypted() {
Person source = new Person();
source.id = "id-1";
source.wallet = "secret-wallet-id";
template.save(source);
Person loaded = template.query(Person.class).matching(where("wallet").is(source.wallet)).firstValue();
assertThat(loaded).isNull();
}
@Test // GH-4284
void updateSimpleTypeEncryptedFieldWithNewValue() {
Person source = new Person();
source.id = "id-1";
template.save(source);
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value"))
.first();
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
.loadedMatches(it -> assertThat(it.getSsn()).isEqualTo("secret-value"));
}
@Test // GH-4284
void updateComplexTypeEncryptedFieldWithNewValue() {
Person source = new Person();
source.id = "id-1";
template.save(source);
Address address = new Address();
address.city = "SFO";
address.street = "---";
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("address", address)).first();
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
.loadedMatches(it -> assertThat(it.getAddress()).isEqualTo(address));
}
@Test // GH-4284
void updateEncryptedFieldInNestedElementWithNewValue() {
Person source = new Person();
source.id = "id-1";
source.encryptedZip = new AddressWithEncryptedZip();
source.encryptedZip.city = "Boston";
source.encryptedZip.street = "central square";
template.save(source);
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("encryptedZip.zip", "179"))
.first();
verifyThat(source) //
.identifiedBy(Person::getId) //
.wasSavedMatching(it -> {
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
}) //
.loadedMatches(it -> assertThat(it.getEncryptedZip().getZip()).isEqualTo("179"));
}
@Test
void aggregationWithMatch() {
Person person = new Person();
person.id = "id-1";
person.name = "p1-name";
person.ssn = "mySecretSSN";
template.save(person);
AggregationResults<Person> aggregationResults = template.aggregateAndReturn(Person.class)
.by(newAggregation(Person.class, Aggregation.match(where("ssn").is(person.ssn)))).all();
assertThat(aggregationResults.getMappedResults()).containsExactly(person);
}
@Test
void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local",
new DataKeyOptions().keyAltNames(Collections.singletonList("user-1")));
BsonBinary user2key = mongoClientEncryption.getClientEncryption().createDataKey("local",
new DataKeyOptions().keyAltNames(Collections.singletonList("user-2")));
Person p1 = new Person();
p1.id = "id-1";
p1.name = "user-1";
p1.ssn = "ssn";
p1.viaAltKeyNameField = "value-1";
Person p2 = new Person();
p2.id = "id-2";
p2.name = "user-2";
p2.viaAltKeyNameField = "value-1";
Person p3 = new Person();
p3.id = "id-3";
p3.name = "user-1";
p3.viaAltKeyNameField = "value-1";
template.save(p1);
template.save(p2);
template.save(p3);
template.execute(Person.class, collection -> {
collection.find(new Document()).forEach(it -> System.out.println(it.toJson()));
return null;
});
// remove the key and invalidate encrypted data
mongoClientEncryption.getClientEncryption().deleteKey(user2key);
// clear the 60 second key cache within the mongo client
mongoClientEncryption.destroy();
assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1);
assertThatExceptionOfType(PermissionDeniedDataAccessException.class)
.isThrownBy(() -> template.query(Person.class).matching(where("id").is(p2.id)).firstValue());
}
<T> SaveAndLoadAssert<T> verifyThat(T source) {
return new SaveAndLoadAssert<>(source);
}
class SaveAndLoadAssert<T> {
T source;
Function<T, ?> idProvider;
SaveAndLoadAssert(T source) {
this.source = source;
}
SaveAndLoadAssert<T> identifiedBy(Function<T, ?> idProvider) {
this.idProvider = idProvider;
return this;
}
SaveAndLoadAssert<T> wasSavedAs(Document expected) {
return wasSavedMatching(it -> Assertions.assertThat(it).isEqualTo(expected));
}
SaveAndLoadAssert<T> wasSavedMatching(Consumer<Document> saved) {
EncryptionTests.this.assertSaved(source, idProvider, saved);
return this;
}
SaveAndLoadAssert<T> loadedMatches(Consumer<T> expected) {
EncryptionTests.this.assertLoaded(source, idProvider, expected);
return this;
}
SaveAndLoadAssert<T> loadedIsEqualToSource() {
return loadedIsEqualTo(source);
}
SaveAndLoadAssert<T> loadedIsEqualTo(T expected) {
return loadedMatches(it -> Assertions.assertThat(it).isEqualTo(expected));
}
}
<T> void assertSaved(T source, Function<T, ?> idProvider, Consumer<Document> dbValue) {
Document savedDocument = template.execute(Person.class, collection -> {
return collection.find(new Document("_id", idProvider.apply(source))).first();
});
dbValue.accept(savedDocument);
}
<T> void assertLoaded(T source, Function<T, ?> idProvider, Consumer<T> loadedValue) {
T loaded = template.query((Class<T>) source.getClass()).matching(where("id").is(idProvider.apply(source)))
.firstValue();
loadedValue.accept(loaded);
}
public class EncryptionTests extends AbstractEncryptionTestBase {
@Configuration
static class Config extends AbstractMongoClientConfiguration {
@@ -430,6 +74,7 @@ public class EncryptionTests {
}
@Bean
@Override
public MongoClient mongoClient() {
return super.mongoClient();
}
@@ -438,7 +83,8 @@ public class EncryptionTests {
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
converterConfigurationAdapter
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext));
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext))
.useNativeDriverJavaTimeCodecs();
}
@Bean
@@ -470,57 +116,21 @@ public class EncryptionTests {
MongoCollection<Document> collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test");
collection.drop(); // Clear old data
final byte[] localMasterKey = new byte[96];
byte[] localMasterKey = new byte[96];
new SecureRandom().nextBytes(localMasterKey);
Map<String, Map<String, Object>> kmsProviders = new HashMap<>() {
{
put("local", new HashMap<>() {
{
put("key", localMasterKey);
}
});
}
};
Map<String, Map<String, Object>> kmsProviders = Map.of("local", Map.of("key", localMasterKey));
// Create the ClientEncryption instance
ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder()
return ClientEncryptionSettings.builder()
.keyVaultMongoClientSettings(
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build())
.keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build();
return clientEncryptionSettings;
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) //
.keyVaultNamespace(keyVaultNamespace.getFullName()) //
.kmsProviders(kmsProviders) //
.build();
}
}
static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
CachingMongoClientEncryption(Supplier<ClientEncryption> source) {
super(() -> {
if (cache.get() != null) {
return cache.get();
}
ClientEncryption clientEncryption = source.get();
cache.set(clientEncryption);
return clientEncryption;
});
}
@Override
public void destroy() {
ClientEncryption clientEncryption = cache.get();
if (clientEncryption != null) {
clientEncryption.close();
cache.set(null);
}
}
}
@Data
@org.springframework.data.mongodb.core.mapping.Document("test")
static class Person {

View File

@@ -16,6 +16,15 @@
package org.springframework.data.mongodb.observability;
import static io.micrometer.core.tck.MeterRegistryAssert.*;
import static org.mockito.Mockito.*;
import io.micrometer.common.KeyValues;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.bson.BsonDocument;
import org.bson.BsonString;
@@ -33,18 +42,12 @@ import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandStartedEvent;
import com.mongodb.event.CommandSucceededEvent;
import io.micrometer.common.KeyValues;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
/**
* Series of test cases exercising {@link MongoObservationCommandListener}.
*
* @author Marcin Grzejszczak
* @author Greg Turnquist
* @author Mark Paluch
*/
class MongoObservationCommandListenerTests {
@@ -176,6 +179,38 @@ class MongoObservationCommandListenerTests {
assertThatTimerRegisteredWithTags();
}
@Test // GH-4481
void completionShouldIgnoreIncompatibleObservationContext() {
// given
RequestContext traceRequestContext = getContext();
Observation observation = mock(Observation.class);
traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation);
// when
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
verify(observation).getContext();
verifyNoMoreInteractions(observation);
}
@Test // GH-4481
void failureShouldIgnoreIncompatibleObservationContext() {
// given
RequestContext traceRequestContext = getContext();
Observation observation = mock(Observation.class);
traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation);
// when
listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null));
verify(observation).getContext();
verifyNoMoreInteractions(observation);
}
private RequestContext getContext() {
return ((SynchronousContextProvider) ContextProviderFactory.create(observationRegistry)).getContext();
}

View File

@@ -88,7 +88,7 @@ class Person {
----
Account account = …
tempate.insert(account); <2>
template.insert(account); <2>
template.update(Person.class)
.matching(where("id").is(…))
@@ -441,7 +441,7 @@ class Entity {
"lastname" : "Long", <2>
}
----
<1> Read/wirte the keys `fn` & `ln` from/to the linkage document based on the lookup query.
<1> Read/write the keys `fn` & `ln` from/to the linkage document based on the lookup query.
<2> Use non _id_ fields for the lookup of the target documents.
====
@@ -477,7 +477,7 @@ class ToDocumentPointerConverter implements Converter<ReferencedObject, Document
}
}
----
<1> Read/wirte the keys `_id` from/to the reference document to use them in the lookup query.
<1> Read/write the keys `_id` from/to the reference document to use them in the lookup query.
<2> The collection name can be read from the reference document using its key.
====

View File

@@ -350,6 +350,14 @@ You can add additional converters to the converter by overriding the `customConv
MongoDB's native JSR-310 support can be enabled through `MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs()`.
Also shown in the preceding example is a `LoggingEventListener`, which logs `MongoMappingEvent` instances that are posted onto Spring's `ApplicationContextEvent` infrastructure.
[TIP]
====
.Java Time Types
We recommend using MongoDB's native JSR-310 support via `MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs()` as described above as it is using an `UTC` based approach.
The default JSR-310 support for `java.time` types inherited from Spring Data Commons uses the local machine timezone as reference and should only be used for backwards compatibility.
====
NOTE: `AbstractMongoClientConfiguration` creates a `MongoTemplate` instance and registers it with the container under the name `mongoTemplate`.
The `base-package` property tells it where to scan for classes annotated with the `@org.springframework.data.mongodb.core.mapping.Document` annotation.

View File

@@ -33,7 +33,7 @@ embedded schema objects that describe properties and subdocuments.
<2> `required` is a property that describes which properties are required in a document. It can be specified optionally, along with other
schema constraints. See MongoDB's documentation on https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords[available keywords].
<3> `properties` is related to a schema object that describes an `object` type. It contains property-specific schema constraints.
<4> `firstname` specifies constraints for the `firsname` field inside the document. Here, it is a string-based `properties` element declaring
<4> `firstname` specifies constraints for the `firstname` field inside the document. Here, it is a string-based `properties` element declaring
possible field values.
<5> `address` is a subdocument defining a schema for values in its `postCode` field.
====

View File

@@ -77,7 +77,7 @@ Therefore, the `Sort` properties are mapped against the methods return type `Per
<4> `$skip`, `$limit` and `$sort` can be passed on via a `Pageable` argument. Same as in <2>, the operators are appended to the pipeline definition. Methods accepting `Pageable` can return `Slice` for easier pagination.
<5> Aggregation methods can return `Stream` to consume results directly from an underlying cursor. Make sure to close the stream after consuming it to release the server-side cursor by either calling `close()` or through `try-with-resources`.
<6> Map the result of an aggregation returning a single `Document` to an instance of a desired `SumValue` target type.
<7> Aggregations resulting in single document holding just an accumulation result like eg. `$sum` can be extracted directly from the result `Document`.
<7> Aggregations resulting in single document holding just an accumulation result like e.g. `$sum` can be extracted directly from the result `Document`.
To gain more control, you might consider `AggregationResult` as method return type as shown in <7>.
<8> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`.
<9> Like in <6>, a single value can be directly obtained from multiple result ``Document``s.

View File

@@ -1,4 +1,4 @@
Spring Data MongoDB 4.1.1 (2023.0.1)
Spring Data MongoDB 4.1.3 (2023.0.3)
Copyright (c) [2010-2019] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -45,6 +45,8 @@ conditions of the subcomponent's license, as noted in the LICENSE file.