Browse Source

DATAMONGO-975 - Add support for extracting date/time components from a field projection.

We added some extract-methods to ProjectionOperationBuilder to be able to extract date / time components from projected fields.

Original pull request: #204.
pull/204/merge
Thomas Darimont 12 years ago committed by Oliver Gierke
parent
commit
6616d6788c
  1. 201
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java
  2. 69
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
  3. 62
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java

201
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.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
import com.mongodb.DBObject; 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 * @author Thomas Darimont
*/ */
public static class ExpressionProjectionOperationBuilder extends AbstractProjectionOperationBuilder { public static class ExpressionProjectionOperationBuilder extends ProjectionOperationBuilder {
private final Object[] params; private final Object[] params;
private final String expression;
/** /**
* Creates a new {@link ExpressionProjectionOperationBuilder} for the given value, {@link ProjectionOperation} and * Creates a new {@link ExpressionProjectionOperationBuilder} for the given value, {@link ProjectionOperation} and
* parameters. * parameters.
* *
* @param value must not be {@literal null}. * @param expression must not be {@literal null}.
* @param operation must not be {@literal null}. * @param operation must not be {@literal null}.
* @param parameters * @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(); 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<Object> getOperationArguments(AggregationOperationContext context) {
List<Object> result = new ArrayList<Object>(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) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#as(java.lang.String) * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#as(java.lang.String)
@ -303,7 +329,11 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
*/ */
@Override @Override
public DBObject toDBObject(AggregationOperationContext context) { 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 static final String FIELD_REFERENCE_NOT_NULL = "Field reference must not be null!";
private final String name; private final String name;
private final ProjectionOperation operation;
private final OperationProjection previousProjection; private final OperationProjection previousProjection;
/** /**
@ -335,7 +364,23 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
super(name, operation); super(name, operation);
this.name = name; 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; this.previousProjection = previousProjection;
} }
@ -521,8 +566,9 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @return * @return
*/ */
public ProjectionOperationBuilder project(String operation, Object... values) { public ProjectionOperationBuilder project(String operation, Object... values) {
OperationProjection projectionOperation = new OperationProjection(Fields.field(name), operation, values); OperationProjection operationProjection = new OperationProjection(Fields.field(value.toString()), operation,
return new ProjectionOperationBuilder(name, this.operation.and(projectionOperation), projectionOperation); 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. * 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 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}. * @param values the values to pass into the operation, must not be {@literal null}.
*/ */
@ -676,18 +722,15 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
@Override @Override
public DBObject toDBObject(AggregationOperationContext context) { public DBObject toDBObject(AggregationOperationContext context) {
BasicDBList values = new BasicDBList(); DBObject inner = new BasicDBObject("$" + operation, getOperationArguments(context));
values.addAll(buildReferences(context));
DBObject inner = new BasicDBObject("$" + operation, values); return new BasicDBObject(getField().getName(), inner);
return new BasicDBObject(this.field.getName(), inner);
} }
private List<Object> buildReferences(AggregationOperationContext context) { protected List<Object> getOperationArguments(AggregationOperationContext context) {
List<Object> result = new ArrayList<Object>(values.size()); List<Object> result = new ArrayList<Object>(values.size());
result.add(context.getReference(field.getTarget()).toString()); result.add(context.getReference(getField().getName()).toString());
for (Object element : values) { for (Object element : values) {
result.add(element instanceof Field ? context.getReference((Field) element).toString() : element); result.add(element instanceof Field ? context.getReference((Field) element).toString() : element);
@ -696,6 +739,15 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return result; 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. * Creates a new instance of this {@link OperationProjection} with the given alias.
* *
@ -703,7 +755,27 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @return * @return
*/ */
public OperationProjection withAlias(String alias) { 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<Object> 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); 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); public abstract DBObject toDBObject(AggregationOperationContext context);
} }
} }

69
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.List;
import java.util.Scanner; import java.util.Scanner;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime; import org.joda.time.LocalDateTime;
import org.junit.After; import org.junit.After;
import org.junit.Before; 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.CollectionCallback;
import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry; 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.core.query.Query;
import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.util.Version; import org.springframework.data.util.Version;
@ -960,6 +963,61 @@ public class AggregationTests {
assertThat(result.getMappedResults(), hasSize(2)); 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<DBObject> 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) { private void assertLikeStats(LikeStats like, String id, long count) {
assertThat(like, is(notNullValue())); assertThat(like, is(notNullValue()));
@ -1095,6 +1153,7 @@ public class AggregationTests {
} }
} }
@SuppressWarnings("unused")
static class Descriptors { static class Descriptors {
private CarDescriptor carDescriptor; private CarDescriptor carDescriptor;
} }
@ -1110,6 +1169,7 @@ public class AggregationTests {
} }
} }
@SuppressWarnings("unused")
static class Entry { static class Entry {
private String make; private String make;
private String model; private String model;
@ -1139,4 +1199,13 @@ public class AggregationTests {
this.timestamp = timestamp; this.timestamp = timestamp;
} }
} }
static class ObjectWithDate {
Date dateValue;
public ObjectWithDate(Date dateValue) {
this.dateValue = dateValue;
}
}
} }

62
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 static org.springframework.data.mongodb.util.DBObjectUtils.*;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.DBObjectTestUtils;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
import com.mongodb.DBObject; 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 dbObject = operation.and("foo").plus(41).as("bar").toDBObject(Aggregation.DEFAULT_CONTEXT);
DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT);
DBObject barClause = DBObjectTestUtils.getAsDBObject(projectClause, "bar"); DBObject barClause = DBObjectTestUtils.getAsDBObject(projectClause, "bar");
BasicDBList addClause = DBObjectTestUtils.getAsDBList(barClause, "$add"); List<Object> addClause = (List<Object>) barClause.get("$add");
assertThat(addClause, hasSize(2)); assertThat(addClause, hasSize(2));
assertThat(addClause.get(0), is((Object) "$foo")); 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\"}}")); 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.<Object> asList(
"$date", 86400000))))));
}
private static DBObject exctractOperation(String field, DBObject fromProjectClause) { private static DBObject exctractOperation(String field, DBObject fromProjectClause) {
return (DBObject) fromProjectClause.get(field); return (DBObject) fromProjectClause.get(field);
} }

Loading…
Cancel
Save