Compare commits
3 Commits
main
...
issue/4426
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53305065c3 | ||
|
|
3b5ec24332 | ||
|
|
a473cb4322 |
4
.mvn/wrapper/maven-wrapper.properties
vendored
4
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -1,2 +1,2 @@
|
||||
#Mon Aug 14 08:53:22 EDT 2023
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip
|
||||
#Tue Jun 13 08:54:58 CEST 2023
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Java versions
|
||||
java.main.tag=17.0.8_7-jdk-focal
|
||||
java.main.tag=17.0.6_10-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.23
|
||||
docker.mongodb.5.0.version=5.0.19
|
||||
docker.mongodb.6.0.version=6.0.8
|
||||
docker.mongodb.4.4.version=4.4.18
|
||||
docker.mongodb.5.0.version=5.0.14
|
||||
docker.mongodb.6.0.version=6.0.4
|
||||
|
||||
# Supported versions of Redis
|
||||
docker.redis.6.version=6.2.13
|
||||
docker.redis.6.version=6.2.10
|
||||
|
||||
# Supported versions of Cassandra
|
||||
docker.cassandra.3.version=3.11.15
|
||||
docker.cassandra.3.version=3.11.14
|
||||
|
||||
# Docker environment settings
|
||||
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring Data MongoDB</name>
|
||||
@@ -27,7 +27,7 @@
|
||||
<project.type>multi</project.type>
|
||||
<dist.id>spring-data-mongodb</dist.id>
|
||||
<springdata.commons>3.2.0-SNAPSHOT</springdata.commons>
|
||||
<mongo>4.10.2</mongo>
|
||||
<mongo>4.9.1</mongo>
|
||||
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
|
||||
<jmh.version>1.19</jmh.version>
|
||||
</properties>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongodb-crypt</artifactId>
|
||||
<version>1.8.0</version>
|
||||
<version>1.6.1</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -203,9 +203,8 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
|
||||
target.properties(nestedProperties.toArray(new JsonSchemaProperty[0])), required));
|
||||
}
|
||||
}
|
||||
JsonSchemaProperty schemaProperty = targetProperties.size() == 1 ? targetProperties.iterator().next()
|
||||
return targetProperties.size() == 1 ? targetProperties.iterator().next()
|
||||
: JsonSchemaProperty.merged(targetProperties);
|
||||
return applyEncryptionDataIfNecessary(property, schemaProperty);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class AggregationOperationRenderer {
|
||||
contextToUse = new InheritingExposedFieldsAggregationOperationContext(fields, contextToUse);
|
||||
} else {
|
||||
contextToUse = fields.exposesNoFields() ? DEFAULT_CONTEXT
|
||||
: new ExposedFieldsAggregationOperationContext(fields, contextToUse);
|
||||
: new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), contextToUse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1515,15 +1515,24 @@ public class ArrayOperators {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Variable implements AggregationVariable {
|
||||
public enum Variable implements Field {
|
||||
|
||||
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();
|
||||
@@ -1531,23 +1540,27 @@ 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}
|
||||
@@ -1579,16 +1592,6 @@ public class ArrayOperators {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isVariable(Field field) {
|
||||
|
||||
for (Variable var : values()) {
|
||||
if (field.getTarget().startsWith(var.getTarget())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<>();
|
||||
List<Field> fields = new ArrayList<Field>();
|
||||
|
||||
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<>();
|
||||
Map<String, Field> reference = new HashMap<String, Field>();
|
||||
|
||||
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<>(existing.fields.size() + 1);
|
||||
this.fields = new ArrayList<Field>(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 (AggregationVariable.isVariable(source)) {
|
||||
if (SystemVariable.isReferingToSystemVariable(source)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -253,12 +253,10 @@ 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()) {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.aggregation;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
|
||||
|
||||
/**
|
||||
@@ -23,7 +22,6 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldRefe
|
||||
* {@link AggregationOperationContext}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
* @since 1.9
|
||||
*/
|
||||
class InheritingExposedFieldsAggregationOperationContext extends ExposedFieldsAggregationOperationContext {
|
||||
@@ -45,11 +43,6 @@ 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) {
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ 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;
|
||||
@@ -430,7 +431,6 @@ public class ReplaceRootOperation implements FieldsExposingAggregationOperation
|
||||
* @param context will never be {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
@Override
|
||||
Document toDocument(AggregationOperationContext context);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 implements AggregationVariable {
|
||||
public enum SystemVariable {
|
||||
|
||||
/**
|
||||
* Variable for the current datetime.
|
||||
@@ -82,6 +82,8 @@ public enum SystemVariable implements AggregationVariable {
|
||||
*/
|
||||
SEARCH_META;
|
||||
|
||||
private static final String PREFIX = "$$";
|
||||
|
||||
/**
|
||||
* Return {@literal true} if the given {@code fieldRef} denotes a well-known system variable, {@literal false}
|
||||
* otherwise.
|
||||
@@ -91,12 +93,13 @@ public enum SystemVariable implements AggregationVariable {
|
||||
*/
|
||||
public static boolean isReferingToSystemVariable(@Nullable String fieldRef) {
|
||||
|
||||
String candidate = variableNameFrom(fieldRef);
|
||||
if (candidate == null) {
|
||||
if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
candidate = candidate.startsWith(PREFIX) ? candidate.substring(2) : candidate;
|
||||
int indexOfFirstDot = fieldRef.indexOf('.');
|
||||
String candidate = fieldRef.substring(2, indexOfFirstDot == -1 ? fieldRef.length() : indexOfFirstDot);
|
||||
|
||||
for (SystemVariable value : values()) {
|
||||
if (value.name().equals(candidate)) {
|
||||
return true;
|
||||
@@ -110,20 +113,4 @@ public enum SystemVariable implements AggregationVariable {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
|
||||
|
||||
protected FieldReference getReferenceFor(Field field) {
|
||||
|
||||
if(entity.getNullable() == null || AggregationVariable.isVariable(field)) {
|
||||
if(entity.getNullable() == null) {
|
||||
return new DirectFieldReference(new ExposedField(field, true));
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ import com.mongodb.DBRef;
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Heesu Jung
|
||||
* @author Divya Srivastava
|
||||
* @author Julia Lee
|
||||
*/
|
||||
public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware {
|
||||
|
||||
@@ -1977,9 +1976,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
}
|
||||
|
||||
if (property.isDocumentReference()) {
|
||||
return (T) dbRefResolver.resolveReference(property,
|
||||
new DocumentReferenceSource(accessor.getDocument(), accessor.get(property)),
|
||||
referenceLookupDelegate, context::convert);
|
||||
return (T) dbRefResolver.resolveReference(property, accessor.get(property), referenceLookupDelegate,
|
||||
context::convert);
|
||||
}
|
||||
|
||||
return super.getPropertyValue(property);
|
||||
|
||||
@@ -1089,7 +1089,6 @@ public class QueryMapper {
|
||||
protected static class MetadataBackedField extends Field {
|
||||
|
||||
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
|
||||
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;
|
||||
@@ -1339,24 +1338,22 @@ public class QueryMapper {
|
||||
return source;
|
||||
}
|
||||
|
||||
List<String> path = new ArrayList<>(segments.length);
|
||||
List<String> path = new ArrayList<>();
|
||||
|
||||
/* 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];
|
||||
for (String segment : Arrays.copyOfRange(segments, 1, segments.length)) {
|
||||
if (segment.startsWith("[") && segment.endsWith("]")) {
|
||||
continue;
|
||||
}
|
||||
if (NUMERIC_SEGMENT.matcher(segment).matches()) {
|
||||
if (segment.matches("\\d+")) {
|
||||
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) {
|
||||
if (path.isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ 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;
|
||||
@@ -64,7 +63,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 bsonValue ? BsonUtils.toJavaType(bsonValue) : decrypted;
|
||||
return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,56 +87,36 @@ 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 -> {
|
||||
if (it instanceof BsonValue bsonValue) {
|
||||
collection.add(BsonUtils.toJavaType(bsonValue));
|
||||
} else {
|
||||
collection.add(context.read(it, persistentProperty.getActualType()));
|
||||
}
|
||||
});
|
||||
|
||||
iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it)));
|
||||
return collection;
|
||||
} else {
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
|
||||
iterable.forEach(it -> {
|
||||
if (it instanceof BsonValue bsonValue) {
|
||||
collection.add(context.read(BsonUtils.toJavaType(bsonValue), persistentProperty.getActualType()));
|
||||
} else {
|
||||
collection.add(context.read(it, persistentProperty.getActualType()));
|
||||
}
|
||||
collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType()));
|
||||
});
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (!persistentProperty.isEntity() && decryptedValue instanceof BsonValue bsonValue) {
|
||||
if (persistentProperty.isMap() && persistentProperty.getType() != Document.class) {
|
||||
return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue));
|
||||
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -255,11 +255,6 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
|
||||
return delegate.isWritable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return delegate.isReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImmutable() {
|
||||
return delegate.isImmutable();
|
||||
|
||||
@@ -31,7 +31,6 @@ import java.util.Set;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.domain.KeysetScrollPosition;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.OffsetScrollPosition;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
@@ -67,7 +66,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
private @Nullable Field fieldSpec = null;
|
||||
private Sort sort = Sort.unsorted();
|
||||
private long skip;
|
||||
private Limit limit = Limit.unlimited();
|
||||
private int limit;
|
||||
|
||||
private KeysetScrollPosition keysetScrollPosition;
|
||||
private @Nullable ReadConcern readConcern;
|
||||
@@ -156,30 +155,10 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
* @return this.
|
||||
*/
|
||||
public Query limit(int limit) {
|
||||
this.limit = limit > 0 ? Limit.of(limit) : Limit.unlimited();
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of returned documents to {@link Limit}.
|
||||
*
|
||||
* @param limit number of documents to return.
|
||||
* @return this.
|
||||
* @since 4.2
|
||||
*/
|
||||
public Query limit(Limit limit) {
|
||||
|
||||
Assert.notNull(limit, "Limit must not be null");
|
||||
|
||||
if (limit.isUnlimited()) {
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
// retain zero/negative semantics for unlimited.
|
||||
return limit(limit.max());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the query to use the given hint when being executed. The {@code hint} can either be an index name or a
|
||||
* json {@link Document} representation.
|
||||
@@ -275,7 +254,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.limit = pageable.toLimit();
|
||||
this.limit = pageable.getPageSize();
|
||||
this.skip = pageable.getOffset();
|
||||
|
||||
return with(pageable.getSort());
|
||||
@@ -478,7 +457,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
* @since 4.1
|
||||
*/
|
||||
public boolean isLimited() {
|
||||
return this.limit.isLimited();
|
||||
return this.limit > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,7 +468,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
* @see #isLimited()
|
||||
*/
|
||||
public int getLimit() {
|
||||
return limit.isUnlimited() ? 0 : this.limit.max();
|
||||
return this.limit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -704,8 +683,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
};
|
||||
|
||||
target.skip = source.getSkip();
|
||||
|
||||
target.limit = source.isLimited() ? Limit.of(source.getLimit()) : Limit.unlimited();
|
||||
target.limit = source.getLimit();
|
||||
target.hint = source.getHint();
|
||||
target.collation = source.getCollation();
|
||||
target.restrictedTypes = new HashSet<>(source.getRestrictedTypes());
|
||||
@@ -768,7 +746,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
result += 31 * nullSafeHashCode(sort);
|
||||
result += 31 * nullSafeHashCode(hint);
|
||||
result += 31 * skip;
|
||||
result += 31 * limit.hashCode();
|
||||
result += 31 * limit;
|
||||
result += 31 * nullSafeHashCode(meta);
|
||||
result += 31 * nullSafeHashCode(collation.orElse(null));
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ 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.
|
||||
@@ -1140,9 +1139,7 @@ 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?
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(algorithm)) {
|
||||
enc.append("algorithm", algorithm);
|
||||
}
|
||||
enc.append("algorithm", algorithm);
|
||||
|
||||
propertySpecification.append("encrypt", enc);
|
||||
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
*/
|
||||
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;
|
||||
@@ -33,6 +27,10 @@ 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.
|
||||
*
|
||||
@@ -128,54 +126,50 @@ public class MongoObservationCommandListener implements CommandListener {
|
||||
@Override
|
||||
public void commandSucceeded(CommandSucceededEvent event) {
|
||||
|
||||
doInObservation(event.getRequestContext(), (observation, context) -> {
|
||||
|
||||
context.setCommandSucceededEvent(event);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Command succeeded - will stop observation [" + observation + "]");
|
||||
}
|
||||
|
||||
observation.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commandFailed(CommandFailedEvent event) {
|
||||
|
||||
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) {
|
||||
RequestContext requestContext = event.getRequestContext();
|
||||
|
||||
if (requestContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Observation observation = requestContext.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
|
||||
if (observation == null || !(observation.getContext()instanceof MongoHandlerContext context)) {
|
||||
if (observation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
action.accept(observation, context);
|
||||
MongoHandlerContext context = (MongoHandlerContext) observation.getContext();
|
||||
context.setCommandSucceededEvent(event);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Command succeeded - will stop observation [" + observation + "]");
|
||||
}
|
||||
|
||||
observation.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commandFailed(CommandFailedEvent event) {
|
||||
|
||||
RequestContext requestContext = event.getRequestContext();
|
||||
|
||||
if (requestContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Observation observation = requestContext.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
|
||||
if (observation == null) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
@@ -118,11 +117,6 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor {
|
||||
return delegate.getUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Limit getLimit() {
|
||||
return delegate.getLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value with the underlying {@link MongoWriter}.
|
||||
*
|
||||
|
||||
@@ -15,33 +15,26 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.bson.*;
|
||||
import org.bson.codecs.Codec;
|
||||
import org.bson.codecs.DocumentCodec;
|
||||
import org.bson.codecs.EncoderContext;
|
||||
import org.bson.codecs.configuration.CodecConfigurationException;
|
||||
import org.bson.codecs.configuration.CodecRegistry;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.json.JsonParseException;
|
||||
import org.bson.types.Binary;
|
||||
import org.bson.types.Decimal128;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.mongodb.CodecRegistryProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -110,7 +103,7 @@ public class BsonUtils {
|
||||
return dbo.toMap();
|
||||
}
|
||||
|
||||
return new Document(bson.toBsonDocument(Document.class, codecRegistry));
|
||||
return new Document((Map) bson.toBsonDocument(Document.class, codecRegistry));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,22 +280,36 @@ public class BsonUtils {
|
||||
*/
|
||||
public static Object toJavaType(BsonValue 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;
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,21 +321,6 @@ public class BsonUtils {
|
||||
* @since 3.0
|
||||
*/
|
||||
public static BsonValue simpleToBsonValue(Object source) {
|
||||
return simpleToBsonValue(source, MongoClientSettings.getDefaultCodecRegistry());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a given simple value (eg. {@link String}, {@link Long}) to its corresponding {@link BsonValue}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param codecRegistry The {@link CodecRegistry} used as a fallback to convert types using native {@link Codec}. Must
|
||||
* not be {@literal null}.
|
||||
* @return the corresponding {@link BsonValue} representation.
|
||||
* @throws IllegalArgumentException if {@literal source} does not correspond to a {@link BsonValue} type.
|
||||
* @since 4.2
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegistry) {
|
||||
|
||||
if (source instanceof BsonValue bsonValue) {
|
||||
return bsonValue;
|
||||
@@ -366,35 +358,17 @@ 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 Date date) {
|
||||
new BsonDateTime(date.getTime());
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Object value = source;
|
||||
if (ClassUtils.isPrimitiveArray(source.getClass())) {
|
||||
value = CollectionUtils.arrayToList(source);
|
||||
}
|
||||
|
||||
Codec codec = codecRegistry.get(value.getClass());
|
||||
BsonCapturingWriter writer = new BsonCapturingWriter(value.getClass());
|
||||
codec.encode(writer, value,
|
||||
ObjectUtils.isArray(value) || value instanceof Collection<?> ? EncoderContext.builder().build() : null);
|
||||
return writer.getCapturedValue();
|
||||
} catch (CodecConfigurationException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Unable to convert %s to BsonValue.", source != null ? source.getClass().getName() : "null"));
|
||||
}
|
||||
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.
|
||||
@@ -695,7 +669,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)));
|
||||
@@ -717,9 +691,8 @@ public class BsonUtils {
|
||||
|
||||
private static String toString(Map<?, ?> source) {
|
||||
|
||||
// Avoid String.format for performance
|
||||
return iterableToDelimitedString(source.entrySet(), "{ ", " }",
|
||||
entry -> "\"" + entry.getKey() + "\" : " + toJson(entry.getValue()));
|
||||
entry -> String.format("\"%s\" : %s", entry.getKey(), toJson(entry.getValue())));
|
||||
}
|
||||
|
||||
private static String toString(Collection<?> source) {
|
||||
@@ -735,160 +708,4 @@ public class BsonUtils {
|
||||
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
static class BsonCapturingWriter extends AbstractBsonWriter {
|
||||
|
||||
private final List<BsonValue> values = new ArrayList<>(0);
|
||||
|
||||
public BsonCapturingWriter(Class<?> type) {
|
||||
super(new BsonWriterSettings());
|
||||
|
||||
if (ClassUtils.isAssignable(Map.class, type)) {
|
||||
setContext(new Context(null, BsonContextType.DOCUMENT));
|
||||
} else if (ClassUtils.isAssignable(List.class, type) || type.isArray()) {
|
||||
setContext(new Context(null, BsonContextType.ARRAY));
|
||||
} else {
|
||||
setContext(new Context(null, BsonContextType.DOCUMENT));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
BsonValue getCapturedValue() {
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (!getContext().getContextType().equals(BsonContextType.ARRAY)) {
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
return new BsonArray(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteStartDocument() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteEndDocument() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeStartArray() {
|
||||
setState(State.VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndArray() {
|
||||
setState(State.NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteStartArray() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteEndArray() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteBinaryData(BsonBinary value) {
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteBoolean(boolean value) {
|
||||
values.add(BsonBoolean.valueOf(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteDateTime(long value) {
|
||||
values.add(new BsonDateTime(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteDBPointer(BsonDbPointer value) {
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteDouble(double value) {
|
||||
values.add(new BsonDouble(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteInt32(int value) {
|
||||
values.add(new BsonInt32(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteInt64(long value) {
|
||||
values.add(new BsonInt64(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteDecimal128(Decimal128 value) {
|
||||
values.add(new BsonDecimal128(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteJavaScript(String value) {
|
||||
values.add(new BsonJavaScript(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteJavaScriptWithScope(String value) {
|
||||
throw new UnsupportedOperationException("Cannot capture JavaScriptWith");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteMaxKey() {}
|
||||
|
||||
@Override
|
||||
protected void doWriteMinKey() {}
|
||||
|
||||
@Override
|
||||
protected void doWriteNull() {
|
||||
values.add(new BsonNull());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteObjectId(ObjectId value) {
|
||||
values.add(new BsonObjectId(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteRegularExpression(BsonRegularExpression value) {
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteString(String value) {
|
||||
values.add(new BsonString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteSymbol(String value) {
|
||||
values.add(new BsonSymbol(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteTimestamp(BsonTimestamp value) {
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteUndefined() {
|
||||
values.add(new BsonUndefined());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
values.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,17 +271,6 @@ 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
|
||||
@@ -687,9 +676,4 @@ class MappingMongoJsonSchemaCreatorUnitTests {
|
||||
static class PropertyClashWithA {
|
||||
Integer aNonEncrypted;
|
||||
}
|
||||
|
||||
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
|
||||
static class WithEncryptedEntityLikeProperty {
|
||||
@Encrypted SomeDomainType domainTypeValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ 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 {
|
||||
@@ -1266,32 +1265,6 @@ 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);
|
||||
}
|
||||
|
||||
static class SingleRefRoot {
|
||||
|
||||
String id;
|
||||
@@ -2276,40 +2249,4 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
return "MongoTemplateDocumentReferenceTests.WithListOfRefs(id=" + this.getId() + ", refs=" + this.getRefs() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
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() + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,8 @@ 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,13 +46,11 @@ import com.mongodb.client.model.ValidationLevel;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link CollectionOptions#validation(ValidationOptions)} using
|
||||
* {@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}.
|
||||
* {@link org.springframework.data.mongodb.core.validation.CriteriaValidator} and
|
||||
* {@link org.springframework.data.mongodb.core.validation.DocumentValidator}.
|
||||
*
|
||||
* @author Andreas Zink
|
||||
* @author Christoph Strobl
|
||||
* @author Julia Lee
|
||||
*/
|
||||
@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
|
||||
public class MongoTemplateValidationTests {
|
||||
@@ -190,20 +186,6 @@ 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);
|
||||
}
|
||||
@@ -289,10 +271,4 @@ public class MongoTemplateValidationTests {
|
||||
return "MongoTemplateValidationTests.SimpleBean(nonNullString=" + this.getNonNullString() + ", rangedInteger=" + this.getRangedInteger() + ", customFieldName=" + this.getCustomFieldName() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -91,7 +91,6 @@ import com.mongodb.client.MongoCollection;
|
||||
* @author Sergey Shcherbakov
|
||||
* @author Minsu Kim
|
||||
* @author Sangyong Choi
|
||||
* @author Julia Lee
|
||||
*/
|
||||
@ExtendWith(MongoTemplateExtension.class)
|
||||
public class AggregationTests {
|
||||
@@ -120,7 +119,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, Item.class);
|
||||
Sales.class, Sales2.class, Employee.class, Art.class, Venue.class);
|
||||
|
||||
mongoTemplate.dropCollection(INPUT_COLLECTION);
|
||||
mongoTemplate.dropCollection("personQueryTemp");
|
||||
@@ -1993,42 +1992,6 @@ 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);
|
||||
@@ -2351,21 +2314,19 @@ public class AggregationTests {
|
||||
}
|
||||
}
|
||||
|
||||
// DATAMONGO-1491, GH-4443
|
||||
// DATAMONGO-1491
|
||||
static class Item {
|
||||
|
||||
@org.springframework.data.mongodb.core.mapping.Field("item_id") //
|
||||
String itemId;
|
||||
Integer quantity;
|
||||
Long price;
|
||||
List<String> tags = new ArrayList<>();
|
||||
|
||||
Item(String itemId, Integer quantity, Long price, List<String> tags) {
|
||||
Item(String itemId, Integer quantity, Long price) {
|
||||
|
||||
this.itemId = itemId;
|
||||
this.quantity = quantity;
|
||||
this.price = price;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public static ItemBuilder builder() {
|
||||
@@ -2424,7 +2385,6 @@ public class AggregationTests {
|
||||
private String itemId;
|
||||
private Integer quantity;
|
||||
private Long price;
|
||||
private List<String> tags;
|
||||
|
||||
ItemBuilder() {}
|
||||
|
||||
@@ -2443,13 +2403,8 @@ public class AggregationTests {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ItemBuilder tags(List<String> tags) {
|
||||
this.tags = tags;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Item build() {
|
||||
return new Item(itemId, quantity, price, tags);
|
||||
return new Item(itemId, quantity, price);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
||||
@@ -49,7 +49,6 @@ import com.mongodb.client.model.Projections;
|
||||
* @author Thomas Darimont
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Julia Lee
|
||||
*/
|
||||
public class AggregationUnitTests {
|
||||
|
||||
@@ -613,7 +612,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).append("entries", 1)));
|
||||
assertThat(document).isEqualTo(new Document("$project", new Document("_id", 1).append("renamed-field", 1)));
|
||||
}
|
||||
|
||||
@Test // GH-4038
|
||||
@@ -654,22 +653,6 @@ 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");
|
||||
@@ -689,7 +672,5 @@ public class AggregationUnitTests {
|
||||
|
||||
@org.springframework.data.mongodb.core.mapping.Field("renamed-field") private String foo;
|
||||
|
||||
private List<String> entries = new ArrayList<>();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,8 @@ 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;
|
||||
@@ -456,30 +453,6 @@ 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")
|
||||
public static class FooPerson {
|
||||
|
||||
@@ -584,9 +557,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ 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.*;
|
||||
|
||||
@@ -105,7 +106,6 @@ import com.mongodb.DBRef;
|
||||
* @author Mark Paluch
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Heesu Jung
|
||||
* @author Julia Lee
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MappingMongoConverterUnitTests {
|
||||
@@ -2619,7 +2619,7 @@ class MappingMongoConverterUnitTests {
|
||||
void projectShouldReadSimpleInterfaceProjection() {
|
||||
|
||||
org.bson.Document source = new org.bson.Document("birthDate",
|
||||
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay(systemDefault()).toInstant())).append("foo", "Walter");
|
||||
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay().toInstant(ZoneOffset.UTC))).append("foo", "Walter");
|
||||
|
||||
EntityProjectionIntrospector discoverer = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
|
||||
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
|
||||
@@ -2637,7 +2637,7 @@ class MappingMongoConverterUnitTests {
|
||||
void projectShouldReadSimpleDtoProjection() {
|
||||
|
||||
org.bson.Document source = new org.bson.Document("birthDate",
|
||||
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay(systemDefault()).toInstant())).append("foo", "Walter");
|
||||
Date.from(LocalDate.of(1999, 12, 1).atStartOfDay().toInstant(ZoneOffset.UTC))).append("foo", "Walter");
|
||||
|
||||
EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
|
||||
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
|
||||
|
||||
@@ -1,756 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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 org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Encryption tests for client having {@link AutoEncryptionSettings#isBypassAutoEncryption()}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Julia Lee
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = BypassAutoEncryptionTest.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 {
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,16 +15,721 @@
|
||||
*/
|
||||
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.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
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.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;
|
||||
|
||||
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.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
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = AbstractEncryptionTestBase.EncryptionConfig.class)
|
||||
public class EncryptionTests extends AbstractEncryptionTestBase {
|
||||
@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);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config 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));
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
final byte[] localMasterKey = new byte[96];
|
||||
new SecureRandom().nextBytes(localMasterKey);
|
||||
Map<String, Map<String, Object>> kmsProviders = new HashMap<>() {
|
||||
{
|
||||
put("local", new HashMap<>() {
|
||||
{
|
||||
put("key", localMasterKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create the ClientEncryption instance
|
||||
ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder()
|
||||
.keyVaultMongoClientSettings(
|
||||
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build())
|
||||
.keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build();
|
||||
return clientEncryptionSettings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, name, ssn, wallet, address, encryptedZip, listOfString, listOfComplex, viaAltKeyNameField,
|
||||
mapOfString, mapOfComplex);
|
||||
}
|
||||
|
||||
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() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
@@ -98,18 +97,6 @@ class QueryTests {
|
||||
assertThat(q.getQueryObject()).isEqualTo(Document
|
||||
.parse("{ \"name\" : { \"$gte\" : \"M\" , \"$lte\" : \"T\"} , \"age\" : { \"$not\" : { \"$gt\" : 22}}}"));
|
||||
assertThat(q.getLimit()).isEqualTo(50);
|
||||
|
||||
q.limit(Limit.unlimited());
|
||||
assertThat(q.getLimit()).isZero();
|
||||
assertThat(q.isLimited()).isFalse();
|
||||
|
||||
q.limit(Limit.of(10));
|
||||
assertThat(q.getLimit()).isEqualTo(10);
|
||||
assertThat(q.isLimited()).isTrue();
|
||||
|
||||
q.limit(Limit.of(-1));
|
||||
assertThat(q.getLimit()).isZero();
|
||||
assertThat(q.isLimited()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -16,15 +16,6 @@
|
||||
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;
|
||||
@@ -42,12 +33,18 @@ 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 {
|
||||
|
||||
@@ -179,38 +176,6 @@ 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();
|
||||
}
|
||||
|
||||
@@ -213,17 +213,6 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
|
||||
assertThat(page).contains(carter);
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void appliesLimitToScrollingCorrectly() {
|
||||
|
||||
Window<Person> page = repository.findByLastnameLikeOrderByLastnameAscFirstnameAsc("*a*",
|
||||
ScrollPosition.keyset(), Limit.of(2));
|
||||
|
||||
assertThat(page.isLast()).isFalse();
|
||||
assertThat(page.size()).isEqualTo(2);
|
||||
assertThat(page).contains(carter);
|
||||
}
|
||||
|
||||
@Test // GH-4308
|
||||
void appliesScrollPositionWithProjectionCorrectly() {
|
||||
|
||||
@@ -247,14 +236,6 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
|
||||
assertThat(page).contains(carter, stefan);
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void executesFinderCorrectlyWithSortAndLimit() {
|
||||
|
||||
List<Person> page = repository.findByLastnameLike("*a*", Sort.by(Direction.ASC, "lastname", "firstname"), Limit.of(2));
|
||||
|
||||
assertThat(page).containsExactly(carter, stefan);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executesPagedFinderWithAnnotatedQueryCorrectly() {
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Range;
|
||||
@@ -127,9 +126,6 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
|
||||
ScrollPosition scrollPosition);
|
||||
|
||||
Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
|
||||
ScrollPosition scrollPosition, Limit limit);
|
||||
|
||||
/**
|
||||
* Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards
|
||||
* supported).
|
||||
@@ -149,8 +145,6 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
*/
|
||||
Page<Person> findByLastnameLike(String lastname, Pageable pageable);
|
||||
|
||||
List<Person> findByLastnameLike(String lastname, Sort sort, Limit limit);
|
||||
|
||||
@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
|
||||
Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -494,30 +493,6 @@ class AbstractMongoQueryUnitTests {
|
||||
assertThat(captor.getValue().getHint()).isEqualTo("idx-ln");
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void limitShouldBeAppliedToQuery() {
|
||||
|
||||
createQueryForMethod("findWithLimit", String.class, Limit.class).execute(new Object[] { "dalinar", Limit.of(42) });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getLimit()).isEqualTo(42);
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void sortAndLimitShouldBeAppliedToQuery() {
|
||||
|
||||
createQueryForMethod("findWithSortAndLimit", String.class, Sort.class, Limit.class)
|
||||
.execute(new Object[] { "dalinar", Sort.by("fn"), Limit.of(42) });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getLimit()).isEqualTo(42);
|
||||
assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("fn", 1));
|
||||
}
|
||||
|
||||
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
|
||||
return createQueryForMethod(Repo.class, methodName, paramTypes);
|
||||
}
|
||||
@@ -639,10 +614,6 @@ class AbstractMongoQueryUnitTests {
|
||||
|
||||
@Hint("idx-fn")
|
||||
void findWithHintByFirstname(String firstname);
|
||||
|
||||
List<Person> findWithLimit(String firstname, Limit limit);
|
||||
|
||||
List<Person> findWithSortAndLimit(String firstname, Sort sort, Limit limit);
|
||||
}
|
||||
|
||||
// DATAMONGO-1872
|
||||
|
||||
@@ -17,19 +17,10 @@ package org.springframework.data.mongodb.util.json;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.BsonArray;
|
||||
import org.bson.BsonDouble;
|
||||
import org.bson.BsonInt32;
|
||||
import org.bson.BsonInt64;
|
||||
@@ -38,9 +29,7 @@ import org.bson.BsonString;
|
||||
import org.bson.Document;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.data.mongodb.util.BsonUtils;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
@@ -116,9 +105,9 @@ class BsonUtilsTest {
|
||||
@Test // GH-3571
|
||||
void asCollectionConvertsArrayToCollection() {
|
||||
|
||||
Object source = new String[] { "one", "two" };
|
||||
Object source = new String[]{"one", "two"};
|
||||
|
||||
assertThat((Collection) BsonUtils.asCollection(source)).containsExactly("one", "two");
|
||||
assertThat((Collection)BsonUtils.asCollection(source)).containsExactly("one", "two");
|
||||
}
|
||||
|
||||
@Test // GH-3571
|
||||
@@ -126,7 +115,7 @@ class BsonUtilsTest {
|
||||
|
||||
Object source = 100L;
|
||||
|
||||
assertThat((Collection) BsonUtils.asCollection(source)).containsExactly(source);
|
||||
assertThat((Collection)BsonUtils.asCollection(source)).containsExactly(source);
|
||||
}
|
||||
|
||||
@Test // GH-3702
|
||||
@@ -137,41 +126,4 @@ class BsonUtilsTest {
|
||||
assertThat(BsonUtils.supportsBson(new BasicDBList())).isTrue();
|
||||
assertThat(BsonUtils.supportsBson(Collections.emptyMap())).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest // GH-4432
|
||||
@MethodSource("javaTimeInstances")
|
||||
void convertsJavaTimeTypesToBsonDateTime(Temporal source) {
|
||||
|
||||
assertThat(BsonUtils.simpleToBsonValue(source))
|
||||
.isEqualTo(new Document("value", source).toBsonDocument().get("value"));
|
||||
}
|
||||
|
||||
@ParameterizedTest // GH-4432
|
||||
@MethodSource("collectionLikeInstances")
|
||||
void convertsCollectionLikeToBsonArray(Object source) {
|
||||
|
||||
assertThat(BsonUtils.simpleToBsonValue(source))
|
||||
.isEqualTo(new Document("value", source).toBsonDocument().get("value"));
|
||||
}
|
||||
|
||||
@Test // GH-4432
|
||||
void convertsPrimitiveArrayToBsonArray() {
|
||||
|
||||
assertThat(BsonUtils.simpleToBsonValue(new int[] { 1, 2, 3 }))
|
||||
.isEqualTo(new BsonArray(List.of(new BsonInt32(1), new BsonInt32(2), new BsonInt32(3))));
|
||||
}
|
||||
|
||||
static Stream<Arguments> javaTimeInstances() {
|
||||
|
||||
return Stream.of(Arguments.of(Instant.now()), Arguments.of(LocalDate.now()), Arguments.of(LocalDateTime.now()),
|
||||
Arguments.of(LocalTime.now()));
|
||||
}
|
||||
|
||||
static Stream<Arguments> collectionLikeInstances() {
|
||||
|
||||
return Stream.of(Arguments.of(new String[] { "1", "2", "3" }), Arguments.of(List.of("1", "2", "3")),
|
||||
Arguments.of(new Integer[] { 1, 2, 3 }), Arguments.of(List.of(1, 2, 3)),
|
||||
Arguments.of(new Date[] { new Date() }), Arguments.of(List.of(new Date())),
|
||||
Arguments.of(new LocalDate[] { LocalDate.now() }), Arguments.of(List.of(LocalDate.now())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class Person {
|
||||
----
|
||||
Account account = …
|
||||
|
||||
template.insert(account); <2>
|
||||
tempate.insert(account); <2>
|
||||
|
||||
template.update(Person.class)
|
||||
.matching(where("id").is(…))
|
||||
@@ -441,7 +441,7 @@ class Entity {
|
||||
"lastname" : "Long", <2>
|
||||
}
|
||||
----
|
||||
<1> Read/write the keys `fn` & `ln` from/to the linkage document based on the lookup query.
|
||||
<1> Read/wirte 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/write the keys `_id` from/to the reference document to use them in the lookup query.
|
||||
<1> Read/wirte 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.
|
||||
====
|
||||
|
||||
|
||||
@@ -350,14 +350,6 @@ 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.
|
||||
|
||||
@@ -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 `firstname` field inside the document. Here, it is a string-based `properties` element declaring
|
||||
<4> `firstname` specifies constraints for the `firsname` 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.
|
||||
====
|
||||
|
||||
@@ -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 e.g. `$sum` can be extracted directly from the result `Document`.
|
||||
<7> Aggregations resulting in single document holding just an accumulation result like eg. `$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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Spring Data MongoDB 4.2 M2 (2023.1.0)
|
||||
Spring Data MongoDB 4.1 GA (2023.0.0)
|
||||
Copyright (c) [2010-2019] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
@@ -44,8 +44,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user