From c2436bcdfcc52a0c1a68b8b41c5356342bc43afe Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 1 Jul 2019 10:46:48 +0200 Subject: [PATCH] DATAMONGO-2312 - Add support for array projections. Original pull request: #770. --- .../core/aggregation/ProjectionOperation.java | 134 ++++++++++++++++++ .../ProjectionOperationUnitTests.java | 69 +++++++++ 2 files changed, 203 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java index 18f48b574..dadd535e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.bson.Document; @@ -177,6 +178,48 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { return new ProjectionOperation(this.projections, FieldProjection.from(fields, true)); } + /** + * Includes the current {@link ProjectionOperation} as an array with given name.
+ * If you want to specify array values directly use {@link #andArrayOf(Object...)}. + * + * @param name the target property name. + * @return new instance of {@link ProjectionOperation}. + * @since 2.2 + */ + public ProjectionOperation asArray(String name) { + + return new ProjectionOperation(Collections.emptyList(), + Collections.singletonList(new ArrayProjection(Fields.field(name), (List) this.projections))); + } + + /** + * Includes the given values ({@link Field field references}, {@link AggregationExpression expression}, plain values) + * as an array.
+ * The target property name needs to be set via {@link ArrayProjectionOperationBuilder#as(String)}. + * + * @param values must not be {@literal null}. + * @return new instance of {@link ArrayProjectionOperationBuilder}. + * @throws IllegalArgumentException if the required argument it {@literal null}. + * @since 2.2 + */ + public ArrayProjectionOperationBuilder andArrayOf(Object... values) { + + ArrayProjectionOperationBuilder builder = new ArrayProjectionOperationBuilder(this); + + for (Object value : values) { + + if (value instanceof Field) { + builder.and((Field) value); + } else if (value instanceof AggregationExpression) { + builder.and((AggregationExpression) value); + } else { + builder.and(value); + } + } + + return builder; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields() @@ -1743,4 +1786,95 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { return context.getMappedObject(projections, type); } } + + /** + * @author Christoph Strobl + * @since 2.2 + */ + public static class ArrayProjectionOperationBuilder { + + private ProjectionOperation target; + private final List projections; + + public ArrayProjectionOperationBuilder(ProjectionOperation target) { + + this.target = target; + this.projections = new ArrayList<>(); + } + + public ArrayProjectionOperationBuilder and(AggregationExpression expression) { + + this.projections.add(expression); + return this; + } + + public ArrayProjectionOperationBuilder and(Field field) { + + this.projections.add(field); + return this; + } + + public ArrayProjectionOperationBuilder and(Object value) { + + this.projections.add(value); + return this; + } + + /** + * Create the {@link ProjectionOperation} for the array property with given {@literal name}. + * + * @param name The target property name. Must not be {@literal null}. + * @return new instance of {@link ArrayProjectionOperationBuilder}. + */ + public ProjectionOperation as(String name) { + + return new ProjectionOperation(target.projections, + Collections.singletonList(new ArrayProjection(Fields.field(name), this.projections))); + } + } + + /** + * @author Christoph Strobl + * @since 2.2 + */ + static class ArrayProjection extends Projection { + + private final Field targetField; + private final List projections; + + public ArrayProjection(Field targetField, List projections) { + + super(targetField); + this.targetField = targetField; + this.projections = projections; + } + + @Override + public Document toDocument(AggregationOperationContext context) { + + return new Document(targetField.getName(), + projections.stream().map(it -> toArrayEntry(it, context)).collect(Collectors.toList())); + } + + private Object toArrayEntry(Object projection, AggregationOperationContext ctx) { + + if (projection instanceof Field) { + return ctx.getReference((Field) projection).toString(); + } + + if (projection instanceof AggregationExpression) { + return ((AggregationExpression) projection).toDocument(ctx); + } + + if (projection instanceof FieldProjection) { + return ctx.getReference(((FieldProjection) projection).getExposedField().getTarget()).toString(); + } + + if (projection instanceof Projection) { + ((Projection) projection).toDocument(ctx); + } + + return projection; + } + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java index 1faf08f26..32a1f6bc1 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java @@ -29,6 +29,7 @@ import java.util.List; import org.bson.Document; import org.junit.Test; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.mongodb.core.DocumentTestUtils; @@ -2157,6 +2158,67 @@ public class ProjectionOperationUnitTests { assertThat(projectClause).isEmpty(); } + @Test // DATAMONGO-2312 + public void simpleFieldReferenceAsArray() { + + org.bson.Document doc = Aggregation.newAggregation(project("x", "y", "someField").asArray("myArray")) + .toDocument("coll", Aggregation.DEFAULT_CONTEXT); + + assertThat(doc).isEqualTo(Document.parse( + "{\"aggregate\":\"coll\", \"pipeline\":[ { $project: { myArray: [ \"$x\", \"$y\", \"$someField\" ] } } ] }")); + } + + @Test // DATAMONGO-2312 + public void mappedFieldReferenceAsArray() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + + org.bson.Document doc = Aggregation + .newAggregation(BookWithFieldAnnotation.class, project("title", "author").asArray("myArray")) + .toDocument("coll", new TypeBasedAggregationOperationContext(BookWithFieldAnnotation.class, mappingContext, + new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)))); + + assertThat(doc).isEqualTo(Document + .parse("{\"aggregate\":\"coll\", \"pipeline\":[ { $project: { myArray: [ \"$ti_t_le\", \"$author\" ] } } ] }")); + } + + @Test // DATAMONGO-2312 + public void arrayWithNullValue() { + + Document doc = project() // + .andArrayOf(Fields.field("field-1"), null, "value").as("myArray") // + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(doc).isEqualTo(Document.parse("{ $project: { \"myArray\" : [ \"$field-1\", null, \"value\" ] } }")); + } + + @Test // DATAMONGO-2312 + public void nestedArrayField() { + + Document doc = project("_id", "value") // + .andArrayOf(Fields.field("field-1"), "plain - string", ArithmeticOperators.valueOf("field-1").sum().and(10)) + .as("myArray") // + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(doc).isEqualTo(Document.parse( + "{ $project: { \"_id\" : 1, \"value\" : 1, \"myArray\" : [ \"$field-1\", \"plain - string\", { \"$sum\" : [\"$field-1\", 10] } ] } } ] }")); + } + + @Test // DATAMONGO-2312 + public void nestedMappedFieldReferenceInArrayField() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + + Document doc = project("author") // + .andArrayOf(Fields.field("title"), "plain - string", ArithmeticOperators.valueOf("title").sum().and(10)) + .as("myArray") // + .toDocument(new TypeBasedAggregationOperationContext(BookWithFieldAnnotation.class, mappingContext, + new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)))); + + assertThat(doc).isEqualTo(Document.parse( + "{ $project: { \"author\" : 1, \"myArray\" : [ \"$ti_t_le\", \"plain - string\", { \"$sum\" : [\"$ti_t_le\", 10] } ] } } ] }")); + } + private static Document exctractOperation(String field, Document fromProjectClause) { return (Document) fromProjectClause.get(field); } @@ -2167,6 +2229,13 @@ public class ProjectionOperationUnitTests { Author author; } + @Data + static class BookWithFieldAnnotation { + + @Field("ti_t_le") String title; + Author author; + } + @Data static class BookRenamed { @Field("ti_tl_e") String title;