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 @@ + +