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 45139b23d..5212fc1d7 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 @@ -24,7 +24,6 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedFi import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection; import org.springframework.util.Assert; -import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -235,26 +234,53 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { } /** + * An {@link ProjectionOperationBuilder} that is used for SpEL expression based projections. + * * @author Thomas Darimont */ - public static class ExpressionProjectionOperationBuilder extends AbstractProjectionOperationBuilder { + public static class ExpressionProjectionOperationBuilder extends ProjectionOperationBuilder { private final Object[] params; + private final String expression; /** * Creates a new {@link ExpressionProjectionOperationBuilder} for the given value, {@link ProjectionOperation} and * parameters. * - * @param value must not be {@literal null}. + * @param expression must not be {@literal null}. * @param operation must not be {@literal null}. * @param parameters */ - public ExpressionProjectionOperationBuilder(Object value, ProjectionOperation operation, Object[] parameters) { + public ExpressionProjectionOperationBuilder(String expression, ProjectionOperation operation, Object[] parameters) { - super(value, operation); + super(expression, operation, null); + this.expression = expression; this.params = parameters.clone(); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder#project(java.lang.String, java.lang.Object[]) + */ + @Override + public ProjectionOperationBuilder project(String operation, final Object... values) { + + OperationProjection operationProjection = new OperationProjection(Fields.field(value.toString()), operation, + values) { + @Override + protected List getOperationArguments(AggregationOperationContext context) { + + List result = new ArrayList(values.length + 1); + result.add(ExpressionProjection.toMongoExpression(context, + ExpressionProjectionOperationBuilder.this.expression, ExpressionProjectionOperationBuilder.this.params)); + result.addAll(Arrays.asList(values)); + + return result; + } + }; + + return new ProjectionOperationBuilder(value, this.operation.and(operationProjection), operationProjection); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#as(java.lang.String) @@ -303,7 +329,11 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { */ @Override public DBObject toDBObject(AggregationOperationContext context) { - return new BasicDBObject(getExposedField().getName(), TRANSFORMER.transform(expression, context, params)); + return new BasicDBObject(getExposedField().getName(), toMongoExpression(context, expression, params)); + } + + protected static Object toMongoExpression(AggregationOperationContext context, String expression, Object[] params) { + return TRANSFORMER.transform(expression, context, params); } } } @@ -320,7 +350,6 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { private static final String FIELD_REFERENCE_NOT_NULL = "Field reference must not be null!"; private final String name; - private final ProjectionOperation operation; private final OperationProjection previousProjection; /** @@ -335,7 +364,23 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { super(name, operation); this.name = name; - this.operation = operation; + this.previousProjection = previousProjection; + } + + /** + * Creates a new {@link ProjectionOperationBuilder} for the field with the given value on top of the given + * {@link ProjectionOperation}. + * + * @param value + * @param operation + * @param previousProjection + */ + protected ProjectionOperationBuilder(Object value, ProjectionOperation operation, + OperationProjection previousProjection) { + + super(value, operation); + + this.name = null; this.previousProjection = previousProjection; } @@ -521,8 +566,9 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { * @return */ public ProjectionOperationBuilder project(String operation, Object... values) { - OperationProjection projectionOperation = new OperationProjection(Fields.field(name), operation, values); - return new ProjectionOperationBuilder(name, this.operation.and(projectionOperation), projectionOperation); + OperationProjection operationProjection = new OperationProjection(Fields.field(value.toString()), operation, + values); + return new ProjectionOperationBuilder(value, this.operation.and(operationProjection), operationProjection); } /** @@ -653,7 +699,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { /** * Creates a new {@link OperationProjection} for the given field. * - * @param name the name of the field to add the operation projection for, must not be {@literal null} or empty. + * @param field the name of the field to add the operation projection for, must not be {@literal null} or empty. * @param operation the actual operation key, must not be {@literal null} or empty. * @param values the values to pass into the operation, must not be {@literal null}. */ @@ -676,18 +722,15 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { @Override public DBObject toDBObject(AggregationOperationContext context) { - BasicDBList values = new BasicDBList(); - values.addAll(buildReferences(context)); + DBObject inner = new BasicDBObject("$" + operation, getOperationArguments(context)); - DBObject inner = new BasicDBObject("$" + operation, values); - - return new BasicDBObject(this.field.getName(), inner); + return new BasicDBObject(getField().getName(), inner); } - private List buildReferences(AggregationOperationContext context) { + protected List getOperationArguments(AggregationOperationContext context) { List result = new ArrayList(values.size()); - result.add(context.getReference(field.getTarget()).toString()); + result.add(context.getReference(getField().getName()).toString()); for (Object element : values) { result.add(element instanceof Field ? context.getReference((Field) element).toString() : element); @@ -696,6 +739,15 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { return result; } + /** + * Returns the field that holds the {@link OperationProjection}. + * + * @return + */ + protected Field getField() { + return field; + } + /** * Creates a new instance of this {@link OperationProjection} with the given alias. * @@ -703,7 +755,27 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { * @return */ public OperationProjection withAlias(String alias) { - return new OperationProjection(Fields.field(alias, this.field.getName()), operation, values.toArray()); + + final Field aliasedField = Fields.field(alias, this.field.getName()); + return new OperationProjection(aliasedField, operation, values.toArray()) { + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.OperationProjection#getField() + */ + @Override + protected Field getField() { + return aliasedField; + } + + @Override + protected List getOperationArguments(AggregationOperationContext context) { + + // We have to make sure that we use the arguments from the "previous" OperationProjection that we replace + // with this new instance. + + return OperationProjection.this.getOperationArguments(context); + } + }; } } @@ -735,6 +807,96 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { return new BasicDBObject(name, nestedObject); } } + + /** + * Extracts the minute from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractMinute() { + return project("minute"); + } + + /** + * Extracts the hour from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractHour() { + return project("hour"); + } + + /** + * Extracts the second from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractSecond() { + return project("second"); + } + + /** + * Extracts the millisecond from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractMillisecond() { + return project("millisecond"); + } + + /** + * Extracts the year from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractYear() { + return project("year"); + } + + /** + * Extracts the month from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractMonth() { + return project("month"); + } + + /** + * Extracts the week from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractWeek() { + return project("week"); + } + + /** + * Extracts the dayOfYear from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractDayOfYear() { + return project("dayOfYear"); + } + + /** + * Extracts the dayOfMonth from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractDayOfMonth() { + return project("dayOfMonth"); + } + + /** + * Extracts the dayOfWeek from a date expression. + * + * @return + */ + public ProjectionOperationBuilder extractDayOfWeek() { + return project("dayOfWeek"); + } } /** @@ -775,5 +937,4 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { */ public abstract DBObject toDBObject(AggregationOperationContext context); } - } 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 fbd1666f8..cc5be660f 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 @@ -31,6 +31,8 @@ import java.util.Date; import java.util.List; import java.util.Scanner; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.joda.time.LocalDateTime; import org.junit.After; import org.junit.Before; @@ -49,6 +51,7 @@ import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry; +import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.util.Version; @@ -960,6 +963,61 @@ public class AggregationTests { assertThat(result.getMappedResults(), hasSize(2)); } + /** + * @see DATAMONGO-975 + */ + @Test + public void shouldRetrieveDateTimeFragementsCorrectly() throws Exception { + + mongoTemplate.dropCollection(ObjectWithDate.class); + + DateTime dateTime = new DateTime() // + .withYear(2014) // + .withMonthOfYear(2) // + .withDayOfMonth(7) // + .withTime(3, 4, 5, 6).toDateTime(DateTimeZone.UTC).toDateTimeISO(); + + ObjectWithDate owd = new ObjectWithDate(dateTime.toDate()); + mongoTemplate.insert(owd); + + ProjectionOperation dateProjection = Aggregation.project() // + .and("dateValue").extractHour().as("hour") // + .and("dateValue").extractMinute().as("min") // + .and("dateValue").extractSecond().as("second") // + .and("dateValue").extractMillisecond().as("millis") // + .and("dateValue").extractYear().as("year") // + .and("dateValue").extractMonth().as("month") // + .and("dateValue").extractWeek().as("week") // + .and("dateValue").extractDayOfYear().as("dayOfYear") // + .and("dateValue").extractDayOfMonth().as("dayOfMonth") // + .and("dateValue").extractDayOfWeek().as("dayOfWeek") // + .andExpression("dateValue + 86400000").extractDayOfYear().as("dayOfYearPlus1Day") // + .andExpression("dateValue + 86400000").project("dayOfYear").as("dayOfYearPlus1DayManually") // + ; + + Aggregation agg = newAggregation(dateProjection); + AggregationResults result = mongoTemplate.aggregate(agg, ObjectWithDate.class, DBObject.class); + + assertThat(result.getMappedResults(), hasSize(1)); + DBObject dbo = result.getMappedResults().get(0); + + assertThat(dbo.get("hour"), is((Object) dateTime.getHourOfDay())); + assertThat(dbo.get("min"), is((Object) dateTime.getMinuteOfHour())); + assertThat(dbo.get("second"), is((Object) dateTime.getSecondOfMinute())); + assertThat(dbo.get("millis"), is((Object) dateTime.getMillisOfSecond())); + assertThat(dbo.get("year"), is((Object) dateTime.getYear())); + assertThat(dbo.get("month"), is((Object) dateTime.getMonthOfYear())); + // dateTime.getWeekOfWeekyear()) returns 6 since for MongoDB the week starts on sunday and not on monday. + assertThat(dbo.get("week"), is((Object) 5)); + assertThat(dbo.get("dayOfYear"), is((Object) dateTime.getDayOfYear())); + assertThat(dbo.get("dayOfMonth"), is((Object) dateTime.getDayOfMonth())); + + // dateTime.getDayOfWeek() + assertThat(dbo.get("dayOfWeek"), is((Object) 6)); + assertThat(dbo.get("dayOfYearPlus1Day"), is((Object) dateTime.plusDays(1).getDayOfYear())); + assertThat(dbo.get("dayOfYearPlus1DayManually"), is((Object) dateTime.plusDays(1).getDayOfYear())); + } + private void assertLikeStats(LikeStats like, String id, long count) { assertThat(like, is(notNullValue())); @@ -1095,6 +1153,7 @@ public class AggregationTests { } } + @SuppressWarnings("unused") static class Descriptors { private CarDescriptor carDescriptor; } @@ -1110,6 +1169,7 @@ public class AggregationTests { } } + @SuppressWarnings("unused") static class Entry { private String make; private String model; @@ -1139,4 +1199,13 @@ public class AggregationTests { this.timestamp = timestamp; } } + + static class ObjectWithDate { + + Date dateValue; + + public ObjectWithDate(Date dateValue) { + this.dateValue = dateValue; + } + } } 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 2c0b2fbc5..e12406e5f 100644 --- 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 @@ -20,12 +20,12 @@ import static org.junit.Assert.*; import static org.springframework.data.mongodb.util.DBObjectUtils.*; import java.util.Arrays; +import java.util.List; import org.junit.Test; import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; -import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -91,7 +91,7 @@ public class ProjectionOperationUnitTests { DBObject dbObject = operation.and("foo").plus(41).as("bar").toDBObject(Aggregation.DEFAULT_CONTEXT); DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject barClause = DBObjectTestUtils.getAsDBObject(projectClause, "bar"); - BasicDBList addClause = DBObjectTestUtils.getAsDBList(barClause, "$add"); + List addClause = (List) barClause.get("$add"); assertThat(addClause, hasSize(2)); assertThat(addClause.get(0), is((Object) "$foo")); @@ -276,6 +276,64 @@ public class ProjectionOperationUnitTests { is("{ \"$project\" : { \"grossSalesPrice\" : { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\" , 2]} , \"bar\" : \"$foo\"}}")); } + /** + * @see DATAMONGO-975 + */ + @Test + public void shouldRenderDateTimeFragmentExtractionsForSimpleFieldProjectionsCorrectly() { + + ProjectionOperation operation = Aggregation.project() // + .and("date").extractHour().as("hour") // + .and("date").extractMinute().as("min") // + .and("date").extractSecond().as("second") // + .and("date").extractMillisecond().as("millis") // + .and("date").extractYear().as("year") // + .and("date").extractMonth().as("month") // + .and("date").extractWeek().as("week") // + .and("date").extractDayOfYear().as("dayOfYear") // + .and("date").extractDayOfMonth().as("dayOfMonth") // + .and("date").extractDayOfWeek().as("dayOfWeek") // + ; + + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + assertThat(dbObject, is(notNullValue())); + + DBObject projected = exctractOperation("$project", dbObject); + + assertThat(projected.get("hour"), is((Object) new BasicDBObject("$hour", Arrays.asList("$date")))); + assertThat(projected.get("min"), is((Object) new BasicDBObject("$minute", Arrays.asList("$date")))); + assertThat(projected.get("second"), is((Object) new BasicDBObject("$second", Arrays.asList("$date")))); + assertThat(projected.get("millis"), is((Object) new BasicDBObject("$millisecond", Arrays.asList("$date")))); + assertThat(projected.get("year"), is((Object) new BasicDBObject("$year", Arrays.asList("$date")))); + assertThat(projected.get("month"), is((Object) new BasicDBObject("$month", Arrays.asList("$date")))); + assertThat(projected.get("week"), is((Object) new BasicDBObject("$week", Arrays.asList("$date")))); + assertThat(projected.get("dayOfYear"), is((Object) new BasicDBObject("$dayOfYear", Arrays.asList("$date")))); + assertThat(projected.get("dayOfMonth"), is((Object) new BasicDBObject("$dayOfMonth", Arrays.asList("$date")))); + assertThat(projected.get("dayOfWeek"), is((Object) new BasicDBObject("$dayOfWeek", Arrays.asList("$date")))); + } + + /** + * @see DATAMONGO-975 + */ + @Test + public void shouldRenderDateTimeFragmentExtractionsForExpressionProjectionsCorrectly() throws Exception { + + ProjectionOperation operation = Aggregation.project() // + .andExpression("date + 86400000") // + .extractDayOfYear() // + .as("dayOfYearPlus1Day") // + ; + + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + assertThat(dbObject, is(notNullValue())); + + DBObject projected = exctractOperation("$project", dbObject); + assertThat( + projected.get("dayOfYearPlus1Day"), + is((Object) new BasicDBObject("$dayOfYear", Arrays.asList(new BasicDBObject("$add", Arrays. asList( + "$date", 86400000)))))); + } + private static DBObject exctractOperation(String field, DBObject fromProjectClause) { return (DBObject) fromProjectClause.get(field); }