Compare commits

..

23 Commits

Author SHA1 Message Date
Oliver Gierke
946617f634 DATAMONGO-1775 - Release version 1.10.8 (Ingalls SR8). 2017-10-11 16:42:42 +02:00
Oliver Gierke
50bb03004f DATAMONGO-1775 - Prepare 1.10.8 (Ingalls SR8). 2017-10-11 16:41:08 +02:00
Oliver Gierke
2f68d8c85d DATAMONGO-1775 - Updated changelog. 2017-10-11 16:40:59 +02:00
Mark Paluch
ebfbc4e9d0 DATAMONGO-1776 - Updated changelog. 2017-10-02 11:41:32 +02:00
Christoph Strobl
b96707a0e2 DATAMONGO-1784 - Polishing.
Update JavaDoc, enforce nullability constraints and add tests.

Original Pull Request: #501
2017-09-20 13:20:11 +02:00
Sergey Shcherbakov
7523eedd8d DATAMONGO-1784 - Add expression support to GroupOperation#sum().
We now allow passing an AggregationExpression to GroupOperation.sum which allows construction of more complex expressions.

Original Pull Request: #501
2017-09-20 13:20:04 +02:00
Christoph Strobl
ae8df6b705 DATAMONGO-1782 - Polishing.
toCyclePath now returns an empty String when Path does not cycle.
Also split and add tests.

Original Pull Request: #500
2017-09-19 09:42:01 +02:00
Mark Paluch
72a0a5623a DATAMONGO-1782 - Detect type cycles using PersistentProperty paths.
We now rely on PersistentProperty paths to detect cycles between types. Cycles are detected when building up the path object and traversing PersistentProperty stops after the cycle was hit for the second time to generated indexes for at least one hierarchy level.

Previously, we used String-based property dot paths and compared whether paths to a particular property was already found by a substring search which caused false positives if a property was reachable via multiple paths.

Original Pull Request: #500
2017-09-19 09:41:25 +02:00
Oliver Gierke
5f8f858d89 DATAMONGO-1754 - Updated changelog. 2017-09-11 17:42:45 +02:00
Mark Paluch
8db4feeef0 DATAMONGO-1755 - After release cleanups. 2017-09-11 12:23:36 +02:00
Mark Paluch
b9a392168d DATAMONGO-1755 - Prepare next development iteration. 2017-09-11 12:23:35 +02:00
Mark Paluch
479dc3a0d6 DATAMONGO-1755 - Release version 1.10.7 (Ingalls SR7). 2017-09-11 11:45:05 +02:00
Mark Paluch
59ebbd3d35 DATAMONGO-1755 - Prepare 1.10.7 (Ingalls SR7). 2017-09-11 11:44:20 +02:00
Mark Paluch
38556f522f DATAMONGO-1755 - Updated changelog. 2017-09-11 11:44:15 +02:00
Christoph Strobl
166304849a DATAMONGO-1772 - Fix UpdateMapper type key rendering for abstract list elements contained in concrete typed ones.
Original pull request: #497.
2017-09-05 10:58:55 +02:00
Mark Paluch
f71b38b731 DATAMONGO-1768 - Polishing.
Extend javadocs. Make methods static/reorder methods where possible. Formatting.

Original pull request: #496.
2017-08-25 10:48:42 +02:00
Christoph Strobl
c4af78d81d DATAMONGO-1768 - Allow ignoring type restriction when issuing QBE.
We now allow to remove the type restriction inferred by the QBE mapping via an ignored path expression on the ExampleMatcher. This allows to create untyped QBE expressions returning all entities matching the query without limiting the result to types assignable to the probe itself.

Original pull request: #496.
2017-08-25 10:41:37 +02:00
Oliver Gierke
a281ec83b5 DATAMONGO-1765 - Polishing.
Formatting.
2017-08-07 17:35:11 +02:00
Oliver Gierke
407087b3a7 DATAMONGO-1765 - DefaultDbRefResolver now maps duplicate references correctly.
On bulk resolution of a DBRef array we now map the resulting documents back to their ids to make sure that reoccurring identifiers are mapped to the corresponding documents.
2017-08-07 17:35:11 +02:00
Mark Paluch
90411decce DATAMONGO-1756 - Polishing.
Add author tag.

