diff --git a/pom.xml b/pom.xml index 340e6922b4..1e2af1b7d1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2200-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index bb7d9f03cc..bd69fab575 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2200-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index b32dcba387..c1fd1add50 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2200-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b611cf01a8..5cc3d7e70b 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2200-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java index 5cf5c852cc..32f2f6e9aa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java @@ -244,6 +244,19 @@ public static ProjectionOperation project(Fields fields) { return new ProjectionOperation(fields); } + /** + * Creates a new {@link ProjectionOperation} including all top level fields of the given given {@link Class}. + * + * @param type must not be {@literal null}. + * @return new instance of {@link ProjectionOperation}. + * @since 2.2 + */ + public static ProjectionOperation project(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return new ProjectionOperation(type); + } + /** * Factory method to create a new {@link UnwindOperation} for the field with the given name. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java index a0e9bcde63..e73441958b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java @@ -15,9 +15,18 @@ */ package org.springframework.data.mongodb.core.aggregation; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + import org.bson.Document; +import org.springframework.beans.BeanUtils; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * The context for an {@link AggregationOperation}. @@ -66,4 +75,32 @@ default Document getMappedObject(Document document) { * @return */ FieldReference getReference(String name); + + /** + * Returns the {@link Fields} exposed by the type. May be a {@literal class} or an {@literal interface}. + * + * @param type must not be {@literal null}. + * @return never {@literal null}. + * @since 2.2 + */ + default Fields getFields(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + List fields = Arrays.stream(BeanUtils.getPropertyDescriptors(type)) // + .filter(it -> { // object and default methods + Method method = it.getReadMethod(); + if (method == null) { + return false; + } + if (ReflectionUtils.isObjectMethod(method)) { + return false; + } + return !method.isDefault(); + }) // + .map(PropertyDescriptor::getName) // + .collect(Collectors.toList()); + + return Fields.fields(fields.toArray(new String[0])); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java index 5bb01ffbb8..1603e2cc4a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java @@ -81,6 +81,15 @@ public FieldReference getReference(String name) { return getReference(null, name); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getFields(java.lang.Class) + */ + @Override + public Fields getFields(Class type) { + return rootContext.getFields(type); + } + /** * Returns a {@link FieldReference} to the given {@link Field} with the given {@code name}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java index 3f84da4e43..bcdce3bab6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java @@ -78,4 +78,13 @@ public FieldReference getReference(Field field) { public FieldReference getReference(String name) { return new ExpressionFieldReference(delegate.getReference(name)); } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getFields(java.lang.Class) + */ + @Override + public Fields getFields(Class type) { + return delegate.getFields(type); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java index a82673cc92..911b69f2e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java @@ -91,6 +91,15 @@ public FieldReference getReference(String name) { return delegate.getReference(name); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getFields(java.lang.Class) + */ + @Override + public Fields getFields(Class type) { + return delegate.getFields(type); + } + @SuppressWarnings("unchecked") private Document doPrefix(Document source) { 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 0527005435..3b0e94e426 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 @@ -30,6 +30,7 @@ import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * Encapsulates the aggregation framework {@code $project}-operation. @@ -73,6 +74,16 @@ public ProjectionOperation(Fields fields) { this(NONE, ProjectionOperationBuilder.FieldProjection.from(fields)); } + /** + * Creates a new {@link ProjectionOperation} including all top level fields of the given {@link Class type}. + * + * @param type must not be {@literal null}. + * @since 2.2 + */ + public ProjectionOperation(Class type) { + this(NONE, Collections.singletonList(new TypeProjection(type))); + } + /** * Copy constructor to allow building up {@link ProjectionOperation} instances from already existing * {@link Projection}s. @@ -1700,4 +1711,36 @@ public Document toDocument(AggregationOperationContext context) { return new Document(field.getName(), expression.toDocument(context)); } } + + /** + * A {@link Projection} including all top level fields of the given target type mapped to include potentially + * deviating field names. + * + * @since 2.2 + * @author Christoph Strobl + */ + static class TypeProjection extends Projection { + + private final Class type; + + TypeProjection(Class type) { + + super(Fields.field(type.getSimpleName())); + this.type = type; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + + Document projections = new Document(); + + Fields fields = context.getFields(type); + fields.asList().forEach(it -> projections.append(it.getName(), 1)); + return context.getMappedObject(projections, type); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java index 06c125c530..ce116e40c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java @@ -17,9 +17,12 @@ import static org.springframework.data.mongodb.core.aggregation.Fields.*; +import java.util.ArrayList; +import java.util.List; + import org.bson.Document; import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; @@ -101,6 +104,23 @@ public FieldReference getReference(String name) { return getReferenceFor(field(name)); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getFields(java.lang.Class) + */ + @Override + public Fields getFields(Class type) { + + MongoPersistentEntity entity = mappingContext.getPersistentEntity(type); + if (entity == null) { + return AggregationOperationContext.super.getFields(type); + } + + List fields = new ArrayList<>(); + entity.doWithProperties((SimplePropertyHandler) it -> fields.add(it.getName())); + return Fields.fields(fields.toArray(new String[fields.size()])); + } + private FieldReference getReferenceFor(Field field) { PersistentPropertyPath propertyPath = mappingContext 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 92c26eb288..5e1c7354ea 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 @@ -41,6 +41,12 @@ import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; import org.springframework.data.mongodb.core.aggregation.StringOperators.Concat; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; /** * Unit tests for {@link ProjectionOperation}. @@ -61,7 +67,7 @@ public class ProjectionOperationUnitTests { @Test(expected = IllegalArgumentException.class) // DATAMONGO-586 public void rejectsNullFields() { - new ProjectionOperation(null); + new ProjectionOperation((Fields) null); } @Test // DATAMONGO-586 @@ -2099,6 +2105,60 @@ public void shouldRenderDateFromStringWithFormat() { "{ $project : { newDate: { $dateFromString: { dateString : \"2017-02-08T12:10:40.787\", format : \"dd/mm/yyyy\" } } } }")); } + @Test // DATAMONGO-2200 + public void typeProjectionShouldIncludeTopLevelFieldsOfType() { + + ProjectionOperation operation = Aggregation.project(Book.class); + + Document document = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + Document projectClause = DocumentTestUtils.getAsDocument(document, PROJECT); + + assertThat(projectClause) // + .hasSize(2) // + .containsEntry("title", 1) // + .containsEntry("author", 1); + } + + @Test // DATAMONGO-2200 + public void typeProjectionShouldMapFieldNames() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); + + Document document = Aggregation.project(BookRenamed.class) + .toDocument(new TypeBasedAggregationOperationContext(Book.class, mappingContext, new QueryMapper(converter))); + Document projectClause = DocumentTestUtils.getAsDocument(document, PROJECT); + + assertThat(projectClause) // + .hasSize(2) // + .containsEntry("ti_tl_e", 1) // + .containsEntry("author", 1); + } + + @Test // DATAMONGO-2200 + public void typeProjectionShouldIncludeInterfaceProjectionValues() { + + ProjectionOperation operation = Aggregation.project(ProjectionInterface.class); + + Document document = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + Document projectClause = DocumentTestUtils.getAsDocument(document, PROJECT); + + assertThat(projectClause) // + .hasSize(1) // + .containsEntry("title", 1); + } + + @Test // DATAMONGO-2200 + public void typeProjectionShouldBeEmptyIfNoPropertiesFound() { + + ProjectionOperation operation = Aggregation.project(EmptyType.class); + + Document document = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + Document projectClause = DocumentTestUtils.getAsDocument(document, PROJECT); + + assertThat(projectClause).isEmpty(); + } + private static Document exctractOperation(String field, Document fromProjectClause) { return (Document) fromProjectClause.get(field); } @@ -2109,6 +2169,12 @@ static class Book { Author author; } + @Data + static class BookRenamed { + @Field("ti_tl_e") String title; + Author author; + } + @Data static class Author { String first; @@ -2116,4 +2182,12 @@ static class Author { String middle; } + interface ProjectionInterface { + String getTitle(); + } + + static class EmptyType { + + } + }