From 7dd94949d5ce1d600061f771c54af3ceb69f5cc2 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Fri, 19 Jul 2013 16:35:21 +0200 Subject: [PATCH] DATAMONGO-586 - Initial support for automatic field reference resolution. Added automatic field reference resolution which removes the need to have in depth knowledge on how aggregation steps structures the output. Introduced AggregateOperationContext abstraction to hold the information of available fields for an aggregation step. Introduced ContextConsumingAggregateOperation and ContextProducingAggregateOperation abstractions to be able to distinguish operations. Updates test cases to reflect the API changes. --- .../AbstractAggregateOperation.java | 16 +- ...bstractContextAwareAggregateOperation.java | 63 ++++ ...actContextProducingAggregateOperation.java | 39 +++ .../AggregateOperationContext.java | 63 ++++ .../mongodb/core/aggregation/Aggregation.java | 69 +++-- .../core/aggregation/BackendFields.java | 2 +- .../BasicAggregateOperationContext.java | 62 ++++ .../ContextConsumingAggregateOperation.java | 36 +++ .../ContextProducingAggregateOperation.java | 26 ++ .../core/aggregation/GeoNearOperation.java | 4 - .../core/aggregation/GroupOperation.java | 279 +++++++++--------- .../core/aggregation/LimitOperation.java | 6 +- .../core/aggregation/MatchOperation.java | 1 + .../aggregation/NoopAggreationOperation.java | 3 + .../core/aggregation/ProjectionOperation.java | 110 +++++-- .../core/aggregation/ReferenceUtil.java | 13 + .../core/aggregation/SkipOperation.java | 6 +- .../core/aggregation/SortOperation.java | 18 +- .../core/aggregation/TypedAggregation.java | 5 + .../core/aggregation/UnwindOperation.java | 11 +- .../aggregation/AggregationPipelineTests.java | 8 +- .../core/aggregation/AggregationTests.java | 110 ++++--- .../core/aggregation/ProjectionTests.java | 33 +-- .../src/test/resources/logback.xml | 2 + 24 files changed, 702 insertions(+), 283 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextAwareAggregateOperation.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextProducingAggregateOperation.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregateOperationContext.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BasicAggregateOperationContext.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregateOperation.java index fc7a5c909..b61190d50 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregateOperation.java @@ -31,9 +31,6 @@ abstract class AbstractAggregateOperation implements AggregationOperation { this.operationName = operationName; } - /** - * @return the operationName - */ public String getOperationName() { return operationName; } @@ -42,19 +39,18 @@ abstract class AbstractAggregateOperation implements AggregationOperation { return OPERATOR_PREFIX + getOperationName(); } - /** - * @return the argument for the operation - */ - public abstract Object getOperationArgument(); + public Object getOperationArgument() { + return new BasicDBObject(); + } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDbObject() + */ @Override public DBObject toDbObject() { return new BasicDBObject(getOperationCommand(), getOperationArgument()); } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ @Override public String toString() { return String.valueOf(toDbObject()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextAwareAggregateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextAwareAggregateOperation.java new file mode 100644 index 000000000..e6d36994f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextAwareAggregateOperation.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * @author Thomas Darimont + */ +abstract class AbstractContextAwareAggregateOperation extends AbstractAggregateOperation implements + ContextConsumingAggregateOperation { + + public AbstractContextAwareAggregateOperation(String operationName) { + super(operationName); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() + */ + @Override + public Object getOperationArgument() { + throw new UnsupportedOperationException(String.format("This is not supported on an instance of %s", + ContextConsumingAggregateOperation.class.getName())); + } + + /** + * Creates the argument for the aggregation operation from the given {@code inputAggregateOperationContext} + * + * @param inputAggregateOperationContext + * @return the argument for the operation + */ + public abstract Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext); + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#toDbObject() + */ + @Override + public DBObject toDbObject() { + throw new UnsupportedOperationException(String.format("This is not supported on an instance of %s", + ContextConsumingAggregateOperation.class.getName())); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ContextAwareAggregateOperation#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregateOperationContext) + */ + public DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext) { + return new BasicDBObject(getOperationCommand(), getOperationArgument(inputAggregateOperationContext)); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextProducingAggregateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextProducingAggregateOperation.java new file mode 100644 index 000000000..ece76090f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextProducingAggregateOperation.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +/** + * @author Thomas Darimont + */ +abstract class AbstractContextProducingAggregateOperation extends AbstractContextAwareAggregateOperation implements + ContextProducingAggregateOperation { + + private final AggregateOperationContext outputAggregateOperationContext; + + public AbstractContextProducingAggregateOperation(String operationName) { + super(operationName); + this.outputAggregateOperationContext = createAggregateContext(); + } + + private AggregateOperationContext createAggregateContext() { + return new BasicAggregateOperationContext(); + } + + public AggregateOperationContext getOutputAggregateOperationContext() { + return this.outputAggregateOperationContext; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregateOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregateOperationContext.java new file mode 100644 index 000000000..959d578a3 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregateOperationContext.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import java.util.Map; + +/** + * A {@code AggregateOperationContext} holds information about available fields for the aggregation steps. + * + * @author Thomas Darimont + */ +interface AggregateOperationContext { + + Map getAvailableFields(); + + /** + * @param fieldName + * @return the alias for the given fieldName if present in available fields. If the given field is not available the + * given fieldName is return instead. + */ + String returnFieldNameAliasIfAvailableOr(String fieldName); + + /** + * @param fieldName + * @return true if the a field with the given field name is available. + */ + boolean isFieldAvailable(String fieldName); + + /** + * Registers a field with the given {@code fieldName} as available field. + * + * @param fieldName + */ + void registerAvailableField(String fieldName); + + /** + * Registers a field with the given {@code fieldName} as field available with the given {@code availableFieldName} as + * an alias. + * + * @param fieldName + */ + void registerAvailableField(String fieldName, String availableFieldName); + + /** + * Removes the field with the given fieldName from the available fields. + * + * @param fieldName + */ + void unregisterAvailableField(String fieldName); +} 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 87a142a4c..be45da42f 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 @@ -23,7 +23,6 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -73,40 +72,53 @@ public class Aggregation { private List getOperationObjects() { + AggregateOperationContext aggregateOperationContext = createInitialAggregateOperationContext(); List operationObjects = new ArrayList(); for (AggregationOperation operation : operations) { - operationObjects.add(operation.toDbObject()); + + if (operation instanceof NoopAggreationOperation) { + continue; + } + + operationObjects.add(toOperationObject(operation, aggregateOperationContext)); + if (operation instanceof ContextProducingAggregateOperation) { + aggregateOperationContext = ((ContextProducingAggregateOperation) operation) + .getOutputAggregateOperationContext(); + } } return operationObjects; } - /* (non-Javadoc) - * @see java.lang.Object#toString() + /** + * @param aggregateOperationContext + * @param operation + * @return the {@link DBObject} representation of the given {@link AggregationOperation} */ - @Override - public String toString() { - return StringUtils.collectionToCommaDelimitedString(operations); + protected DBObject toOperationObject(AggregationOperation operation, + AggregateOperationContext aggregateOperationContext) { + + DBObject operationObject; + if (operation instanceof ContextConsumingAggregateOperation) { + operationObject = ((ContextConsumingAggregateOperation) operation).toDbObject(aggregateOperationContext); + } else { + operationObject = operation.toDbObject(); + } + return operationObject; } - /** - * Factory method to create a new {@link GroupOperation} for the given {@code id}. - * - * @param id, must not be {@literal null} - * @return - */ - public static GroupOperation group(DBObject id) { - return new GroupOperation(id); + protected AggregateOperationContext createInitialAggregateOperationContext() { + return new BasicAggregateOperationContext(); } /** * Factory method to create a new {@link GroupOperation} for the given {@code idFields}. * * @param idField the first idField to use, must not be {@literal null}. - * @param moreIdFields more id fields to use, can be {@literal null}. + * @param additionalIdFields more id fields to use, can be {@literal null}. * @return */ - public static GroupOperation group(String idField, String... moreIdFields) { - return new GroupOperation(idField, moreIdFields); + public static GroupOperation group(String idField, String... additionalIdFields) { + return new GroupOperation(fields(idField, additionalIdFields)); } /** @@ -221,15 +233,6 @@ public class Aggregation { return new SortOperation(sort); } - /** - * Factory method to create a new empty {@link Fields} container for key-value pairs. - * - * @return - */ - public static Fields fields() { - return fields(new String[0]); - } - /** * Factory method to create a new {@link Fields} container for key-value pairs from the given {@code fieldNames}. *

@@ -246,8 +249,16 @@ public class Aggregation { * * @return */ - public static Fields fields(String... fieldNames) { - return new BackendFields(fieldNames); + public static Fields fields(String fieldName, String... additionalFieldNames) { + return new BackendFields(additionalFieldNames).and(fieldName); + } + + public static Fields fields() { + return new BackendFields(); + } + + public static Fields pick(String fieldName, Object fieldNameOrValue) { + return fields().and(fieldName, fieldNameOrValue); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BackendFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BackendFields.java index a4d7937d9..fa036769f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BackendFields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BackendFields.java @@ -26,7 +26,7 @@ class BackendFields implements Fields { * @return */ public Fields and(String name) { - return and(name, ReferenceUtil.safeReference(name)); + return and(name, name); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BasicAggregateOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BasicAggregateOperationContext.java new file mode 100644 index 000000000..34092421d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BasicAggregateOperationContext.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Map based implementation of {@link AggregateOperationContext}. + * + * @author Thomas Darimont + */ +public class BasicAggregateOperationContext implements AggregateOperationContext { + + private Map availableFields = new LinkedHashMap(); + + @Override + public Map getAvailableFields() { + return new HashMap(getAvailableFieldsInternal()); + } + + protected Map getAvailableFieldsInternal() { + return this.availableFields; + } + + @Override + public void registerAvailableField(String fieldName) { + registerAvailableField(fieldName, fieldName); + } + + @Override + public void registerAvailableField(String fieldName, String availableFieldName) { + getAvailableFieldsInternal().put(fieldName, availableFieldName); + } + + public String returnFieldNameAliasIfAvailableOr(String fieldName) { + return isFieldAvailable(fieldName) ? getAvailableFieldsInternal().get(fieldName) : fieldName; + } + + public boolean isFieldAvailable(String fieldName) { + return getAvailableFieldsInternal().containsKey(ReferenceUtil.safeNonReference(fieldName)); + } + + @Override + public void unregisterAvailableField(String fieldName) { + getAvailableFieldsInternal().remove(fieldName); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java new file mode 100644 index 000000000..658165834 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import com.mongodb.DBObject; + +/** + * Represents one single operation in an aggregation pipeline that is aware of an {@link AggregateOperationContext}. The + * {@code AggregateOperationContext} can be used to resolve the correct field reference expression for field references. + * + * @author Thomas Darimont + */ +public interface ContextConsumingAggregateOperation extends AggregationOperation { + + /** + * Creates a {@link DBObject} representation backing this object and considers the field references from the given + * {@link AggregateOperationContext}. + * + * @param inputAggregateOperationContext + * @return + */ + DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java new file mode 100644 index 000000000..ef5579e1c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +/** + * Represents one single operation in an aggregation pipeline that is aware of an {@link AggregateOperationContext} that + * produces an {@link AggregateOperationContext} as output. + * + * @author Thomas Darimont + */ +public interface ContextProducingAggregateOperation extends AggregationOperation { + AggregateOperationContext getOutputAggregateOperationContext(); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java index d10b45ca0..11b1499e0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java @@ -31,12 +31,8 @@ public class GeoNearOperation extends AbstractAggregateOperation { this.nearQuery = nearQuery; } - /* (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() - */ @Override public Object getOperationArgument() { return nearQuery.toDBObject(); } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java index 0a0781f3a..3279d78eb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.core.aggregation; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.Map.Entry; import org.springframework.util.Assert; @@ -32,15 +32,10 @@ import com.mongodb.DBObject; * @author Thomas Darimont * @since 1.3 */ -public class GroupOperation extends AbstractAggregateOperation { +public class GroupOperation extends AbstractContextProducingAggregateOperation { final Object id; - final Map fields = new HashMap(); - - public GroupOperation(Object id) { - super("group"); - this.id = id; - } + final List ops = new ArrayList(); /** * Creates a $group operation with _id referencing to a field of the document. The returned @@ -53,72 +48,129 @@ public class GroupOperation extends AbstractAggregateOperation { * @param id * @param moreIdFields */ - public GroupOperation(String idField, String... moreIdFields) { - this(createGroupIdFrom(idField, moreIdFields)); + public GroupOperation(Fields fields) { + super("group"); + this.id = createGroupIdFrom(fields); } /** - * @param idField - * @param moreIdFields + * @param fields * @return */ - private static Object createGroupIdFrom(String idField, String[] moreIdFields) { + private Object createGroupIdFrom(Fields fields) { - Assert.notNull(idField, "idField must not be null!"); - Object result = ReferenceUtil.safeReference(idField); + Assert.notNull(fields, "fields must not be null!"); + Map values = fields.getValues(); + Assert.notEmpty(values, "fields.values must not be empty!"); - if (moreIdFields != null && moreIdFields.length > 0) { - DBObject idReferences = new BasicDBObject(moreIdFields.length + 1); - idReferences.put(ReferenceUtil.safeNonReference(idField), ReferenceUtil.safeReference(idField)); - - for (String additionalIdField : moreIdFields) { - idReferences.put(ReferenceUtil.safeNonReference(additionalIdField), - ReferenceUtil.safeReference(additionalIdField)); - } - - result = idReferences; + DBObject idReferences = new BasicDBObject(values.size()); + for (Map.Entry entry : values.entrySet()) { + String idFieldName = ReferenceUtil.safeNonReference(entry.getKey()); + Object idFieldValue = entry.getValue() instanceof String ? ReferenceUtil.safeReference(entry.getValue() + .toString()) : entry.getValue(); + idReferences.put(idFieldName, idFieldValue); } - - return result; - } - - public GroupOperation(Fields idFields) { - this((Object) idFields.getValues()); + return idReferences; } /* (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() */ @Override - public Object getOperationArgument() { - return createProjection(); - } + public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { - /** - * @return - */ - private DBObject createProjection() { - DBObject projection = new BasicDBObject(ReferenceUtil.ID_KEY, id); + DBObject projection = new BasicDBObject(); - for (Entry entry : fields.entrySet()) { - projection.put(entry.getKey(), entry.getValue()); + Object idToUse = id; + if (idToUse instanceof DBObject) { + idToUse = createGroupIdObject((DBObject) idToUse, inputAggregateOperationContext); } + projection.put(ReferenceUtil.ID_KEY, idToUse); + + for (GroupingOperation op : ops) { + projection.put(op.alias, op.toDbObject(inputAggregateOperationContext)); + } + return projection; } - public GroupOperation addField(String key, DBObject value) { + /** + * @param idCandidate + * @param inputAggregateOperationContext + * @return + */ + private Object createGroupIdObject(DBObject groupIdObject, AggregateOperationContext inputAggregateOperationContext) { - Assert.hasText(key, "Key is empty"); - Assert.notNull(value, "Value is null"); - - String trimmedKey = key.trim(); - - if (ReferenceUtil.ID_KEY.equals(trimmedKey)) { - throw new IllegalArgumentException("_id field can only be set in constructor"); + Object simpleIdOrNull = returnIfGroupIdIsSingleFieldReference(inputAggregateOperationContext, groupIdObject); + if (simpleIdOrNull != null) { + return simpleIdOrNull; } - fields.put(key, value); - return this; + DBObject idObject = new BasicDBObject(); + for (String idFieldName : groupIdObject.keySet()) { + + Object idFieldValue = groupIdObject.get(idFieldName); + Object idFieldValueOrNull = returnIfFieldValueReferencesAvailableField(inputAggregateOperationContext, + idFieldName, idFieldValue); + if (idFieldValueOrNull != null) { + idFieldValue = idFieldValueOrNull; + } + + getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName)); + idObject.put(idFieldName, idFieldValue); + } + + return idObject; + } + + private Object returnIfGroupIdIsSingleFieldReference(AggregateOperationContext inputAggregateOperationContext, + DBObject idObject) { + + if (idObject.keySet().size() != 1) { + return null; + } + + return returnIfFieldNameIsSimpleReference(inputAggregateOperationContext, idObject, idObject.keySet().iterator() + .next()); + } + + private Object returnIfFieldValueReferencesAvailableField(AggregateOperationContext inputAggregateOperationContext, + String idFieldName, Object idFieldValue) { + + Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null"); + + if (!ReferenceUtil.isValueFieldReference(idFieldName, idFieldValue)) { + return null; + } + + if (!inputAggregateOperationContext.isFieldAvailable(idFieldName)) { + return null; + } + + String idFieldNameToUse = inputAggregateOperationContext.returnFieldNameAliasIfAvailableOr(idFieldName); + return ReferenceUtil.safeReference(inputAggregateOperationContext instanceof GroupOperation ? ReferenceUtil + .id(idFieldNameToUse) : idFieldNameToUse); + } + + private Object returnIfFieldNameIsSimpleReference(AggregateOperationContext inputAggregateOperationContext, + DBObject idObject, String idFieldName) { + + Object idFieldValue = idObject.get(idFieldName); + + if (!idFieldValueIsSimpleIdFieldExpression(idFieldName, idFieldValue)) { + return null; + } + + getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName)); + idFieldValue = ReferenceUtil.safeReference(inputAggregateOperationContext + .returnFieldNameAliasIfAvailableOr(idFieldName)); + + return idFieldValue; + } + + private static boolean idFieldValueIsSimpleIdFieldExpression(String idFieldName, Object idFieldValue) { + return idFieldValue instanceof String + && idFieldName.equals(ReferenceUtil.safeNonReference(idFieldValue.toString())); } /** @@ -271,7 +323,7 @@ public class GroupOperation extends AbstractAggregateOperation { * @return */ public GroupOperation count(String name, double increment) { - return addField(name, new BasicDBObject("$sum", increment)); + return sum(name, increment); } /** @@ -292,6 +344,10 @@ public class GroupOperation extends AbstractAggregateOperation { return count(name, 1); } + private GroupOperation sum(String name, Object field) { + return addOperation("$sum", name, field); + } + /** * Adds a field with the $sum * operation. @@ -308,7 +364,7 @@ public class GroupOperation extends AbstractAggregateOperation { * @return */ public GroupOperation sum(String name, String field) { - return addOperation("$sum", name, field); + return sum(name, (Object) field); } /** @@ -329,106 +385,37 @@ public class GroupOperation extends AbstractAggregateOperation { return sum(field, field); } - protected GroupOperation addOperation(String operation, String name, String field) { - return addField(name, new BasicDBObject(operation, ReferenceUtil.safeReference(field))); + protected GroupOperation addOperation(String operation, String name, Object field) { + + getOutputAggregateOperationContext().registerAvailableField(name); + this.ops.add(new GroupingOperation(operation, name, field)); + return this; } - /** - * Creates a $group operation with a id that consists of multiple fields. Using - * {@link IdField#idField(String)} or {@link IdField#idField(String, String)} you can easily create complex id fields - * like: - * - *

-	 * 
-	 * group(idField("path"), idField("pageView", "page.views"), idField("field3"))
-	 * 
-	 * 
- * - * which would result in: - * - *
-	 * 
-	 *     {$group: {_id: {path: "$path", pageView: "$page.views", field3: "$field3"}}}
-	 * 
-	 * 
- * - * @param idFields - * @return - */ - // TODO still relevant? IdField is probably a better abstraction than Fields?! - public static GroupOperation group(IdField... idFields) { - Assert.notNull(idFields, "Combined id is null"); + static class GroupingOperation { + final String operation; + final String alias; + final Object fieldNameOrValue; - BasicDBObject id = new BasicDBObject(); - for (IdField idField : idFields) { - id.put(idField.getKey(), idField.getValue()); + public GroupingOperation(String operation, String alias, Object fieldNameOrValue) { + this.operation = operation; + this.alias = alias; + this.fieldNameOrValue = fieldNameOrValue; } - return new GroupOperation(id); - } + public DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext) { - /** - * Represents a single field in a complex id of a $group operation. For example: - * - *
-	 *     {$group: {_id: {key: "$value"}}}
-	 * 
- */ - public static class IdField { + Object fieldNameOrValueToUse = fieldNameOrValue; - private final String key; - private final String value; + if (fieldNameOrValue instanceof String) { + if (inputAggregateOperationContext != null) { + fieldNameOrValueToUse = inputAggregateOperationContext + .returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse); + } + fieldNameOrValueToUse = ReferenceUtil.safeReference((String) fieldNameOrValueToUse); + } - /** - * Creates a new {@link IdField} with the given key and value. - * - * @param key must not be {@literal null} or empty. - * @param value must not be {@literal null} or empty. - */ - public IdField(String key, String value) { - - Assert.hasText(key, "Key must not be null or empty"); - Assert.hasText(value, "Value must not be null or empty"); - - this.key = ReferenceUtil.safeNonReference(key); - this.value = ReferenceUtil.safeReference(value); - } - - public String getKey() { - return key; - } - - public String getValue() { - return value; - } - - /** - * Creates an id field with the name of the referenced field: - * - *
-		 * _id : { field : "$field" }
-		 * 
- * - * @param field reference to a field of the document - * @return the id field - */ - public static IdField idField(String field) { - return new IdField(field, field); - } - - /** - * Creates an id field with key and reference. - * - *
-		 * _id: {key: "$field"}
-		 * 
- * - * @param key the key - * @param field reference to a field of the document - * @return the id field - */ - public static IdField idField(String key, String field) { - return new IdField(key, field); + return new BasicDBObject(operation, fieldNameOrValueToUse); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LimitOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LimitOperation.java index 2e59649f7..36a432ded 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LimitOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LimitOperation.java @@ -16,6 +16,9 @@ package org.springframework.data.mongodb.core.aggregation; /** + * Encapsulates the {@code $limit}-operation + * + * @see http://docs.mongodb.org/manual/reference/aggregation/limit/ * @author Thomas Darimont */ class LimitOperation extends AbstractAggregateOperation { @@ -30,9 +33,6 @@ class LimitOperation extends AbstractAggregateOperation { this.maxElements = maxElements; } - /* (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() - */ @Override public Object getOperationArgument() { return maxElements; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java index 4afda0525..74e82a09f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java @@ -23,6 +23,7 @@ import com.mongodb.DBObject; /** * Encapsulates the {@code $match}-operation * + * @see http://docs.mongodb.org/manual/reference/aggregation/match/ * @author Sebastian Herold * @author Thomas Darimont * @since 1.3 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NoopAggreationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NoopAggreationOperation.java index f081afbcd..f3ec5cc08 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NoopAggreationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NoopAggreationOperation.java @@ -19,9 +19,12 @@ import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** + * Represents a skippable AggregationOperation that is not considered for execution. + * * @author Thomas Darimont */ public class NoopAggreationOperation implements AggregationOperation { + public DBObject toDbObject() { return new BasicDBObject(); } 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 fce1165ea..7acc0833e 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 @@ -18,7 +18,9 @@ package org.springframework.data.mongodb.core.aggregation; import java.util.ArrayList; import java.util.Collections; import java.util.EmptyStackException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Stack; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -29,40 +31,35 @@ import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** - * Projection of field to be used in an {@link Aggregation}. + * Encapsulates the aggregation framework {@code $project}-operation. *

- * A projection is similar to a {@link Field} inclusion/exclusion but more powerful. It can generate new fields, change - * values of given field etc. + * Projection of field to be used in an {@link Aggregation}. A projection is similar to a {@link Field} + * inclusion/exclusion but more powerful. It can generate new fields, change values of given field etc. *

* + * @see http://docs.mongodb.org/manual/reference/aggregation/project/ * @author Tobias Trelle * @author Thomas Darimont * @since 1.3 */ -public class ProjectionOperation extends AbstractAggregateOperation { +public class ProjectionOperation extends AbstractContextProducingAggregateOperation { /** Stack of key names. Size is 0 or 1. */ private final Stack reference = new Stack(); - private final DBObject projection = new BasicDBObject(); + private final Map projection = new HashMap(); private DBObject rightHandExpression; - /** - * Create an empty projection. - */ - public ProjectionOperation() { - super("project"); - } - /** * This convenience constructor excludes the field {@code _id} and includes the given fields. * * @param includes Keys of field to include, must not be {@literal null} or empty. */ public ProjectionOperation(String... includes) { - this(); - Assert.notEmpty(includes); + super("project"); + + Assert.notNull(includes, "includes must not be null"); exclude("_id"); for (String key : includes) { @@ -76,7 +73,15 @@ public class ProjectionOperation extends AbstractAggregateOperation { * @param targetClass */ public ProjectionOperation(Class targetClass) { - this(); + this(extractFieldsFrom(targetClass)); + } + + /** + * @param targetClass + * @return + */ + private static String[] extractFieldsFrom(Class targetClass) { + return new String[0]; } /** @@ -87,6 +92,8 @@ public class ProjectionOperation extends AbstractAggregateOperation { public final ProjectionOperation exclude(String key) { Assert.hasText(key, "Missing key"); + getOutputAggregateOperationContext().unregisterAvailableField(ReferenceUtil.safeNonReference(key)); + projection.put(key, 0); return this; } @@ -102,6 +109,7 @@ public class ProjectionOperation extends AbstractAggregateOperation { safePop(); reference.push(key); + getOutputAggregateOperationContext().registerAvailableField(key); return this; } @@ -116,7 +124,10 @@ public class ProjectionOperation extends AbstractAggregateOperation { Assert.hasText(key, "Missing key"); try { - projection.put(key, rightHandSide(ReferenceUtil.safeReference(reference.pop()))); + String rhsFieldName = reference.pop(); + getOutputAggregateOperationContext().unregisterAvailableField(ReferenceUtil.safeNonReference(rhsFieldName)); + getOutputAggregateOperationContext().registerAvailableField(ReferenceUtil.safeNonReference(key)); + projection.put(key, rightHandSide(ReferenceUtil.safeReference(rhsFieldName))); } catch (EmptyStackException e) { throw new InvalidDataAccessApiUsageException("Invalid use of as()", e); } @@ -152,7 +163,6 @@ public class ProjectionOperation extends AbstractAggregateOperation { private ProjectionOperation arithmeticOperation(String op, Number n) { Assert.notNull(n, "Missing number"); - rightHandExpression = createArrayObject(op, ReferenceUtil.safeReference(reference.peek()), n); return this; } @@ -187,10 +197,26 @@ public class ProjectionOperation extends AbstractAggregateOperation { Assert.notNull(key, "Missing Key"); Assert.notNull(value); - this.projection.put(key, value instanceof Fields ? ((Fields) value).getValues() : value); + + getOutputAggregateOperationContext().registerAvailableField(key); + registerAvailableFieldsRecursive(key, value); + this.projection.put(key, value); + return this; } + private void registerAvailableFieldsRecursive(String outerKey, Object value) { + + if (value instanceof Fields) { + Map values = ((Fields) value).getValues(); + for (String key : values.keySet()) { + String innerKey = outerKey + "." + key; + getOutputAggregateOperationContext().registerAvailableField(innerKey); + registerAvailableFieldsRecursive(innerKey, values.get(key)); + } + } + } + /** * @param name * @param value @@ -205,9 +231,53 @@ public class ProjectionOperation extends AbstractAggregateOperation { * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() */ @Override - public Object getOperationArgument() { + public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { + Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null"); safePop(); - return projection; + + DBObject projectionObject = new BasicDBObject(); + for (Map.Entry entry : projection.entrySet()) { + Object fieldNameOrValueToUse = entry.getValue(); + + DBObject fieldsObject = returnIfValueIsIdFields(inputAggregateOperationContext, fieldNameOrValueToUse); + if (fieldsObject != null) { + projectionObject.put(entry.getKey(), fieldsObject != null ? fieldsObject : fieldNameOrValueToUse); + continue; + } + + if (fieldNameOrValueToUse instanceof String) { + String fieldName = inputAggregateOperationContext + .returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse); + fieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName); + } + + projectionObject.put(entry.getKey(), fieldNameOrValueToUse); + } + + return projectionObject; + } + + private DBObject returnIfValueIsIdFields(AggregateOperationContext inputAggregateOperationContext, + Object fieldNameOrValueToUse) { + + Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null"); + + if (!(fieldNameOrValueToUse instanceof Fields)) { + return null; + } + + DBObject fieldsObject = new BasicDBObject(); + for (Map.Entry fieldsEntry : ((Fields) fieldNameOrValueToUse).getValues().entrySet()) { + + Object fieldsEntryFieldNameOrValueToUse = fieldsEntry.getValue(); + if (fieldsEntryFieldNameOrValueToUse instanceof String && inputAggregateOperationContext != null) { + String fieldName = inputAggregateOperationContext + .returnFieldNameAliasIfAvailableOr((String) fieldsEntryFieldNameOrValueToUse); + fieldsEntryFieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName); + } + fieldsObject.put(fieldsEntry.getKey(), fieldsEntryFieldNameOrValueToUse); + } + return fieldsObject; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java index 4b732b3e8..82bc19d2b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java @@ -105,4 +105,17 @@ class ReferenceUtil { return ID_KEY + "." + fieldName; } + /** + *

+	 * a: $a -> true
+	 * 
+ * + * @param idFieldName + * @param idFieldValue + * @return true if {@code idFieldValue} corresponds to the given {@code idFieldName} e.g. + */ + public static boolean isValueFieldReference(String idFieldName, Object idFieldValue) { + return idFieldValue instanceof String && idFieldName.equals(safeNonReference((String) idFieldValue)); + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SkipOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SkipOperation.java index 58adfc81e..18dc68036 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SkipOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SkipOperation.java @@ -16,6 +16,9 @@ package org.springframework.data.mongodb.core.aggregation; /** + * Encapsulates the aggregation framework {@code $skip}-operation. + * + * @see http://docs.mongodb.org/manual/reference/aggregation/skip/ * @author Thomas Darimont */ public class SkipOperation extends AbstractAggregateOperation { @@ -30,9 +33,6 @@ public class SkipOperation extends AbstractAggregateOperation { this.skipCount = skipCount; } - /* (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() - */ @Override public Object getOperationArgument() { return skipCount; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortOperation.java index 74e4e57c9..a57a4df5f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortOperation.java @@ -22,9 +22,12 @@ import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** + * Encapsulates the aggregation framework {@code $sort}-operation. + * + * @see http://docs.mongodb.org/manual/reference/aggregation/sort/#pipe._S_sort * @author Thomas Darimont */ -public class SortOperation extends AbstractAggregateOperation { +public class SortOperation extends AbstractContextAwareAggregateOperation implements ContextConsumingAggregateOperation { private Sort sort; @@ -50,17 +53,14 @@ public class SortOperation extends AbstractAggregateOperation { * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() */ @Override - public Object getOperationArgument() { - return createSortProperties(); - } + public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { + + Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null!"); - /** - * @return - */ - private DBObject createSortProperties() { DBObject sortProperties = new BasicDBObject(); for (org.springframework.data.domain.Sort.Order order : sort) { - sortProperties.put(order.getProperty(), order.isAscending() ? 1 : -1); + String fieldName = inputAggregateOperationContext.returnFieldNameAliasIfAvailableOr(order.getProperty()); + sortProperties.put(fieldName, order.isAscending() ? 1 : -1); } return sortProperties; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypedAggregation.java index 3715a1ad7..504f0f206 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypedAggregation.java @@ -41,4 +41,9 @@ public class TypedAggregation extends Aggregation { return inputType; } + protected AggregateOperationContext createInitialAggregateOperationContext() { + + // TODO construct initial aggregate operation context from input type. + return super.createInitialAggregateOperationContext(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java index e50f08440..51f3b2a4b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java @@ -18,20 +18,27 @@ package org.springframework.data.mongodb.core.aggregation; import org.springframework.util.Assert; /** + * Encapsulates the aggregation framework {@code $unwind}-operation. + * + * @see http://docs.mongodb.org/manual/reference/aggregation/unwind/#pipe._S_unwind * @author Thomas Darimont */ -public class UnwindOperation extends AbstractAggregateOperation { +public class UnwindOperation extends AbstractContextProducingAggregateOperation implements + ContextConsumingAggregateOperation { private final String fieldName; public UnwindOperation(String fieldName) { + super("unwind"); Assert.notNull(fieldName); this.fieldName = fieldName; + + getOutputAggregateOperationContext().registerAvailableField(fieldName, fieldName); } @Override - public Object getOperationArgument() { + public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { return ReferenceUtil.safeReference(fieldName); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationPipelineTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationPipelineTests.java index 1a3106550..9070fbb10 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationPipelineTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationPipelineTests.java @@ -50,13 +50,13 @@ public class AggregationPipelineTests { @Test public void unwindOperation() { - assertSingleDBObject("$unwind", "$field", unwind("$field").toDbObject()); + assertSingleDBObject("$unwind", "$field", unwind("$field").toDbObject(new BasicAggregateOperationContext())); } @Test public void unwindOperationWithAddedPrefix() { - assertSingleDBObject("$unwind", "$field", unwind("field").toDbObject()); + assertSingleDBObject("$unwind", "$field", unwind("field").toDbObject(new BasicAggregateOperationContext())); } @Test @@ -71,7 +71,7 @@ public class AggregationPipelineTests { @Test public void sortOperation() { - DBObject sortDoc = sort(ASC, "n").toDbObject(); + DBObject sortDoc = sort(ASC, "n").toDbObject(new BasicAggregateOperationContext()); DBObject orderDoc = getAsDBObject(sortDoc, "$sort"); assertThat(orderDoc, is(notNullValue())); assertSingleDBObject("n", 1, orderDoc); @@ -80,7 +80,7 @@ public class AggregationPipelineTests { @Test public void projectOperation() { - DBObject projectionDoc = project("a").toDbObject(); + DBObject projectionDoc = project("a").toDbObject(new BasicAggregateOperationContext()); DBObject fields = getAsDBObject(projectionDoc, "$project"); assertThat(fields, is(notNullValue())); assertSingleDBObject("a", 1, fields); 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 fd79f7a43..b67e4a1ee 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 @@ -70,6 +70,11 @@ public class AggregationTests { public void setUp() { cleanDb(); initSampleDataIfNecessary(); + + CommandResult result = mongoTemplate.executeCommand("{ buildInfo: 1 }"); + Object version = result.get("version"); + + LOGGER.debug("Server uses MongoDB Version: {}", version); } @After @@ -149,7 +154,7 @@ public class AggregationTests { Aggregation agg = newAggregation( // project("tags"), // unwind("tags"), // - group($("tags")).count("n"), // + group("tags").count("n"), // project().field("tag", $id()).field("n", 1), // sort(DESC, "n") // ); @@ -175,7 +180,7 @@ public class AggregationTests { Aggregation agg = newAggregation(// project("tags"), // unwind("tags"), // - group($("tags")).count("n"), // + group("tags").count("n"), // project().field("tag", $id()).field("n", 1), // sort(DESC, "n") // ); @@ -198,7 +203,7 @@ public class AggregationTests { Aggregation agg = newAggregation( // project("tags"), // unwind("tags"), // - group($("tags")).count("count"), // + group("tags").count("count"), // limit(2) // ); @@ -222,30 +227,54 @@ public class AggregationTests { assertThat(fields, is(notNullValue())); assertThat(fields.getValues(), is(notNullValue())); assertThat(fields.getValues().size(), is(4)); - assertThat(fields.getValues().get("a"), is((Object) "$a")); - assertThat(fields.getValues().get("b"), is((Object) "$b")); - assertThat(fields.getValues().get("c"), is((Object) "$c")); + assertThat(fields.getValues().get("a"), is((Object) "a")); + assertThat(fields.getValues().get("b"), is((Object) "b")); + assertThat(fields.getValues().get("c"), is((Object) "c")); assertThat(fields.getValues().get("d"), is((Object) 42)); } @Test - public void groupFactoryMethodWithFieldsAndSumOperation() { + public void shouldCreateSimpleIdForGroupOperationWithSingleSimpleIdField() { - Fields fields = fields("a", "b").and("c").and("d", 42); - GroupOperation groupOperation = group(fields).sum("e"); + Fields fields = fields("a"); + GroupOperation groupOperation = new GroupOperation(fields); - assertThat(groupOperation, is(notNullValue())); - assertThat(groupOperation.toDbObject(), is(notNullValue())); - assertThat(groupOperation.id, is(notNullValue())); - assertThat(groupOperation.id, is((Object) fields.getValues())); - assertThat(groupOperation.fields, is(notNullValue())); - assertThat(groupOperation.fields.size(), is(1)); - assertThat(groupOperation.fields.containsKey("e"), is(true)); - assertThat(groupOperation.fields.get("e"), is(notNullValue())); - assertThat(groupOperation.fields.get("e").get("$sum"), is(notNullValue())); - assertThat(groupOperation.fields.get("e").get("$sum"), is((Object) "$e")); + DBObject dbObject = groupOperation.toDbObject(new BasicAggregateOperationContext()); + assertThat(dbObject, is(notNullValue())); + assertThat(dbObject.get("$group"), is(notNullValue())); + assertThat(((DBObject) dbObject.get("$group")).get(id()), is(notNullValue())); + assertThat(((DBObject) dbObject.get("$group")).get(id()), is((Object) "$a")); } + @Test + public void shouldCreateComplexIdForGroupOperationWithSingleComplexIdField() { + + Fields fields = fields().and("a", 42); + GroupOperation groupOperation = new GroupOperation(fields); + + assertThat(groupOperation.toDbObject(new BasicAggregateOperationContext()), is(notNullValue())); + assertThat(groupOperation.id, is(notNullValue())); + assertThat(groupOperation.id, is((Object) new BasicDBObject("a", 42))); + } + + // @Test + // public void groupFactoryMethodWithMultipleFieldsAndSumOperation() { + // + // Fields fields = fields("a", "b").pick("c").pick("d", 42); + // GroupOperation groupOperation = group(fields).sum("e"); + // + // assertThat(groupOperation, is(notNullValue())); + // assertThat(groupOperation.toDbObject(null), is(notNullValue())); + // assertThat(groupOperation.id, is(notNullValue())); + // assertThat(groupOperation.id, is((Object) new BasicDBObject(fields.getValues()))); + // assertThat(groupOperation.fields, is(notNullValue())); + // assertThat(groupOperation.fields.size(), is(1)); + // assertThat(groupOperation.fields.containsKey("e"), is(true)); + // assertThat(groupOperation.fields.get("e"), is(notNullValue())); + // assertThat(groupOperation.fields.get("e").get("$sum"), is(notNullValue())); + // assertThat(groupOperation.fields.get("e").get("$sum"), is((Object) "$e")); + // } + @Test public void complexAggregationFrameworkUsageLargestAndSmallestCitiesByState() { /* @@ -308,20 +337,19 @@ public class AggregationTests { ) */ - TypedAggregation agg = newAggregation( - ZipInfo.class, // - group("state", "city").sum("pop"), // group("state", "city") -> _id: {state: $state, city: $city} - sort(ASC, "pop", id("state"), id("city")), // - group($id("state")) // $id("state") -> _id : $_id.state - .last("biggestCity", $id("city")) // - .last("biggestPop", $("pop")) // - .first("smallestCity", $id("city")) // - .first("smallestPop", $("pop")), // + TypedAggregation agg = newAggregation(ZipInfo.class, // + group("state", "city").sum("pop"), // + sort(ASC, "pop", "state", "city"), // + group("state") // + .last("biggestCity", "city") // + .last("biggestPop", "pop") // + .first("smallestCity", "city") // + .first("smallestPop", "pop"), // project(ZipInfoStats.class) // .field("_id", 0) // - .field("state", $id()) // $id() -> $_id - .field("biggestCity", fields().and("name", $("biggestCity")).and("population", $("biggestPop"))) // - .field("smallestCity", fields().and("name", $("smallestCity")).and("population", $("smallestPop"))), + .field("state", id()) // + .field("biggestCity", pick("name", "biggestCity").and("population", "biggestPop")) // + .field("smallestCity", pick("name", "smallestCity").and("population", "smallestPop")), // sort(ASC, "state") // ); @@ -359,10 +387,12 @@ public class AggregationTests { @Test public void findStatesWithPopulationOver10MillionAggregationExample() { /* - //complex mongodb aggregation framework example from http://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state + //complex mongodb aggregation framework example from + http://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state + db.zipcodes.aggregate( { - $group:{ + $group: { _id:"$state", totalPop:{ $sum:"$pop"} } @@ -371,7 +401,7 @@ public class AggregationTests { $sort: { _id: 1, "totalPop": 1 } }, { - $match:{ + $match: { totalPop: { $gte:10*1000*1000 } } } @@ -379,7 +409,7 @@ public class AggregationTests { */ TypedAggregation agg = newAggregation(ZipInfo.class, // - group("state").sum("totalPop", $("pop")), // fields("state", "city") -> state: $state, city: $city + group("state").sum("totalPop", "pop"), // sort(ASC, id(), "totalPop"), // match(where("totalPop").gte(10 * 1000 * 1000)) // ); @@ -407,6 +437,16 @@ public class AggregationTests { createUserWithLikesDocuments(); + /* + ... + $group: { + _id:"$like", + number:{ $sum:1} + } + ... + + */ + TypedAggregation agg = newAggregation(UserWithLikes.class, // unwind("likes"), // group("likes").count("number"), // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionTests.java index 16e7a72f6..e8c886a44 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionTests.java @@ -38,8 +38,8 @@ public class ProjectionTests { public void emptyProjection() { DBObject raw = safeExtractDbObjectFromProjection(project()); - assertThat(raw, is(notNullValue())); - assertThat(raw.toMap().isEmpty(), is(true)); + assertThat(raw.toMap().size(), is(1)); + assertThat((Integer) raw.get("_id"), is(0)); } @Test(expected = IllegalArgumentException.class) @@ -69,14 +69,19 @@ public class ProjectionTests { public void exclude() { DBObject raw = safeExtractDbObjectFromProjection(project().exclude("a")); - assertSingleDBObject("a", 0, raw); + assertThat(raw.toMap().size(), is(2)); + assertThat((Integer) raw.get("_id"), is(0)); + assertThat((Integer) raw.get("a"), is(0)); + } @Test public void includeAlias() { DBObject raw = safeExtractDbObjectFromProjection(project().include("a").as("b")); - assertSingleDBObject("b", "$a", raw); + assertThat(raw.toMap().size(), is(2)); + assertThat((Integer) raw.get("_id"), is(0)); + assertThat((String) raw.get("b"), is("$a")); } @Test(expected = InvalidDataAccessApiUsageException.class) @@ -94,10 +99,10 @@ public class ProjectionTests { public void plus() { DBObject raw = safeExtractDbObjectFromProjection(project().include("a").plus(10)); - assertNotNullDBObject(raw); + assertThat(raw, is(notNullValue())); DBObject addition = (DBObject) raw.get("a"); - assertNotNullDBObject(addition); + assertThat(addition, is(notNullValue())); List summands = (List) addition.get("$add"); assertThat(summands, is(notNullValue())); @@ -111,10 +116,10 @@ public class ProjectionTests { public void plusWithAlias() { DBObject raw = safeExtractDbObjectFromProjection(project().include("a").plus(10).as("b")); - assertNotNullDBObject(raw); + assertThat(raw, is(notNullValue())); DBObject addition = (DBObject) raw.get("b"); - assertNotNullDBObject(addition); + assertThat(addition, is(notNullValue())); List summands = (List) addition.get("$add"); assertThat(summands, is(notNullValue())); @@ -137,8 +142,8 @@ public class ProjectionTests { private static DBObject safeExtractDbObjectFromProjection(ProjectionOperation projectionOperation) { assertThat(projectionOperation, is(notNullValue())); - DBObject dbObject = projectionOperation.toDbObject(); - assertNotNullDBObject(dbObject); + DBObject dbObject = projectionOperation.toDbObject(new BasicAggregateOperationContext()); + assertThat(dbObject, is(notNullValue())); Object projection = dbObject.get("$project"); assertThat("Expected non null value for key $project ", projection, is(notNullValue())); assertTrue("projection contents should be a " + DBObject.class.getSimpleName(), projection instanceof DBObject); @@ -148,13 +153,7 @@ public class ProjectionTests { private static void assertSingleDBObject(String key, Object value, DBObject doc) { - assertNotNullDBObject(doc); + assertThat(doc, is(notNullValue())); assertThat(doc.get(key), is(value)); } - - private static void assertNotNullDBObject(DBObject doc) { - - assertThat(doc, is(notNullValue())); - assertThat(doc.toMap().size(), is(1)); - } } diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 5ecc71909..3c6add815 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -10,6 +10,8 @@ + +