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 {
+
+ }
+
}