Compare commits
3 Commits
4.2.0-M2
...
issue/4429
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db4a1be5cb | ||
|
|
2e336a12e9 | ||
|
|
3e517e9c84 |
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
|
||||
#Mon Jul 03 09:49:43 CEST 2023
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Java versions
|
||||
java.main.tag=17.0.8_7-jdk-focal
|
||||
java.main.tag=17.0.7_7-jdk-focal
|
||||
java.next.tag=20-jdk-jammy
|
||||
|
||||
# Docker container images - standard
|
||||
@@ -7,12 +7,12 @@ 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.22
|
||||
docker.mongodb.5.0.version=5.0.18
|
||||
docker.mongodb.6.0.version=6.0.7
|
||||
|
||||
# Supported versions of Redis
|
||||
docker.redis.6.version=6.2.13
|
||||
docker.redis.6.version=6.2.12
|
||||
|
||||
# Supported versions of Cassandra
|
||||
docker.cassandra.3.version=3.11.15
|
||||
|
||||
16
pom.xml
16
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.0-M2</version>
|
||||
<version>4.2.x-4429-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring Data MongoDB</name>
|
||||
@@ -15,7 +15,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>3.2.0-M2</version>
|
||||
<version>3.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
@@ -26,7 +26,7 @@
|
||||
<properties>
|
||||
<project.type>multi</project.type>
|
||||
<dist.id>spring-data-mongodb</dist.id>
|
||||
<springdata.commons>3.2.0-M2</springdata.commons>
|
||||
<springdata.commons>3.2.0-SNAPSHOT</springdata.commons>
|
||||
<mongo>4.10.2</mongo>
|
||||
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
|
||||
<jmh.version>1.19</jmh.version>
|
||||
@@ -144,6 +144,16 @@
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestone</id>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.0-M2</version>
|
||||
<version>4.2.x-4429-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-M2</version>
|
||||
<version>4.2.x-4429-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-M2</version>
|
||||
<version>4.2.x-4429-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,12 @@ package org.springframework.data.mongodb.core;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
@@ -187,16 +191,19 @@ public interface MongoOperations extends FluentMongoOperations {
|
||||
|
||||
return new SessionScoped() {
|
||||
|
||||
private final Object lock = new Object();
|
||||
private @Nullable ClientSession session = null;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private @Nullable ClientSession session;
|
||||
|
||||
@Override
|
||||
public <T> T execute(SessionCallback<T> action, Consumer<ClientSession> onComplete) {
|
||||
|
||||
synchronized (lock) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (session == null) {
|
||||
session = sessionProvider.get();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
@@ -134,7 +137,8 @@ public final class LazyLoadingProxyFactory {
|
||||
}
|
||||
|
||||
return prepareProxyFactory(propertyType,
|
||||
() -> new LazyLoadingInterceptor(property, callback, source, exceptionTranslator)).getProxy(LazyLoadingProxy.class.getClassLoader());
|
||||
() -> new LazyLoadingInterceptor(property, callback, source, exceptionTranslator))
|
||||
.getProxy(LazyLoadingProxy.class.getClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,6 +175,8 @@ public final class LazyLoadingProxyFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
private final MongoPersistentProperty property;
|
||||
private final DbRefResolverCallback callback;
|
||||
private final Object source;
|
||||
@@ -339,25 +345,29 @@ public final class LazyLoadingProxyFactory {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private synchronized Object resolve() {
|
||||
private Object resolve() {
|
||||
|
||||
if (resolved) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
if (resolved) {
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace(String.format("Accessing already resolved lazy loading property %s.%s",
|
||||
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace(String.format("Accessing already resolved lazy loading property %s.%s",
|
||||
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace(String.format("Resolving lazy loading property %s.%s",
|
||||
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
|
||||
}
|
||||
|
||||
try {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace(String.format("Resolving lazy loading property %s.%s",
|
||||
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
|
||||
}
|
||||
|
||||
return callback.resolve(property);
|
||||
|
||||
return executeWhileLocked(lock.writeLock(), () -> callback.resolve(property));
|
||||
} catch (RuntimeException ex) {
|
||||
|
||||
DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(ex);
|
||||
@@ -370,6 +380,16 @@ public final class LazyLoadingProxyFactory {
|
||||
translatedException != null ? translatedException : ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T executeWhileLocked(Lock lock, Supplier<T> stuff) {
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
return stuff.get();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.data.mongodb.core.messaging;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
@@ -39,7 +41,7 @@ import com.mongodb.client.MongoCursor;
|
||||
*/
|
||||
abstract class CursorReadingTask<T, R> implements Task {
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
private final Lock lock = new ReentrantLock();
|
||||
|
||||
private final MongoTemplate template;
|
||||
private final SubscriptionRequest<T, R, RequestOptions> request;
|
||||
@@ -86,19 +88,14 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
state = State.CANCELLED;
|
||||
}
|
||||
doWhileLocked(lock, () -> state = State.CANCELLED);
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
state = State.CANCELLED;
|
||||
}
|
||||
|
||||
doWhileLocked(lock, () -> state = State.CANCELLED);
|
||||
errorHandler.handleError(e);
|
||||
}
|
||||
}
|
||||
@@ -114,30 +111,32 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
*/
|
||||
private void start() {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
doWhileLocked(lock, () -> {
|
||||
if (!State.RUNNING.equals(state)) {
|
||||
state = State.STARTING;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
do {
|
||||
|
||||
boolean valid = false;
|
||||
// boolean valid = false;
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
boolean valid = executeWhileLocked(lock, () -> {
|
||||
|
||||
if (State.STARTING.equals(state)) {
|
||||
|
||||
MongoCursor<T> cursor = execute(() -> initCursor(template, request.getRequestOptions(), targetType));
|
||||
valid = isValidCursor(cursor);
|
||||
if (valid) {
|
||||
this.cursor = cursor;
|
||||
state = State.RUNNING;
|
||||
} else if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
if (!State.STARTING.equals(state)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
MongoCursor<T> cursor = execute(() -> initCursor(template, request.getRequestOptions(), targetType));
|
||||
boolean isValid = isValidCursor(cursor);
|
||||
if (isValid) {
|
||||
this.cursor = cursor;
|
||||
state = State.RUNNING;
|
||||
} else if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
return isValid;
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
|
||||
@@ -145,9 +144,7 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
state = State.CANCELLED;
|
||||
}
|
||||
doWhileLocked(lock, () -> state = State.CANCELLED);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
@@ -163,7 +160,7 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
@Override
|
||||
public void cancel() throws DataAccessResourceFailureException {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
doWhileLocked(lock, () -> {
|
||||
|
||||
if (State.RUNNING.equals(state) || State.STARTING.equals(state)) {
|
||||
this.state = State.CANCELLED;
|
||||
@@ -171,7 +168,7 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -181,10 +178,7 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
return state;
|
||||
}
|
||||
return executeWhileLocked(lock, () -> state);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -220,13 +214,12 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
@Nullable
|
||||
private T getNext() {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
return executeWhileLocked(lock, () -> {
|
||||
if (State.RUNNING.equals(state)) {
|
||||
return cursor.tryNext();
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException(String.format("Cursor %s is not longer open", cursor));
|
||||
throw new IllegalStateException(String.format("Cursor %s is not longer open", cursor));
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isValidCursor(@Nullable MongoCursor<?> cursor) {
|
||||
@@ -263,4 +256,23 @@ abstract class CursorReadingTask<T, R> implements Task {
|
||||
throw translated != null ? translated : e;
|
||||
}
|
||||
}
|
||||
|
||||
private static void doWhileLocked(Lock lock, Runnable action) {
|
||||
|
||||
executeWhileLocked(lock, () -> {
|
||||
action.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <T> T executeWhileLocked(Lock lock, Supplier<T> stuff) {
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
return stuff.get();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -35,8 +39,7 @@ import org.springframework.util.ObjectUtils;
|
||||
/**
|
||||
* Simple {@link Executor} based {@link MessageListenerContainer} implementation for running {@link Task tasks} like
|
||||
* listening to MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> and tailable
|
||||
* cursors.
|
||||
* <br />
|
||||
* cursors. <br />
|
||||
* This message container creates long-running tasks that are executed on {@link Executor}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
@@ -49,9 +52,11 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
|
||||
private final TaskFactory taskFactory;
|
||||
private final Optional<ErrorHandler> errorHandler;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
private final Map<SubscriptionRequest, Subscription> subscriptions = new LinkedHashMap<>();
|
||||
|
||||
ReadWriteLock lifecycleMonitor = new ReentrantReadWriteLock();
|
||||
ReadWriteLock subscriptionMonitor = new ReentrantReadWriteLock();
|
||||
|
||||
private boolean running = false;
|
||||
|
||||
/**
|
||||
@@ -109,43 +114,34 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
|
||||
@Override
|
||||
public void start() {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
doWhileLocked(lifecycleMonitor.writeLock(), () -> {
|
||||
if (!this.running) {
|
||||
subscriptions.values().stream() //
|
||||
.filter(it -> !it.isActive()) //
|
||||
.filter(TaskSubscription.class::isInstance) //
|
||||
.map(TaskSubscription.class::cast) //
|
||||
.map(TaskSubscription::getTask) //
|
||||
.forEach(taskExecutor::execute);
|
||||
|
||||
if (this.running) {
|
||||
return;
|
||||
running = true;
|
||||
}
|
||||
|
||||
subscriptions.values().stream() //
|
||||
.filter(it -> !it.isActive()) //
|
||||
.filter(TaskSubscription.class::isInstance) //
|
||||
.map(TaskSubscription.class::cast) //
|
||||
.map(TaskSubscription::getTask) //
|
||||
.forEach(taskExecutor::execute);
|
||||
|
||||
running = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
|
||||
doWhileLocked(lifecycleMonitor.writeLock(), () -> {
|
||||
if (this.running) {
|
||||
|
||||
subscriptions.values().forEach(Cancelable::cancel);
|
||||
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
return running;
|
||||
}
|
||||
return executeWhileLocked(lifecycleMonitor.readLock(), () -> running);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -170,36 +166,32 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
|
||||
|
||||
@Override
|
||||
public Optional<Subscription> lookup(SubscriptionRequest<?, ?, ?> request) {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
return Optional.ofNullable(subscriptions.get(request));
|
||||
}
|
||||
return executeWhileLocked(subscriptionMonitor.readLock(), () -> Optional.ofNullable(subscriptions.get(request)));
|
||||
}
|
||||
|
||||
public Subscription register(SubscriptionRequest request, Task task) {
|
||||
|
||||
Subscription subscription = new TaskSubscription(task);
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
|
||||
return executeWhileLocked(this.subscriptionMonitor.writeLock(), () ->
|
||||
{
|
||||
if (subscriptions.containsKey(request)) {
|
||||
return subscriptions.get(request);
|
||||
}
|
||||
|
||||
Subscription subscription = new TaskSubscription(task);
|
||||
this.subscriptions.put(request, subscription);
|
||||
|
||||
if (this.running) {
|
||||
if (this.isRunning()) {
|
||||
taskExecutor.execute(task);
|
||||
}
|
||||
}
|
||||
return subscription;
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Subscription subscription) {
|
||||
|
||||
synchronized (lifecycleMonitor) {
|
||||
doWhileLocked(this.subscriptionMonitor.writeLock(), () -> {
|
||||
|
||||
if (subscriptions.containsValue(subscription)) {
|
||||
|
||||
@@ -209,6 +201,25 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
|
||||
|
||||
subscriptions.values().remove(subscription);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void doWhileLocked(Lock lock, Runnable action) {
|
||||
|
||||
executeWhileLocked(lock, () -> {
|
||||
action.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <T> T executeWhileLocked(Lock lock, Supplier<T> stuff) {
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
return stuff.get();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.2 M1 (2023.1.0)
|
||||
Copyright (c) [2010-2019] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
@@ -46,6 +46,5 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user