Original pull request: #491.
2017-08-02 08:52:45 +02:00
Christoph Strobl
71135395c1 DATAMONGO-1756 - Fix nested field name resolution for arithmetic aggregation ops.
Original pull request: #491.
2017-08-02 08:52:45 +02:00
Oliver Gierke
9c43ece3a7 DATAMONGO-1750 - After release cleanups. 2017-07-27 00:21:39 +02:00
Oliver Gierke
283bfce2fe DATAMONGO-1750 - Prepare next development iteration. 2017-07-27 00:15:08 +02:00
24 changed files with 733 additions and 213 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.10.6.RELEASE</version>
<version>1.10.8.RELEASE</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>1.9.6.RELEASE</version>
<version>1.9.8.RELEASE</version>
</parent>
<modules>
@@ -28,7 +28,7 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>1.13.6.RELEASE</springdata.commons>
<springdata.commons>1.13.8.RELEASE</springdata.commons>
<mongo>2.14.3</mongo>
<mongo.osgi>2.13.0</mongo.osgi>
<jmh.version>1.19</jmh.version>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.10.6.RELEASE</version>
<version>1.10.8.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.10.6.RELEASE</version>
<version>1.10.8.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -48,7 +48,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.10.6.RELEASE</version>
<version>1.10.8.RELEASE</version>
</dependency>
<dependency>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.10.6.RELEASE</version>
<version>1.10.8.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.10.6.RELEASE</version>
<version>1.10.8.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.10.6.RELEASE</version>
<version>1.10.8.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -19,12 +19,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -40,6 +38,7 @@ import com.mongodb.DBObject;
* @author Oliver Gierke
* @author Gustavo de Geus
* @author Christoph Strobl
* @author Sergey Shcherbakov
* @since 1.3
* @see <a href="https://docs.mongodb.org/manual/reference/aggregation/group/">MongoDB Aggregation Framework: $group</a>
*/
@@ -157,6 +156,21 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
return sum(reference, null);
}
/**
* Generates an {@link GroupOperationBuilder} for an {@code $sum}-expression for the given
* {@link AggregationExpression}.
*
* @param expr must not be {@literal null}.
* @return new instance of {@link GroupOperationBuilder}. Never {@literal null}.
* @throws IllegalArgumentException when {@code expr} is {@literal null}.
* @since 1.10.8
*/
public GroupOperationBuilder sum(AggregationExpression expr) {
Assert.notNull(expr, "Expr must not be null!");
return newBuilder(GroupOps.SUM, null, expr);
}
private GroupOperationBuilder sum(String reference, Object value) {
return newBuilder(GroupOps.SUM, reference, value);
}
@@ -196,7 +210,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
/**
* Generates an {@link GroupOperationBuilder} for an {@code $last}-expression for the given {@link AggregationExpression}.
* Generates an {@link GroupOperationBuilder} for an {@code $last}-expression for the given
* {@link AggregationExpression}.
*
* @param expr
* @return
@@ -216,7 +231,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
/**
* Generates an {@link GroupOperationBuilder} for a {@code $first}-expression for the given {@link AggregationExpression}.
* Generates an {@link GroupOperationBuilder} for a {@code $first}-expression for the given
* {@link AggregationExpression}.
*
* @param expr
* @return
@@ -236,7 +252,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
/**
* Generates an {@link GroupOperationBuilder} for an {@code $avg}-expression for the given {@link AggregationExpression}.
* Generates an {@link GroupOperationBuilder} for an {@code $avg}-expression for the given
* {@link AggregationExpression}.
*
* @param expr
* @return
@@ -280,7 +297,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
/**
* Generates an {@link GroupOperationBuilder} for an {@code $min}-expression that for the given {@link AggregationExpression}.
* Generates an {@link GroupOperationBuilder} for an {@code $min}-expression that for the given
* {@link AggregationExpression}.
*
* @param expr
* @return
@@ -300,7 +318,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
/**
* Generates an {@link GroupOperationBuilder} for an {@code $max}-expression that for the given {@link AggregationExpression}.
* Generates an {@link GroupOperationBuilder} for an {@code $max}-expression that for the given
* {@link AggregationExpression}.
*
* @param expr
* @return
@@ -322,7 +341,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
/**
* Generates an {@link GroupOperationBuilder} for an {@code $stdDevSamp}-expression that for the given {@link AggregationExpression}.
* Generates an {@link GroupOperationBuilder} for an {@code $stdDevSamp}-expression that for the given
* {@link AggregationExpression}.
*
* @param expr must not be {@literal null}.
* @return never {@literal null}.
@@ -344,7 +364,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
}
/**
* Generates an {@link GroupOperationBuilder} for an {@code $stdDevPop}-expression that for the given {@link AggregationExpression}.
* Generates an {@link GroupOperationBuilder} for an {@code $stdDevPop}-expression that for the given
* {@link AggregationExpression}.
*
* @param expr must not be {@literal null}.
* @return never {@literal null}.
@@ -418,7 +439,8 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
private static enum GroupOps implements Keyword {
SUM("$sum"), LAST("$last"), FIRST("$first"), PUSH("$push"), AVG("$avg"), MIN("$min"), MAX("$max"), ADD_TO_SET("$addToSet"), STD_DEV_POP("$stdDevPop"), STD_DEV_SAMP("$stdDevSamp");
SUM("$sum"), LAST("$last"), FIRST("$first"), PUSH("$push"), AVG("$avg"), MIN("$min"), MAX("$max"), ADD_TO_SET(
"$addToSet"), STD_DEV_POP("$stdDevPop"), STD_DEV_SAMP("$stdDevSamp");
private String mongoOperator;
@@ -426,7 +448,6 @@ public class GroupOperation implements FieldsExposingAggregationOperation {
this.mongoOperator = mongoOperator;
}
@Override
public String toString() {
return mongoOperator;

View File

@@ -1408,7 +1408,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
protected List<Object> getOperationArguments(AggregationOperationContext context) {
List<Object> result = new ArrayList<Object>(values.size());
result.add(context.getReference(getField().getName()).toString());
result.add(context.getReference(getField()).toString());
for (Object element : values) {

View File

@@ -25,7 +25,9 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@@ -46,7 +48,6 @@ import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DB;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
@@ -62,6 +63,8 @@ import com.mongodb.DBRef;
*/
public class DefaultDbRefResolver implements DbRefResolver {
private static final String ID = "_id";
private final MongoDbFactory mongoDbFactory;
private final PersistenceExceptionTranslator exceptionTranslator;
private final ObjenesisStd objenesis;
@@ -144,10 +147,37 @@ public class DefaultDbRefResolver implements DbRefResolver {
ids.add(ref.getId());
}
Map<Object, DBObject> documentsById = getDocumentsById(ids, collection);
List<DBObject> result = new ArrayList<DBObject>(ids.size());
for (Object id : ids) {
result.add(documentsById.get(id));
}
return result;
}
/**
* Returns all documents with the given ids contained in the given collection mapped by their ids.
*
* @param ids must not be {@literal null}.
* @param collection must not be {@literal null} or empty.
* @return
*/
private Map<Object, DBObject> getDocumentsById(List<Object> ids, String collection) {
Assert.notNull(ids, "Ids must not be null!");
Assert.hasText(collection, "Collection must not be null or empty!");
DB db = mongoDbFactory.getDb();
List<DBObject> result = db.getCollection(collection)
.find(new BasicDBObjectBuilder().add("_id", new BasicDBObject("$in", ids)).get()).toArray();
Collections.sort(result, new DbRefByReferencePositionComparator(ids));
BasicDBObject query = new BasicDBObject(ID, new BasicDBObject("$in", ids));
List<DBObject> documents = db.getCollection(collection).find(query).toArray();
Map<Object, DBObject> result = new HashMap<Object, DBObject>(documents.size());
for (DBObject document : documents) {
result.put(document.get(ID), document);
}
return result;
}
@@ -469,7 +499,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
*/
@Override
public int compare(DBObject o1, DBObject o2) {
return Integer.compare(reference.indexOf(o1.get("_id")), reference.indexOf(o2.get("_id")));
return Integer.compare(reference.indexOf(o1.get(ID)), reference.indexOf(o2.get(ID)));
}
}
}

View File

