/* * 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. * 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.aggregation; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.DocumentTestUtils.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.core.aggregation.Fields.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import java.util.Arrays; import java.util.List; import org.bson.Document; import org.bson.types.ObjectId; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; import org.springframework.data.convert.CustomConversions; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Criteria; /** * Unit tests for {@link TypeBasedAggregationOperationContext}. * * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class TypeBasedAggregationOperationContextUnitTests { MongoMappingContext context; MappingMongoConverter converter; QueryMapper mapper; @Mock DbRefResolver dbRefResolver; @Before public void setUp() { this.context = new MongoMappingContext(); this.converter = new MappingMongoConverter(dbRefResolver, context); this.mapper = new QueryMapper(converter); } @Test public void findsSimpleReference() { assertThat(getContext(Foo.class).getReference("bar"), is(notNullValue())); } @Test(expected = MappingException.class) public void rejectsInvalidFieldReference() { getContext(Foo.class).getReference("foo"); } @Test // DATAMONGO-741 public void returnsReferencesToNestedFieldsCorrectly() { AggregationOperationContext context = getContext(Foo.class); Field field = field("bar.name"); assertThat(context.getReference("bar.name"), is(notNullValue())); assertThat(context.getReference(field), is(notNullValue())); assertThat(context.getReference(field), is(context.getReference("bar.name"))); } @Test // DATAMONGO-806 public void aliasesIdFieldCorrectly() { AggregationOperationContext context = getContext(Foo.class); assertThat(context.getReference("id"), is((FieldReference) new DirectFieldReference(new ExposedField(field("id", "_id"), true)))); } @Test // DATAMONGO-912 public void shouldUseCustomConversionIfPresentAndConversionIsRequiredInFirstStage() { CustomConversions customConversions = customAgeConversions(); converter.setCustomConversions(customConversions); customConversions.registerConvertersIn((GenericConversionService) converter.getConversionService()); AggregationOperationContext context = getContext(FooPerson.class); MatchOperation matchStage = match(Criteria.where("age").is(new Age(10))); ProjectionOperation projectStage = project("age", "name"); org.bson.Document agg = newAggregation(matchStage, projectStage).toDocument("test", context); org.bson.Document age = getValue( (org.bson.Document) getValue(getPipelineElementFromAggregationAt(agg, 0), "$match"), "age"); assertThat(age, is(new org.bson.Document("v", 10))); } @Test // DATAMONGO-912 public void shouldUseCustomConversionIfPresentAndConversionIsRequiredInLaterStage() { CustomConversions customConversions = customAgeConversions(); converter.setCustomConversions(customConversions); customConversions.registerConvertersIn((GenericConversionService) converter.getConversionService()); AggregationOperationContext context = getContext(FooPerson.class); MatchOperation matchStage = match(Criteria.where("age").is(new Age(10))); ProjectionOperation projectStage = project("age", "name"); org.bson.Document agg = newAggregation(projectStage, matchStage).toDocument("test", context); org.bson.Document age = getValue( (org.bson.Document) getValue(getPipelineElementFromAggregationAt(agg, 1), "$match"), "age"); assertThat(age, is(new org.bson.Document("v", 10))); } @Test // DATAMONGO-960 public void rendersAggregationOptionsInTypedAggregationContextCorrectly() { AggregationOperationContext context = getContext(FooPerson.class); TypedAggregation<FooPerson> agg = newAggregation(FooPerson.class, project("name", "age")) // .withOptions( newAggregationOptions().allowDiskUse(true).explain(true).cursor(new org.bson.Document("foo", 1)).build()); org.bson.Document document = agg.toDocument("person", context); org.bson.Document projection = getPipelineElementFromAggregationAt(document, 0); assertThat(projection.containsKey("$project"), is(true)); assertThat(projection.get("$project"), is((Object) new org.bson.Document("name", 1).append("age", 1))); assertThat(document.get("allowDiskUse"), is((Object) true)); assertThat(document.get("explain"), is((Object) true)); assertThat(document.get("cursor"), is((Object) new org.bson.Document("foo", 1))); } @Test // DATAMONGO-1585 public void rendersSortOfProjectedFieldCorrectly() { TypeBasedAggregationOperationContext context = getContext(MeterData.class); TypedAggregation<MeterData> agg = newAggregation(MeterData.class, project().and("counterName").as("counter"), // sort(Direction.ASC, "counter")); Document dbo = agg.toDocument("meterData", context); Document sort = getPipelineElementFromAggregationAt(dbo, 1); Document definition = (Document) sort.get("$sort"); assertThat(definition.get("counter"), is(equalTo((Object) 1))); } @Test // DATAMONGO-1586 public void rendersFieldAliasingProjectionCorrectly() { AggregationOperationContext context = getContext(FooPerson.class); TypedAggregation<FooPerson> agg = newAggregation(FooPerson.class, project() // .and("name").as("person_name") // .and("age.value").as("age")); Document dbo = agg.toDocument("person", context); Document projection = getPipelineElementFromAggregationAt(dbo, 0); assertThat(getAsDocument(projection, "$project"), isBsonObject() // .containing("person_name", "$name") // .containing("age", "$age.value")); } @Test // DATAMONGO-1133 public void shouldHonorAliasedFieldsInGroupExpressions() { TypeBasedAggregationOperationContext context = getContext(MeterData.class); TypedAggregation<MeterData> agg = newAggregation(MeterData.class, group("counterName").sum("counterVolume").as("totalCounterVolume")); org.bson.Document document = agg.toDocument("meterData", context); org.bson.Document group = getPipelineElementFromAggregationAt(document, 0); org.bson.Document definition = (org.bson.Document) group.get("$group"); assertThat(definition.get("_id"), is(equalTo((Object) "$counter_name"))); } @Test // DATAMONGO-1326, DATAMONGO-1585 public void lookupShouldInheritFieldsFromInheritingAggregationOperation() { TypeBasedAggregationOperationContext context = getContext(MeterData.class); TypedAggregation<MeterData> agg = newAggregation(MeterData.class, lookup("OtherCollection", "resourceId", "otherId", "lookup"), // sort(Direction.ASC, "resourceId", "counterName")); org.bson.Document document = agg.toDocument("meterData", context); org.bson.Document sort = getPipelineElementFromAggregationAt(document, 1); org.bson.Document definition = (org.bson.Document) sort.get("$sort"); assertThat(definition.get("resourceId"), is(equalTo((Object) 1))); assertThat(definition.get("counter_name"), is(equalTo((Object) 1))); } @Test // DATAMONGO-1326 public void groupLookupShouldInheritFieldsFromPreviousAggregationOperation() { TypeBasedAggregationOperationContext context = getContext(MeterData.class); TypedAggregation<MeterData> agg = newAggregation(MeterData.class, group().min("resourceId").as("foreignKey"), lookup("OtherCollection", "foreignKey", "otherId", "lookup"), sort(Direction.ASC, "foreignKey")); org.bson.Document document = agg.toDocument("meterData", context); org.bson.Document sort = getPipelineElementFromAggregationAt(document, 2); org.bson.Document definition = (org.bson.Document) sort.get("$sort"); assertThat(definition.get("foreignKey"), is(equalTo((Object) 1))); } @Test // DATAMONGO-1326 public void lookupGroupAggregationShouldUseCorrectGroupField() { TypeBasedAggregationOperationContext context = getContext(MeterData.class); TypedAggregation<MeterData> agg = newAggregation(MeterData.class, lookup("OtherCollection", "resourceId", "otherId", "lookup"), group().min("lookup.otherkey").as("something_totally_different")); org.bson.Document document = agg.toDocument("meterData", context); org.bson.Document group = getPipelineElementFromAggregationAt(document, 1); org.bson.Document definition = (org.bson.Document) group.get("$group"); org.bson.Document field = (org.bson.Document) definition.get("something_totally_different"); assertThat(field.get("$min"), is(equalTo((Object) "$lookup.otherkey"))); } @Test // DATAMONGO-1326 public void lookupGroupAggregationShouldOverwriteExposedFields() { TypeBasedAggregationOperationContext context = getContext(MeterData.class); TypedAggregation<MeterData> agg = newAggregation(MeterData.class, lookup("OtherCollection", "resourceId", "otherId", "lookup"), group().min("lookup.otherkey").as("something_totally_different"), sort(Direction.ASC, "something_totally_different")); org.bson.Document document = agg.toDocument("meterData", context); org.bson.Document sort = getPipelineElementFromAggregationAt(document, 2); org.bson.Document definition = (org.bson.Document) sort.get("$sort"); assertThat(definition.get("something_totally_different"), is(equalTo((Object) 1))); } @Test(expected = IllegalArgumentException.class) // DATAMONGO-1326 public void lookupGroupAggregationShouldFailInvalidFieldReference() { TypeBasedAggregationOperationContext context = getContext(MeterData.class); TypedAggregation<MeterData> agg = newAggregation(MeterData.class, lookup("OtherCollection", "resourceId", "otherId", "lookup"), group().min("lookup.otherkey").as("something_totally_different"), sort(Direction.ASC, "resourceId")); agg.toDocument("meterData", context); } @Test // DATAMONGO-861 public void rendersAggregationConditionalInTypedAggregationContextCorrectly() { AggregationOperationContext context = getContext(FooPerson.class); TypedAggregation<FooPerson> agg = newAggregation(FooPerson.class, project("name") // .and("age") // .applyCondition( ConditionalOperators.when(Criteria.where("age.value").lt(10)).then(new Age(0)).otherwiseValueOf("age")) // ); Document document = agg.toDocument("person", context); Document projection = getPipelineElementFromAggregationAt(document, 0); assertThat(projection.containsKey("$project"), is(true)); Document project = getValue(projection, "$project"); Document age = getValue(project, "age"); assertThat(getValue(age, "$cond"), isBsonObject().containing("then.value", 0)); assertThat(getValue(age, "$cond"), isBsonObject().containing("then._class", Age.class.getName())); assertThat(getValue(age, "$cond"), isBsonObject().containing("else", "$age")); } /** * .AggregationUnitTests */ @Test // DATAMONGO-861, DATAMONGO-1542 public void rendersAggregationIfNullInTypedAggregationContextCorrectly() { AggregationOperationContext context = getContext(FooPerson.class); TypedAggregation<FooPerson> agg = newAggregation(FooPerson.class, project("name") // .and("age") // .applyCondition(ConditionalOperators.ifNull("age").then(new Age(0))) // ); Document document = agg.toDocument("person", context); Document projection = getPipelineElementFromAggregationAt(document, 0); assertThat(projection.containsKey("$project"), is(true)); Document project = getValue(projection, "$project"); Document age = getValue(project, "age"); assertThat(age, is(Document.parse( "{ $ifNull: [ \"$age\", { \"_class\":\"org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContextUnitTests$Age\", \"value\": 0} ] }"))); assertThat(age, isBsonObject().containing("$ifNull.[0]", "$age")); assertThat(age, isBsonObject().containing("$ifNull.[1].value", 0)); assertThat(age, isBsonObject().containing("$ifNull.[1]._class", Age.class.getName())); } @org.springframework.data.mongodb.core.mapping.Document(collection = "person") public static class FooPerson { final ObjectId id; final String name; final Age age; @PersistenceConstructor FooPerson(ObjectId id, String name, Age age) { this.id = id; this.name = name; this.age = age; } } public static class Age { final int value; Age(int value) { this.value = value; } } public CustomConversions customAgeConversions() { return new MongoCustomConversions(Arrays.asList(ageWriteConverter(), ageReadConverter())); } Converter<Age, org.bson.Document> ageWriteConverter() { return new Converter<Age, org.bson.Document>() { @Override public org.bson.Document convert(Age age) { return new org.bson.Document("v", age.value); } }; } Converter<org.bson.Document, Age> ageReadConverter() { return new Converter<org.bson.Document, Age>() { @Override public Age convert(org.bson.Document document) { return new Age(((Integer) document.get("v"))); } }; } @SuppressWarnings("unchecked") static org.bson.Document getPipelineElementFromAggregationAt(org.bson.Document agg, int index) { return ((List<org.bson.Document>) agg.get("pipeline")).get(index); } @SuppressWarnings("unchecked") static <T> T getValue(org.bson.Document o, String key) { return (T) o.get(key); } private TypeBasedAggregationOperationContext getContext(Class<?> type) { return new TypeBasedAggregationOperationContext(type, context, mapper); } static class Foo { @Id String id; Bar bar; } static class Bar { String name; } }