diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 6bdf4b881..2399ee2a4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -311,13 +311,12 @@ public interface MongoOperations { * * @param aggregation The {@link TypedAggregation} specification holding the aggregation operations, must not be * {@literal null}. - * @param inputCollectionName The name of the input collection to use for the aggreation. + * @param collectionName The name of the input collection to use for the aggreation. * @param outputType The parameterized type of the returned list, must not be {@literal null}. * @return The results of the aggregation operation. * @since 1.3 */ - AggregationResults aggregate(TypedAggregation aggregation, String inputCollectionName, - Class outputType); + AggregationResults aggregate(TypedAggregation aggregation, String collectionName, Class outputType); /** * Execute an aggregation operation. The raw results will be mapped to the given entity class. The name of the @@ -329,33 +328,33 @@ public interface MongoOperations { * @return The results of the aggregation operation. * @since 1.3 */ - AggregationResults aggregate(TypedAggregation aggregation, Class outputType); + AggregationResults aggregate(TypedAggregation aggregation, Class outputType); /** * Execute an aggregation operation. The raw results will be mapped to the given entity class. * - * @param inputType the inputType where the aggregation operation will read from, must not be {@literal null} or - * empty. * @param aggregation The {@link Aggregation} specification holding the aggregation operations, must not be * {@literal null}. + * @param inputType the inputType where the aggregation operation will read from, must not be {@literal null} or + * empty. * @param outputType The parameterized type of the returned list, must not be {@literal null}. * @return The results of the aggregation operation. * @since 1.3 */ - AggregationResults aggregate(Class inputType, Aggregation aggregation, Class outputType); + AggregationResults aggregate(Aggregation aggregation, Class inputType, Class outputType); /** * Execute an aggregation operation. The raw results will be mapped to the given entity class. * - * @param inputCollectionName the collection where the aggregation operation will read from, must not be - * {@literal null} or empty. * @param aggregation The {@link Aggregation} specification holding the aggregation operations, must not be * {@literal null}. + * @param collectionName the collection where the aggregation operation will read from, must not be {@literal null} or + * empty. * @param outputType The parameterized type of the returned list, must not be {@literal null}. * @return The results of the aggregation operation. * @since 1.3 */ - AggregationResults aggregate(String inputCollectionName, Aggregation aggregation, Class outputType); + AggregationResults aggregate(Aggregation aggregation, String collectionName, Class outputType); /** * Execute a map-reduce operation. The map-reduce operation will be formed with an output type of INLINE diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index eee16e108..2dff69a9c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -54,7 +54,10 @@ import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.Fields; +import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -1215,31 +1218,47 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { } @Override - public AggregationResults aggregate(TypedAggregation aggregation, Class outputType) { + public AggregationResults aggregate(TypedAggregation aggregation, Class outputType) { return aggregate(aggregation, determineCollectionName(aggregation.getInputType()), outputType); } @Override - public AggregationResults aggregate(TypedAggregation aggregation, String inputCollectionName, + public AggregationResults aggregate(TypedAggregation aggregation, String inputCollectionName, Class outputType) { - return aggregate(inputCollectionName, aggregation, outputType); + + Assert.notNull(aggregation, "Aggregation pipeline must not be null!"); + + AggregationOperationContext context = new TypeBasedAggregationOperationContext(aggregation.getInputType(), + mappingContext, queryMapper); + return aggregate(aggregation, inputCollectionName, outputType, context); } - public AggregationResults aggregate(Class inputType, Aggregation aggregation, Class outputType) { - return aggregate(determineCollectionName(inputType), aggregation, outputType); + @Override + public AggregationResults aggregate(Aggregation aggregation, Class inputType, Class outputType) { + + return aggregate(aggregation, determineCollectionName(inputType), outputType, + new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper)); } - public AggregationResults aggregate(String inputCollectionName, Aggregation aggregation, - Class outputType) { + @Override + public AggregationResults aggregate(Aggregation aggregation, String collectionName, Class outputType) { + return aggregate(aggregation, collectionName, outputType, null); + } - Assert.notNull(inputCollectionName, "Collection name is missing"); - Assert.notNull(aggregation, "Aggregation pipeline is missing"); - Assert.notNull(outputType, "Entity class is missing"); + protected AggregationResults aggregate(Aggregation aggregation, String collectionName, Class outputType, + AggregationOperationContext context) { - // prepare command - DBObject command = aggregation.toDbObject(inputCollectionName); + Assert.hasText(collectionName, "Collection name must not be null or empty!"); + Assert.notNull(aggregation, "Aggregation pipeline must not be null!"); + Assert.notNull(outputType, "Output type must not be null!"); + + AggregationOperationContext rootContext = context == null ? Aggregation.DEFAULT_CONTEXT : context; + DBObject command = aggregation.toDbObject(collectionName, rootContext); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Executing aggregation: {}", serializeToJsonSafely(command)); + } - // execute command CommandResult commandResult = executeCommand(command); handleCommandError(commandResult, command); @@ -1247,7 +1266,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { @SuppressWarnings("unchecked") Iterable resultSet = (Iterable) commandResult.get("result"); List mappedResults = new ArrayList(); - DbObjectCallback callback = new ReadDbObjectCallback(mongoConverter, outputType); + DbObjectCallback callback = new UnwrapAndReadDbObjectCallback(mongoConverter, outputType); + for (DBObject dbObject : resultSet) { mappedResults.add(callback.doWith(dbObject)); } @@ -1920,6 +1940,35 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { } } + class UnwrapAndReadDbObjectCallback extends ReadDbObjectCallback { + + public UnwrapAndReadDbObjectCallback(EntityReader reader, Class type) { + super(reader, type); + } + + @Override + public T doWith(DBObject object) { + + Object idField = object.get(Fields.UNDERSCORE_ID); + + if (!(idField instanceof DBObject)) { + return super.doWith(object); + } + + DBObject toMap = new BasicDBObject(); + DBObject nested = (DBObject) idField; + toMap.putAll(nested); + + for (String key : object.keySet()) { + if (!Fields.UNDERSCORE_ID.equals(key)) { + toMap.put(key, object.get(key)); + } + } + + return super.doWith(toMap); + } + } + private enum DefaultWriteConcernResolver implements WriteConcernResolver { INSTANCE; 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 deleted file mode 100644 index b61190d50..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregateOperation.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 AbstractAggregateOperation implements AggregationOperation { - private final String operationName; - - /** - * @param operationName - */ - public AbstractAggregateOperation(String operationName) { - this.operationName = operationName; - } - - public String getOperationName() { - return operationName; - } - - public String getOperationCommand() { - return OPERATOR_PREFIX + getOperationName(); - } - - 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()); - } - - @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 deleted file mode 100644 index e6d36994f..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextAwareAggregateOperation.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 deleted file mode 100644 index ece76090f..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextProducingAggregateOperation.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 deleted file mode 100644 index 959d578a3..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregateOperationContext.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 be45da42f..3dbc20299 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 @@ -15,13 +15,19 @@ */ package org.springframework.data.mongodb.core.aggregation; +import static org.springframework.data.mongodb.core.aggregation.Fields.*; + import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.NearQuery; +import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.util.Assert; import com.mongodb.BasicDBObject; @@ -31,305 +37,249 @@ import com.mongodb.DBObject; * An {@code Aggregation} is a representation of a list of aggregation steps to be performed by the MongoDB Aggregation * Framework. * - * @author Tobias Trelle - Original API and Implementation - * @author Thomas Darimont - Refactoring, embedded DSL + * @author Tobias Trelle + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.3 */ -public class Aggregation { +public class Aggregation { + + public static final AggregationOperationContext DEFAULT_CONTEXT = new NoOpAggregationOperationContext(); - private final List operations = new ArrayList(); + private final List operations; /** * Creates a new {@link Aggregation} from the given {@link AggregationOperation}s. * * @param operations must not be {@literal null} or empty. */ - public Aggregation(AggregationOperation... operations) { - registerOperations(operations); - } - - private void registerOperations(AggregationOperation... operations) { - - Assert.notNull(operations, "Operations must not be null!"); - Assert.isTrue(operations.length > 0, "operations must not be empty!"); - for (AggregationOperation operation : operations) { - Assert.notNull(operation, "Operation is not allowed to be null"); - this.operations.add(operation); - } + public static Aggregation newAggregation(AggregationOperation... operations) { + return new Aggregation(operations); } /** - * Converts this {@link Aggregation} specification to a {@link DBObject}. + * Creates a new {@link TypedAggregation} for the given type and {@link AggregationOperation}s. * - * @param inputCollectionName the name of the input collection - * @return the {@code DBObject} representing this aggregation + * @param type must not be {@literal null}. + * @param operations must not be {@literal null} or empty. */ - public DBObject toDbObject(String inputCollectionName) { - - DBObject command = new BasicDBObject("aggregate", inputCollectionName); - command.put("pipeline", getOperationObjects()); - return command; + public static TypedAggregation newAggregation(Class type, AggregationOperation... operations) { + return new TypedAggregation(type, operations); } - private List getOperationObjects() { - - AggregateOperationContext aggregateOperationContext = createInitialAggregateOperationContext(); - List operationObjects = new ArrayList(); - for (AggregationOperation operation : operations) { + /** + * Creates a new {@link Aggregation} from the given {@link AggregationOperation}s. + * + * @param aggregationOperations must not be {@literal null} or empty. + */ + protected Aggregation(AggregationOperation... aggregationOperations) { - if (operation instanceof NoopAggreationOperation) { - continue; - } + Assert.notNull(aggregationOperations, "AggregationOperations must not be null!"); + Assert.isTrue(aggregationOperations.length > 0, "At least one AggregationOperation has to be provided"); - operationObjects.add(toOperationObject(operation, aggregateOperationContext)); - if (operation instanceof ContextProducingAggregateOperation) { - aggregateOperationContext = ((ContextProducingAggregateOperation) operation) - .getOutputAggregateOperationContext(); - } - } - return operationObjects; + this.operations = Arrays.asList(aggregationOperations); } /** - * @param aggregateOperationContext - * @param operation - * @return the {@link DBObject} representation of the given {@link AggregationOperation} + * A pointer to the previous {@link AggregationOperation}. + * + * @return */ - protected DBObject toOperationObject(AggregationOperation operation, - AggregateOperationContext aggregateOperationContext) { - - DBObject operationObject; - if (operation instanceof ContextConsumingAggregateOperation) { - operationObject = ((ContextConsumingAggregateOperation) operation).toDbObject(aggregateOperationContext); - } else { - operationObject = operation.toDbObject(); - } - return operationObject; - } - - protected AggregateOperationContext createInitialAggregateOperationContext() { - return new BasicAggregateOperationContext(); + public static String previousOperation() { + return "_id"; } /** - * Factory method to create a new {@link GroupOperation} for the given {@code idFields}. + * Creates a new {@link ProjectionOperation} including the given fields. * - * @param idField the first idField to use, must not be {@literal null}. - * @param additionalIdFields more id fields to use, can be {@literal null}. + * @param fields must not be {@literal null}. * @return */ - public static GroupOperation group(String idField, String... additionalIdFields) { - return new GroupOperation(fields(idField, additionalIdFields)); + public static ProjectionOperation project(String... fields) { + return project(fields(fields)); } /** - * Factory method to create a new {@link GroupOperation} for the given {@code idFields}. + * Creates a new {@link ProjectionOperation} includeing the given {@link Fields}. * - * @param idFields + * @param fields must not be {@literal null}. * @return */ - public static GroupOperation group(Fields idFields) { - return new GroupOperation(idFields); + public static ProjectionOperation project(Fields fields) { + return new ProjectionOperation(fields); } /** - * Factory method to create a new {@link ProjectionOperation} for the given {@code fields}. The {@code _id} field is - * implicitly excluded. + * Factory method to create a new {@link UnwindOperation} for the field with the given name. * - * @param fields a list of fields to include in the projection. - * @return The {@link ProjectionOperation}. + * @param fieldName must not be {@literal null} or empty. + * @return */ - public static ProjectionOperation project(String... fields) { - return new ProjectionOperation(fields); + public static UnwindOperation unwind(String field) { + return new UnwindOperation(field(field)); } /** - * Factory method to create a new {@link ProjectionOperation}. + * Creates a new {@link GroupOperation} for the given fields. * - * @return The {@link ProjectionOperation}. + * @param fields must not be {@literal null}. + * @return */ - public static ProjectionOperation project() { - return new ProjectionOperation(); + public static GroupOperation group(String... fields) { + return group(fields(fields)); } /** - * Factory method to create a new {@link ProjectionOperation} for the given {@code targetClass}. + * Creates a new {@link GroupOperation} for the given {@link Fields}. * - * @param targetClass + * @param fields must not be {@literal null}. * @return */ - public static ProjectionOperation project(Class targetClass) { - return new ProjectionOperation(targetClass); + public static GroupOperation group(Fields fields) { + return new GroupOperation(fields); } /** - * Factory method to create a new {@link MatchOperation} for the given {@link Criteria}. + * Factory method to create a new {@link SortOperation} for the given {@link Sort}. * - * @param criteria must not be {@literal null} + * @param sort must not be {@literal null}. * @return */ - public static MatchOperation match(Criteria criteria) { - return new MatchOperation(criteria); + public static SortOperation sort(Sort sort) { + return new SortOperation(sort); } /** - * Factory method to create a new {@link UnwindOperation} for the given {@literal fieldName}. + * Factory method to create a new {@link SortOperation} for the given sort {@link Direction} and {@code fields}. * - * @param fieldName {@link UnwindOperation}. + * @param direction must not be {@literal null}. + * @param fields must not be {@literal null}. * @return */ - public static UnwindOperation unwind(String fieldName) { - return new UnwindOperation(fieldName); + public static SortOperation sort(Direction direction, String... fields) { + return new SortOperation(new Sort(direction, fields)); } /** - * Factory method to create a new {@link SkipOperation} for the given {@code skipCount}. + * Creates a new {@link SkipOperation} skipping the given number of elements. * - * @param skipCount the number of documents to skip. + * @param elementsToSkip must not be less than zero. * @return */ - public static SkipOperation skip(int skipCount) { - return new SkipOperation(skipCount); + public static SkipOperation skip(int elementsToSkip) { + return new SkipOperation(elementsToSkip); } /** - * Factory method to create a new {@link LimitOperation} for the given {@code maxElements}. + * Creates a new {@link LimitOperation} limiting the result to the given number of elements. * - * @param maxElements, the max number of documents to return. + * @param maxElements must not be less than zero. * @return */ - public static LimitOperation limit(int maxElements) { + public static LimitOperation limit(long maxElements) { return new LimitOperation(maxElements); } /** - * Factory method to create a new {@link GeoNearOperation} for the given {@code nearQuery}. + * Creates a new {@link MatchOperation} using the given {@link Criteria}. * - * @param nearQuery, must not be {@literal null}. + * @param criteria must not be {@literal null}. * @return */ - public static GeoNearOperation geoNear(NearQuery nearQuery) { - return new GeoNearOperation(nearQuery); + public static MatchOperation match(Criteria criteria) { + return new MatchOperation(criteria); } /** - * Factory method to create a new {@link SortOperation} for the given sort {@link Direction}  {@code direction} and - * {@code fields}. + * Creates a new {@link Fields} instance for the given field names. * - * @param direction, the sort direction, must not be {@literal null}. + * @see Fields#fields(String...) * @param fields must not be {@literal null}. * @return */ - public static SortOperation sort(Sort.Direction direction, String... fields) { - return sort(new Sort(direction, fields)); + public static Fields fields(String... fields) { + return Fields.fields(fields); } /** - * Factory method to create a new {@link SortOperation} for the given {@link Sort}. + * Creates a new {@link Fields} instance from the given field name and target reference. * - * @param sort + * @param name must not be {@literal null} or empty. + * @param target must not be {@literal null} or empty. * @return */ - public static SortOperation sort(Sort sort) { - return new SortOperation(sort); + public static Fields bind(String name, String target) { + return Fields.from(field(name, target)); } /** - * Factory method to create a new {@link Fields} container for key-value pairs from the given {@code fieldNames}. - *

- * A call to fields("a","b","c") generates: - *

- * - *

-	 * {    
-	 *   a: $a,
-	 *   b: $b,
-	 *   c: $c
-	 * }
-	 * 
+ * Converts this {@link Aggregation} specification to a {@link DBObject}. * - * @return + * @param inputCollectionName the name of the input collection + * @return the {@code DBObject} representing this aggregation */ - public static Fields fields(String fieldName, String... additionalFieldNames) { - return new BackendFields(additionalFieldNames).and(fieldName); - } + public DBObject toDbObject(String inputCollectionName, AggregationOperationContext rootContext) { - public static Fields fields() { - return new BackendFields(); - } + AggregationOperationContext context = rootContext; + List operationDocuments = new ArrayList(operations.size()); - public static Fields pick(String fieldName, Object fieldNameOrValue) { - return fields().and(fieldName, fieldNameOrValue); - } + for (AggregationOperation operation : operations) { - /** - * A convenience shortcut to {@link ReferenceUtil#$id()} - * - * @return - */ - public static String $id() { - return ReferenceUtil.$id(); - } + operationDocuments.add(operation.toDBObject(context)); - /** - * A convenience shortcut to {@link ReferenceUtil#$(String)} - * - * @return - */ - public static String $(String fieldName) { - return ReferenceUtil.$(fieldName); - } + if (operation instanceof AggregationOperationContext) { + context = (AggregationOperationContext) operation; + } + } - /** - * A convenience shortcut to {@link ReferenceUtil#$id(String)} - * - * @return - */ - public static String $id(String fieldName) { - return ReferenceUtil.$id(fieldName); - } + DBObject command = new BasicDBObject("aggregate", inputCollectionName); + command.put("pipeline", operationDocuments); - /** - * A convenience shortcut to {@link ReferenceUtil#ID_KEY} - * - * @return - */ - public static String id() { - return ReferenceUtil.ID_KEY; + return command; } - /** - * A convenience shortcut to {@link ReferenceUtil#id(String)} - * - * @return + /* + * (non-Javadoc) + * @see java.lang.Object#toString() */ - public static String id(String fieldName) { - return ReferenceUtil.id(fieldName); + @Override + public String toString() { + return SerializationUtils + .serializeToJsonSafely(toDbObject("__collection__", new NoOpAggregationOperationContext())); } /** - * Creates a new {@link Aggregation}. + * Simple {@link AggregationOperationContext} that just returns {@link FieldReference}s as is. * - * @param the input type of the {@link Aggregation}. - * @param the output type of the {@link Aggregation}. - * @param inputType - * @param operations - * @return + * @author Oliver Gierke */ - public static TypedAggregation newAggregation(Class inputType, AggregationOperation... operations) { - return new TypedAggregation(inputType, operations); - } + private static class NoOpAggregationOperationContext implements AggregationOperationContext { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(com.mongodb.DBObject) + */ + @Override + public DBObject getMappedObject(DBObject dbObject) { + return dbObject; + } - /** - * Creates a new {@link Aggregation}. - * - * @param the input type of the {@link Aggregation}. - * @param the output type of the {@link Aggregation}. - * @param inputType - * @param operations - * @return - */ - public static Aggregation newAggregation(AggregationOperation... operations) { - return new Aggregation(operations); - } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(org.springframework.data.mongodb.core.aggregation.ExposedFields.AvailableField) + */ + @Override + public FieldReference getReference(Field field) { + return new FieldReference(new ExposedField(field, true)); + } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(java.lang.String) + */ + @Override + public FieldReference getReference(String name) { + return new FieldReference(new ExposedField(new AggregationField(name), true)); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java index 1fbfec606..8d67f94f0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java @@ -22,16 +22,16 @@ import com.mongodb.DBObject; * * @author Sebastian Herold * @author Thomas Darimont + * @author Oliver Gierke * @since 1.3 */ public interface AggregationOperation { - String OPERATOR_PREFIX = "$"; - /** - * Creates a {@link DBObject} representation backing this object. + * Turns the {@link AggregationOperation} into a {@link DBObject} by using the given + * {@link AggregationOperationContext}. * * @return the DBObject */ - DBObject toDbObject(); + DBObject toDBObject(AggregationOperationContext context); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java new file mode 100644 index 000000000..d2a2a952b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java @@ -0,0 +1,55 @@ +/* + * 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 org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; + +import com.mongodb.DBObject; + +/** + * The context for an {@link AggregationOperation}. + * + * @author Oliver Gierke + * @since 1.3 + */ +public interface AggregationOperationContext { + + /** + * Returns the mapped {@link DBObject}, potentially converting the source considering mapping metadata etc. + * + * @param dbObject will never be {@literal null}. + * @return must not be {@literal null}. + */ + DBObject getMappedObject(DBObject dbObject); + + /** + * Returns a {@link FieldReference} for the given field or {@literal null} if the context does not expose the given + * field. + * + * @param field must not be {@literal null}. + * @return + */ + FieldReference getReference(Field field); + + /** + * Returns the {@link FieldReference} for the field with the given name or {@literal null} if the context does not + * expose a field with the given name. + * + * @param name must not be {@literal null} or empty. + * @return + */ + FieldReference getReference(String name); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java index ac48fe553..8c0fac644 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java @@ -58,10 +58,21 @@ public class AggregationResults implements Iterable { * * @return */ - public List getAggregationResult() { + public List getMappedResults() { return mappedResults; } + /** + * Returns the unique mapped result. Assumes no result or exactly one. + * + * @return + * @throws IllegalArgumentException in case more than one result is available. + */ + public T getUniqueMappedResult() { + Assert.isTrue(mappedResults.size() < 2, "Expected unique result or null, but got more than one!"); + return mappedResults.size() == 1 ? mappedResults.get(0) : null; + } + /* * (non-Javadoc) * @see java.lang.Iterable#iterator() 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 deleted file mode 100644 index fa036769f..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BackendFields.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.springframework.data.mongodb.core.aggregation; - -import java.util.HashMap; -import java.util.Map; - -/** - * Implementation of {@link Fields} - * - * @author Thomas Darimont - */ -class BackendFields implements Fields { - - private Map values = new HashMap(); - - /** - * @param names - */ - public BackendFields(String... names) { - for (String name : names) { - and(name); - } - } - - /** - * @param name - * @return - */ - public Fields and(String name) { - return and(name, name); - } - - /** - * @param name - * @param value - * @return - */ - public Fields and(String name, Object value) { - this.values.put(name, value); - return this; - } - - /** - * @return - */ - public Map getValues() { - return new HashMap(this.values); - } -} 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 deleted file mode 100644 index 34092421d..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BasicAggregateOperationContext.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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/ExposedFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java new file mode 100644 index 000000000..68d7e4b1c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java @@ -0,0 +1,286 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.util.Assert; +import org.springframework.util.CompositeIterator; + +/** + * Value object to capture the fields exposed by an {@link AggregationOperation}. + * + * @author Oliver Gierke + * @since 1.3 + */ +public class ExposedFields implements Iterable { + + private static final List NO_FIELDS = Collections.emptyList(); + private static final ExposedFields EMPTY = new ExposedFields(NO_FIELDS, NO_FIELDS); + + private final List originalFields; + private final List syntheticFields; + + /** + * Creates a new {@link ExposedFields} instance from the given {@link ExposedField}s. + * + * @param fields must not be {@literal null}. + * @return + */ + public static ExposedFields from(ExposedField... fields) { + return from(Arrays.asList(fields)); + } + + /** + * Creates a new {@link ExposedFields} instance from the given {@link ExposedField}s. + * + * @param fields must not be {@literal null}. + * @return + */ + private static ExposedFields from(List fields) { + + ExposedFields result = EMPTY; + + for (ExposedField field : fields) { + result = result.and(field); + } + + return result; + } + + /** + * Creates synthetic {@link ExposedFields} from the given {@link Fields}. + * + * @param fields must not be {@literal null}. + * @return + */ + public static ExposedFields synthetic(Fields fields) { + return createFields(fields, true); + } + + /** + * Creates non-synthetic {@link ExposedFields} from the given {@link Fields}. + * + * @param fields must not be {@literal null}. + * @return + */ + public static ExposedFields nonSynthetic(Fields fields) { + return createFields(fields, false); + } + + /** + * Creates a new {@link ExposedFields} instance for the given fields in either sythetic or non-synthetic way. + * + * @param fields must not be {@literal null}. + * @param synthetic + * @return + */ + private static ExposedFields createFields(Fields fields, boolean synthetic) { + + Assert.notNull(fields, "Fields must not be null!"); + List result = new ArrayList(); + + for (Field field : fields) { + result.add(new ExposedField(field, synthetic)); + } + + return ExposedFields.from(result); + } + + /** + * Creates a new {@link ExposedFields} with the given orignals and synthetics. + * + * @param originals must not be {@literal null}. + * @param synthetic must not be {@literal null}. + */ + private ExposedFields(List originals, List synthetic) { + + this.originalFields = originals; + this.syntheticFields = synthetic; + } + + /** + * Creates a new {@link ExposedFields} adding the given {@link ExposedField}. + * + * @param field must not be {@literal null}. + * @return + */ + public ExposedFields and(ExposedField field) { + + Assert.notNull(field, "Exposed field must not be null!"); + + ArrayList result = new ArrayList(); + result.addAll(field.synthetic ? syntheticFields : originalFields); + result.add(field); + + return new ExposedFields(field.synthetic ? originalFields : result, field.synthetic ? result : syntheticFields); + } + + /** + * Returns the field with the given name or {@literal null} if no field with the given name is available. + * + * @param name + * @return + */ + public ExposedField getField(String name) { + + for (ExposedField field : this) { + if (field.canBeReferredToBy(name)) { + return field; + } + } + + return null; + } + + /** + * Returns whether the {@link ExposedFields} exposes a single field only. + * + * @return + */ + public boolean exposesSingleFieldOnly() { + return originalFields.size() + syntheticFields.size() == 1; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + + CompositeIterator iterator = new CompositeIterator(); + iterator.add(syntheticFields.iterator()); + iterator.add(originalFields.iterator()); + + return iterator; + } + + /** + * A single exposed field. + * + * @author Oliver Gierke + */ + static class ExposedField implements Field { + + private final boolean synthetic; + private final Field field; + + /** + * Creates a new {@link ExposedField} with the given key. + * + * @param key must not be {@literal null} or empty. + * @param synthetic whether the exposed field is synthetic. + */ + public ExposedField(String key, boolean synthetic) { + this(Fields.field(key), synthetic); + } + + /** + * Creates a new {@link ExposedField} for the given {@link Field}. + * + * @param delegate must not be {@literal null}. + * @param synthetic whether the exposed field is synthetic. + */ + public ExposedField(Field delegate, boolean synthetic) { + + this.field = delegate; + this.synthetic = synthetic; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.Field#getKey() + */ + @Override + public String getName() { + return field.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.Field#getTarget() + */ + @Override + public String getTarget() { + return field.getTarget(); + } + + /** + * Returns whether the field can be referred to using the given name. + * + * @param input + * @return + */ + public boolean canBeReferredToBy(String input) { + return getTarget().equals(input); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("AggregationField: %s, synthetic: %s", field, synthetic); + } + } + + /** + * A reference to an {@link ExposedField}. + * + * @author Oliver Gierke + */ + static class FieldReference { + + private final ExposedField field; + + /** + * Creates a new {@link FieldReference} for the given {@link ExposedField}. + * + * @param field must not be {@literal null}. + */ + public FieldReference(ExposedField field) { + + Assert.notNull(field, "ExposedField must not be null!"); + this.field = field; + } + + /** + * Returns the raw, unqualified reference, i.e. the field reference without a {@literal $} prefix. + * + * @return + */ + public String getRaw() { + String target = field.getTarget(); + return field.synthetic ? target : String.format("%s.%s", Fields.UNDERSCORE_ID, target); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("$%s", getRaw()); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java new file mode 100644 index 000000000..f12a43135 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java @@ -0,0 +1,67 @@ +/* + * 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 org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; + +import com.mongodb.DBObject; + +/** + * Support class to implement {@link AggregationOperation}s that will become an {@link AggregationOperationContext} as + * well defining {@link ExposedFields}. + * + * @author Oliver Gierke + * @since 1.3 + */ +public abstract class ExposedFieldsAggregationOperationContext implements AggregationOperationContext { + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(com.mongodb.DBObject) + */ + @Override + public DBObject getMappedObject(DBObject dbObject) { + return dbObject; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(org.springframework.data.mongodb.core.aggregation.ExposedFields.AvailableField) + */ + @Override + public FieldReference getReference(Field field) { + return getReference(field.getName()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(java.lang.String) + */ + @Override + public FieldReference getReference(String name) { + + ExposedField field = getFields().getField(name); + + if (field != null) { + return new FieldReference(field); + } + + throw new IllegalArgumentException(String.format("Invalid reference '%s'!", name)); + } + + protected abstract ExposedFields getFields(); +} 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/Field.java similarity index 63% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NoopAggreationOperation.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Field.java index f3ec5cc08..76a01f1af 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/Field.java @@ -15,17 +15,25 @@ */ package org.springframework.data.mongodb.core.aggregation; -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; - /** - * Represents a skippable AggregationOperation that is not considered for execution. + * Abstraction for a field. * - * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.3 */ -public class NoopAggreationOperation implements AggregationOperation { +public interface Field { + + /** + * Returns the name of the field. + * + * @return must not be {@literal null}. + */ + String getName(); - public DBObject toDbObject() { - return new BasicDBObject(); - } + /** + * Returns the target of the field. In case no explicit target is available {@link #getName()} should be returned. + * + * @return must not be {@literal null}. + */ + String getTarget(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java index 505b608e5..bb505566e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java @@ -15,17 +15,221 @@ */ package org.springframework.data.mongodb.core.aggregation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + /** - * Fields is a collection of key-value pairs. + * Value object to capture a list of {@link Field} instances. * - * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.3 */ -public interface Fields { - Map getValues(); +public class Fields implements Iterable { + + private static final String AMBIGUOUS_EXCEPTION = "Found two fields both using '%s' as name: %s and %s! Please " + + "customize your field definitions to get to unique field names!"; + + public static String UNDERSCORE_ID = "_id"; + public static String UNDERSCORE_ID_REF = "$_id"; + + private final List fields; + + /** + * Creates a new {@link Fields} instance from the given {@link Fields}. + * + * @param fields must not be {@literal null} or empty. + * @return + */ + public static Fields from(Field... fields) { + + Assert.notNull(fields, "Fields must not be null!"); + return new Fields(Arrays.asList(fields)); + } + + /** + * Creates a new {@link Fields} instance for {@link Field}s with the given names. + * + * @param names must not be {@literal null}. + * @return + */ + public static Fields fields(String... names) { + + Assert.notNull(names, "Field names must not be null!"); + + List fields = new ArrayList(); + + for (String name : names) { + fields.add(field(name)); + } + + return new Fields(fields); + } + + /** + * Creates a {@link Field} with the given name. + * + * @param name must not be {@literal null} or empty. + * @return + */ + public static Field field(String name) { + return new AggregationField(name); + } + + public static Field field(String name, String target) { + Assert.hasText(target, "Target must not be null or empty!"); + return new AggregationField(name, target); + } + + /** + * Creates a new {@link Fields} instance using the given {@link Field}s. + * + * @param fields must not be {@literal null}. + */ + private Fields(List fields) { + + Assert.notNull(fields, "Fields must not be null!"); + + this.fields = verify(fields); + } + + private static final List verify(List fields) { + + Map reference = new HashMap(); + + for (Field field : fields) { + + String name = field.getName(); + Field found = reference.get(name); + + if (found != null) { + throw new IllegalArgumentException(String.format(AMBIGUOUS_EXCEPTION, name, found, field)); + } + + reference.put(name, field); + } + + return fields; + } + + private Fields(Fields existing, Field tail) { + + this.fields = new ArrayList(existing.fields.size() + 1); + this.fields.addAll(existing.fields); + this.fields.add(tail); + } + + /** + * Creates a new {@link Fields} instance with a new {@link Field} of the given name added. + * + * @param name must not be {@literal null}. + * @return + */ + public Fields and(String name) { + return and(new AggregationField(name)); + } + + public Fields and(String name, String target) { + return and(new AggregationField(name, target)); + } + + public Fields and(Field field) { + return new Fields(this, field); + } + + public Fields and(Fields fields) { + + Fields result = this; + + for (Field field : fields) { + result = result.and(field); + } + + return result; + } + + public Field getField(String name) { + + for (Field field : fields) { + if (field.getName().equals(name)) { + return field; + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return fields.iterator(); + } + + /** + * Value object to encapsulate a field in an aggregation operation. + * + * @author Oliver Gierke + */ + static class AggregationField implements Field { + + private final String name; + private final String target; + + /** + * Creates an aggregation fieldwith the given name. As no target is set explicitly, the name will be used as target + * as well. + * + * @param key + */ + public AggregationField(String key) { + this(key, null); + } + + public AggregationField(String name, String target) { + + Assert.hasText(name, "AggregationField name must not be null or empty!"); + + if (target == null && name.contains(".")) { + this.name = name.substring(name.indexOf(".") + 1); + this.target = name; + } else { + this.name = name; + this.target = target; + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.Field#getKey() + */ + public String getName() { + return name; + } - Fields and(String name); + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.Field#getAlias() + */ + public String getTarget() { + return StringUtils.hasText(this.target) ? this.target : this.name; + } - Fields and(String name, Object value); + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("AggregationField - name: %s, target: %s", name, target); + } + } } 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 11b1499e0..d30a73727 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 @@ -18,21 +18,29 @@ package org.springframework.data.mongodb.core.aggregation; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.util.Assert; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + /** * @author Thomas Darimont + * @since 1.3 */ -public class GeoNearOperation extends AbstractAggregateOperation { +public class GeoNearOperation implements AggregationOperation { private final NearQuery nearQuery; public GeoNearOperation(NearQuery nearQuery) { - super("geoNear"); + Assert.notNull(nearQuery); this.nearQuery = nearQuery; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override - public Object getOperationArgument() { - return nearQuery.toDBObject(); + public DBObject toDBObject(AggregationOperationContext context) { + return new BasicDBObject("$geoNear", context.getMappedObject(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 3279d78eb..205edc469 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 @@ -17,9 +17,12 @@ package org.springframework.data.mongodb.core.aggregation; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Locale; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -30,392 +33,232 @@ import com.mongodb.DBObject; * @see http://docs.mongodb.org/manual/reference/aggregation/group/#stage._S_group * @author Sebastian Herold * @author Thomas Darimont + * @author Oliver Gierke * @since 1.3 */ -public class GroupOperation extends AbstractContextProducingAggregateOperation { +public class GroupOperation extends ExposedFieldsAggregationOperationContext implements AggregationOperation { - final Object id; - final List ops = new ArrayList(); + private final ExposedFields nonSynthecticFields; + private final List operations; /** - * Creates a $group operation with _id referencing to a field of the document. The returned - * db object equals to + * Creates a new {@link GroupOperation} including the given {@link Fields}. * - *
-	 * {_id: "$field"}
-	 * 
- * - * @param id - * @param moreIdFields + * @param fields must not be {@literal null}. */ public GroupOperation(Fields fields) { - super("group"); - this.id = createGroupIdFrom(fields); + + this.nonSynthecticFields = ExposedFields.nonSynthetic(fields); + this.operations = new ArrayList(); } /** - * @param fields + * Creates a new {@link GroupOperation} from the given {@link GroupOperation} and the given {@link Operation}. + * + * @param current must not be {@literal null}. + * @param operation must not be {@literal null}. + */ + protected GroupOperation(GroupOperation current, Operation operation) { + + Assert.notNull(current, "GroupOperation must not be null!"); + Assert.notNull(operation, "Operation must not be null!"); + + this.nonSynthecticFields = current.nonSynthecticFields; + this.operations = new ArrayList(current.operations.size() + 1); + this.operations.addAll(current.operations); + this.operations.add(operation); + } + + /** + * Creates a new {@link GroupOperation} from the current one adding the given {@link Operation}. + * + * @param operation must not be {@literal null}. * @return */ - private Object createGroupIdFrom(Fields fields) { - - Assert.notNull(fields, "fields must not be null!"); - Map values = fields.getValues(); - Assert.notEmpty(values, "fields.values must not be empty!"); - - 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 idReferences; + protected GroupOperation and(Operation operation) { + return new GroupOperation(this, operation); } - /* (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() + /** + * Returns a {@link GroupOperationBuilder} to build a grouping operation for the field with the given name + * + * @param field must not be {@literal null} or empty. + * @return */ - @Override - public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { + public GroupOperationBuilder and(String field) { + return new GroupOperationBuilder(field, this); + } + + public class GroupOperationBuilder { + + private final String name; + private final GroupOperation current; + + public GroupOperationBuilder(String name, GroupOperation current) { - DBObject projection = new BasicDBObject(); + Assert.hasText(name, "Field name must not be null or empty!"); + Assert.notNull(current, "GroupOperation must not be null!"); - Object idToUse = id; - if (idToUse instanceof DBObject) { - idToUse = createGroupIdObject((DBObject) idToUse, inputAggregateOperationContext); + this.name = name; + this.current = current; } - projection.put(ReferenceUtil.ID_KEY, idToUse); - for (GroupingOperation op : ops) { - projection.put(op.alias, op.toDbObject(inputAggregateOperationContext)); + public GroupOperation count() { + return sum(1); } - return projection; - } + public GroupOperation count(String reference) { + return sum(reference, 1); + } - /** - * @param idCandidate - * @param inputAggregateOperationContext - * @return - */ - private Object createGroupIdObject(DBObject groupIdObject, AggregateOperationContext inputAggregateOperationContext) { + public GroupOperation sum() { + return sum(name); + } - Object simpleIdOrNull = returnIfGroupIdIsSingleFieldReference(inputAggregateOperationContext, groupIdObject); - if (simpleIdOrNull != null) { - return simpleIdOrNull; + public GroupOperation sum(String reference) { + return sum(reference, null); } - DBObject idObject = new BasicDBObject(); - for (String idFieldName : groupIdObject.keySet()) { + public GroupOperation sum(Object value) { + return sum(null, value); + } - Object idFieldValue = groupIdObject.get(idFieldName); - Object idFieldValueOrNull = returnIfFieldValueReferencesAvailableField(inputAggregateOperationContext, - idFieldName, idFieldValue); - if (idFieldValueOrNull != null) { - idFieldValue = idFieldValueOrNull; - } + public GroupOperation sum(String reference, Object value) { + return current.and(new Operation(GroupOps.SUM, name, reference, value)); + } - getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName)); - idObject.put(idFieldName, idFieldValue); + public GroupOperation addToSet() { + return addToSet(null); } - return idObject; - } + public GroupOperation addToSet(String reference) { + return current.and(new Operation(GroupOps.ADD_TO_SET, name, reference, null)); + } - private Object returnIfGroupIdIsSingleFieldReference(AggregateOperationContext inputAggregateOperationContext, - DBObject idObject) { + public GroupOperation last() { + return last(null); + } - if (idObject.keySet().size() != 1) { - return null; + public GroupOperation last(String reference) { + return current.and(new Operation(GroupOps.LAST, name, reference, null)); } - return returnIfFieldNameIsSimpleReference(inputAggregateOperationContext, idObject, idObject.keySet().iterator() - .next()); - } + public GroupOperation first() { + return first(null); + } - private Object returnIfFieldValueReferencesAvailableField(AggregateOperationContext inputAggregateOperationContext, - String idFieldName, Object idFieldValue) { + public GroupOperation first(String reference) { + return current.and(new Operation(GroupOps.FIRST, name, reference, null)); + } - Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null"); + public GroupOperation avg() { + return avg(null); + } - if (!ReferenceUtil.isValueFieldReference(idFieldName, idFieldValue)) { - return null; + public GroupOperation avg(String reference) { + return current.and(new Operation(GroupOps.AVG, name, reference, null)); } + } - if (!inputAggregateOperationContext.isFieldAvailable(idFieldName)) { - return null; + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getFields() + */ + @Override + public ExposedFields getFields() { + + ExposedFields fields = this.nonSynthecticFields.and(new ExposedField(Fields.UNDERSCORE_ID, true)); + + for (Operation operation : operations) { + fields = fields.and(operation.asField()); } - String idFieldNameToUse = inputAggregateOperationContext.returnFieldNameAliasIfAvailableOr(idFieldName); - return ReferenceUtil.safeReference(inputAggregateOperationContext instanceof GroupOperation ? ReferenceUtil - .id(idFieldNameToUse) : idFieldNameToUse); + return fields; } - private Object returnIfFieldNameIsSimpleReference(AggregateOperationContext inputAggregateOperationContext, - DBObject idObject, String idFieldName) { + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public com.mongodb.DBObject toDBObject(AggregationOperationContext context) { - Object idFieldValue = idObject.get(idFieldName); + BasicDBObject operationObject = new BasicDBObject(); - if (!idFieldValueIsSimpleIdFieldExpression(idFieldName, idFieldValue)) { - return null; - } + if (nonSynthecticFields.exposesSingleFieldOnly()) { - getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName)); - idFieldValue = ReferenceUtil.safeReference(inputAggregateOperationContext - .returnFieldNameAliasIfAvailableOr(idFieldName)); + FieldReference reference = context.getReference(nonSynthecticFields.iterator().next()); + operationObject.put(Fields.UNDERSCORE_ID, reference.toString()); - return idFieldValue; - } + } else { - private static boolean idFieldValueIsSimpleIdFieldExpression(String idFieldName, Object idFieldValue) { - return idFieldValue instanceof String - && idFieldName.equals(ReferenceUtil.safeNonReference(idFieldValue.toString())); - } + BasicDBObject inner = new BasicDBObject(); - /** - * Adds a field with the $addToSet operation. - * - *
-	 * { $group : {
-	 *      _id : "$id_field",
-	 *     name : { $addToSet : "$field" }
-	 * }}
-	 * 
- * - * @see http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_addToSet - * @param name key of the field. - * @param field reference to a field of the document. - * @return - */ - public GroupOperation addToSet(String name, String field) { - return addOperation("$addToSet", name, field); - } + for (ExposedField field : nonSynthecticFields) { + FieldReference reference = context.getReference(field); + inner.put(field.getName(), reference.toString()); + } - /** - * Adds a field with the {@code $first} operation. - * - *
-	 * { $group : {
-	 *      _id : "$id_field",
-	 *     name : { $first : "$field" }
-	 * }}
-	 * 
- * - * @see http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_first - * @param name key of the field - * @param field reference to a field of the document - * @return - */ - public GroupOperation first(String name, String field) { - return addOperation("$first", name, field); - } + operationObject.put(Fields.UNDERSCORE_ID, inner); + } - /** - * Adds a field with the $last - * operation. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$last: "$field"}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @param field reference to a field of the document - * @return - */ - public GroupOperation last(String name, String field) { - return addOperation("$last", name, field); - } + for (Operation operation : operations) { + operationObject.putAll(operation.toDBObject(context)); + } - /** - * Adds a field with the $max - * operation. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$max: "$field"}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @param field reference to a field of the document - * @return - */ - public GroupOperation max(String name, String field) { - return addOperation("$max", name, field); + return new BasicDBObject("$group", operationObject); } - /** - * Adds a field with the $min - * operation. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$min: "$field"}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @param field reference to a field of the document - * @return - */ - public GroupOperation min(String name, String field) { - return addOperation("$min", name, field); - } + interface Keyword { - /** - * Adds a field with the $avg - * operation. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$avg: "$field"}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @param field reference to a field of the document - * @return - */ - public GroupOperation avg(String name, String field) { - return addOperation("$avg", name, field); + String toString(); } - /** - * Adds a field with the $push - * operation. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$push: "$field"}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @param field reference to a field of the document - * @return - */ - public GroupOperation push(String name, String field) { - return addOperation("$push", name, field); - } + private static enum GroupOps implements Keyword { - /** - * Adds a field with the $sum - * operation with a constant value. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$sum: increment}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @param increment increment for each item - * @return - */ - public GroupOperation count(String name, double increment) { - return sum(name, increment); - } + SUM, LAST, FIRST, PUSH, AVG, MIN, MAX, ADD_TO_SET, COUNT; - /** - * Adds a field with the $sum - * operation count every item. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$sum: 1}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @return - */ - public GroupOperation count(String name) { - return count(name, 1); - } + @Override + public String toString() { - private GroupOperation sum(String name, Object field) { - return addOperation("$sum", name, field); - } + String[] parts = name().split("_"); - /** - * Adds a field with the $sum - * operation. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          name: {$sum: "$field"}
-	 *     }}
-	 * 
- * - * @param name key of the field - * @param field reference to a field of the document - * @return - */ - public GroupOperation sum(String name, String field) { - return sum(name, (Object) field); - } + StringBuilder builder = new StringBuilder(); - /** - * Adds a field with the $sum - * operation. - * - *
-	 *     {$group: {
-	 *          _id: "$id_field",
-	 *          field: {$sum: "$field"}
-	 *     }}
-	 * 
- * - * @param field reference to a field of the document - * @return - */ - public GroupOperation sum(String field) { - return sum(field, field); + for (String part : parts) { + String lowerCase = part.toLowerCase(Locale.US); + builder.append(builder.length() == 0 ? lowerCase : StringUtils.capitalize(lowerCase)); + } + + return "$" + builder.toString(); + } } - protected GroupOperation addOperation(String operation, String name, Object field) { + static class Operation implements AggregationOperation { - getOutputAggregateOperationContext().registerAvailableField(name); - this.ops.add(new GroupingOperation(operation, name, field)); - return this; - } + private final Keyword op; + private final String key; + private final String reference; + private final Object value; - static class GroupingOperation { - final String operation; - final String alias; - final Object fieldNameOrValue; + public Operation(Keyword op, String key, String reference, Object value) { - public GroupingOperation(String operation, String alias, Object fieldNameOrValue) { - this.operation = operation; - this.alias = alias; - this.fieldNameOrValue = fieldNameOrValue; + this.op = op; + this.key = key; + this.reference = reference; + this.value = value; } - public DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext) { - - Object fieldNameOrValueToUse = fieldNameOrValue; + public ExposedField asField() { + return new ExposedField(key, true); + } - if (fieldNameOrValue instanceof String) { - if (inputAggregateOperationContext != null) { - fieldNameOrValueToUse = inputAggregateOperationContext - .returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse); - } - fieldNameOrValueToUse = ReferenceUtil.safeReference((String) fieldNameOrValueToUse); - } + public DBObject toDBObject(AggregationOperationContext context) { + return new BasicDBObject(key, new BasicDBObject(op.toString(), getValue(context))); + } - return new BasicDBObject(operation, fieldNameOrValueToUse); + public Object getValue(AggregationOperationContext context) { + return reference == null ? value : context.getReference(reference).toString(); } } } 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 36a432ded..ef6b02a37 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 @@ -15,13 +15,20 @@ */ package org.springframework.data.mongodb.core.aggregation; +import org.springframework.util.Assert; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + /** * Encapsulates the {@code $limit}-operation * * @see http://docs.mongodb.org/manual/reference/aggregation/limit/ * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.3 */ -class LimitOperation extends AbstractAggregateOperation { +class LimitOperation implements AggregationOperation { private final long maxElements; @@ -29,12 +36,17 @@ class LimitOperation extends AbstractAggregateOperation { * @param maxElements Number of documents to consider. */ public LimitOperation(long maxElements) { - super("limit"); + + Assert.isTrue(maxElements >= 0, "Maximum number of elements must be greater or equal to zero!"); this.maxElements = maxElements; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override - public Object getOperationArgument() { - return maxElements; + public DBObject toDBObject(AggregationOperationContext context) { + return new BasicDBObject("$limit", 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 74e82a09f..d8c65bb94 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 @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.util.Assert; +import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** @@ -26,10 +27,12 @@ import com.mongodb.DBObject; * @see http://docs.mongodb.org/manual/reference/aggregation/match/ * @author Sebastian Herold * @author Thomas Darimont + * @author Oliver Gierke * @since 1.3 */ -public class MatchOperation extends AbstractAggregateOperation { - private final DBObject criteria; +public class MatchOperation implements AggregationOperation { + + private final Criteria criteria; /** * Creates a new {@link MatchOperation} for the given {@link Criteria}. @@ -38,16 +41,16 @@ public class MatchOperation extends AbstractAggregateOperation { */ public MatchOperation(Criteria criteria) { - super("match"); Assert.notNull(criteria, "Criteria must not be null!"); - this.criteria = criteria.getCriteriaObject(); + this.criteria = criteria; } - /* (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override - public Object getOperationArgument() { - return criteria; + public DBObject toDBObject(AggregationOperationContext context) { + return new BasicDBObject("$match", context.getMappedObject(criteria.getCriteriaObject())); } } 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 7acc0833e..bc0165dfe 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 @@ -16,268 +16,386 @@ package org.springframework.data.mongodb.core.aggregation; import java.util.ArrayList; +import java.util.Arrays; 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; -import org.springframework.data.mongodb.core.query.Field; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection; import org.springframework.util.Assert; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** - * Encapsulates the aggregation framework {@code $project}-operation. - *

- * 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. + * Encapsulates the aggregation framework {@code $project}-operation. 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 + * @author Oliver Gierke * @since 1.3 */ -public class ProjectionOperation extends AbstractContextProducingAggregateOperation { +public class ProjectionOperation extends ExposedFieldsAggregationOperationContext implements AggregationOperation { - /** Stack of key names. Size is 0 or 1. */ - private final Stack reference = new Stack(); - private final Map projection = new HashMap(); + private static final List NONE = Collections.emptyList(); - private DBObject rightHandExpression; + private final List projections; - /** - * 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) { + public ProjectionOperation() { + this(NONE, NONE); + } - super("project"); + public ProjectionOperation(Fields fields) { + this(NONE, ProjectionOperationBuilder.FieldProjection.from(fields, true)); + } - Assert.notNull(includes, "includes must not be null"); - exclude("_id"); + private ProjectionOperation(List current, List projections) { - for (String key : includes) { - include(key); - } + this.projections = new ArrayList(current.size() + projections.size()); + this.projections.addAll(current); + this.projections.addAll(projections); + } + + protected ProjectionOperation and(Projection projection) { + return new ProjectionOperation(this.projections, Arrays.asList(projection)); } /** - * Create an empty projection. + * Creates a new {@link ProjectionOperationBuilder} to define a projection for the field with the given name. * - * @param targetClass + * @param name must not be {@literal null} or empty. + * @return */ - public ProjectionOperation(Class targetClass) { - this(extractFieldsFrom(targetClass)); + public ProjectionOperationBuilder and(String name) { + return new ProjectionOperationBuilder(name, this); } /** - * @param targetClass + * Excludes the given fields from the projection. + * + * @param fields must not be {@literal null}. * @return */ - private static String[] extractFieldsFrom(Class targetClass) { - return new String[0]; + public ProjectionOperation andExclude(String... fields) { + List excludeProjections = FieldProjection.from(Fields.fields(fields), false); + return new ProjectionOperation(this.projections, excludeProjections); } /** - * Excludes a given field. + * Includes the given fields into the projection. * - * @param key The key of the field. + * @param fields must not be {@literal null}. + * @return */ - public final ProjectionOperation exclude(String key) { - - Assert.hasText(key, "Missing key"); - getOutputAggregateOperationContext().unregisterAvailableField(ReferenceUtil.safeNonReference(key)); + public ProjectionOperation andInclude(String... fields) { - projection.put(key, 0); - return this; + List projections = FieldProjection.from(Fields.fields(fields), true); + return new ProjectionOperation(this.projections, projections); } /** - * Includes a given field. + * Includes the given fields into the projection. * - * @param key The key of the field, must not be {@literal null} or empty. + * @param fields must not be {@literal null}. + * @return + */ + public ProjectionOperation andInclude(Fields fields) { + return new ProjectionOperation(this.projections, FieldProjection.from(fields, true)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext#getFields() */ - public final ProjectionOperation include(String key) { + @Override + protected ExposedFields getFields() { - Assert.hasText(key, "Missing key"); + ExposedFields fields = null; - safePop(); - reference.push(key); - getOutputAggregateOperationContext().registerAvailableField(key); + for (Projection projection : projections) { + ExposedField field = projection.getField(); + fields = fields == null ? ExposedFields.from(field) : fields.and(field); + } - return this; + return fields; } - /** - * Sets the key for a computed field. - * - * @param key must not be {@literal null} or empty. + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ - public final ProjectionOperation as(String key) { + @Override + public DBObject toDBObject(AggregationOperationContext context) { - Assert.hasText(key, "Missing key"); + BasicDBObject fieldObject = new BasicDBObject(); - try { - 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); + for (Projection projection : projections) { + fieldObject.putAll(projection.toDBObject(context)); } - return this; + return new BasicDBObject("$project", fieldObject); } /** - * Sets the key for a computed field. + * Builder for {@link ProjectionOperation}s on a field. * - * @param key must not be {@literal null} or empty. + * @author Oliver Gierke */ - public final ProjectionOperation asSelf() { + public static class ProjectionOperationBuilder { - try { - String selfRef = reference.pop(); - projection.put(selfRef, rightHandSide(ReferenceUtil.safeReference(selfRef))); - } catch (EmptyStackException e) { - throw new InvalidDataAccessApiUsageException("Invalid use of as()", e); - } + private final String name; + private final ProjectionOperation operation; - return this; - } + /** + * Creates a new {@link ProjectionOperationBuilder} for the field with the given name on top of the given + * {@link ProjectionOperation}. + * + * @param name must not be {@literal null} or empty. + * @param operation must not be {@literal null}. + */ + public ProjectionOperationBuilder(String name, ProjectionOperation operation) { - public final ProjectionOperation plus(Number n) { - return arithmeticOperation("add", n); - } + Assert.hasText(name, "Field name must not be null or empty!"); + Assert.notNull(operation, "ProjectionOperation must not be null!"); - public final ProjectionOperation minus(Number n) { - return arithmeticOperation("substract", n); - } + this.name = name; + this.operation = operation; + } - private ProjectionOperation arithmeticOperation(String op, Number n) { + /** + * Projects the result of the previous operation onto the current field. Will automatically add an exclusion for + * {@code _id} as what would be held in it by default will now go into the field just projected into. + * + * @return + */ + public ProjectionOperation previousOperation() { - Assert.notNull(n, "Missing number"); - rightHandExpression = createArrayObject(op, ReferenceUtil.safeReference(reference.peek()), n); - return this; - } + return this.operation.andExclude(Fields.UNDERSCORE_ID) // + .and(new PreviousOperationProjection(name)); + } - private DBObject createArrayObject(String op, Object... items) { + /** + * Defines a nested field binding for the current field. + * + * @param fields must not be {@literal null}. + * @return + */ + public ProjectionOperation nested(Fields fields) { + return this.operation.and(new NestedFieldProjection(name, fields)); + } - List list = new ArrayList(); - Collections.addAll(list, items); + public ProjectionOperation plus(Number number) { + Assert.notNull(number, "Number must not be null!"); + return project("add", number); + } - return new BasicDBObject(ReferenceUtil.safeReference(op), list); - } + public ProjectionOperation minus(Number number) { + Assert.notNull(number, "Number must not be null!"); + return project("substract", number); + } - private void safePop() { + /** + * Adds a generic projection for the current field. + * + * @param operation the operation key, e.g. {@code $add}. + * @param values the values to be set for the projection operation. + * @return + */ + public ProjectionOperation project(String operation, Object... values) { + return this.operation.and(new OperationProjection(name, operation, values)); + } - if (!reference.empty()) { - projection.put(reference.pop(), rightHandSide(1)); + /** + * A {@link Projection} to pull in the result of the previous operation. + * + * @author Oliver Gierke + */ + static class PreviousOperationProjection extends Projection { + + private final String name; + + /** + * Creates a new {@link PreviousOperationProjection} for the field with the given name. + * + * @param name must not be {@literal null} or empty. + */ + public PreviousOperationProjection(String name) { + super(Fields.field(name)); + this.name = name; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDBObject(AggregationOperationContext context) { + return new BasicDBObject(name, Fields.UNDERSCORE_ID_REF); + } } - } - private Object rightHandSide(Object defaultValue) { - Object value = rightHandExpression != null ? rightHandExpression : defaultValue; - rightHandExpression = null; - return value; - } + /** + * A {@link FieldProjection} to map a result of a previous {@link AggregationOperation} to a new field. + * + * @author Oliver Gierke + */ + static class FieldProjection extends Projection { + + private final Field field; + private final Object value; + + /** + * Creates a new {@link FieldProjection} for the field of the given name, assigning the given value. + * + * @param name must not be {@literal null} or empty. + * @param value + */ + public FieldProjection(String name, Object value) { + this(Fields.field(name), value); + } - /** - * @param string - * @param projection - * @return - */ - public ProjectionOperation addField(String key, Object value) { + private FieldProjection(Field field, Object value) { + + super(field); - Assert.notNull(key, "Missing Key"); - Assert.notNull(value); + this.field = field; + this.value = value; + } - getOutputAggregateOperationContext().registerAvailableField(key); - registerAvailableFieldsRecursive(key, value); - this.projection.put(key, value); + /** + * Factory method to easily create {@link FieldProjection}s for the given {@link Fields}. + * + * @param fields the {@link Fields} to in- or exclude, must not be {@literal null}. + * @param include whether to include or exclude the fields. + * @return + */ + public static List from(Fields fields, boolean include) { - return this; - } + Assert.notNull(fields, "Fields must not be null!"); + List projections = new ArrayList(); + + for (Field field : fields) { + projections.add(new FieldProjection(field, include ? null : 0)); + } + + return projections; + } - private void registerAvailableFieldsRecursive(String outerKey, Object value) { + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDBObject(AggregationOperationContext context) { - 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)); + if (value != null) { + return new BasicDBObject(field.getName(), value); + } + + FieldReference reference = context.getReference(field.getTarget()); + return new BasicDBObject(field.getName(), reference.toString()); } } - } - /** - * @param name - * @param value - * @return - */ - public ProjectionOperation field(String name, Object value) { - addField(name, value); - return this; - } + static class OperationProjection extends Projection { - /* (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() - */ - @Override - public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { + private final String name; + private final String operation; + private final List values; - Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null"); - safePop(); + /** + * Creates a new {@link OperationProjection} for the given field. + * + * @param name the name of the field to add the operation projection for, must not be {@literal null} or empty. + * @param operation the actual operation key, must not be {@literal null} or empty. + * @param values the values to pass into the operation, must not be {@literal null}. + */ + public OperationProjection(String name, String operation, Object... values) { - DBObject projectionObject = new BasicDBObject(); - for (Map.Entry entry : projection.entrySet()) { - Object fieldNameOrValueToUse = entry.getValue(); + super(Fields.field(name)); - DBObject fieldsObject = returnIfValueIsIdFields(inputAggregateOperationContext, fieldNameOrValueToUse); - if (fieldsObject != null) { - projectionObject.put(entry.getKey(), fieldsObject != null ? fieldsObject : fieldNameOrValueToUse); - continue; + Assert.hasText(operation, "Operation must not be null or empty!"); + Assert.notNull(values, "Values must not be null!"); + + this.name = name; + this.operation = operation; + this.values = Arrays.asList(values); } - if (fieldNameOrValueToUse instanceof String) { - String fieldName = inputAggregateOperationContext - .returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse); - fieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName); + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDBObject(AggregationOperationContext context) { + + List values = buildReferences(context); + DBObject inner = new BasicDBObject(operation, values.size() == 1 ? values.get(0) : values.toArray()); + + return new BasicDBObject(name, inner); } - projectionObject.put(entry.getKey(), fieldNameOrValueToUse); + private List buildReferences(AggregationOperationContext context) { + + List result = new ArrayList(values.size()); + + for (Object element : values) { + result.add(element instanceof Field ? context.getReference((Field) element).toString() : element); + } + + return result; + } } - return projectionObject; - } + static class NestedFieldProjection extends Projection { - private DBObject returnIfValueIsIdFields(AggregateOperationContext inputAggregateOperationContext, - Object fieldNameOrValueToUse) { + private final String name; + private final Fields fields; - Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null"); + public NestedFieldProjection(String name, Fields fields) { - if (!(fieldNameOrValueToUse instanceof Fields)) { - return null; - } + super(Fields.field(name)); + this.name = name; + this.fields = fields; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDBObject(AggregationOperationContext context) { + + DBObject nestedObject = new BasicDBObject(); - DBObject fieldsObject = new BasicDBObject(); - for (Map.Entry fieldsEntry : ((Fields) fieldNameOrValueToUse).getValues().entrySet()) { + for (Field field : fields) { + nestedObject.put(field.getName(), context.getReference(field.getTarget()).toString()); + } - Object fieldsEntryFieldNameOrValueToUse = fieldsEntry.getValue(); - if (fieldsEntryFieldNameOrValueToUse instanceof String && inputAggregateOperationContext != null) { - String fieldName = inputAggregateOperationContext - .returnFieldNameAliasIfAvailableOr((String) fieldsEntryFieldNameOrValueToUse); - fieldsEntryFieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName); + return new BasicDBObject(name, nestedObject); } - fieldsObject.put(fieldsEntry.getKey(), fieldsEntryFieldNameOrValueToUse); } - return fieldsObject; + } + + private static abstract class Projection { + + private final ExposedField field; + + public Projection(Field name) { + + Assert.notNull(name, "Field must not be null!"); + this.field = new ExposedField(name, true); + } + + public ExposedField getField() { + return field; + } + + public abstract DBObject toDBObject(AggregationOperationContext context); } } 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 deleted file mode 100644 index 82bc19d2b..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 org.springframework.util.Assert; - -/** - * Utility class for mongo db reference operator $ - * - * @author Sebastian Herold - * @author Thomas Darimont - * @since 1.3 - */ -class ReferenceUtil { - - public static final String ID_KEY = "_id"; - - private static final String REFERENCE_PREFIX = "$"; - - /** - * Ensures that the returned string begins with {@link #REFERENCE_PREFIX}. - * - * @param key reference key with or without {@link #REFERENCE_PREFIX} at the beginning. - * @return key that definitely begins with {@link #REFERENCE_PREFIX}. - */ - public static String safeReference(String key) { - - Assert.hasText(key); - - if (!key.startsWith(REFERENCE_PREFIX)) { - return REFERENCE_PREFIX + key; - } else { - return key; - } - } - - /** - * Ensures that the returned string does not start with {@link #REFERENCE_PREFIX}. - * - * @param field reference key with or without {@link #REFERENCE_PREFIX} at the beginning. - * @return key that definitely does not begin with {@link #REFERENCE_PREFIX}. - */ - public static String safeNonReference(String field) { - - Assert.hasText(field); - - if (field.startsWith(REFERENCE_PREFIX)) { - return field.substring(REFERENCE_PREFIX.length()); - } - - return field; - } - - /** - * @return $_id - */ - public static String $id() { - return $(ID_KEY); - } - - /** - *
-	 *  $("a") -> $a
-	 * 
- * - * @param fieldName - * @return the field name prefixed with {@literal $} - * @see #safeReference(String) - */ - public static String $(String fieldName) { - return safeReference(fieldName); - } - - /** - *
-	 * 	$id("a") -> $_id.a
-	 * 
- * - * @param fieldName - * @return - * @see #safeNonReference(String) - */ - public static String $id(String fieldName) { - return $id() + "." + safeNonReference(fieldName); - } - - /** - * @param fieldName - * @return - */ - public static String id(String fieldName) { - 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 18dc68036..99c2dda20 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 @@ -15,26 +15,40 @@ */ package org.springframework.data.mongodb.core.aggregation; +import org.springframework.util.Assert; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + /** * Encapsulates the aggregation framework {@code $skip}-operation. * * @see http://docs.mongodb.org/manual/reference/aggregation/skip/ * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.3 */ -public class SkipOperation extends AbstractAggregateOperation { +public class SkipOperation implements AggregationOperation { private final long skipCount; /** + * Creates a new {@link SkipOperation} skipping the given number of elements. + * * @param skipCount number of documents to skip. */ public SkipOperation(long skipCount) { - super("skip"); + + Assert.isTrue(skipCount >= 0, "Skip count must not be negative!"); this.skipCount = skipCount; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override - public Object getOperationArgument() { - return skipCount; + public DBObject toDBObject(AggregationOperationContext context) { + return new BasicDBObject("$skip", 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 a57a4df5f..b405c4309 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 @@ -16,6 +16,9 @@ package org.springframework.data.mongodb.core.aggregation; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.util.Assert; import com.mongodb.BasicDBObject; @@ -26,42 +29,48 @@ import com.mongodb.DBObject; * * @see http://docs.mongodb.org/manual/reference/aggregation/sort/#pipe._S_sort * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.3 */ -public class SortOperation extends AbstractContextAwareAggregateOperation implements ContextConsumingAggregateOperation { +public class SortOperation implements AggregationOperation { - private Sort sort; + private final Sort sort; /** - * @param sort + * Creates a new {@link SortOperation} for the given {@link Sort} instance. + * + * @param sort must not be {@literal null}. */ public SortOperation(Sort sort) { - super("sort"); - Assert.notNull(sort); + Assert.notNull(sort, "Sort must not be null!"); this.sort = sort; } - public SortOperation and(Sort sort) { - return new SortOperation(this.sort.and(sort)); + public SortOperation and(Direction direction, String... fields) { + return and(new Sort(direction, fields)); } - public SortOperation and(Sort.Direction direction, String... fields) { - return and(new Sort(direction, fields)); + public SortOperation and(Sort sort) { + return new SortOperation(this.sort.and(sort)); } - /* (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument() + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override - public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { + public DBObject toDBObject(AggregationOperationContext context) { - Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null!"); + BasicDBObject object = new BasicDBObject(); - DBObject sortProperties = new BasicDBObject(); - for (org.springframework.data.domain.Sort.Order order : sort) { - String fieldName = inputAggregateOperationContext.returnFieldNameAliasIfAvailableOr(order.getProperty()); - sortProperties.put(fieldName, order.isAscending() ? 1 : -1); + for (Order order : sort) { + + // Check reference + FieldReference reference = context.getReference(order.getProperty()); + object.put(reference.getRaw(), order.isAscending() ? 1 : -1); } - return sortProperties; + + return new BasicDBObject("$sort", object); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java new file mode 100644 index 000000000..81644b620 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java @@ -0,0 +1,102 @@ +/* + * 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 static org.springframework.data.mongodb.core.aggregation.Fields.*; + +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.util.Assert; + +import com.mongodb.DBObject; + +/** + * {@link AggregationOperationContext} aware of a particular type and a {@link MappingContext} to potentially translate + * property references into document field names. + * + * @author Oliver Gierke + * @since 1.3 + */ +public class TypeBasedAggregationOperationContext implements AggregationOperationContext { + + private final Class type; + private final MappingContext, MongoPersistentProperty> mappingContext; + private final QueryMapper mapper; + + /** + * Creates a new {@link TypeBasedAggregationOperationContext} for the given type, {@link MappingContext} and + * {@link QueryMapper}. + * + * @param type must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @param mapper must not be {@literal null}. + */ + public TypeBasedAggregationOperationContext(Class type, + MappingContext, MongoPersistentProperty> mappingContext, QueryMapper mapper) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(mappingContext, "MappingContext must not be null!"); + Assert.notNull(mapper, "QueryMapper must not be null!"); + + this.type = type; + this.mappingContext = mappingContext; + this.mapper = mapper; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(com.mongodb.DBObject) + */ + @Override + public DBObject getMappedObject(DBObject dbObject) { + return mapper.getMappedObject(dbObject, mappingContext.getPersistentEntity(type)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(org.springframework.data.mongodb.core.aggregation.ExposedFields.AvailableField) + */ + @Override + public FieldReference getReference(Field field) { + + PropertyPath.from(field.getName(), type); + return getReferenceFor(field); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(java.lang.String) + */ + @Override + public FieldReference getReference(String name) { + PropertyPath path = PropertyPath.from(name, type); + + PersistentPropertyPath propertyPath = mappingContext.getPersistentPropertyPath(path); + + return getReferenceFor(field(path.getLeafProperty().getSegment(), + propertyPath.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE))); + } + + private FieldReference getReferenceFor(Field field) { + return new FieldReference(new ExposedField(field, true)); + } +} 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 504f0f206..9a13dc2a6 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 @@ -15,14 +15,17 @@ */ package org.springframework.data.mongodb.core.aggregation; +import org.springframework.util.Assert; + /** * A {@code TypedAggregation} is a special {@link Aggregation} that holds information of the input aggregation type. * * @author Thomas Darimont + * @author Oliver Gierke */ -public class TypedAggregation extends Aggregation { +public class TypedAggregation extends Aggregation { - private Class inputType; + private final Class inputType; /** * Creates a new {@link TypedAggregation} from the given {@link AggregationOperation}s. @@ -30,20 +33,19 @@ public class TypedAggregation extends Aggregation { * @param operations must not be {@literal null} or empty. */ public TypedAggregation(Class inputType, AggregationOperation... operations) { + super(operations); + + Assert.notNull(inputType, "Input type must not be null!"); this.inputType = inputType; } /** - * @return the inputType + * Returns the input type for the {@link Aggregation}. + * + * @return the inputType will never be {@literal null}. */ - public Class getInputType() { + public Class getInputType() { 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 51f3b2a4b..110bbd190 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 @@ -15,30 +15,50 @@ */ package org.springframework.data.mongodb.core.aggregation; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.util.Assert; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + /** * Encapsulates the aggregation framework {@code $unwind}-operation. * * @see http://docs.mongodb.org/manual/reference/aggregation/unwind/#pipe._S_unwind * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.3 */ -public class UnwindOperation extends AbstractContextProducingAggregateOperation implements - ContextConsumingAggregateOperation { +public class UnwindOperation extends ExposedFieldsAggregationOperationContext implements AggregationOperation { - private final String fieldName; + private final ExposedField field; - public UnwindOperation(String fieldName) { + /** + * Creates a new {@link UnwindOperation} for the given {@link Field}. + * + * @param field must not be {@literal null}. + */ + public UnwindOperation(Field field) { - super("unwind"); - Assert.notNull(fieldName); - this.fieldName = fieldName; + Assert.notNull(field); + this.field = new ExposedField(field, true); + } - getOutputAggregateOperationContext().registerAvailableField(fieldName, fieldName); + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext#getFields() + */ + @Override + protected ExposedFields getFields() { + return ExposedFields.from(field); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override - public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) { - return ReferenceUtil.safeReference(fieldName); + public DBObject toDBObject(AggregationOperationContext context) { + return new BasicDBObject("$unwind", context.getReference(field).toString()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java index 48d22806a..a098ec022 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java @@ -1,19 +1,5 @@ -/* - * 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. - */ /** - * @author Thomas Darimont + * Support for the MongoDB aggregation framework. + * @since 1.3 */ package org.springframework.data.mongodb.core.aggregation; \ No newline at end of file diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 47710ac9e..a803bc628 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -356,7 +356,7 @@ public class QueryMapper { protected final String name; /** - * Creates a new {@link Field} without meta-information but the given name. + * Creates a new {@link DocumentField} without meta-information but the given name. * * @param name must not be {@literal null} or empty. */ @@ -367,7 +367,7 @@ public class QueryMapper { } /** - * Returns a new {@link Field} with the given name. + * Returns a new {@link DocumentField} with the given name. * * @param name must not be {@literal null} or empty. * @return @@ -423,7 +423,7 @@ public class QueryMapper { } /** - * Extension of {@link Field} to be backed with mapping metadata. + * Extension of {@link DocumentField} to be backed with mapping metadata. * * @author Oliver Gierke */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index 624ed64b5..dfa1e7465 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -51,7 +51,7 @@ import org.springframework.util.StringUtils; public class BasicMongoPersistentEntity extends BasicPersistentEntity implements MongoPersistentEntity, ApplicationContextAware { - private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @Field annotation!"; + private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @DocumentField annotation!"; private final String collection; private final SpelExpressionParser parser; private final StandardEvaluationContext context; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java index de5cbe386..8d6deb88a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java @@ -18,7 +18,7 @@ package org.springframework.data.mongodb.core.mapping; /** * SPI interface to determine how to name document fields in cases the field name is not manually defined. * - * @see Field + * @see DocumentField * @see PropertyNameFieldNamingStrategy * @see CamelCaseAbbreviatingFieldNamingStrategy * @since 1.3 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java index cafff20bb..e24bb4bcf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java @@ -73,7 +73,7 @@ public class Field { */ public Field position(String field, int value) { - Assert.hasText(field, "Field must not be null or empty!"); + Assert.hasText(field, "DocumentField must not be null or empty!"); postionKey = field; positionValue = value; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UnwrapAndReadDbObjectCallbackUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UnwrapAndReadDbObjectCallbackUnitTests.java new file mode 100644 index 000000000..4f842dcfb --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UnwrapAndReadDbObjectCallbackUnitTests.java @@ -0,0 +1,95 @@ +/* + * 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; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.MongoTemplate.UnwrapAndReadDbObjectCallback; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +import com.mongodb.BasicDBObject; + +/** + * Unit tests for {@link UnwrapAndReadDbObjectCallback}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class UnwrapAndReadDbObjectCallbackUnitTests { + + @Mock MongoDbFactory factory; + + UnwrapAndReadDbObjectCallback callback; + + @Before + public void setUp() { + + MongoTemplate template = new MongoTemplate(factory); + MappingMongoConverter converter = new MappingMongoConverter(factory, new MongoMappingContext()); + + this.callback = template.new UnwrapAndReadDbObjectCallback(converter, Target.class); + } + + @Test + public void usesFirstLevelValues() { + + Target target = callback.doWith(new BasicDBObject("foo", "bar")); + + assertThat(target.id, is(nullValue())); + assertThat(target.foo, is("bar")); + } + + @Test + public void unwrapsUnderscoreIdIfBasicDBObject() { + + Target target = callback.doWith(new BasicDBObject("_id", new BasicDBObject("foo", "bar"))); + + assertThat(target.id, is(nullValue())); + assertThat(target.foo, is("bar")); + } + + @Test + public void firstLevelPropertiesTrumpNestedOnes() { + + Target target = callback.doWith(new BasicDBObject("_id", new BasicDBObject("foo", "bar")).append("foo", "foobar")); + + assertThat(target.id, is(nullValue())); + assertThat(target.foo, is("foobar")); + } + + @Test + public void keepsUnderscoreIdIfScalarValue() { + + Target target = callback.doWith(new BasicDBObject("_id", "bar").append("foo", "foo")); + + assertThat(target.id, is("bar")); + assertThat(target.foo, is("foo")); + } + + static class Target { + + String id; + String foo; + } +} 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 deleted file mode 100644 index 9070fbb10..000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationPipelineTests.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.springframework.data.domain.Sort.Direction.*; -import static org.springframework.data.mongodb.core.DBObjectUtils.*; -import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; - -import org.junit.Test; -import org.springframework.data.mongodb.core.query.Criteria; - -import com.mongodb.DBObject; - -/** - * Tests of the {@link AggregationPipeline}. - * - * @see DATAMONGO-586 - * @author Tobias Trelle - * @author Thomas Darimont - */ -public class AggregationPipelineTests { - - @Test - public void limitOperation() { - - assertSingleDBObject("$limit", 42L, limit(42).toDbObject()); - } - - @Test - public void skipOperation() { - - assertSingleDBObject("$skip", 5L, skip(5).toDbObject()); - } - - @Test - public void unwindOperation() { - - assertSingleDBObject("$unwind", "$field", unwind("$field").toDbObject(new BasicAggregateOperationContext())); - } - - @Test - public void unwindOperationWithAddedPrefix() { - - assertSingleDBObject("$unwind", "$field", unwind("field").toDbObject(new BasicAggregateOperationContext())); - } - - @Test - public void matchOperation() { - - DBObject match = match(new Criteria("title").is("Doc 1")).toDbObject(); - DBObject criteriaDoc = getAsDBObject(match, "$match"); - assertThat(criteriaDoc, is(notNullValue())); - assertSingleDBObject("title", "Doc 1", criteriaDoc); - } - - @Test - public void sortOperation() { - - DBObject sortDoc = sort(ASC, "n").toDbObject(new BasicAggregateOperationContext()); - DBObject orderDoc = getAsDBObject(sortDoc, "$sort"); - assertThat(orderDoc, is(notNullValue())); - assertSingleDBObject("n", 1, orderDoc); - } - - @Test - public void projectOperation() { - - DBObject projectionDoc = project("a").toDbObject(new BasicAggregateOperationContext()); - DBObject fields = getAsDBObject(projectionDoc, "$project"); - assertThat(fields, is(notNullValue())); - assertSingleDBObject("a", 1, fields); - } - - private static void assertSingleDBObject(String key, Object value, DBObject doc) { - assertThat(doc.get(key), is(value)); - } -} 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 b67e4a1ee..81b5cf4ad 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 @@ -55,26 +55,24 @@ import com.mongodb.util.JSON; * @see DATAMONGO-586 * @author Tobias Trelle * @author Thomas Darimont + * @author Oliver Gierke */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") public class AggregationTests { private static final String INPUT_COLLECTION = "aggregation_test_collection"; - private static boolean initialized = false; private static final Logger LOGGER = LoggerFactory.getLogger(AggregationTests.class); + private static boolean initialized = false; + @Autowired MongoTemplate mongoTemplate; @Before public void setUp() { + cleanDb(); initSampleDataIfNecessary(); - - CommandResult result = mongoTemplate.executeCommand("{ buildInfo: 1 }"); - Object version = result.get("version"); - - LOGGER.debug("Server uses MongoDB Version: {}", version); } @After @@ -97,8 +95,9 @@ public class AggregationTests { if (!initialized) { - CommandResult result = mongoTemplate.executeCommand(new BasicDBObject("buildInfo", 1)); - LOGGER.error(result.toString()); + CommandResult result = mongoTemplate.executeCommand("{ buildInfo: 1 }"); + Object version = result.get("version"); + LOGGER.debug("Server uses MongoDB Version: {}", version); mongoTemplate.dropCollection(ZipInfo.class); mongoTemplate.execute(ZipInfo.class, new CollectionCallback() { @@ -133,17 +132,17 @@ public class AggregationTests { @Test(expected = IllegalArgumentException.class) public void shouldHandleMissingInputCollection() { - mongoTemplate.aggregate((String) null, new Aggregation(), TagCount.class); + mongoTemplate.aggregate(newAggregation(), (String) null, TagCount.class); } @Test(expected = IllegalArgumentException.class) public void shouldHandleMissingAggregationPipeline() { - mongoTemplate.aggregate(INPUT_COLLECTION, null, TagCount.class); + mongoTemplate.aggregate(null, INPUT_COLLECTION, TagCount.class); } @Test(expected = IllegalArgumentException.class) public void shouldHandleMissingEntityClass() { - mongoTemplate.aggregate(INPUT_COLLECTION, new Aggregation(), null); + mongoTemplate.aggregate(newAggregation(), INPUT_COLLECTION, null); } @Test @@ -151,20 +150,22 @@ public class AggregationTests { createTagDocuments(); - Aggregation agg = newAggregation( // + Aggregation agg = newAggregation( // project("tags"), // unwind("tags"), // - group("tags").count("n"), // - project().field("tag", $id()).field("n", 1), // + group("tags") // + .and("n").count(), // + project("n") // + .and("tag").previousOperation(), // sort(DESC, "n") // ); - AggregationResults results = mongoTemplate.aggregate(INPUT_COLLECTION, agg, TagCount.class); + AggregationResults results = mongoTemplate.aggregate(agg, INPUT_COLLECTION, TagCount.class); assertThat(results, is(notNullValue())); assertThat(results.getServerUsed(), is("/127.0.0.1:27017")); - List tagCount = results.getAggregationResult(); + List tagCount = results.getMappedResults(); assertThat(tagCount, is(notNullValue())); assertThat(tagCount.size(), is(3)); @@ -177,20 +178,22 @@ public class AggregationTests { @Test public void shouldAggregateEmptyCollection() { - Aggregation agg = newAggregation(// + Aggregation aggregation = newAggregation(// project("tags"), // unwind("tags"), // - group("tags").count("n"), // - project().field("tag", $id()).field("n", 1), // + group("tags") // + .and("n").count(), // + project("n") // + .and("tag").previousOperation(), // sort(DESC, "n") // ); - AggregationResults results = mongoTemplate.aggregate(INPUT_COLLECTION, agg, TagCount.class); + AggregationResults results = mongoTemplate.aggregate(aggregation, INPUT_COLLECTION, TagCount.class); assertThat(results, is(notNullValue())); assertThat(results.getServerUsed(), is("/127.0.0.1:27017")); - List tagCount = results.getAggregationResult(); + List tagCount = results.getMappedResults(); assertThat(tagCount, is(notNullValue())); assertThat(tagCount.size(), is(0)); @@ -200,19 +203,21 @@ public class AggregationTests { public void shouldDetectResultMismatch() { createTagDocuments(); - Aggregation agg = newAggregation( // + + Aggregation aggregation = newAggregation( // project("tags"), // unwind("tags"), // - group("tags").count("count"), // + group("tags") // + .and("count").count(), // limit(2) // ); - AggregationResults results = mongoTemplate.aggregate(INPUT_COLLECTION, agg, TagCount.class); + AggregationResults results = mongoTemplate.aggregate(aggregation, INPUT_COLLECTION, TagCount.class); assertThat(results, is(notNullValue())); assertThat(results.getServerUsed(), is("/127.0.0.1:27017")); - List tagCount = results.getAggregationResult(); + List tagCount = results.getMappedResults(); assertThat(tagCount, is(notNullValue())); assertThat(tagCount.size(), is(2)); @@ -220,61 +225,6 @@ public class AggregationTests { assertTagCount(null, 0, tagCount.get(1)); } - @Test - public void fieldsFactoryMethod() { - - Fields fields = fields("a", "b").and("c").and("d", 42); - 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("d"), is((Object) 42)); - } - - @Test - public void shouldCreateSimpleIdForGroupOperationWithSingleSimpleIdField() { - - Fields fields = fields("a"); - GroupOperation groupOperation = new GroupOperation(fields); - - 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() { /* @@ -337,31 +287,31 @@ public class AggregationTests { ) */ - TypedAggregation agg = newAggregation(ZipInfo.class, // - group("state", "city").sum("pop"), // + TypedAggregation aggregation = newAggregation(ZipInfo.class, // + group("state", "city").and("pop").sum("population"), // 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()) // - .field("biggestCity", pick("name", "biggestCity").and("population", "biggestPop")) // - .field("smallestCity", pick("name", "smallestCity").and("population", "smallestPop")), // + .and("biggestCity").last("city") // + .and("biggestPop").last("pop") // + .and("smallestCity").first("city") // + .and("smallestPop").first("pop"), // + project() // + // .and(previousOperation()).exclude() // + .and("state").previousOperation() // + .and("biggestCity").nested(bind("name", "biggestCity").and("population", "biggestPop")) // + .and("smallestCity").nested(bind("name", "smallestCity").and("population", "smallestPop")), // sort(ASC, "state") // ); - assertThat(agg, is(notNullValue())); - assertThat(agg.toString(), is(notNullValue())); + assertThat(aggregation, is(notNullValue())); + assertThat(aggregation.toString(), is(notNullValue())); - AggregationResults result = mongoTemplate.aggregate(agg, ZipInfoStats.class); + AggregationResults result = mongoTemplate.aggregate(aggregation, ZipInfoStats.class); assertThat(result, is(notNullValue())); - assertThat(result.getAggregationResult(), is(notNullValue())); - assertThat(result.getAggregationResult().size(), is(51)); + assertThat(result.getMappedResults(), is(notNullValue())); + assertThat(result.getMappedResults().size(), is(51)); - ZipInfoStats firstZipInfoStats = result.getAggregationResult().get(0); + ZipInfoStats firstZipInfoStats = result.getMappedResults().get(0); assertThat(firstZipInfoStats, is(notNullValue())); assertThat(firstZipInfoStats.id, is(nullValue())); assertThat(firstZipInfoStats.state, is("AK")); @@ -372,7 +322,7 @@ public class AggregationTests { assertThat(firstZipInfoStats.biggestCity.name, is("ANCHORAGE")); assertThat(firstZipInfoStats.biggestCity.population, is(183987)); - ZipInfoStats lastZipInfoStats = result.getAggregationResult().get(50); + ZipInfoStats lastZipInfoStats = result.getMappedResults().get(50); assertThat(lastZipInfoStats, is(notNullValue())); assertThat(lastZipInfoStats.id, is(nullValue())); assertThat(lastZipInfoStats.state, is("WY")); @@ -408,9 +358,10 @@ public class AggregationTests { ) */ - TypedAggregation agg = newAggregation(ZipInfo.class, // - group("state").sum("totalPop", "pop"), // - sort(ASC, id(), "totalPop"), // + TypedAggregation agg = newAggregation(ZipInfo.class, // + group("state") // + .and("totalPop").sum("population"), // + sort(ASC, previousOperation(), "totalPop"), // match(where("totalPop").gte(10 * 1000 * 1000)) // ); @@ -419,10 +370,10 @@ public class AggregationTests { AggregationResults result = mongoTemplate.aggregate(agg, StateStats.class); assertThat(result, is(notNullValue())); - assertThat(result.getAggregationResult(), is(notNullValue())); - assertThat(result.getAggregationResult().size(), is(7)); + assertThat(result.getMappedResults(), is(notNullValue())); + assertThat(result.getMappedResults().size(), is(7)); - StateStats stateStats = result.getAggregationResult().get(0); + StateStats stateStats = result.getMappedResults().get(0); assertThat(stateStats, is(notNullValue())); assertThat(stateStats.id, is("CA")); assertThat(stateStats.state, is(nullValue())); @@ -447,12 +398,12 @@ public class AggregationTests { */ - TypedAggregation agg = newAggregation(UserWithLikes.class, // + TypedAggregation agg = newAggregation(UserWithLikes.class, // unwind("likes"), // - group("likes").count("number"), // + group("likes").and("number").count(), // sort(DESC, "number"), // limit(5), // - sort(ASC, id()) // + sort(ASC, previousOperation()) // ); assertThat(agg, is(notNullValue())); @@ -460,14 +411,14 @@ public class AggregationTests { AggregationResults result = mongoTemplate.aggregate(agg, LikeStats.class); assertThat(result, is(notNullValue())); - assertThat(result.getAggregationResult(), is(notNullValue())); - assertThat(result.getAggregationResult().size(), is(5)); - - assertLikeStats(result.getAggregationResult().get(0), "a", 4); - assertLikeStats(result.getAggregationResult().get(1), "b", 2); - assertLikeStats(result.getAggregationResult().get(2), "c", 4); - assertLikeStats(result.getAggregationResult().get(3), "d", 2); - assertLikeStats(result.getAggregationResult().get(4), "e", 3); + assertThat(result.getMappedResults(), is(notNullValue())); + assertThat(result.getMappedResults().size(), is(5)); + + assertLikeStats(result.getMappedResults().get(0), "a", 4); + assertLikeStats(result.getMappedResults().get(1), "b", 2); + assertLikeStats(result.getMappedResults().get(2), "c", 4); + assertLikeStats(result.getMappedResults().get(3), "d", 2); + assertLikeStats(result.getMappedResults().get(4), "e", 3); } private void assertLikeStats(LikeStats like, String id, long count) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java new file mode 100644 index 000000000..fb0a933f2 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java @@ -0,0 +1,46 @@ +/* + * 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 org.junit.Test; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; + +/** + * @author Oliver Gierke + */ +public class AggregationUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullAggregationOperation() { + Aggregation.newAggregation((AggregationOperation[]) null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullTypedAggregationOperation() { + Aggregation.newAggregation(String.class, (AggregationOperation[]) null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNoAggregationOperation() { + Aggregation.newAggregation(new AggregationOperation[0]); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNoTypedAggregationOperation() { + Aggregation.newAggregation(String.class, new AggregationOperation[0]); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java new file mode 100644 index 000000000..50ef25076 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java @@ -0,0 +1,55 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; + +/** + * Unit tests for {@link ExposedFields}. + * + * @author Oliver Gierke + */ +public class ExposedFieldsUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFields() { + ExposedFields.from((ExposedField) null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFieldsForSynthetics() { + ExposedFields.synthetic(null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFieldsForNonSynthetics() { + ExposedFields.nonSynthetic(null); + } + + @Test + public void exposesSingleField() { + + ExposedFields fields = ExposedFields.synthetic(Fields.fields("foo")); + assertThat(fields.exposesSingleFieldOnly(), is(true)); + + fields = fields.and(new ExposedField("bar", true)); + assertThat(fields.exposesSingleFieldOnly(), is(false)); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java new file mode 100644 index 000000000..54a3fffd9 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java @@ -0,0 +1,115 @@ +/* + * 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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.aggregation.Fields.*; + +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; + +/** + * Unit tests for {@link Fields}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class FieldsUnitTests { + + @Rule public ExpectedException exception = ExpectedException.none(); + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFieldVarArgs() { + Fields.from((Field[]) null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFieldNameVarArgs() { + Fields.fields((String[]) null); + } + + @Test + public void createsFieldFromNameOnly() { + verify(Fields.field("foo"), "foo", null); + } + + @Test + public void createsFieldFromNameAndTarget() { + verify(Fields.field("foo", "bar"), "foo", "bar"); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFieldName() { + Fields.field(null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFieldNameIfTargetGiven() { + Fields.field(null, "foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyFieldName() { + Fields.field(""); + } + + @Test + public void createsFieldsFromFieldInstances() { + + AggregationField reference = new AggregationField("foo"); + Fields fields = Fields.from(reference); + + assertThat(fields, is(Matchers. iterableWithSize(1))); + assertThat(fields, hasItem(reference)); + } + + @Test + public void aliasesPathExpressionsIntoLeafForImplicits() { + verify(Fields.field("foo.bar"), "bar", "foo.bar"); + } + + @Test + public void fieldsFactoryMethod() { + + Fields fields = fields("a", "b").and("c").and("d", "e"); + + assertThat(fields, is(Matchers. iterableWithSize(4))); + + verify(fields.getField("a"), "a", null); + verify(fields.getField("b"), "b", null); + verify(fields.getField("c"), "c", null); + verify(fields.getField("d"), "d", "e"); + } + + @Test + public void rejectsAmbiguousFieldNames() { + + exception.expect(IllegalArgumentException.class); + + fields("b", "a.b"); + } + + private static void verify(Field field, String name, String target) { + + assertThat(field, is(notNullValue())); + assertThat(field.getName(), is(name)); + assertThat(field.getTarget(), is(target != null ? target : name)); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java similarity index 50% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java index 658165834..b1eb43bd2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java @@ -15,22 +15,30 @@ */ package org.springframework.data.mongodb.core.aggregation; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.mongodb.core.DBObjectUtils; +import org.springframework.data.mongodb.core.query.NearQuery; + 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. + * Unit tests for {@link GeoNearOperation}. * - * @author Thomas Darimont + * @author Oliver Gierke */ -public interface ContextConsumingAggregateOperation extends AggregationOperation { +public class GeoNearOperationUnitTests { + + @Test + public void rendersNearQueryAsAggregationOperation() { + + NearQuery query = NearQuery.near(10.0, 10.0); + GeoNearOperation operation = new GeoNearOperation(query); + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - /** - * 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); + DBObject nearClause = DBObjectUtils.getAsDBObject(dbObject, "$geoNear"); + assertThat(nearClause, is(query.toDBObject())); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java new file mode 100644 index 000000000..5bed23d1e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java @@ -0,0 +1,87 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.aggregation.Fields.*; + +import org.junit.Test; +import org.springframework.data.mongodb.core.DBObjectUtils; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Unit tests for {@link GroupOperation}. + * + * @author Oliver Gierke + */ +public class GroupOperationUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFields() { + new GroupOperation(null); + } + + @Test + public void createsGroupOperationWithSingleField() { + + GroupOperation operation = new GroupOperation(fields("a")); + + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + DBObject groupClause = DBObjectUtils.getAsDBObject(dbObject, "$group"); + + assertThat(groupClause.get(UNDERSCORE_ID), is((Object) "$a")); + } + + @Test + public void createsGroupOperationWithMultipleFields() { + + GroupOperation operation = new GroupOperation(fields("a").and("b", "c")); + + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + DBObject groupClause = DBObjectUtils.getAsDBObject(dbObject, "$group"); + DBObject idClause = DBObjectUtils.getAsDBObject(groupClause, UNDERSCORE_ID); + + assertThat(idClause.get("a"), is((Object) "$a")); + assertThat(idClause.get("b"), is((Object) "$c")); + } + + @Test + public void shouldCreateComplexIdForGroupOperationWithSingleComplexIdField() { + + // Fields fields = fields().and("a", 42); + // GroupOperation groupOperation = new GroupOperation(fields()); + // + // assertThat(groupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT), 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").and("c"); // .and("d", 42); + GroupOperation groupOperation = new GroupOperation(fields).and("e").sum(); + + DBObject dbObject = groupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT); + + DBObject groupClause = DBObjectUtils.getAsDBObject(dbObject, "$group"); + DBObject eOp = DBObjectUtils.getAsDBObject(groupClause, "e"); + assertThat(eOp, is((DBObject) new BasicDBObject("$sum", "$e"))); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java new file mode 100644 index 000000000..98de7d038 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.mongodb.core.DBObjectUtils; + +import com.mongodb.DBObject; + +/** + * Unit tests for {@link ProjectionOperation}. + * + * @author Oliver Gierke + */ +public class ProjectionOperationUnitTests { + + static final String PROJECT = "$project"; + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullFields() { + new ProjectionOperation(null); + } + + @Test + public void declaresBackReferenceCorrectly() { + + ProjectionOperation operation = new ProjectionOperation(); + operation = operation.and("prop").previousOperation(); + + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + assertThat(projectClause.get("prop"), is((Object) Fields.UNDERSCORE_ID_REF)); + } + + @Test + public void alwaysUsesExplicitReference() { + + ProjectionOperation operation = new ProjectionOperation(Fields.fields("foo").and("bar", "foobar")); + + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + + assertThat(projectClause.get("foo"), is((Object) "$foo")); + assertThat(projectClause.get("bar"), is((Object) "$foobar")); + } +} 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 deleted file mode 100644 index e8c886a44..000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionTests.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; - -import java.util.List; - -import org.junit.Test; -import org.springframework.dao.InvalidDataAccessApiUsageException; - -import com.mongodb.DBObject; - -/** - * Tests of {@link ProjectionOperation}. - * - * @see DATAMONGO-586 - * @author Tobias Trelle - */ -public class ProjectionTests { - - @Test - public void emptyProjection() { - - DBObject raw = safeExtractDbObjectFromProjection(project()); - assertThat(raw.toMap().size(), is(1)); - assertThat((Integer) raw.get("_id"), is(0)); - } - - @Test(expected = IllegalArgumentException.class) - public void shouldDetectNullIncludesInConstructor() { - new ProjectionOperation((String[]) null); - } - - @Test - public void includesWithConstructor() { - - DBObject raw = safeExtractDbObjectFromProjection(project("a", "b")); - assertThat(raw, is(notNullValue())); - assertThat(raw.toMap().size(), is(3)); - assertThat((Integer) raw.get("_id"), is(0)); - assertThat((Integer) raw.get("a"), is(1)); - assertThat((Integer) raw.get("b"), is(1)); - } - - @Test - public void include() { - - DBObject raw = safeExtractDbObjectFromProjection(project().include("a")); - assertSingleDBObject("a", 1, raw); - } - - @Test - public void exclude() { - - DBObject raw = safeExtractDbObjectFromProjection(project().exclude("a")); - 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")); - assertThat(raw.toMap().size(), is(2)); - assertThat((Integer) raw.get("_id"), is(0)); - assertThat((String) raw.get("b"), is("$a")); - } - - @Test(expected = InvalidDataAccessApiUsageException.class) - public void shouldDetectAliasWithoutInclude() { - project().as("b"); - } - - @Test(expected = InvalidDataAccessApiUsageException.class) - public void shouldDetectDuplicateAlias() { - project().include("a").as("b").as("c"); - } - - @Test - @SuppressWarnings("unchecked") - public void plus() { - - DBObject raw = safeExtractDbObjectFromProjection(project().include("a").plus(10)); - assertThat(raw, is(notNullValue())); - - DBObject addition = (DBObject) raw.get("a"); - assertThat(addition, is(notNullValue())); - - List summands = (List) addition.get("$add"); - assertThat(summands, is(notNullValue())); - assertThat(summands.size(), is(2)); - assertThat((String) summands.get(0), is("$a")); - assertThat((Integer) summands.get(1), is(10)); - } - - @Test - @SuppressWarnings("unchecked") - public void plusWithAlias() { - - DBObject raw = safeExtractDbObjectFromProjection(project().include("a").plus(10).as("b")); - assertThat(raw, is(notNullValue())); - - DBObject addition = (DBObject) raw.get("b"); - assertThat(addition, is(notNullValue())); - - List summands = (List) addition.get("$add"); - assertThat(summands, is(notNullValue())); - assertThat(summands.size(), is(2)); - assertThat((String) summands.get(0), is("$a")); - assertThat((Integer) summands.get(1), is(10)); - } - - @Test - public void projectionWithFields() { - ProjectionOperation projectionOperation = 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"))); - - assertThat(projectionOperation, is(notNullValue())); - } - - private static DBObject safeExtractDbObjectFromProjection(ProjectionOperation projectionOperation) { - - assertThat(projectionOperation, is(notNullValue())); - 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); - - return DBObject.class.cast(projection); - } - - private static void assertSingleDBObject(String key, Object value, DBObject doc) { - - assertThat(doc, is(notNullValue())); - assertThat(doc.get(key), is(value)); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SkipOperationUnitTests.java similarity index 52% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SkipOperationUnitTests.java index ef5579e1c..60bd05d06 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SkipOperationUnitTests.java @@ -15,12 +15,33 @@ */ package org.springframework.data.mongodb.core.aggregation; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +import com.mongodb.DBObject; + /** - * Represents one single operation in an aggregation pipeline that is aware of an {@link AggregateOperationContext} that - * produces an {@link AggregateOperationContext} as output. + * Unit tests for {@link SkipOperation}. * - * @author Thomas Darimont + * @author Oliver Gierke */ -public interface ContextProducingAggregateOperation extends AggregationOperation { - AggregateOperationContext getOutputAggregateOperationContext(); +public class SkipOperationUnitTests { + + static final String OP = "$skip"; + + @Test(expected = IllegalArgumentException.class) + public void rejectsNegativeSkip() { + new SkipOperation(-1L); + } + + @Test + public void rendersSkipOperation() { + + SkipOperation operation = new SkipOperation(10L); + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(dbObject.get(OP), is((Object) 10L)); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java new file mode 100644 index 000000000..b1c760a0d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java @@ -0,0 +1,56 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.DBObjectUtils.*; + +import org.junit.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; + +import com.mongodb.DBObject; + +/** + * Unit tests for {@link SortOperation}. + * + * @author Oliver Gierke + */ +public class SortOperationUnitTests { + + @Test + public void createsDBObjectForAscendingSortCorrectly() { + + SortOperation operation = new SortOperation(new Sort(Direction.ASC, "foobar")); + DBObject result = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + + DBObject sortValue = getAsDBObject(result, "$sort"); + assertThat(sortValue, is(notNullValue())); + assertThat(sortValue.get("foobar"), is((Object) 1)); + } + + @Test + public void createsDBObjectForDescendingSortCorrectly() { + + SortOperation operation = new SortOperation(new Sort(Direction.DESC, "foobar")); + DBObject result = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + + DBObject sortValue = getAsDBObject(result, "$sort"); + assertThat(sortValue, is(notNullValue())); + assertThat(sortValue.get("foobar"), is((Object) (0 - 1))); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/FieldUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/FieldUnitTests.java index 29085ea53..7c801f5ca 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/FieldUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/FieldUnitTests.java @@ -21,7 +21,7 @@ import static org.junit.Assert.*; import org.junit.Test; /** - * Unit tests for {@link Field}. + * Unit tests for {@link DocumentField}. * * @author Oliver Gierke */