@@ -28,6 +28,7 @@ import java.util.Stack;
import java.util.regex.Pattern;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.NullHandler;
import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
@@ -41,6 +42,7 @@ import org.springframework.data.repository.core.support.ExampleMatcherAccessor;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -48,9 +50,13 @@ import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Mapper from {@link Example} to a query {@link DBObject}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.8
* @see Example
* @see org.springframework.data.domain.ExampleMatcher
*/
public class MongoExampleMapper {
@@ -58,6 +64,11 @@ public class MongoExampleMapper {
private final MongoConverter converter;
private final Map<StringMatcher, Type> stringMatcherPartMapping = new HashMap<StringMatcher, Type>();
/**
* Create a new {@link MongoTypeMapper} given {@link MongoConverter}.
*
* @param converter must not be {@literal null}.
*/
public MongoExampleMapper(MongoConverter converter) {
this.converter = converter;
@@ -99,8 +110,10 @@ public class MongoExampleMapper {
DBObject reference = (DBObject) converter.convertToMongoType(example.getProbe());
if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getProbe()).getIdentifier() == null) {
reference.removeField(entity.getIdProperty().getFieldName());
if (entity.hasIdProperty() && ClassUtils.isAssignable(entity.getType(), example.getProbeType())) {
if (entity.getIdentifierAccessor(example.getProbe()).getIdentifier() == null) {
reference.removeField(entity.getIdProperty().getFieldName());
}
}
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher());
@@ -111,80 +124,7 @@ public class MongoExampleMapper {
: new BasicDBObject(SerializationUtils.flattenMap(reference));
DBObject result = example.getMatcher().isAllMatching() ? flattened : orConcatenate(flattened);
this.converter.getTypeMapper().writeTypeRestrictions(result, getTypesToMatch(example));
return result;
}
private static DBObject orConcatenate(DBObject source) {
List<DBObject> foo = new ArrayList<DBObject>(source.keySet().size());
for (String key : source.keySet()) {
foo.add(new BasicDBObject(key, source.get(key)));
}
return new BasicDBObject("$or", foo);
}
private Set<Class<?>> getTypesToMatch(Example<?> example) {
Set<Class<?>> types = new HashSet<Class<?>>();
for (TypeInformation<?> reference : mappingContext.getManagedTypes()) {
if (example.getProbeType().isAssignableFrom(reference.getType())) {
types.add(reference.getType());
}
}
return types;
}
private String getMappedPropertyPath(String path, Class<?> probeType) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(probeType);
Iterator<String> parts = Arrays.asList(path.split("\\.")).iterator();
final Stack<MongoPersistentProperty> stack = new Stack<MongoPersistentProperty>();
List<String> resultParts = new ArrayList<String>();
while (parts.hasNext()) {
final String part = parts.next();
MongoPersistentProperty prop = entity.getPersistentProperty(part);
if (prop == null) {
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
@Override
public void doWithPersistentProperty(MongoPersistentProperty property) {
if (property.getFieldName().equals(part)) {
stack.push(property);
}
}
});
if (stack.isEmpty()) {
return "";
}
prop = stack.pop();
}
resultParts.add(prop.getName());
if (prop.isEntity() && mappingContext.hasPersistentEntityFor(prop.getActualType())) {
entity = mappingContext.getPersistentEntity(prop.getActualType());
} else {
break;
}
}
return StringUtils.collectionToDelimitedString(resultParts, ".");
return updateTypeRestrictions(result, example);
}
private void applyPropertySpecs(String path, DBObject source, Class<?> probeType,
@@ -246,7 +186,102 @@ public class MongoExampleMapper {
}
}
private boolean isEmptyIdProperty(Entry<String, Object> entry) {
private String getMappedPropertyPath(String path, Class<?> probeType) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(probeType);
Iterator<String> parts = Arrays.asList(path.split("\\.")).iterator();
final Stack<MongoPersistentProperty> stack = new Stack<MongoPersistentProperty>();
List<String> resultParts = new ArrayList<String>();
while (parts.hasNext()) {
final String part = parts.next();
MongoPersistentProperty prop = entity.getPersistentProperty(part);
if (prop == null) {
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
@Override
public void doWithPersistentProperty(MongoPersistentProperty property) {
if (property.getFieldName().equals(part)) {
stack.push(property);
}
}
});
if (stack.isEmpty()) {
return "";
}
prop = stack.pop();
}
resultParts.add(prop.getName());
if (prop.isEntity() && mappingContext.hasPersistentEntityFor(prop.getActualType())) {
entity = mappingContext.getPersistentEntity(prop.getActualType());
} else {
break;
}
}
return StringUtils.collectionToDelimitedString(resultParts, ".");
}
private DBObject updateTypeRestrictions(DBObject query, Example example) {
DBObject result = new BasicDBObject();
if (isTypeRestricting(example.getMatcher())) {
result.putAll(query);
this.converter.getTypeMapper().writeTypeRestrictions(result, getTypesToMatch(example));
return result;
}
for (String key : query.keySet()) {
if (!this.converter.getTypeMapper().isTypeKey(key)) {
result.put(key, query.get(key));
}
}
return result;
}
private boolean isTypeRestricting(ExampleMatcher matcher) {
if (matcher.getIgnoredPaths().isEmpty()) {
return true;
}
for (String path : matcher.getIgnoredPaths()) {
if (this.converter.getTypeMapper().isTypeKey(path)) {
return false;
}
}
return true;
}
private Set<Class<?>> getTypesToMatch(Example<?> example) {
Set<Class<?>> types = new HashSet<Class<?>>();
for (TypeInformation<?> reference : mappingContext.getManagedTypes()) {
if (example.getProbeType().isAssignableFrom(reference.getType())) {
types.add(reference.getType());
}
}
return types;
}
private static boolean isEmptyIdProperty(Entry<String, Object> entry) {
return entry.getKey().equals("_id") && entry.getValue() == null;
}
@@ -272,4 +307,15 @@ public class MongoExampleMapper {
dbo.put("$options", "i");
}
}
private static DBObject orConcatenate(DBObject source) {
List<DBObject> or = new ArrayList<DBObject>(source.keySet().size());
for (String key : source.keySet()) {
or.add(new BasicDBObject(key, source.get(key)));
}
return new BasicDBObject("$or", or);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-2017 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.
@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.convert;
import java.util.Collection;
import java.util.Map.Entry;
import org.springframework.core.convert.converter.Converter;
@@ -161,6 +162,10 @@ public class UpdateMapper extends QueryMapper {
return info;
}
if (source instanceof Collection) {
return NESTED_DOCUMENT;
}
if (!type.equals(source.getClass())) {
return info;
}

View File

@@ -15,12 +15,17 @@
*/
package org.springframework.data.mongodb.core.index;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
@@ -29,9 +34,11 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mongodb.core.index.Index.Duplicates;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.CycleGuard.Path;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.TextIndexIncludeOptions.IncludeStrategy;
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexDefinitionBuilder;
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexedFieldSpec;
@@ -41,6 +48,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
@@ -57,6 +65,7 @@ import com.mongodb.util.JSON;
* @author Christoph Strobl
* @author Thomas Darimont
* @author Martin Macko
* @author Mark Paluch
* @since 1.5
*/
public class MongoPersistentEntityIndexResolver implements IndexResolver {
@@ -99,7 +108,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
Document document = root.findAnnotation(Document.class);
Assert.notNull(document, "Given entity is not collection root.");
final List<IndexDefinitionHolder> indexInformation = new ArrayList<MongoPersistentEntityIndexResolver.IndexDefinitionHolder>();
final List<IndexDefinitionHolder> indexInformation = new ArrayList<IndexDefinitionHolder>();
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", root.getCollection(), root));
indexInformation.addAll(potentiallyCreateTextIndexDefinition(root));
@@ -113,7 +122,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
try {
if (persistentProperty.isEntity()) {
indexInformation.addAll(resolveIndexForClass(persistentProperty.getTypeInformation().getActualType(),
persistentProperty.getFieldName(), root.getCollection(), guard));
persistentProperty.getFieldName(), Path.of(persistentProperty), root.getCollection(), guard));
}
IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(
@@ -136,31 +145,35 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
* Recursively resolve and inspect properties of given {@literal type} for indexes to be created.
*
* @param type
* @param path The {@literal "dot} path.
* @param dotPath The {@literal "dot} path.
* @param path {@link PersistentProperty} path for cycle detection.
* @param collection
* @param guard
* @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property
* types. Will never be {@code null}.
*/
private List<IndexDefinitionHolder> resolveIndexForClass(final TypeInformation<?> type, final String path,
final String collection, final CycleGuard guard) {
private List<IndexDefinitionHolder> resolveIndexForClass(final TypeInformation<?> type, final String dotPath,
final Path path, final String collection, final CycleGuard guard) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(type);
final List<IndexDefinitionHolder> indexInformation = new ArrayList<MongoPersistentEntityIndexResolver.IndexDefinitionHolder>();
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(path, collection, entity));
final List<IndexDefinitionHolder> indexInformation = new ArrayList<IndexDefinitionHolder>();
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity));
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
@Override
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
String propertyDotPath = (StringUtils.hasText(path) ? path + "." : "") + persistentProperty.getFieldName();
guard.protect(persistentProperty, path);
String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "")
+ persistentProperty.getFieldName();
Path propertyPath = path.append(persistentProperty);
guard.protect(persistentProperty, propertyPath);
if (persistentProperty.isEntity()) {
try {
indexInformation.addAll(resolveIndexForClass(persistentProperty.getTypeInformation().getActualType(),
propertyDotPath, collection, guard));
propertyDotPath, propertyPath, collection, guard));
} catch (CyclicPropertyReferenceException e) {
LOGGER.info(e.getMessage());
}
@@ -174,7 +187,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
});
indexInformation.addAll(resolveIndexesForDbrefs(path, collection, entity));
indexInformation.addAll(resolveIndexesForDbrefs(dotPath, collection, entity));
return indexInformation;
}
@@ -212,8 +225,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
try {
appendTextIndexInformation("", indexDefinitionBuilder, root, new TextIndexIncludeOptions(IncludeStrategy.DEFAULT),
new CycleGuard());
appendTextIndexInformation("", Path.empty(), indexDefinitionBuilder, root,
new TextIndexIncludeOptions(IncludeStrategy.DEFAULT), new CycleGuard());
} catch (CyclicPropertyReferenceException e) {
LOGGER.info(e.getMessage());
}
@@ -229,15 +242,16 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
private void appendTextIndexInformation(final String dotPath, final TextIndexDefinitionBuilder indexDefinitionBuilder,
final MongoPersistentEntity<?> entity, final TextIndexIncludeOptions includeOptions, final CycleGuard guard) {
private void appendTextIndexInformation(final String dotPath, final Path path,
final TextIndexDefinitionBuilder indexDefinitionBuilder, final MongoPersistentEntity<?> entity,
final TextIndexIncludeOptions includeOptions, final CycleGuard guard) {
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
@Override
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
guard.protect(persistentProperty, dotPath);
guard.protect(persistentProperty, path);
if (persistentProperty.isExplicitLanguageProperty() && !StringUtils.hasText(dotPath)) {
indexDefinitionBuilder.withLanguageOverride(persistentProperty.getFieldName());
@@ -250,6 +264,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "")
+ persistentProperty.getFieldName();
Path propertyPath = path.append(persistentProperty);
Float weight = indexed != null ? indexed.weight()
: (includeOptions.getParentFieldSpec() != null ? includeOptions.getParentFieldSpec().getWeight() : 1.0F);
@@ -262,7 +278,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
try {
appendTextIndexInformation(propertyDotPath, indexDefinitionBuilder,
appendTextIndexInformation(propertyDotPath, propertyPath, indexDefinitionBuilder,
mappingContext.getPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard);
} catch (CyclicPropertyReferenceException e) {
LOGGER.info(e.getMessage());
@@ -291,7 +307,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
protected List<IndexDefinitionHolder> createCompoundIndexDefinitions(String dotPath, String fallbackCollection,
MongoPersistentEntity<?> entity) {
List<IndexDefinitionHolder> indexDefinitions = new ArrayList<MongoPersistentEntityIndexResolver.IndexDefinitionHolder>();
List<IndexDefinitionHolder> indexDefinitions = new ArrayList<IndexDefinitionHolder>();
CompoundIndexes indexes = entity.findAnnotation(CompoundIndexes.class);
if (indexes != null) {
@@ -482,53 +498,38 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
* to detect potential cycles within the references.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
static class CycleGuard {
private final Map<String, List<Path>> propertyTypeMap;
CycleGuard() {
this.propertyTypeMap = new LinkedHashMap<String, List<Path>>();
}
private final Set<String> seenProperties = new HashSet<String>();
/**
* Detect a cycle in a property path if the property was seen at least once.
*
* @param property The property to inspect
* @param path The path under which the property can be reached.
* @param path The type path under which the property can be reached.
* @throws CyclicPropertyReferenceException in case a potential cycle is detected.
* @see Path#cycles(MongoPersistentProperty, String)
* @see Path#isCycle()
*/
void protect(MongoPersistentProperty property, String path) throws CyclicPropertyReferenceException {
void protect(MongoPersistentProperty property, Path path) throws CyclicPropertyReferenceException {
String propertyTypeKey = createMapKey(property);
if (propertyTypeMap.containsKey(propertyTypeKey)) {
if (!seenProperties.add(propertyTypeKey)) {
List<Path> paths = propertyTypeMap.get(propertyTypeKey);
for (Path existingPath : paths) {
if (existingPath.cycles(property, path) && property.isEntity()) {
paths.add(new Path(property, path));
throw new CyclicPropertyReferenceException(property.getFieldName(), property.getOwner().getType(),
existingPath.getPath());
}
if (path.isCycle()) {
throw new CyclicPropertyReferenceException(property.getFieldName(), property.getOwner().getType(),
path.toCyclePath());
}
paths.add(new Path(property, path));
} else {
ArrayList<Path> paths = new ArrayList<Path>();
paths.add(new Path(property, path));
propertyTypeMap.put(propertyTypeKey, paths);
}
}
private String createMapKey(MongoPersistentProperty property) {
return property.getOwner().getType().getSimpleName() + ":" + property.getFieldName();
return ClassUtils.getShortName(property.getOwner().getType()) + ":" + property.getFieldName();
}
/**
* Path defines the property and its full path from the document root. <br />
* Path defines the full property path from the document root. <br />
* A {@link Path} with {@literal spring.data.mongodb} would be created for the property {@code Three.mongodb}.
*
* <pre>
@@ -549,39 +550,117 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
* </pre>
*
* @author Christoph Strobl
* @author Mark Paluch
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
static class Path {
private final MongoPersistentProperty property;
private final String path;
private static final Path EMPTY = new Path(Collections.<PersistentProperty<?>> emptyList(), false);
Path(MongoPersistentProperty property, String path) {
private final List<PersistentProperty<?>> elements;
private final boolean cycle;
this.property = property;
this.path = path;
}
public String getPath() {
return path;
/**
* @return an empty {@link Path}.
* @since 1.10.8
*/
static Path empty() {
return EMPTY;
}
/**
* Checks whether the given property is owned by the same entity and if it has been already visited by a subset of
* the current path. Given {@literal foo.bar.bar} cycles if {@literal foo.bar} has already been visited and
* {@code class Bar} contains a property of type {@code Bar}. The previously mentioned path would not cycle if
* {@code class Bar} contained a property of type {@code SomeEntity} named {@literal bar}.
* Creates a new {@link Path} from the initial {@link PersistentProperty}.
*
* @param property
* @param path
* @return
* @param initial must not be {@literal null}.
* @return the new {@link Path}.
* @since 1.10.8
*/
boolean cycles(MongoPersistentProperty property, String path) {
static Path of(PersistentProperty<?> initial) {
return new Path(Collections.<PersistentProperty<?>> singletonList(initial), false);
}
if (!property.getOwner().equals(this.property.getOwner())) {
return false;
/**
* Creates a new {@link Path} by appending a {@link PersistentProperty breadcrumb} to the path.
*
* @param breadcrumb must not be {@literal null}.
* @return the new {@link Path}.
* @since 1.10.8
*/
Path append(PersistentProperty<?> breadcrumb) {
List<PersistentProperty<?>> elements = new ArrayList<PersistentProperty<?>>(this.elements.size() + 1);
elements.addAll(this.elements);
elements.add(breadcrumb);
return new Path(elements, this.elements.contains(breadcrumb));
}
/**
* @return {@literal true} if a cycle was detected.
* @since 1.10.8
*/
public boolean isCycle() {
return cycle;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return this.elements.isEmpty() ? "(empty)" : toPath(this.elements.iterator());
}
/**
* Returns the cycle path truncated to the first discovered cycle. The result for the path
* {@literal foo.bar.baz.bar} is {@literal bar -> baz -> bar}.
*
* @return the cycle path truncated to the first discovered cycle.
* @since 1.10.8
*/
String toCyclePath() {
if(!cycle) {
return "";
}
return path.equals(this.path) || path.contains(this.path + ".") || path.contains("." + this.path);
for (int i = 0; i < this.elements.size(); i++) {
int index = indexOf(this.elements, this.elements.get(i), i + 1);
if (index != -1) {
return toPath(this.elements.subList(i, index + 1).iterator());
}
}
return toString();
}
private static <T> int indexOf(List<T> haystack, T needle, int offset) {
for (int i = offset; i < haystack.size(); i++) {
if (haystack.get(i).equals(needle)) {
return i;
}
}
return -1;
}
private static String toPath(Iterator<PersistentProperty<?>> iterator) {
StringBuilder builder = new StringBuilder();
while (iterator.hasNext()) {
builder.append(iterator.next().getName());
if (iterator.hasNext()) {
builder.append(" -> ");
}
}
return builder.toString();
}
}
}

View File

@@ -166,6 +166,32 @@ public class QueryByExampleTests {
assertThat(result, hasItems(p1, p2));
}
@Test // DATAMONGO-1768
public void typedExampleMatchesNothingIfTypesDoNotMatch() {
NotAPersonButStillMatchingFields probe = new NotAPersonButStillMatchingFields();
probe.lastname = "stark";
Query query = new Query(new Criteria().alike(Example.of(probe)));
List<Person> result = operations.find(query, Person.class);
assertThat(result, hasSize(0));
}
@Test // DATAMONGO-1768
public void untypedExampleMatchesCorrectly() {
NotAPersonButStillMatchingFields probe = new NotAPersonButStillMatchingFields();
probe.lastname = "stark";
Query query = new Query(
new Criteria().alike(Example.of(probe, ExampleMatcher.matching().withIgnorePaths("_class"))));
List<Person> result = operations.find(query, Person.class);
assertThat(result, hasSize(2));
assertThat(result, hasItems(p1, p3));
}
@Document(collection = "dramatis-personae")
@EqualsAndHashCode
@ToString
@@ -175,4 +201,12 @@ public class QueryByExampleTests {
String firstname, middlename;
@Field("last_name") String lastname;
}
@EqualsAndHashCode
@ToString
static class NotAPersonButStillMatchingFields {
String firstname, middlename;
@Field("last_name") String lastname;
}
}

View File

@@ -88,6 +88,7 @@ import com.mongodb.util.JSON;
* @author Christoph Strobl
* @author Mark Paluch
* @author Nikolay Bogdanov
* @author Sergey Shcherbakov
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:infrastructure.xml")
@@ -151,7 +152,7 @@ public class AggregationTests {
/**
* Imports the sample dataset (zips.json) if necessary (e.g. if it doesn't exist yet). The dataset can originally be
* found on the mongodb aggregation framework example website:
*
*
* @see <a href="https://docs.mongodb.org/manual/tutorial/aggregation-examples/">MongoDB Aggregation Examples</a>
*/
private void initSampleDataIfNecessary() {
@@ -343,7 +344,7 @@ public class AggregationTests {
public void complexAggregationFrameworkUsageLargestAndSmallestCitiesByState() {
/*
//complex mongodb aggregation framework example from https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state
db.zipInfo.aggregate(
db.zipInfo.aggregate(
{
$group: {
_id: {
@@ -450,18 +451,18 @@ public class AggregationTests {
@Test // DATAMONGO-586
public void findStatesWithPopulationOver10MillionAggregationExample() {
/*
//complex mongodb aggregation framework example from
//complex mongodb aggregation framework example from
https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state
db.zipcodes.aggregate(
db.zipcodes.aggregate(
{
$group: {
_id:"$state",
totalPop:{ $sum:"$pop"}
}
},
{
$sort: { _id: 1, "totalPop": 1 }
{
$sort: { _id: 1, "totalPop": 1 }
},
{
$match: {
@@ -726,6 +727,49 @@ public class AggregationTests {
assertThat(((Number) good.get("score")).longValue(), is(equalTo(9000L)));
}
@Test // DATAMONGO-1784
public void shouldAllowSumUsingConditionalExpressions() {
mongoTemplate.dropCollection(CarPerson.class);
CarPerson person1 = new CarPerson("first1", "last1", new CarDescriptor.Entry("MAKE1", "MODEL1", 2000),
new CarDescriptor.Entry("MAKE1", "MODEL2", 2001));
CarPerson person2 = new CarPerson("first2", "last2", new CarDescriptor.Entry("MAKE3", "MODEL4", 2014));
CarPerson person3 = new CarPerson("first3", "last3", new CarDescriptor.Entry("MAKE2", "MODEL5", 2015));
mongoTemplate.save(person1);
mongoTemplate.save(person2);
mongoTemplate.save(person3);
TypedAggregation<CarPerson> agg = Aggregation.newAggregation(CarPerson.class,
unwind("descriptors.carDescriptor.entries"), //
project() //
.and(ConditionalOperators //
.when(Criteria.where("descriptors.carDescriptor.entries.make").is("MAKE1")).then("good")
.otherwise("meh"))
.as("make") //
.and("descriptors.carDescriptor.entries.model").as("model") //
.and("descriptors.carDescriptor.entries.year").as("year"), //
group("make").sum(ConditionalOperators //
.when(Criteria.where("year").gte(2012)) //
.then(1) //
.otherwise(9000)).as("score"),
sort(ASC, "make"));
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, DBObject.class);
assertThat(result.getMappedResults(), hasSize(2));
DBObject meh = result.getMappedResults().get(0);
assertThat((String) meh.get("_id"), is(equalTo("meh")));
assertThat(((Number) meh.get("score")).longValue(), is(equalTo(2L)));
DBObject good = result.getMappedResults().get(1);
assertThat((String) good.get("_id"), is(equalTo("good")));
assertThat(((Number) good.get("score")).longValue(), is(equalTo(18000L)));
}
/**
* @see <a href=
* "https://docs.mongodb.com/manual/tutorial/aggregation-with-user-preference-data/#return-the-five-most-common-likes">Return

View File

@@ -40,7 +40,7 @@ import com.mongodb.util.JSON;
/**
* Unit tests for {@link Aggregation}.
*
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
@@ -564,6 +564,16 @@ public class AggregationUnitTests {
assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.else", "no-answer"));
}
@Test // DATAMONGO-1756
public void projectOperationShouldRenderNestedFieldNamesCorrectly() {
DBObject agg = newAggregation(project().and("value1.value").plus("value2.value").as("val")).toDbObject("collection",
Aggregation.DEFAULT_CONTEXT);
assertThat((BasicDBObject) extractPipelineElement(agg, 0, "$project"), is(equalTo(new BasicDBObject("val",
new BasicDBObject("$add", new BasicDbListBuilder().add("$value1.value").add("$value2.value").get())))));
}
private DBObject extractPipelineElement(DBObject agg, int index, String operation) {
List<DBObject> pipeline = (List<DBObject>) agg.get("pipeline");

View File

@@ -24,6 +24,7 @@ import java.util.Arrays;
import org.junit.Test;
import org.springframework.data.mongodb.core.DBObjectTestUtils;
import org.springframework.data.mongodb.core.query.Criteria;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -218,6 +219,31 @@ public class GroupOperationUnitTests {
assertThat(push, is((DBObject) new BasicDBObject("$stdDevPop", "$field")));
}
@Test // DATAMONGO-1784
public void shouldRenderSumWithExpressionInGroup() {
GroupOperation groupOperation = Aggregation //
.group("username") //
.sum(ConditionalOperators //
.when(Criteria.where("foo").is("bar")) //
.then(1) //
.otherwise(-1)) //
.as("foobar");
DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation);
DBObject foobar = DBObjectTestUtils.getAsDBObject(groupClause, "foobar");
assertThat((DBObject) foobar.get("$sum"),
is((DBObject) new BasicDBObject("$cond",
new BasicDBObject("if", new BasicDBObject("$eq", Arrays.asList("$foo", "bar"))).append("then", 1)
.append("else", -1))));
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1784
public void sumWithNullExpressionShouldThrowException() {
Aggregation.group("username").sum((AggregationExpression) null);
}
private DBObject extractDbObjectFromGroupOperation(GroupOperation groupOperation) {
DBObject dbObject = groupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT);
DBObject groupClause = DBObjectTestUtils.getAsDBObject(dbObject, "$group");

View File

@@ -47,6 +47,7 @@ import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.test.util.BasicDbListBuilder;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -54,10 +55,11 @@ import com.mongodb.util.JSON;
/**
* Unit tests for {@link TypeBasedAggregationOperationContext}.
*
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Mark Paluch
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class TypeBasedAggregationOperationContextUnitTests {
@@ -336,6 +338,19 @@ public class TypeBasedAggregationOperationContextUnitTests {
assertThat(age, isBsonObject().containing("$ifNull.[1]._class", Age.class.getName()));
}
@Test // DATAMONGO-1756
public void projectOperationShouldRenderNestedFieldNamesCorrectlyForTypedAggregation() {
AggregationOperationContext context = getContext(Wrapper.class);
DBObject agg = newAggregation(Wrapper.class, project().and("nested1.value1").plus("nested2.value2").as("val"))
.toDbObject("collection", context);
BasicDBObject project = (BasicDBObject) getPipelineElementFromAggregationAt(agg, 0).get("$project");
assertThat(project, is(equalTo(new BasicDBObject("val", new BasicDBObject("$add",
new BasicDbListBuilder().add("$nested1.value1").add("$field2.nestedValue2").get())))));
}
@Document(collection = "person")
public static class FooPerson {
@@ -406,4 +421,15 @@ public class TypeBasedAggregationOperationContextUnitTests {
String name;
}
static class Wrapper {
Nested nested1;
@org.springframework.data.mongodb.core.mapping.Field("field2") Nested nested2;
}
static class Nested {
String value1;
@org.springframework.data.mongodb.core.mapping.Field("nestedValue2") String value2;
}
}

View File

@@ -64,7 +64,7 @@ public class DefaultDbRefResolverUnitTests {
when(factoryMock.getDb()).thenReturn(dbMock);
when(dbMock.getCollection(anyString())).thenReturn(collectionMock);
when(collectionMock.find(Mockito.any(DBObject.class))).thenReturn(cursorMock);
when(cursorMock.toArray()).thenReturn(Collections.<DBObject>emptyList());
when(cursorMock.toArray()).thenReturn(Collections.<DBObject> emptyList());
resolver = new DefaultDbRefResolver(factoryMock);
}
@@ -100,7 +100,7 @@ public class DefaultDbRefResolverUnitTests {
@Test // DATAMONGO-1194
public void bulkFetchShouldReturnEarlyForEmptyLists() {
resolver.bulkFetch(Collections.<DBRef>emptyList());
resolver.bulkFetch(Collections.<DBRef> emptyList());
verify(collectionMock, never()).find(Mockito.any(DBObject.class));
}
@@ -118,4 +118,17 @@ public class DefaultDbRefResolverUnitTests {
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2)), contains(o1, o2));
}
@Test // DATAMONGO-1765
public void bulkFetchContainsDuplicates() {
DBObject document = new BasicDBObject("_id", new ObjectId());
DBRef ref1 = new DBRef("collection-1", document.get("_id"));
DBRef ref2 = new DBRef("collection-1", document.get("_id"));
when(cursorMock.toArray()).thenReturn(Arrays.asList(document));
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2)), contains(document, document));
}
}

View File

@@ -24,6 +24,7 @@ import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.bson.BSONObject;
@@ -36,8 +37,7 @@ import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.domain.ExampleMatcher.*;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.ClassWithGeoTypes;
@@ -47,6 +47,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.test.util.IsBsonObject;
import org.springframework.data.util.TypeInformation;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
@@ -288,7 +289,7 @@ public class MongoExampleMapperUnitTests {
DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(WithDBRef.class));
com.mongodb.DBRef reference = getTypedValue(dbo, "referenceDocument", com.mongodb.DBRef.class);
assertThat(reference.getId(), Is.<Object>is("200"));
assertThat(reference.getId(), Is.<Object> is("200"));
assertThat(reference.getCollectionName(), is("refDoc"));
}
@@ -311,8 +312,8 @@ public class MongoExampleMapperUnitTests {
DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(WithDBRef.class));
assertThat(dbo.get("legacyPoint.x"), Is.<Object>is(10D));
assertThat(dbo.get("legacyPoint.y"), Is.<Object>is(20D));
assertThat(dbo.get("legacyPoint.x"), Is.<Object> is(10D));
assertThat(dbo.get("legacyPoint.y"), Is.<Object> is(20D));
}
@Test // DATAMONGO-1245
@@ -426,6 +427,52 @@ public class MongoExampleMapperUnitTests {
assertThat(mapper.getMappedExample(example), isBsonObject().containing("$or").containing("_class"));
}
@Test // DATAMONGO-1768
public void allowIgnoringTypeRestrictionBySettingUpTypeKeyAsAnIgnoredPath() {
WrapperDocument probe = new WrapperDocument();
probe.flatDoc = new FlatDocument();
probe.flatDoc.stringValue = "conflux";
DBObject dbo = mapper.getMappedExample(Example.of(probe, ExampleMatcher.matching().withIgnorePaths("_class")));
assertThat(dbo, isBsonObject().notContaining("_class"));
}
@Test // DATAMONGO-1768
public void allowIgnoringTypeRestrictionBySettingUpTypeKeyAsAnIgnoredPathWhenUsingCustomTypeMapper() {
WrapperDocument probe = new WrapperDocument();
probe.flatDoc = new FlatDocument();
probe.flatDoc.stringValue = "conflux";
MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(new DefaultDbRefResolver(factory), context);
mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper() {
@Override
public boolean isTypeKey(String key) {
return "_foo".equals(key);
}
@Override
public void writeTypeRestrictions(DBObject dbo, Set<Class<?>> restrictedTypes) {
dbo.put("_foo", "bar");
}
@Override
public void writeType(TypeInformation<?> info, DBObject sink) {
sink.put("_foo", "bar");
}
});
mappingMongoConverter.afterPropertiesSet();
DBObject dbo = new MongoExampleMapper(mappingMongoConverter)
.getMappedExample(Example.of(probe, ExampleMatcher.matching().withIgnorePaths("_foo")));
assertThat(dbo, isBsonObject().notContaining("_class").notContaining("_foo"));
}
static class FlatDocument {
@Id String id;

View File

@@ -901,6 +901,34 @@ public class UpdateMapperUnitTests {
}
}
@Test // DATAMONGO-1772
public void mappingShouldAddTypeKeyInListOfInterfaceTypeContainedInConcreteObjectCorrectly() {
ConcreteInner inner = new ConcreteInner();
inner.interfaceTypeList = Collections.<SomeInterfaceType> singletonList(new SomeInterfaceImpl());
List<ConcreteInner> list = Collections.singletonList(inner);
DBObject mappedUpdate = mapper.getMappedObject(new Update().set("concreteInnerList", list).getUpdateObject(),
context.getPersistentEntity(Outer.class));
assertThat(mappedUpdate, isBsonObject().containing("$set.concreteInnerList.[0].interfaceTypeList.[0]._class")
.notContaining("$set.concreteInnerList.[0]._class"));
}
@Test // DATAMONGO-1772
public void mappingShouldAddTypeKeyInListOfAbstractTypeContainedInConcreteObjectCorrectly() {
ConcreteInner inner = new ConcreteInner();
inner.abstractTypeList = Collections.<SomeAbstractType> singletonList(new SomeInterfaceImpl());
List<ConcreteInner> list = Collections.singletonList(inner);
DBObject mappedUpdate = mapper.getMappedObject(new Update().set("concreteInnerList", list).getUpdateObject(),
context.getPersistentEntity(Outer.class));
assertThat(mappedUpdate, isBsonObject().containing("$set.concreteInnerList.[0].abstractTypeList.[0]._class")
.notContaining("$set.concreteInnerList.[0]._class"));
}
static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes {
ListModelWrapper concreteTypeWithListAttributeOfInterfaceType;
}
@@ -1187,4 +1215,26 @@ public class UpdateMapperUnitTests {
Integer intValue;
int primIntValue;
}
static class Outer {
List<ConcreteInner> concreteInnerList;
}
static class ConcreteInner {
List<SomeInterfaceType> interfaceTypeList;
List<SomeAbstractType> abstractTypeList;
}
interface SomeInterfaceType {
}
static abstract class SomeAbstractType {
}
static class SomeInterfaceImpl extends SomeAbstractType implements SomeInterfaceType {
}
}

View File

@@ -65,7 +65,7 @@ public class MongoPersistentEntityIndexResolverUnitTests {
/**
* Test resolution of {@link Indexed}.
*
*
* @author Christoph Strobl
*/
public static class IndexResolutionTests {
@@ -310,7 +310,7 @@ public class MongoPersistentEntityIndexResolverUnitTests {
/**
* Test resolution of {@link GeoSpatialIndexed}.
*
*
* @author Christoph Strobl
*/
public static class GeoSpatialIndexResolutionTests {
@@ -423,7 +423,7 @@ public class MongoPersistentEntityIndexResolverUnitTests {
/**
* Test resolution of {@link CompoundIndexes}.
*
*
* @author Christoph Strobl
*/
public static class CompoundIndexResolutionTests {
@@ -914,6 +914,17 @@ public class MongoPersistentEntityIndexResolverUnitTests {
.resolveIndexForEntity(selfCyclingEntity);
}
@Test // DATAMONGO-1782
public void shouldAllowMultiplePathsToDeeplyType() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
NoCycleManyPathsToDeepValueObject.class);
assertIndexPathAndCollection("l3.valueObject.value", "rules", indexDefinitions.get(0));
assertIndexPathAndCollection("l2.l3.valueObject.value", "rules", indexDefinitions.get(1));
assertThat(indexDefinitions, hasSize(2));
}
@Test // DATAMONGO-1025
public void shouldUsePathIndexAsIndexNameForDocumentsHavingNamedNestedCompoundIndexFixedOnCollection() {
@@ -1071,6 +1082,25 @@ public class MongoPersistentEntityIndexResolverUnitTests {
@Indexed String foo;
}
@Document(collection = "rules")
static class NoCycleManyPathsToDeepValueObject {
private NoCycleLevel3 l3;
private NoCycleLevel2 l2;
}
static class NoCycleLevel2 {
private NoCycleLevel3 l3;
}
static class NoCycleLevel3 {
private ValueObject valueObject;
}
static class ValueObject {
@Indexed private String value;
}
@Document
static class SimilarityHolingBean {
@@ -1187,7 +1217,6 @@ public class MongoPersistentEntityIndexResolverUnitTests {
static class EntityWithGenericTypeWrapperAsElement {
List<GenericEntityWrapper<DocumentWithNamedIndex>> listWithGeneircTypeElement;
}
}
private static List<IndexDefinitionHolder> prepareMappingContextAndResolveIndexForType(Class<?> type) {

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.mongodb.core.index;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@@ -31,8 +31,9 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
/**
* Unit tests for {@link Path}.
*
*
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class PathUnitTests {
@@ -45,39 +46,50 @@ public class PathUnitTests {
when(entityMock.getType()).thenReturn((Class) Object.class);
}
@Test // DATAMONGO-962
public void shouldIdentifyCycleForOwnerOfSameTypeAndMatchingPath() {
@Test // DATAMONGO-962, DATAMONGO-1782
public void shouldIdentifyCycle() {
MongoPersistentProperty property = createPersistentPropertyMock(entityMock, "foo");
assertThat(new Path(property, "foo.bar").cycles(property, "foo.bar.bar"), is(true));
MongoPersistentProperty foo = createPersistentPropertyMock(entityMock, "foo");
MongoPersistentProperty bar = createPersistentPropertyMock(entityMock, "bar");
Path path = Path.of(foo).append(bar).append(bar);
assertThat(path.isCycle(), is(true));
assertThat(path.toCyclePath(), is(equalTo("bar -> bar")));
assertThat(path.toString(), is(equalTo("foo -> bar -> bar")));
}
@Test // DATAMONGO-962
@SuppressWarnings("rawtypes")
public void shouldAllowMatchingPathForDifferentOwners() {
@Test // DATAMONGO-1782
public void isCycleShouldReturnFalseWhenNoCyclePresent() {
MongoPersistentProperty existing = createPersistentPropertyMock(entityMock, "foo");
MongoPersistentProperty foo = createPersistentPropertyMock(entityMock, "foo");
MongoPersistentProperty bar = createPersistentPropertyMock(entityMock, "bar");
MongoPersistentEntity entityOfDifferentType = Mockito.mock(MongoPersistentEntity.class);
when(entityOfDifferentType.getType()).thenReturn(String.class);
MongoPersistentProperty toBeVerified = createPersistentPropertyMock(entityOfDifferentType, "foo");
Path path = Path.of(foo).append(bar);
assertThat(new Path(existing, "foo.bar").cycles(toBeVerified, "foo.bar.bar"), is(false));
assertThat(path.isCycle(), is(false));
assertThat(path.toCyclePath(), is(equalTo("")));
assertThat(path.toString(), is(equalTo("foo -> bar")));
}
@Test // DATAMONGO-962
public void shouldAllowEqaulPropertiesOnDifferentPaths() {
@Test // DATAMONGO-1782
public void isCycleShouldReturnFalseCycleForNonEqualProperties() {
MongoPersistentProperty property = createPersistentPropertyMock(entityMock, "foo");
assertThat(new Path(property, "foo.bar").cycles(property, "foo2.bar.bar"), is(false));
MongoPersistentProperty foo = createPersistentPropertyMock(entityMock, "foo");
MongoPersistentProperty bar = createPersistentPropertyMock(entityMock, "bar");
MongoPersistentProperty bar2 = createPersistentPropertyMock(mock(MongoPersistentEntity.class), "bar");
assertThat(Path.of(foo).append(bar).append(bar2).isCycle(), is(false));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private MongoPersistentProperty createPersistentPropertyMock(MongoPersistentEntity owner, String fieldname) {
private static MongoPersistentProperty createPersistentPropertyMock(MongoPersistentEntity owner, String fieldname) {
MongoPersistentProperty property = Mockito.mock(MongoPersistentProperty.class);
when(property.getOwner()).thenReturn(owner);
when(property.getFieldName()).thenReturn(fieldname);
when(property.getName()).thenReturn(fieldname);
return property;
}
}

View File

@@ -1,6 +1,54 @@
Spring Data MongoDB Changelog
=============================
Changes in version 1.10.8.RELEASE (2017-10-11)
----------------------------------------------
* DATAMONGO-1784 - Add support for AggregationExpression in GroupOperation.sum.
* DATAMONGO-1782 - CyclicPropertyReferenceException on index resolution.
* DATAMONGO-1775 - Release 1.10.8 (Ingalls SR8).
Changes in version 2.0.0.RELEASE (2017-10-02)
---------------------------------------------
* DATAMONGO-1791 - Adapt to changed Spring Framework 5 documentation structure.
* DATAMONGO-1787 - Add explicit automatic module name for Java 9.
* DATAMONGO-1786 - Adapt tests to nullability enforcement in repository query methods.
* DATAMONGO-1785 - Upgrade to OpenWebBeans 2.0.1.
* DATAMONGO-1784 - Add support for AggregationExpression in GroupOperation.sum.
* DATAMONGO-1782 - CyclicPropertyReferenceException on index resolution.
* DATAMONGO-1781 - Update what's new in reference documentation.
* DATAMONGO-1779 - Query.limit(N) on empty query is not applied.
* DATAMONGO-1778 - Update.equals(…) fails to recognize an equal Update object.
* DATAMONGO-1777 - Update.toString(…) does not pretty-print modifiers.
* DATAMONGO-1776 - Release 2.0 GA (Kay).
Changes in version 2.0.0.RC3 (2017-09-11)
-----------------------------------------
* DATAMONGO-1774 - ReactiveMongoOperations#remove(Mono, java.lang.String) ends up in an infinite loop.
* DATAMONGO-1772 - Type hint not added when updating nested list elements with inheritance.
* DATAMONGO-1771 - Fix MongoDB setup for travis-ci.
* DATAMONGO-1770 - Upgrade to mongo-java-driver 3.5.0 and mongodb-driver-reactivestreams 1.6.0.
* DATAMONGO-1768 - QueryByExample FindOne : probe type.
* DATAMONGO-1765 - Duplicate elements in DBRefs list not correctly mapped.
* DATAMONGO-1762 - Introduce nullable annotations for API validation.
* DATAMONGO-1758 - Remove non existing spring-data-mongodb-log4j from benchmarks profile.
* DATAMONGO-1757 - Improve exception message if type to read doesn't match declared type on Map value.
* DATAMONGO-1756 - Aggregation project and arithmetic operation not working with nested fields.
* DATAMONGO-1754 - Release 2.0 RC3 (Kay).
* DATAMONGO-1706 - Adapt to deprecated RxJava 1 CRUD repositories.
* DATAMONGO-1631 - AbstractReactiveMongoConfiguration.mongoDbFactory conflicts with blocking MongoDbFactory.
Changes in version 1.10.7.RELEASE (2017-09-11)
----------------------------------------------
* DATAMONGO-1772 - Type hint not added when updating nested list elements with inheritance.
* DATAMONGO-1768 - QueryByExample FindOne : probe type.
* DATAMONGO-1765 - Duplicate elements in DBRefs list not correctly mapped.
* DATAMONGO-1756 - Aggregation project and arithmetic operation not working with nested fields.
* DATAMONGO-1755 - Release 1.10.7 (Ingalls SR7).
Changes in version 1.10.6.RELEASE (2017-07-26)
----------------------------------------------
* DATAMONGO-1750 - Release 1.10.6 (Ingalls SR6).

View File

@@ -1,4 +1,4 @@
Spring Data MongoDB 1.10.6
Spring Data MongoDB 1.10.8
Copyright (c) [2010-2015] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").