diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index acd30890f..2c016553e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.bson.Document; import org.bson.conversions.Bson; @@ -204,7 +205,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @param databaseName must not be {@literal null} or empty. */ public MongoTemplate(MongoClient mongoClient, String databaseName) { - this(new SimpleMongoDbFactory(mongoClient, databaseName), null); + this(new SimpleMongoDbFactory(mongoClient, databaseName), null); } /** @@ -2576,6 +2577,73 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return fields; } + /** + * Prepare the {@link AggregationOperationContext} for a given aggregation by either returning the context itself it + * is not {@literal null}, create a {@link TypeBasedAggregationOperationContext} if the aggregation contains type + * information (is a {@link TypedAggregation}) or use the {@link Aggregation#DEFAULT_CONTEXT}. + * + * @param aggregation must not be {@literal null}. + * @param context can be {@literal null}. + * @return the root {@link AggregationOperationContext} to use. + */ + private AggregationOperationContext prepareAggregationContext(Aggregation aggregation, + @Nullable AggregationOperationContext context) { + + if (context != null) { + return context; + } + + if (aggregation instanceof TypedAggregation) { + return new TypeBasedAggregationOperationContext(((TypedAggregation) aggregation).getInputType(), mappingContext, + queryMapper); + } + + return Aggregation.DEFAULT_CONTEXT; + } + + /** + * Extract and map the aggregation pipeline. + * + * @param aggregation + * @param context + * @return + */ + private Document aggregationToPipeline(String inputCollectionName, Aggregation aggregation, AggregationOperationContext context) { + + if (!ObjectUtils.nullSafeEquals(context, Aggregation.DEFAULT_CONTEXT)) { + return aggregation.toDocument(inputCollectionName, context); + } + + return queryMapper.getMappedObject(aggregation.toDocument(inputCollectionName, context), Optional.empty()); + } + + /** + * Extract the command and map the aggregation pipeline. + * + * @param aggregation + * @param context + * @return + */ + private Document aggregationToCommand(String collection, Aggregation aggregation, + AggregationOperationContext context) { + + Document command = aggregation.toDocument(collection, context); + + if (!ObjectUtils.nullSafeEquals(context, Aggregation.DEFAULT_CONTEXT)) { + return command; + } + + command.put("pipeline", mapAggregationPipeline(command.get("pipeline", List.class))); + + return command; + } + + private List mapAggregationPipeline(List pipeline) { + + return pipeline.stream().map(val -> queryMapper.getMappedObject(val, Optional.empty())) + .collect(Collectors.toList()); + } + /** * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original * exception if the conversation failed. Thus allows safe re-throwing of the return value. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index d47b84593..bf2b96c27 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -733,8 +733,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.hasText(collectionName, "Collection name must not be null or empty!"); Assert.notNull(outputType, "Output type must not be null!"); - AggregationOperationContext rootContext = context == null ? Aggregation.DEFAULT_CONTEXT : context; - Document command = aggregation.toDocument(collectionName, rootContext); + AggregationOperationContext rootContext = prepareAggregationContext(aggregation, context); + Document command = aggregationToPipeline(collectionName, aggregation, rootContext); AggregationOptions options = AggregationOptions.fromDocument(command); Assert.isTrue(!options.isExplain(), "Cannot use explain option with streaming!"); @@ -752,8 +752,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private Flux aggregateAndMap(MongoCollection collection, List pipeline, AggregationOptions options, ReadDocumentCallback readCallback) { - AggregatePublisher cursor = collection.aggregate(pipeline).allowDiskUse(options.isAllowDiskUse()) - .useCursor(true); + AggregatePublisher cursor = collection.aggregate(pipeline) + .allowDiskUse(options.isAllowDiskUse()); if (options.getCollation().isPresent()) { cursor = cursor.collation(options.getCollation().map(Collation::toMongoCollation).get()); @@ -2197,6 +2197,46 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati }; } + /** + * Prepare the {@link AggregationOperationContext} for a given aggregation by either returning the context itself it + * is not {@literal null}, create a {@link TypeBasedAggregationOperationContext} if the aggregation contains type + * information (is a {@link TypedAggregation}) or use the {@link Aggregation#DEFAULT_CONTEXT}. + * + * @param aggregation must not be {@literal null}. + * @param context can be {@literal null}. + * @return the root {@link AggregationOperationContext} to use. + */ + private AggregationOperationContext prepareAggregationContext(Aggregation aggregation, + @Nullable AggregationOperationContext context) { + + if (context != null) { + return context; + } + + if (aggregation instanceof TypedAggregation) { + return new TypeBasedAggregationOperationContext(((TypedAggregation) aggregation).getInputType(), mappingContext, + queryMapper); + } + + return Aggregation.DEFAULT_CONTEXT; + } + + /** + * Extract and map the aggregation pipeline. + * + * @param aggregation + * @param context + * @return + */ + private Document aggregationToPipeline(String inputCollectionName, Aggregation aggregation, AggregationOperationContext context) { + + if (!ObjectUtils.nullSafeEquals(context, Aggregation.DEFAULT_CONTEXT)) { + return aggregation.toDocument(inputCollectionName, context); + } + + return queryMapper.getMappedObject(aggregation.toDocument(inputCollectionName, context), Optional.empty()); + } + /** * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original * exception if the conversation failed. Thus allows safe re-throwing of the return value. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java index 7fbf8b464..2a8d860dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java @@ -41,7 +41,7 @@ class AggregationOperationRenderer { * {@link Document} representation. * * @param operations must not be {@literal null}. - * @param context must not be {@literal null}. + * @param rootContext must not be {@literal null}. * @return the {@link List} of {@link Document}. */ static List toDocument(List operations, AggregationOperationContext rootContext) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java index 1f454e430..d599b046b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java @@ -660,10 +660,10 @@ public class ReactiveMongoTemplateTests { @Test // DATAMONGO-1444 public void geoNear() { - List venues = Arrays.asList(new Venue("Penn Station", -73.99408, 40.75057), // - new Venue("10gen Office", -73.99171, 40.738868), // - new Venue("Flatiron Building", -73.988135, 40.741404), // - new Venue("Maplewood, NJ", -74.2713, 40.73137)); + List venues = Arrays.asList(TestEntities.geolocation().pennStation(), // + TestEntities.geolocation().tenGenOffice(), // + TestEntities.geolocation().flatironBuilding(), // + TestEntities.geolocation().maplewoodNJ()); StepVerifier.create(template.insertAll(venues)).expectNextCount(4).verifyComplete(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TestEntities.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TestEntities.java new file mode 100644 index 000000000..77933cae3 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TestEntities.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * A simple collection of grouped test entities used throughout the test suite. + * + * @author Christoph Strobl + */ +public class TestEntities { + + private static final GeoEntities GEO = new GeoEntities(); + + public static GeoEntities geolocation() { + return GEO; + } + + public static class GeoEntities { + + /** + *
+		 * X: -73.99408
+		 * Y: 40.75057
+		 * 
+ * + * @return new {@link Venue} + */ + public Venue pennStation() { + return new Venue("Penn Station", -73.99408, 40.75057); + } + + /** + *
+		 * X: -73.99171
+		 * Y: 40.738868
+		 * 
+ * + * @return new {@link Venue} + */ + + public Venue tenGenOffice() { + return new Venue("10gen Office", -73.99171, 40.738868); + } + + /** + *
+		 * X: -73.988135
+		 * Y: 40.741404
+		 * 
+ * + * @return new {@link Venue} + */ + public Venue flatironBuilding() { + return new Venue("Flatiron Building", -73.988135, 40.741404); + } + + /** + *
+		 * X: -74.2713
+		 * Y: 40.73137
+		 * 
+ * + * @return new {@link Venue} + */ + public Venue maplewoodNJ() { + return new Venue("Maplewood, NJ", -74.2713, 40.73137); + } + + public List newYork() { + + List venues = new ArrayList<>(); + + venues.add(pennStation()); + venues.add(tenGenOffice()); + venues.add(flatironBuilding()); + venues.add(new Venue("Players Club", -73.997812, 40.739128)); + venues.add(new Venue("City Bakery ", -73.992491, 40.738673)); + venues.add(new Venue("Splash Bar", -73.992491, 40.738673)); + venues.add(new Venue("Momofuku Milk Bar", -73.985839, 40.731698)); + venues.add(new Venue("Shake Shack", -73.98820, 40.74164)); + venues.add(new Venue("Penn Station", -73.99408, 40.75057)); + venues.add(new Venue("Empire State Building", -73.98602, 40.74894)); + venues.add(new Venue("Ulaanbaatar, Mongolia", 106.9154, 47.9245)); + venues.add(maplewoodNJ()); + + return venues; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 11b39e7dd..e3bfd9492 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -53,10 +53,13 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.geo.Box; import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; import org.springframework.data.mapping.MappingException; import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.TestEntities; import org.springframework.data.mongodb.core.Venue; import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry; import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.Granularities; @@ -143,6 +146,8 @@ public class AggregationTests { mongoTemplate.dropCollection(Sales2.class); mongoTemplate.dropCollection(Employee.class); mongoTemplate.dropCollection(Art.class); + mongoTemplate.dropCollection("personQueryTemp"); + mongoTemplate.dropCollection(Venue.class); } /** @@ -1484,9 +1489,8 @@ public class AggregationTests { @Test // DATAMONGO-1127 public void shouldSupportGeoNearQueriesForAggregationWithDistanceField() { - mongoTemplate.insert(new Venue("Penn Station", -73.99408, 40.75057)); - mongoTemplate.insert(new Venue("10gen Office", -73.99171, 40.738868)); - mongoTemplate.insert(new Venue("Flatiron Building", -73.988135, 40.741404)); + mongoTemplate.insertAll(Arrays.asList(TestEntities.geolocation().pennStation(), + TestEntities.geolocation().tenGenOffice(), TestEntities.geolocation().flatironBuilding())); mongoTemplate.indexOps(Venue.class).ensureIndex(new GeospatialIndex("location")); @@ -1898,6 +1902,36 @@ public class AggregationTests { assertThat(categorizeByYear, hasSize(3)); } + @Test // DATAMONGO-1986 + public void runMatchOperationCriteriaThroughQueryMapperForTypedAggregation() { + + mongoTemplate.insertAll(TestEntities.geolocation().newYork()); + + Aggregation aggregation = newAggregation(Venue.class, + match(Criteria.where("location") + .within(new Box(new Point(-73.99756, 40.73083), new Point(-73.988135, 40.741404)))), + project("id", "location", "name")); + + AggregationResults groupResults = mongoTemplate.aggregate(aggregation, "newyork", Document.class); + + assertThat(groupResults.getMappedResults().size(), is(4)); + } + + @Test // DATAMONGO-1986 + public void runMatchOperationCriteriaThroughQueryMapperForUntypedAggregation() { + + mongoTemplate.insertAll(TestEntities.geolocation().newYork()); + + Aggregation aggregation = newAggregation( + match(Criteria.where("location") + .within(new Box(new Point(-73.99756, 40.73083), new Point(-73.988135, 40.741404)))), + project("id", "location", "name")); + + AggregationResults groupResults = mongoTemplate.aggregate(aggregation, "newyork", Document.class); + + assertThat(groupResults.getMappedResults().size(), is(4)); + } + private void createUsersWithReferencedPersons() { mongoTemplate.dropCollection(User.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java index af4834aa2..9619c82ed 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java @@ -29,7 +29,12 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.geo.Box; +import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.TestEntities; +import org.springframework.data.mongodb.core.Venue; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -65,7 +70,8 @@ public class ReactiveAggregationTests { .create(reactiveMongoTemplate.dropCollection(INPUT_COLLECTION) // .then(reactiveMongoTemplate.dropCollection(OUTPUT_COLLECTION)) // .then(reactiveMongoTemplate.dropCollection(Product.class)) // - .then(reactiveMongoTemplate.dropCollection(City.class))) // + .then(reactiveMongoTemplate.dropCollection(City.class)) // + .then(reactiveMongoTemplate.dropCollection(Venue.class))) // .verifyComplete(); } @@ -128,4 +134,34 @@ public class ReactiveAggregationTests { StepVerifier.create(reactiveMongoTemplate.find(new Query(), City.class, OUTPUT_COLLECTION)).expectNextCount(4) .verifyComplete(); } + + @Test // DATAMONGO-1986 + public void runMatchOperationCriteriaThroughQueryMapperForTypedAggregation() { + + reactiveMongoTemplate.insertAll(TestEntities.geolocation().newYork()).as(StepVerifier::create).expectNextCount(12) + .verifyComplete(); + + Aggregation aggregation = newAggregation(Venue.class, + match(Criteria.where("location") + .within(new Box(new Point(-73.99756, 40.73083), new Point(-73.988135, 40.741404)))), + project("id", "location", "name")); + + reactiveMongoTemplate.aggregate(aggregation, "newyork", Document.class).as(StepVerifier::create).expectNextCount(4) + .verifyComplete(); + } + + @Test // DATAMONGO-1986 + public void runMatchOperationCriteriaThroughQueryMapperForUntypedAggregation() { + + reactiveMongoTemplate.insertAll(TestEntities.geolocation().newYork()).as(StepVerifier::create).expectNextCount(12) + .verifyComplete(); + + Aggregation aggregation = newAggregation( + match(Criteria.where("location") + .within(new Box(new Point(-73.99756, 40.73083), new Point(-73.988135, 40.741404)))), + project("id", "location", "name")); + + reactiveMongoTemplate.aggregate(aggregation, "newyork", Document.class).as(StepVerifier::create).expectNextCount(4) + .verifyComplete(); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationUnitTests.java index 4dce1d056..29b50c59d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationUnitTests.java @@ -62,7 +62,6 @@ public class ReactiveAggregationUnitTests { when(db.getCollection(INPUT_COLLECTION)).thenReturn(collection); when(collection.aggregate(any())).thenReturn(publisher); when(publisher.allowDiskUse(any())).thenReturn(publisher); - when(publisher.useCursor(any())).thenReturn(publisher); when(publisher.collation(any())).thenReturn(publisher); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/AbstractGeoSpatialTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/AbstractGeoSpatialTests.java index 9f1621c25..441602f92 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/AbstractGeoSpatialTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/AbstractGeoSpatialTests.java @@ -38,13 +38,13 @@ import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; import org.springframework.data.mongodb.config.AbstractMongoConfiguration; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.TestEntities; import org.springframework.data.mongodb.core.Venue; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import com.mongodb.Mongo; import com.mongodb.MongoClient; import com.mongodb.WriteConcern; @@ -103,19 +103,7 @@ public abstract class AbstractGeoSpatialTests { } protected void addVenues() { - - template.insert(new Venue("Penn Station", -73.99408, 40.75057)); - template.insert(new Venue("10gen Office", -73.99171, 40.738868)); - template.insert(new Venue("Flatiron Building", -73.988135, 40.741404)); - template.insert(new Venue("Players Club", -73.997812, 40.739128)); - template.insert(new Venue("City Bakery ", -73.992491, 40.738673)); - template.insert(new Venue("Splash Bar", -73.992491, 40.738673)); - template.insert(new Venue("Momofuku Milk Bar", -73.985839, 40.731698)); - template.insert(new Venue("Shake Shack", -73.98820, 40.74164)); - template.insert(new Venue("Penn Station", -73.99408, 40.75057)); - template.insert(new Venue("Empire State Building", -73.98602, 40.74894)); - template.insert(new Venue("Ulaanbaatar, Mongolia", 106.9154, 47.9245)); - template.insert(new Venue("Maplewood, NJ", -74.2713, 40.73137)); + template.insertAll(TestEntities.geolocation().newYork()); } @Test