Compare commits
13 Commits
3.0.0.RC1
...
issue/DATA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32c73b7117 | ||
|
|
b68f3b9510 | ||
|
|
e1df28797a | ||
|
|
c6630aa279 | ||
|
|
bf921cdbd7 | ||
|
|
a1b4e6df59 | ||
|
|
4b3312998a | ||
|
|
c5501db577 | ||
|
|
936a0d35f7 | ||
|
|
5dd91d0b6d | ||
|
|
28510de6c8 | ||
|
|
4bbf4cd5cf | ||
|
|
90bd3f0f18 |
10
Jenkinsfile
vendored
10
Jenkinsfile
vendored
@@ -46,16 +46,16 @@ pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Publish JDK 13 + MongoDB 4.2') {
|
||||
stage('Publish JDK 14 + MongoDB 4.2') {
|
||||
when {
|
||||
changeset "ci/openjdk13-mongodb-4.2/**"
|
||||
changeset "ci/openjdk14-mongodb-4.2/**"
|
||||
}
|
||||
agent { label 'data' }
|
||||
options { timeout(time: 30, unit: 'MINUTES') }
|
||||
|
||||
steps {
|
||||
script {
|
||||
def image = docker.build("springci/spring-data-openjdk13-with-mongodb-4.2.0", "ci/openjdk13-mongodb-4.2/")
|
||||
def image = docker.build("springci/spring-data-openjdk14-with-mongodb-4.2.0", "ci/openjdk14-mongodb-4.2/")
|
||||
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
|
||||
image.push()
|
||||
}
|
||||
@@ -139,10 +139,10 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
stage("test: baseline (jdk13)") {
|
||||
stage("test: baseline (jdk14)") {
|
||||
agent {
|
||||
docker {
|
||||
image 'springci/spring-data-openjdk13-with-mongodb-4.2.0:latest'
|
||||
image 'springci/spring-data-openjdk14-with-mongodb-4.2.0:latest'
|
||||
label 'data'
|
||||
args '-v $HOME:/tmp/jenkins-home'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM adoptopenjdk/openjdk13:latest
|
||||
FROM adoptopenjdk/openjdk14:latest
|
||||
|
||||
ENV TZ=Etc/UTC
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
12
pom.xml
12
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>3.0.0.RC1</version>
|
||||
<version>3.0.0.DATAMONGO-2499-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>2.3.0.RC1</version>
|
||||
<version>2.3.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
@@ -26,8 +26,8 @@
|
||||
<properties>
|
||||
<project.type>multi</project.type>
|
||||
<dist.id>spring-data-mongodb</dist.id>
|
||||
<springdata.commons>2.3.0.RC1</springdata.commons>
|
||||
<mongo>4.0.1</mongo>
|
||||
<springdata.commons>2.3.0.BUILD-SNAPSHOT</springdata.commons>
|
||||
<mongo>4.0.2</mongo>
|
||||
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
|
||||
<jmh.version>1.19</jmh.version>
|
||||
</properties>
|
||||
@@ -134,8 +134,8 @@
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-libs-milestone</id>
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
<id>spring-libs-snapshot</id>
|
||||
<url>https://repo.spring.io/libs-snapshot</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>sonatype-libs-snapshot</id>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>3.0.0.RC1</version>
|
||||
<version>3.0.0.DATAMONGO-2499-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>3.0.0.RC1</version>
|
||||
<version>3.0.0.DATAMONGO-2499-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>3.0.0.RC1</version>
|
||||
<version>3.0.0.DATAMONGO-2499-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import com.mongodb.client.MongoCollection;
|
||||
* query(Human.class)
|
||||
* .inCollection("star-wars")
|
||||
* .as(Jedi.class)
|
||||
* .matching(query(where("firstname").is("luke")))
|
||||
* .matching(where("firstname").is("luke"))
|
||||
* .all();
|
||||
* </code>
|
||||
* </pre>
|
||||
@@ -174,13 +174,13 @@ public interface ExecutableFindOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingFind}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @throws IllegalArgumentException if criteria is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingFind<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default TerminatingFind<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,9 +304,21 @@ public interface ExecutableFindOperation {
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingDistinct}.
|
||||
* @throws IllegalArgumentException if resultType is {@literal null}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
*/
|
||||
TerminatingDistinct<T> matching(Query query);
|
||||
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingDistinct}.
|
||||
* @throws IllegalArgumentException if criteria is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingDistinct<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -152,13 +152,13 @@ public interface ExecutableMapReduceOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingMapReduce}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingMapReduce<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default TerminatingMapReduce<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,13 +124,13 @@ public interface ExecutableRemoveOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingRemove}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingRemove<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default TerminatingRemove<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,13 +215,13 @@ public interface ExecutableUpdateOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link UpdateWithUpdate}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default UpdateWithUpdate<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default UpdateWithUpdate<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,14 @@ import org.springframework.data.mongodb.core.query.Query;
|
||||
* query(Human.class)
|
||||
* .inCollection("star-wars")
|
||||
* .as(Jedi.class)
|
||||
* .matching(query(where("firstname").is("luke")))
|
||||
* .matching(where("firstname").is("luke"))
|
||||
* .all();
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
* @author Juergen Zimmermann
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface ReactiveFindOperation {
|
||||
@@ -148,13 +149,13 @@ public interface ReactiveFindOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingFind}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @throws IllegalArgumentException if criteria is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingFind<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default TerminatingFind<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,9 +273,21 @@ public interface ReactiveFindOperation {
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingDistinct}.
|
||||
* @throws IllegalArgumentException if resultType is {@literal null}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
*/
|
||||
TerminatingDistinct<T> matching(Query query);
|
||||
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingDistinct}.
|
||||
* @throws IllegalArgumentException if criteria is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingDistinct<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -151,13 +151,13 @@ public interface ReactiveMapReduceOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingMapReduce}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingMapReduce<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default TerminatingMapReduce<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,13 +111,13 @@ public interface ReactiveRemoveOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link TerminatingRemove}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default TerminatingRemove<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default TerminatingRemove<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,13 +176,13 @@ public interface ReactiveUpdateOperation {
|
||||
/**
|
||||
* Set the filter {@link CriteriaDefinition criteria} to be used.
|
||||
*
|
||||
* @param criteriaDefinition must not be {@literal null}.
|
||||
* @param criteria must not be {@literal null}.
|
||||
* @return new instance of {@link UpdateWithUpdate}.
|
||||
* @throws IllegalArgumentException if query is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default UpdateWithUpdate<T> matching(CriteriaDefinition criteriaDefinition) {
|
||||
return matching(Query.query(criteriaDefinition));
|
||||
default UpdateWithUpdate<T> matching(CriteriaDefinition criteria) {
|
||||
return matching(Query.query(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -419,7 +419,7 @@ public class QueryMapper {
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<? extends Object> type = value.getClass();
|
||||
Class<?> type = value.getClass();
|
||||
MongoPersistentProperty property = documentField.getProperty();
|
||||
|
||||
if (property.getActualType().isAssignableFrom(type)) {
|
||||
@@ -443,7 +443,7 @@ public class QueryMapper {
|
||||
protected Object convertSimpleOrDocument(Object source, @Nullable MongoPersistentEntity<?> entity) {
|
||||
|
||||
if (source instanceof Example) {
|
||||
return exampleMapper.getMappedExample((Example) source, entity);
|
||||
return exampleMapper.getMappedExample((Example<?>) source, entity);
|
||||
}
|
||||
|
||||
if (source instanceof List) {
|
||||
@@ -922,6 +922,8 @@ public class QueryMapper {
|
||||
*/
|
||||
protected static class MetadataBackedField extends Field {
|
||||
|
||||
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?|\\.\\d+");
|
||||
private static final Pattern DOT_POSITIONAL_PATTERN = 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;
|
||||
@@ -963,7 +965,7 @@ public class QueryMapper {
|
||||
this.entity = entity;
|
||||
this.mappingContext = context;
|
||||
|
||||
this.path = getPath(name);
|
||||
this.path = getPath(removePlaceholders(POSITIONAL_PARAMETER_PATTERN, name));
|
||||
this.property = path == null ? property : path.getLeafProperty();
|
||||
this.association = findAssociation();
|
||||
}
|
||||
@@ -1071,7 +1073,7 @@ public class QueryMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PersistentPropertyPath} for the given <code>pathExpression</code>.
|
||||
* Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}.
|
||||
*
|
||||
* @param pathExpression
|
||||
* @return
|
||||
@@ -1079,8 +1081,8 @@ public class QueryMapper {
|
||||
@Nullable
|
||||
private PersistentPropertyPath<MongoPersistentProperty> getPath(String pathExpression) {
|
||||
|
||||
String rawPath = pathExpression.replaceAll("\\.\\d+", "") //
|
||||
.replaceAll(POSITIONAL_OPERATOR.pattern(), "");
|
||||
String rawPath = removePlaceholders(POSITIONAL_OPERATOR,
|
||||
removePlaceholders(DOT_POSITIONAL_PATTERN, pathExpression));
|
||||
|
||||
PropertyPath path = forName(rawPath);
|
||||
if (path == null || isPathToJavaLangClassProperty(path)) {
|
||||
@@ -1168,13 +1170,17 @@ public class QueryMapper {
|
||||
* @since 1.7
|
||||
*/
|
||||
protected Converter<MongoPersistentProperty, String> getAssociationConverter() {
|
||||
return new AssociationConverter(getAssociation());
|
||||
return new AssociationConverter(name, getAssociation());
|
||||
}
|
||||
|
||||
protected MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() {
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
private static String removePlaceholders(Pattern pattern, String raw) {
|
||||
return pattern.matcher(raw).replaceAll("");
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
@@ -1226,14 +1232,12 @@ public class QueryMapper {
|
||||
static class KeyMapper {
|
||||
|
||||
private final Iterator<String> iterator;
|
||||
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
|
||||
|
||||
public KeyMapper(String key,
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
|
||||
|
||||
this.iterator = Arrays.asList(key.split("\\.")).iterator();
|
||||
this.iterator.next();
|
||||
this.mappingContext = mappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1247,21 +1251,10 @@ public class QueryMapper {
|
||||
StringBuilder mappedName = new StringBuilder(PropertyToFieldNameConverter.INSTANCE.convert(property));
|
||||
boolean inspect = iterator.hasNext();
|
||||
|
||||
int depth = 0;
|
||||
while (inspect) {
|
||||
|
||||
String partial = iterator.next();
|
||||
|
||||
if (depth > 0 && property.isCollectionLike() && property.isEntity() && property.getComponentType() != null) {
|
||||
|
||||
MongoPersistentEntity<?> persistentEntity = mappingContext
|
||||
.getRequiredPersistentEntity(property.getComponentType());
|
||||
MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(partial);
|
||||
if (persistentProperty != null) {
|
||||
partial = mapPropertyName(persistentProperty);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isPositional = (isPositionalParameter(partial) && (property.isMap() || property.isCollectionLike()));
|
||||
|
||||
if (isPositional) {
|
||||
@@ -1269,13 +1262,12 @@ public class QueryMapper {
|
||||
}
|
||||
|
||||
inspect = isPositional && iterator.hasNext();
|
||||
depth++;
|
||||
}
|
||||
|
||||
return mappedName.toString();
|
||||
}
|
||||
|
||||
private static boolean isPositionalParameter(String partial) {
|
||||
static boolean isPositionalParameter(String partial) {
|
||||
|
||||
if ("$".equals(partial)) {
|
||||
return true;
|
||||
@@ -1303,6 +1295,7 @@ public class QueryMapper {
|
||||
*/
|
||||
protected static class AssociationConverter implements Converter<MongoPersistentProperty, String> {
|
||||
|
||||
private final String name;
|
||||
private final MongoPersistentProperty property;
|
||||
private boolean associationFound;
|
||||
|
||||
@@ -1311,10 +1304,11 @@ public class QueryMapper {
|
||||
*
|
||||
* @param association must not be {@literal null}.
|
||||
*/
|
||||
public AssociationConverter(Association<MongoPersistentProperty> association) {
|
||||
public AssociationConverter(String name, Association<MongoPersistentProperty> association) {
|
||||
|
||||
Assert.notNull(association, "Association must not be null!");
|
||||
this.property = association.getInverse();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1332,6 +1326,12 @@ public class QueryMapper {
|
||||
associationFound = true;
|
||||
}
|
||||
|
||||
if (associationFound) {
|
||||
if (name.endsWith("$") && property.isCollectionLike()) {
|
||||
return source.getFieldName() + ".$";
|
||||
}
|
||||
}
|
||||
|
||||
return source.getFieldName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,7 @@ public class UpdateMapper extends QueryMapper {
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Gierke
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
private static class MetadataBackedUpdateField extends MetadataBackedField {
|
||||
|
||||
@@ -289,7 +290,7 @@ public class UpdateMapper extends QueryMapper {
|
||||
public MetadataBackedUpdateField(MongoPersistentEntity<?> entity, String key,
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
|
||||
|
||||
super(key.replaceAll("\\.\\$(\\[.*\\])?", ""), entity, mappingContext);
|
||||
super(key, entity, mappingContext);
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@@ -338,7 +339,7 @@ public class UpdateMapper extends QueryMapper {
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
|
||||
Association<MongoPersistentProperty> association, String key) {
|
||||
|
||||
super(association);
|
||||
super(key, association);
|
||||
this.mapper = new KeyMapper(key, mappingContext);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -394,7 +395,41 @@ public class Criteria implements CriteriaDefinition {
|
||||
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
|
||||
*/
|
||||
public Criteria not() {
|
||||
return not(null);
|
||||
return not((Object)null);
|
||||
}
|
||||
|
||||
public Criteria not(Consumer<Criteria> source) {
|
||||
|
||||
Criteria sink = new Criteria(this.key);
|
||||
source.accept(sink);
|
||||
|
||||
List<Criteria> target = new ArrayList<>();
|
||||
for(Criteria criteria : sink.criteriaChain) {
|
||||
boolean nextIsNot = false;
|
||||
for(Entry<String, Object> entry : criteria.criteria.entrySet()) {
|
||||
if(entry.getKey().equals("$not")) {
|
||||
nextIsNot = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
Criteria extractedSingleCriteria = new Criteria(criteria.key);
|
||||
if(nextIsNot) {
|
||||
extractedSingleCriteria = new Criteria("$not");
|
||||
extractedSingleCriteria.criteria.put(criteria.key, new Document(entry.getKey(), entry.getValue()));
|
||||
nextIsNot = false;
|
||||
} else {
|
||||
extractedSingleCriteria.criteria.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
target.add(extractedSingleCriteria);
|
||||
}
|
||||
}
|
||||
List bsonList = createCriteriaList(target.toArray(new Criteria[0]), true);
|
||||
return registerCriteriaChainElement(new Criteria("$and").is(bsonList));
|
||||
}
|
||||
|
||||
public Criteria not(Criteria... criteria) {
|
||||
List bsonList = createCriteriaList(criteria, true);
|
||||
return registerCriteriaChainElement(new Criteria("$and").is(bsonList));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -660,7 +695,7 @@ public class Criteria implements CriteriaDefinition {
|
||||
* @param criteria
|
||||
*/
|
||||
public Criteria orOperator(Criteria... criteria) {
|
||||
BasicDBList bsonList = createCriteriaList(criteria);
|
||||
List bsonList = createCriteriaList(criteria, false);
|
||||
return registerCriteriaChainElement(new Criteria("$or").is(bsonList));
|
||||
}
|
||||
|
||||
@@ -674,7 +709,7 @@ public class Criteria implements CriteriaDefinition {
|
||||
* @param criteria
|
||||
*/
|
||||
public Criteria norOperator(Criteria... criteria) {
|
||||
BasicDBList bsonList = createCriteriaList(criteria);
|
||||
List bsonList = createCriteriaList(criteria, false);
|
||||
return registerCriteriaChainElement(new Criteria("$nor").is(bsonList));
|
||||
}
|
||||
|
||||
@@ -688,7 +723,7 @@ public class Criteria implements CriteriaDefinition {
|
||||
* @param criteria
|
||||
*/
|
||||
public Criteria andOperator(Criteria... criteria) {
|
||||
BasicDBList bsonList = createCriteriaList(criteria);
|
||||
List bsonList = createCriteriaList(criteria, false);
|
||||
return registerCriteriaChainElement(new Criteria("$and").is(bsonList));
|
||||
}
|
||||
|
||||
@@ -775,16 +810,23 @@ public class Criteria implements CriteriaDefinition {
|
||||
queryCriteria.put(this.key, this.isValue);
|
||||
queryCriteria.putAll(document);
|
||||
} else {
|
||||
queryCriteria.put(this.key, document);
|
||||
if(!document.isEmpty()) {
|
||||
queryCriteria.put(this.key, document);
|
||||
}
|
||||
}
|
||||
|
||||
return queryCriteria;
|
||||
}
|
||||
|
||||
private BasicDBList createCriteriaList(Criteria[] criteria) {
|
||||
BasicDBList bsonList = new BasicDBList();
|
||||
private List createCriteriaList(Criteria[] criteria, boolean not) {
|
||||
List bsonList = new ArrayList();
|
||||
for (Criteria c : criteria) {
|
||||
bsonList.add(c.getCriteriaObject());
|
||||
|
||||
Document co = c.getCriteriaObject();
|
||||
if(not) {
|
||||
co = new Document("$not", co);
|
||||
}
|
||||
bsonList.add(co);
|
||||
}
|
||||
return bsonList;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
|
||||
import org.springframework.data.mongodb.core.MongoOperations;
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
|
||||
@@ -71,6 +72,10 @@ public class StringBasedAggregation extends AbstractMongoQuery {
|
||||
protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProcessor,
|
||||
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
|
||||
|
||||
if (method.isPageQuery() || method.isSliceQuery()) {
|
||||
throw new InvalidMongoDbApiUsageException(String.format("Repository aggregation method '%s' does not support '%s' return type. Please use eg. 'List' instead.", method.getName(), method.getReturnType().getType().getSimpleName()));
|
||||
}
|
||||
|
||||
Class<?> sourceType = method.getDomainClass();
|
||||
Class<?> targetType = typeToRead;
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
@@ -233,8 +232,8 @@ class DefaultBulkOperationsUnitTests {
|
||||
|
||||
verify(beforeSaveCallback).onBeforeSave(personArgumentCaptor.capture(), any(), eq("collection-1"));
|
||||
verify(afterSaveCallback).onAfterSave(personArgumentCaptor.capture(), any(), eq("collection-1"));
|
||||
assertThat(personArgumentCaptor.getAllValues()).extracting("firstName")
|
||||
.containsExactly("init", "before-convert", "before-convert");
|
||||
assertThat(personArgumentCaptor.getAllValues()).extracting("firstName").containsExactly("init", "before-convert",
|
||||
"before-convert");
|
||||
verify(collection).bulkWrite(captor.capture(), any());
|
||||
|
||||
InsertOneModel<Document> updateModel = (InsertOneModel<Document>) captor.getValue().get(0);
|
||||
@@ -340,6 +339,52 @@ class DefaultBulkOperationsUnitTests {
|
||||
.isEqualTo(new org.bson.Document("element", new Document("$gte", 100)));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2502
|
||||
void shouldRetainNestedArrayPathWithPlaceholdersForNoMatchingPaths() {
|
||||
|
||||
ops.updateOne(new BasicQuery("{}"), new Update().set("items.$.documents.0.fileId", "new-id")).execute();
|
||||
|
||||
verify(collection).bulkWrite(captor.capture(), any());
|
||||
|
||||
UpdateOneModel<Document> updateModel = (UpdateOneModel<Document>) captor.getValue().get(0);
|
||||
assertThat(updateModel.getUpdate())
|
||||
.isEqualTo(new Document("$set", new Document("items.$.documents.0.fileId", "new-id")));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2502
|
||||
void shouldRetainNestedArrayPathWithPlaceholdersForMappedEntity() {
|
||||
|
||||
DefaultBulkOperations ops = new DefaultBulkOperations(template, "collection-1",
|
||||
new BulkOperationContext(BulkMode.ORDERED, Optional.of(mappingContext.getPersistentEntity(OrderTest.class)),
|
||||
new QueryMapper(converter), new UpdateMapper(converter), null, null));
|
||||
|
||||
ops.updateOne(new BasicQuery("{}"), Update.update("items.$.documents.0.fileId", "file-id")).execute();
|
||||
|
||||
verify(collection).bulkWrite(captor.capture(), any());
|
||||
|
||||
UpdateOneModel<Document> updateModel = (UpdateOneModel<Document>) captor.getValue().get(0);
|
||||
assertThat(updateModel.getUpdate())
|
||||
.isEqualTo(new Document("$set", new Document("items.$.documents.0.the_file_id", "file-id")));
|
||||
}
|
||||
|
||||
static class OrderTest {
|
||||
|
||||
String id;
|
||||
List<OrderTestItem> items;
|
||||
}
|
||||
|
||||
static class OrderTestItem {
|
||||
|
||||
private String cartId;
|
||||
private List<OrderTestDocument> documents;
|
||||
}
|
||||
|
||||
static class OrderTestDocument {
|
||||
|
||||
@Field("the_file_id")
|
||||
private String fileId;
|
||||
}
|
||||
|
||||
class SomeDomainType {
|
||||
|
||||
@Id String id;
|
||||
|
||||
@@ -536,6 +536,16 @@ class ExecutableFindOperationSupportTests {
|
||||
.isThrownBy(() -> template.query(Person.class).distinct("firstname").as(Long.class).all());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2507
|
||||
void distinctAppliesFilterQuery() {
|
||||
|
||||
assertThat(template.query(Person.class).inCollection(STAR_WARS).distinct("firstname") //
|
||||
.matching(where("lastname").is(luke.lastname)) //
|
||||
.as(String.class) //
|
||||
.all() //
|
||||
).containsExactlyInAnyOrder("luke");
|
||||
}
|
||||
|
||||
interface Contact {}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -59,6 +59,7 @@ import com.mongodb.client.MongoClient;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
* @author Juergen Zimmermann
|
||||
*/
|
||||
@ExtendWith(MongoClientExtension.class)
|
||||
class ReactiveFindOperationSupportTests {
|
||||
@@ -631,6 +632,17 @@ class ReactiveFindOperationSupportTests {
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2507
|
||||
void distinctAppliesFilterQuery() {
|
||||
|
||||
template.query(Person.class).inCollection(STAR_WARS).distinct("firstname") //
|
||||
.matching(where("lastname").is(luke.lastname)) //
|
||||
.as(String.class) //
|
||||
.all() //
|
||||
.as(StepVerifier::create).consumeNextWith(it -> assertThat(it).isEqualTo("luke")) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
interface Contact {}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -966,6 +966,32 @@ public class QueryMapperUnitTests {
|
||||
assertThat(target).isEqualTo(new org.bson.Document("arrayCustomName.$[some_item].nes-ted.$[other_item]", "value"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2502
|
||||
void shouldAllowDeeplyNestedPlaceholders() {
|
||||
|
||||
org.bson.Document target = mapper.getMappedObject(
|
||||
query(where("level0.$[some_item].arrayObj.$[other_item].nested").is("value")).getQueryObject(),
|
||||
context.getPersistentEntity(WithDeepArrayNesting.class));
|
||||
|
||||
assertThat(target).isEqualTo(new org.bson.Document("level0.$[some_item].arrayObj.$[other_item].nested", "value"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2502
|
||||
void shouldAllowDeeplyNestedPlaceholdersWithCustomName() {
|
||||
|
||||
org.bson.Document target = mapper.getMappedObject(
|
||||
query(where("level0.$[some_item].arrayCustomName.$[other_item].nested").is("value")).getQueryObject(),
|
||||
context.getPersistentEntity(WithDeepArrayNesting.class));
|
||||
|
||||
assertThat(target)
|
||||
.isEqualTo(new org.bson.Document("level0.$[some_item].arrayCustomName.$[other_item].nes-ted", "value"));
|
||||
}
|
||||
|
||||
class WithDeepArrayNesting {
|
||||
|
||||
List<WithNestedArray> level0;
|
||||
}
|
||||
|
||||
class WithNestedArray {
|
||||
|
||||
List<NestedArrayOfObj> arrayObj;
|
||||
|
||||
@@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
@@ -350,6 +352,44 @@ class QueryTests {
|
||||
compareQueries(target, source);
|
||||
}
|
||||
|
||||
|
||||
@Test // DATAMONGO-2499
|
||||
void notCombiningSeveralIndedependentCriterias() {
|
||||
|
||||
Query source = new Query(new Criteria().not(Criteria.where("age").gt(30), Criteria.where("age").lt(20)));
|
||||
Document target = source.getQueryObject();
|
||||
|
||||
assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": {\"age\": {\"$lt\": 20}}}]}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2499
|
||||
void notCombiningSingleCriteria() {
|
||||
|
||||
Query source = new Query(Criteria.where("age").not(age -> age.gt(30).lt(20)));
|
||||
Document target = source.getQueryObject();
|
||||
|
||||
assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": {\"age\": {\"$lt\": 20}}}]}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2499
|
||||
void notCombiningSingleCriteriaWithAnother() {
|
||||
|
||||
Query source = new Query(Criteria.where("age").not(age -> age.gt(30).lt(20)).exists(true));
|
||||
Document target = source.getQueryObject();
|
||||
|
||||
assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": {\"age\": {\"$lt\": 20}}}], \"age\": {\"$exists\": true}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2499
|
||||
void notCombiningSingleCriteriaWithNestedNot() {
|
||||
|
||||
Query source = new Query(Criteria.where("age").not(age -> age.gt(30).not().lt(20)));
|
||||
Document target = source.getQueryObject();
|
||||
|
||||
assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": { \"$not\": {\"age\": {\"$lt\": 20}}}}]}"));
|
||||
}
|
||||
|
||||
|
||||
private void compareQueries(Query actual, Query expected) {
|
||||
|
||||
assertThat(actual.getCollation()).isEqualTo(expected.getCollation());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,9 +32,9 @@ public class Address {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @param string2
|
||||
* @param string3
|
||||
* @param street
|
||||
* @param zipcode
|
||||
* @param city
|
||||
*/
|
||||
public Address(String street, String zipcode, String city) {
|
||||
this.street = street;
|
||||
|
||||
@@ -394,4 +394,11 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
|
||||
@Query(value = "{_id:?0}")
|
||||
Optional<org.bson.Document> findDocumentById(String id);
|
||||
|
||||
@Query(value = "{ 'firstname' : ?0, 'lastname' : ?1, 'email' : ?2 , 'age' : ?3, 'sex' : ?4, "
|
||||
+ "'createdAt' : ?5, 'skills' : ?6, 'address.street' : ?7, 'address.zipCode' : ?8, " //
|
||||
+ "'address.city' : ?9, 'uniqueId' : ?10, 'credentials.username' : ?11, 'credentials.password' : ?12 }")
|
||||
Person findPersonByManyArguments(String firstname, String lastname, String email, Integer age, Sex sex,
|
||||
Date createdAt, List<String> skills, String street, String zipCode, //
|
||||
String city, UUID uniqueId, String username, String password);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -31,12 +30,12 @@ import org.springframework.test.context.ContextConfiguration;
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@ContextConfiguration("config/MongoNamespaceIntegrationTests-context.xml")
|
||||
public class RedeclaringRepositoryMethodsTests extends AbstractPersonRepositoryIntegrationTests {
|
||||
class RedeclaringRepositoryMethodsTests extends AbstractPersonRepositoryIntegrationTests {
|
||||
|
||||
@Autowired RedeclaringRepositoryMethodsRepository repository;
|
||||
|
||||
@Test // DATAMONGO-760
|
||||
public void adjustedWellKnownPagedFindAllMethodShouldReturnOnlyTheUserWithFirstnameOliverAugust() {
|
||||
void adjustedWellKnownPagedFindAllMethodShouldReturnOnlyTheUserWithFirstnameOliverAugust() {
|
||||
|
||||
Page<Person> page = repository.findAll(PageRequest.of(0, 2));
|
||||
|
||||
@@ -45,7 +44,7 @@ public class RedeclaringRepositoryMethodsTests extends AbstractPersonRepositoryI
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-760
|
||||
public void adjustedWllKnownFindAllMethodShouldReturnAnEmptyList() {
|
||||
void adjustedWllKnownFindAllMethodShouldReturnAnEmptyList() {
|
||||
|
||||
List<Person> result = repository.findAll();
|
||||
|
||||
|
||||
@@ -17,9 +17,8 @@ package org.springframework.data.mongodb.repository.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReader;
|
||||
@@ -40,14 +39,14 @@ import org.springframework.test.context.ContextConfiguration;
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@ContextConfiguration
|
||||
public class MongoNamespaceIntegrationTests extends AbstractPersonRepositoryIntegrationTests {
|
||||
class MongoNamespaceIntegrationTests extends AbstractPersonRepositoryIntegrationTests {
|
||||
|
||||
DefaultListableBeanFactory factory;
|
||||
BeanDefinitionReader reader;
|
||||
|
||||
@Autowired ApplicationContext context;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
@Override
|
||||
public void setUp() throws InterruptedException {
|
||||
super.setUp();
|
||||
@@ -56,7 +55,7 @@ public class MongoNamespaceIntegrationTests extends AbstractPersonRepositoryInte
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assertDefaultMappingContextIsWired() {
|
||||
void assertDefaultMappingContextIsWired() {
|
||||
|
||||
reader.loadBeanDefinitions(new ClassPathResource("MongoNamespaceIntegrationTests-context.xml", getClass()));
|
||||
BeanDefinition definition = factory.getBeanDefinition("personRepository");
|
||||
@@ -64,7 +63,7 @@ public class MongoNamespaceIntegrationTests extends AbstractPersonRepositoryInte
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-581
|
||||
public void exposesPersistentEntity() {
|
||||
void exposesPersistentEntity() {
|
||||
|
||||
Repositories repositories = new Repositories(context);
|
||||
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(Person.class);
|
||||
|
||||
@@ -34,9 +34,14 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
|
||||
import org.springframework.data.mongodb.core.MongoOperations;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
|
||||
@@ -68,6 +73,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
public class StringBasedAggregationUnitTests {
|
||||
|
||||
SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
@@ -202,6 +208,16 @@ public class StringBasedAggregationUnitTests {
|
||||
assertThat(collationOf(invocation)).isEqualTo(Collation.of("en_US"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2506
|
||||
public void aggregateRaisesErrorOnInvalidReturnType() {
|
||||
|
||||
StringBasedAggregation sba = createAggregationForMethod("invalidPageReturnType", Pageable.class);
|
||||
assertThatExceptionOfType(InvalidMongoDbApiUsageException.class) //
|
||||
.isThrownBy(() -> sba.execute(new Object[] { PageRequest.of(0, 1) })) //
|
||||
.withMessageContaining("invalidPageReturnType") //
|
||||
.withMessageContaining("Page");
|
||||
}
|
||||
|
||||
private AggregationInvocation executeAggregation(String name, Object... args) {
|
||||
|
||||
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
|
||||
@@ -280,6 +296,9 @@ public class StringBasedAggregationUnitTests {
|
||||
|
||||
@Aggregation(pipeline = RAW_GROUP_BY_LASTNAME_STRING, collation = "de_AT")
|
||||
PersonAggregate aggregateWithCollation(Collation collation);
|
||||
|
||||
@Aggregation(RAW_GROUP_BY_LASTNAME_STRING)
|
||||
Page<Person> invalidPageReturnType(Pageable page);
|
||||
}
|
||||
|
||||
static class PersonAggregate {
|
||||
|
||||
Reference in New Issue
Block a user