Browse Source

DATAMONGO-586 - Next round of polishing.

Refined the way the aggregation pipeline gets rendered into a DBObject. More tests. Added $avg and shortcut methods to GroupOperations. Fixed ProjectionOperation to use 1 for implicit references. Made ProjectionOperation publicly visible. Added automatic result unwrapping. API consistency, tests, JavaDoc polish.
pull/58/merge
Oliver Gierke 13 years ago
parent
commit
e57fe346c0
  1. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
  2. 77
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  3. 58
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregateOperation.java
  4. 63
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextAwareAggregateOperation.java
  5. 39
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextProducingAggregateOperation.java
  6. 63
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregateOperationContext.java
  7. 314
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
  8. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java
  9. 55
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java
  10. 13
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java
  11. 48
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BackendFields.java
  12. 62
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BasicAggregateOperationContext.java
  13. 286
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java
  14. 67
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
  15. 24
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Field.java
  16. 216
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java
  17. 16
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
  18. 453
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java
  19. 20
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LimitOperation.java
  20. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java
  21. 412
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java
  22. 121
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java
  23. 22
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SkipOperation.java
  24. 45
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortOperation.java
  25. 102
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
  26. 22
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypedAggregation.java
  27. 40
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java
  28. 18
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java
  29. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  30. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
  31. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java
  32. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java
  33. 95
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UnwrapAndReadDbObjectCallbackUnitTests.java
  34. 92
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationPipelineTests.java
  35. 181
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
  36. 46
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java
  37. 55
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java
  38. 115
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java
  39. 32
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java
  40. 87
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java
  41. 62
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java
  42. 159
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionTests.java
  43. 31
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SkipOperationUnitTests.java
  44. 56
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java
  45. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/FieldUnitTests.java

19
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java

@ -311,13 +311,12 @@ public interface MongoOperations { @@ -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
*/
<I, O> AggregationResults<O> aggregate(TypedAggregation<I, O> aggregation, String inputCollectionName,
Class<O> outputType);
<O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, String collectionName, Class<O> 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 { @@ -329,33 +328,33 @@ public interface MongoOperations {
* @return The results of the aggregation operation.
* @since 1.3
*/
<I, O> AggregationResults<O> aggregate(TypedAggregation<I, O> aggregation, Class<O> outputType);
<O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, Class<O> 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
*/
<I, O> AggregationResults<O> aggregate(Class<I> inputType, Aggregation<I, O> aggregation, Class<O> outputType);
<O> AggregationResults<O> aggregate(Aggregation aggregation, Class<?> inputType, Class<O> 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
*/
<O> AggregationResults<O> aggregate(String inputCollectionName, Aggregation<?, O> aggregation, Class<O> outputType);
<O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> outputType);
/**
* Execute a map-reduce operation. The map-reduce operation will be formed with an output type of INLINE

77
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -54,7 +54,10 @@ import org.springframework.data.mapping.model.BeanWrapper; @@ -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 { @@ -1215,31 +1218,47 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
}
@Override
public <I, O> AggregationResults<O> aggregate(TypedAggregation<I, O> aggregation, Class<O> outputType) {
public <O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, Class<O> outputType) {
return aggregate(aggregation, determineCollectionName(aggregation.getInputType()), outputType);
}
@Override
public <I, O> AggregationResults<O> aggregate(TypedAggregation<I, O> aggregation, String inputCollectionName,
public <O> AggregationResults<O> aggregate(TypedAggregation<?> aggregation, String inputCollectionName,
Class<O> 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 <I, O> AggregationResults<O> aggregate(Class<I> inputType, Aggregation<I, O> aggregation, Class<O> outputType) {
return aggregate(determineCollectionName(inputType), aggregation, outputType);
@Override
public <O> AggregationResults<O> aggregate(Aggregation aggregation, Class<?> inputType, Class<O> outputType) {
return aggregate(aggregation, determineCollectionName(inputType), outputType,
new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper));
}
public <O> AggregationResults<O> aggregate(String inputCollectionName, Aggregation<? extends Object, O> aggregation,
Class<O> outputType) {
@Override
public <O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> 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 <O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> 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 { @@ -1247,7 +1266,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
@SuppressWarnings("unchecked")
Iterable<DBObject> resultSet = (Iterable<DBObject>) commandResult.get("result");
List<O> mappedResults = new ArrayList<O>();
DbObjectCallback<O> callback = new ReadDbObjectCallback<O>(mongoConverter, outputType);
DbObjectCallback<O> callback = new UnwrapAndReadDbObjectCallback<O>(mongoConverter, outputType);
for (DBObject dbObject : resultSet) {
mappedResults.add(callback.doWith(dbObject));
}
@ -1920,6 +1940,35 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { @@ -1920,6 +1940,35 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
}
}
class UnwrapAndReadDbObjectCallback<T> extends ReadDbObjectCallback<T> {
public UnwrapAndReadDbObjectCallback(EntityReader<? super T, DBObject> reader, Class<T> 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;

58
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregateOperation.java

@ -1,58 +0,0 @@ @@ -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());
}
}

63
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextAwareAggregateOperation.java

@ -1,63 +0,0 @@ @@ -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));
}
}

39
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractContextProducingAggregateOperation.java

@ -1,39 +0,0 @@ @@ -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;
}
}

63
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregateOperationContext.java

@ -1,63 +0,0 @@ @@ -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<String, String> 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);
}

314
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java

@ -15,13 +15,19 @@ @@ -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; @@ -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<I, O> {
public class Aggregation {
public static final AggregationOperationContext DEFAULT_CONTEXT = new NoOpAggregationOperationContext();
private final List<AggregationOperation> operations = new ArrayList<AggregationOperation>();
private final List<AggregationOperation> 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 <T> TypedAggregation<T> newAggregation(Class<T> type, AggregationOperation... operations) {
return new TypedAggregation<T>(type, operations);
}
private List<DBObject> getOperationObjects() {
AggregateOperationContext aggregateOperationContext = createInitialAggregateOperationContext();
List<DBObject> operationObjects = new ArrayList<DBObject>();
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}.
* <p>
* A call to fields("a","b","c") generates:
* <p>
*
* <pre>
* {
* a: $a,
* b: $b,
* c: $c
* }
* </pre>
* 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<DBObject> operationDocuments = new ArrayList<DBObject>(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));
if (operation instanceof AggregationOperationContext) {
context = (AggregationOperationContext) operation;
}
}
/**
* A convenience shortcut to {@link ReferenceUtil#$(String)}
*
* @return
*/
public static String $(String fieldName) {
return ReferenceUtil.$(fieldName);
DBObject command = new BasicDBObject("aggregate", inputCollectionName);
command.put("pipeline", operationDocuments);
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()));
}
/**
* A convenience shortcut to {@link ReferenceUtil#ID_KEY}
* Simple {@link AggregationOperationContext} that just returns {@link FieldReference}s as is.
*
* @return
* @author Oliver Gierke
*/
public static String id() {
return ReferenceUtil.ID_KEY;
}
private static class NoOpAggregationOperationContext implements AggregationOperationContext {
/**
* A convenience shortcut to {@link ReferenceUtil#id(String)}
*
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(com.mongodb.DBObject)
*/
public static String id(String fieldName) {
return ReferenceUtil.id(fieldName);
@Override
public DBObject getMappedObject(DBObject dbObject) {
return dbObject;
}
/**
* Creates a new {@link Aggregation}.
*
* @param <I> the input type of the {@link Aggregation}.
* @param <O> the output type of the {@link Aggregation}.
* @param inputType
* @param operations
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(org.springframework.data.mongodb.core.aggregation.ExposedFields.AvailableField)
*/
public static <I, O> TypedAggregation<I, O> newAggregation(Class<I> inputType, AggregationOperation... operations) {
return new TypedAggregation<I, O>(inputType, operations);
@Override
public FieldReference getReference(Field field) {
return new FieldReference(new ExposedField(field, true));
}
/**
* Creates a new {@link Aggregation}.
*
* @param <I> the input type of the {@link Aggregation}.
* @param <O> the output type of the {@link Aggregation}.
* @param inputType
* @param operations
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(java.lang.String)
*/
public static <I, O> Aggregation<I, O> newAggregation(AggregationOperation... operations) {
return new Aggregation<I, O>(operations);
@Override
public FieldReference getReference(String name) {
return new FieldReference(new ExposedField(new AggregationField(name), true));
}
}
}

8
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java

@ -22,16 +22,16 @@ import com.mongodb.DBObject; @@ -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);
}

55
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java

@ -0,0 +1,55 @@ @@ -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);
}

13
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java

@ -58,10 +58,21 @@ public class AggregationResults<T> implements Iterable<T> { @@ -58,10 +58,21 @@ public class AggregationResults<T> implements Iterable<T> {
*
* @return
*/
public List<T> getAggregationResult() {
public List<T> 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()

48
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BackendFields.java

@ -1,48 +0,0 @@ @@ -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<String, Object> values = new HashMap<String, Object>();
/**
* @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<String, Object> getValues() {
return new HashMap<String, Object>(this.values);
}
}

62
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BasicAggregateOperationContext.java

@ -1,62 +0,0 @@ @@ -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<String, String> availableFields = new LinkedHashMap<String, String>();
@Override
public Map<String, String> getAvailableFields() {
return new HashMap<String, String>(getAvailableFieldsInternal());
}
protected Map<String, String> 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);
}
}

286
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java

@ -0,0 +1,286 @@ @@ -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<ExposedField> {
private static final List<ExposedField> NO_FIELDS = Collections.emptyList();
private static final ExposedFields EMPTY = new ExposedFields(NO_FIELDS, NO_FIELDS);
private final List<ExposedField> originalFields;
private final List<ExposedField> 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<ExposedField> 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<ExposedField> result = new ArrayList<ExposedField>();
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<ExposedField> originals, List<ExposedField> 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<ExposedField> result = new ArrayList<ExposedField>();
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<ExposedField> iterator() {
CompositeIterator<ExposedField> iterator = new CompositeIterator<ExposedField>();
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());
}
}
}

67
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java

@ -0,0 +1,67 @@ @@ -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();
}

24
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NoopAggreationOperation.java → spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Field.java

@ -15,17 +15,25 @@ @@ -15,17 +15,25 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Abstraction for a field.
*
* @author Oliver Gierke
* @since 1.3
*/
public interface Field {
/**
* Represents a skippable AggregationOperation that is not considered for execution.
* Returns the name of the field.
*
* @author Thomas Darimont
* @return must not be {@literal null}.
*/
public class NoopAggreationOperation implements AggregationOperation {
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();
}

216
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java

@ -15,17 +15,221 @@ @@ -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;
/**
* Value object to capture a list of {@link Field} instances.
*
* @author Oliver Gierke
* @since 1.3
*/
public class Fields implements Iterable<Field> {
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<Field> 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<Field> fields = new ArrayList<Field>();
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<Field> fields) {
Assert.notNull(fields, "Fields must not be null!");
this.fields = verify(fields);
}
private static final List<Field> verify(List<Field> fields) {
Map<String, Field> reference = new HashMap<String, Field>();
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<Field>(existing.fields.size() + 1);
this.fields.addAll(existing.fields);
this.fields.add(tail);
}
/**
* Fields is a collection of key-value pairs.
* Creates a new {@link Fields} instance with a new {@link Field} of the given name added.
*
* @author Thomas Darimont
* @param name must not be {@literal null}.
* @return
*/
public interface Fields {
Map<String, Object> getValues();
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;
}
Fields and(String name);
public Field getField(String name) {
Fields and(String name, Object value);
for (Field field : fields) {
if (field.getName().equals(name)) {
return field;
}
}
return null;
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Field> 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;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.Field#getAlias()
*/
public String getTarget() {
return StringUtils.hasText(this.target) ? this.target : this.name;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("AggregationField - name: %s, target: %s", name, target);
}
}
}

16
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; @@ -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()));
}
}

453
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; @@ -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; @@ -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<GroupingOperation> ops = new ArrayList<GroupOperation.GroupingOperation>();
private final ExposedFields nonSynthecticFields;
private final List<Operation> operations;
/**
* Creates a <code>$group</code> operation with <code>_id</code> referencing to a field of the document. The returned
* db object equals to
* Creates a new {@link GroupOperation} including the given {@link Fields}.
*
* <pre>
* {_id: "$field"}
* </pre>
*
* @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<Operation>();
}
/**
* @param fields
* @return
* 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}.
*/
private Object createGroupIdFrom(Fields fields) {
protected GroupOperation(GroupOperation current, Operation operation) {
Assert.notNull(fields, "fields must not be null!");
Map<String, Object> values = fields.getValues();
Assert.notEmpty(values, "fields.values must not be empty!");
Assert.notNull(current, "GroupOperation must not be null!");
Assert.notNull(operation, "Operation must not be null!");
DBObject idReferences = new BasicDBObject(values.size());
for (Map.Entry<String, Object> 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;
this.nonSynthecticFields = current.nonSynthecticFields;
this.operations = new ArrayList<Operation>(current.operations.size() + 1);
this.operations.addAll(current.operations);
this.operations.add(operation);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
/**
* Creates a new {@link GroupOperation} from the current one adding the given {@link Operation}.
*
* @param operation must not be {@literal null}.
* @return
*/
@Override
public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) {
DBObject projection = new BasicDBObject();
Object idToUse = id;
if (idToUse instanceof DBObject) {
idToUse = createGroupIdObject((DBObject) idToUse, inputAggregateOperationContext);
}
projection.put(ReferenceUtil.ID_KEY, idToUse);
for (GroupingOperation op : ops) {
projection.put(op.alias, op.toDbObject(inputAggregateOperationContext));
}
return projection;
protected GroupOperation and(Operation operation) {
return new GroupOperation(this, operation);
}
/**
* @param idCandidate
* @param inputAggregateOperationContext
* 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
*/
private Object createGroupIdObject(DBObject groupIdObject, AggregateOperationContext inputAggregateOperationContext) {
Object simpleIdOrNull = returnIfGroupIdIsSingleFieldReference(inputAggregateOperationContext, groupIdObject);
if (simpleIdOrNull != null) {
return simpleIdOrNull;
public GroupOperationBuilder and(String field) {
return new GroupOperationBuilder(field, this);
}
DBObject idObject = new BasicDBObject();
for (String idFieldName : groupIdObject.keySet()) {
public class GroupOperationBuilder {
Object idFieldValue = groupIdObject.get(idFieldName);
Object idFieldValueOrNull = returnIfFieldValueReferencesAvailableField(inputAggregateOperationContext,
idFieldName, idFieldValue);
if (idFieldValueOrNull != null) {
idFieldValue = idFieldValueOrNull;
}
private final String name;
private final GroupOperation current;
getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName));
idObject.put(idFieldName, idFieldValue);
}
public GroupOperationBuilder(String name, GroupOperation current) {
return idObject;
}
Assert.hasText(name, "Field name must not be null or empty!");
Assert.notNull(current, "GroupOperation must not be null!");
private Object returnIfGroupIdIsSingleFieldReference(AggregateOperationContext inputAggregateOperationContext,
DBObject idObject) {
if (idObject.keySet().size() != 1) {
return null;
this.name = name;
this.current = current;
}
return returnIfFieldNameIsSimpleReference(inputAggregateOperationContext, idObject, idObject.keySet().iterator()
.next());
public GroupOperation count() {
return sum(1);
}
private Object returnIfFieldValueReferencesAvailableField(AggregateOperationContext inputAggregateOperationContext,
String idFieldName, Object idFieldValue) {
Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null");
if (!ReferenceUtil.isValueFieldReference(idFieldName, idFieldValue)) {
return null;
public GroupOperation count(String reference) {
return sum(reference, 1);
}
if (!inputAggregateOperationContext.isFieldAvailable(idFieldName)) {
return null;
public GroupOperation sum() {
return sum(name);
}
String idFieldNameToUse = inputAggregateOperationContext.returnFieldNameAliasIfAvailableOr(idFieldName);
return ReferenceUtil.safeReference(inputAggregateOperationContext instanceof GroupOperation ? ReferenceUtil
.id(idFieldNameToUse) : idFieldNameToUse);
public GroupOperation sum(String reference) {
return sum(reference, null);
}
private Object returnIfFieldNameIsSimpleReference(AggregateOperationContext inputAggregateOperationContext,
DBObject idObject, String idFieldName) {
Object idFieldValue = idObject.get(idFieldName);
if (!idFieldValueIsSimpleIdFieldExpression(idFieldName, idFieldValue)) {
return null;
public GroupOperation sum(Object value) {
return sum(null, value);
}
getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName));
idFieldValue = ReferenceUtil.safeReference(inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr(idFieldName));
public GroupOperation sum(String reference, Object value) {
return current.and(new Operation(GroupOps.SUM, name, reference, value));
}
return idFieldValue;
public GroupOperation addToSet() {
return addToSet(null);
}
private static boolean idFieldValueIsSimpleIdFieldExpression(String idFieldName, Object idFieldValue) {
return idFieldValue instanceof String
&& idFieldName.equals(ReferenceUtil.safeNonReference(idFieldValue.toString()));
public GroupOperation addToSet(String reference) {
return current.and(new Operation(GroupOps.ADD_TO_SET, name, reference, null));
}
/**
* Adds a field with the <a
* href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_addToSet">$addToSet operation</a>.
*
* <pre>
* { $group : {
* _id : "$id_field",
* name : { $addToSet : "$field" }
* }}
* </pre>
*
* @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);
public GroupOperation last() {
return last(null);
}
/**
* Adds a field with the {@code $first} operation.
*
* <pre>
* { $group : {
* _id : "$id_field",
* name : { $first : "$field" }
* }}
* </pre>
*
* @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);
public GroupOperation last(String reference) {
return current.and(new Operation(GroupOps.LAST, name, reference, null));
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_last">$last
* operation</a>.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$last: "$field"}
* }}
* </pre>
*
* @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);
public GroupOperation first() {
return first(null);
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_max">$max
* operation</a>.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$max: "$field"}
* }}
* </pre>
*
* @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);
public GroupOperation first(String reference) {
return current.and(new Operation(GroupOps.FIRST, name, reference, null));
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_min">$min
* operation</a>.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$min: "$field"}
* }}
* </pre>
*
* @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);
public GroupOperation avg() {
return avg(null);
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_avg">$avg
* operation</a>.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$avg: "$field"}
* }}
* </pre>
*
* @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);
public GroupOperation avg(String reference) {
return current.and(new Operation(GroupOps.AVG, name, reference, null));
}
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_push">$push
* operation</a>.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$push: "$field"}
* }}
* </pre>
*
* @param name key of the field
* @param field reference to a field of the document
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getFields()
*/
public GroupOperation push(String name, String field) {
return addOperation("$push", name, field);
@Override
public ExposedFields getFields() {
ExposedFields fields = this.nonSynthecticFields.and(new ExposedField(Fields.UNDERSCORE_ID, true));
for (Operation operation : operations) {
fields = fields.and(operation.asField());
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum
* operation</a> with a constant value.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$sum: increment}
* }}
* </pre>
*
* @param name key of the field
* @param increment increment for each item
* @return
*/
public GroupOperation count(String name, double increment) {
return sum(name, increment);
return fields;
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum
* operation</a> count every item.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$sum: 1}
* }}
* </pre>
*
* @param name key of the field
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
public GroupOperation count(String name) {
return count(name, 1);
@Override
public com.mongodb.DBObject toDBObject(AggregationOperationContext context) {
BasicDBObject operationObject = new BasicDBObject();
if (nonSynthecticFields.exposesSingleFieldOnly()) {
FieldReference reference = context.getReference(nonSynthecticFields.iterator().next());
operationObject.put(Fields.UNDERSCORE_ID, reference.toString());
} else {
BasicDBObject inner = new BasicDBObject();
for (ExposedField field : nonSynthecticFields) {
FieldReference reference = context.getReference(field);
inner.put(field.getName(), reference.toString());
}
private GroupOperation sum(String name, Object field) {
return addOperation("$sum", name, field);
operationObject.put(Fields.UNDERSCORE_ID, inner);
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum
* operation</a>.
*
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$sum: "$field"}
* }}
* </pre>
*
* @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);
for (Operation operation : operations) {
operationObject.putAll(operation.toDBObject(context));
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum
* operation</a>.
*
* <pre>
* {$group: {
* _id: "$id_field",
* field: {$sum: "$field"}
* }}
* </pre>
*
* @param field reference to a field of the document
* @return
*/
public GroupOperation sum(String field) {
return sum(field, field);
return new BasicDBObject("$group", operationObject);
}
protected GroupOperation addOperation(String operation, String name, Object field) {
interface Keyword {
getOutputAggregateOperationContext().registerAvailableField(name);
this.ops.add(new GroupingOperation(operation, name, field));
return this;
String toString();
}
static class GroupingOperation {
final String operation;
final String alias;
final Object fieldNameOrValue;
private static enum GroupOps implements Keyword {
SUM, LAST, FIRST, PUSH, AVG, MIN, MAX, ADD_TO_SET, COUNT;
@Override
public String toString() {
public GroupingOperation(String operation, String alias, Object fieldNameOrValue) {
this.operation = operation;
this.alias = alias;
this.fieldNameOrValue = fieldNameOrValue;
String[] parts = name().split("_");
StringBuilder builder = new StringBuilder();
for (String part : parts) {
String lowerCase = part.toLowerCase(Locale.US);
builder.append(builder.length() == 0 ? lowerCase : StringUtils.capitalize(lowerCase));
}
public DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext) {
return "$" + builder.toString();
}
}
static class Operation implements AggregationOperation {
private final Keyword op;
private final String key;
private final String reference;
private final Object value;
Object fieldNameOrValueToUse = fieldNameOrValue;
public Operation(Keyword op, String key, String reference, Object value) {
if (fieldNameOrValue instanceof String) {
if (inputAggregateOperationContext != null) {
fieldNameOrValueToUse = inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse);
this.op = op;
this.key = key;
this.reference = reference;
this.value = value;
}
fieldNameOrValueToUse = ReferenceUtil.safeReference((String) fieldNameOrValueToUse);
public ExposedField asField() {
return new ExposedField(key, true);
}
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();
}
}
}

20
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LimitOperation.java

@ -15,13 +15,20 @@ @@ -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 { @@ -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);
}
}

19
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; @@ -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; @@ -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 { @@ -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()));
}
}

412
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java

@ -16,268 +16,386 @@ @@ -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.
* <p>
* 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.
* <p>
*
* @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<String> reference = new Stack<String>();
private final Map<String, Object> projection = new HashMap<String, Object>();
private static final List<Projection> NONE = Collections.emptyList();
private DBObject rightHandExpression;
private final List<Projection> 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<? extends Projection> current, List<? extends Projection> projections) {
for (String key : includes) {
include(key);
this.projections = new ArrayList<ProjectionOperation.Projection>(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<FieldProjection> 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) {
public ProjectionOperation andInclude(String... fields) {
Assert.hasText(key, "Missing key");
getOutputAggregateOperationContext().unregisterAvailableField(ReferenceUtil.safeNonReference(key));
projection.put(key, 0);
return this;
List<FieldProjection> 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 final ProjectionOperation include(String key) {
public ProjectionOperation andInclude(Fields fields) {
return new ProjectionOperation(this.projections, FieldProjection.from(fields, true));
}
Assert.hasText(key, "Missing key");
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext#getFields()
*/
@Override
protected ExposedFields getFields() {
safePop();
reference.push(key);
getOutputAggregateOperationContext().registerAvailableField(key);
ExposedFields fields = null;
return this;
for (Projection projection : projections) {
ExposedField field = projection.getField();
fields = fields == null ? ExposedFields.from(field) : fields.and(field);
}
/**
* Sets the key for a computed field.
*
* @param key must not be {@literal null} or empty.
return fields;
}
/*
* (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;
/**
* 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) {
Assert.hasText(name, "Field name must not be null or empty!");
Assert.notNull(operation, "ProjectionOperation must not be null!");
this.name = name;
this.operation = operation;
}
return this;
/**
* 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() {
return this.operation.andExclude(Fields.UNDERSCORE_ID) //
.and(new PreviousOperationProjection(name));
}
public final ProjectionOperation plus(Number n) {
return arithmeticOperation("add", n);
/**
* 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));
}
public final ProjectionOperation minus(Number n) {
return arithmeticOperation("substract", n);
public ProjectionOperation plus(Number number) {
Assert.notNull(number, "Number must not be null!");
return project("add", number);
}
private ProjectionOperation arithmeticOperation(String op, Number n) {
public ProjectionOperation minus(Number number) {
Assert.notNull(number, "Number must not be null!");
return project("substract", number);
}
Assert.notNull(n, "Missing number");
rightHandExpression = createArrayObject(op, ReferenceUtil.safeReference(reference.peek()), n);
return this;
/**
* 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));
}
private DBObject createArrayObject(String op, Object... items) {
/**
* A {@link Projection} to pull in the result of the previous operation.
*
* @author Oliver Gierke
*/
static class PreviousOperationProjection extends Projection {
List<Object> list = new ArrayList<Object>();
Collections.addAll(list, items);
private final String name;
return new BasicDBObject(ReferenceUtil.safeReference(op), list);
/**
* 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;
}
private void safePop() {
if (!reference.empty()) {
projection.put(reference.pop(), rightHandSide(1));
/*
* (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);
}
private FieldProjection(Field field, Object value) {
super(field);
this.field = field;
this.value = value;
}
/**
* @param string
* @param projection
* 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 ProjectionOperation addField(String key, Object value) {
public static List<FieldProjection> from(Fields fields, boolean include) {
Assert.notNull(key, "Missing Key");
Assert.notNull(value);
Assert.notNull(fields, "Fields must not be null!");
List<FieldProjection> projections = new ArrayList<FieldProjection>();
getOutputAggregateOperationContext().registerAvailableField(key);
registerAvailableFieldsRecursive(key, value);
this.projection.put(key, value);
for (Field field : fields) {
projections.add(new FieldProjection(field, include ? null : 0));
}
return this;
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<String, Object> 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());
}
}
static class OperationProjection extends Projection {
private final String name;
private final String operation;
private final List<Object> values;
/**
* @param name
* @param value
* @return
* 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 ProjectionOperation field(String name, Object value) {
addField(name, value);
return this;
public OperationProjection(String name, String operation, Object... values) {
super(Fields.field(name));
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);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#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");
safePop();
List<Object> values = buildReferences(context);
DBObject inner = new BasicDBObject(operation, values.size() == 1 ? values.get(0) : values.toArray());
DBObject projectionObject = new BasicDBObject();
for (Map.Entry<String, Object> entry : projection.entrySet()) {
Object fieldNameOrValueToUse = entry.getValue();
DBObject fieldsObject = returnIfValueIsIdFields(inputAggregateOperationContext, fieldNameOrValueToUse);
if (fieldsObject != null) {
projectionObject.put(entry.getKey(), fieldsObject != null ? fieldsObject : fieldNameOrValueToUse);
continue;
return new BasicDBObject(name, inner);
}
if (fieldNameOrValueToUse instanceof String) {
String fieldName = inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse);
fieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName);
private List<Object> buildReferences(AggregationOperationContext context) {
List<Object> result = new ArrayList<Object>(values.size());
for (Object element : values) {
result.add(element instanceof Field ? context.getReference((Field) element).toString() : element);
}
projectionObject.put(entry.getKey(), fieldNameOrValueToUse);
return result;
}
}
static class NestedFieldProjection extends Projection {
private final String name;
private final Fields fields;
return projectionObject;
public NestedFieldProjection(String name, Fields fields) {
super(Fields.field(name));
this.name = name;
this.fields = fields;
}
private DBObject returnIfValueIsIdFields(AggregateOperationContext inputAggregateOperationContext,
Object fieldNameOrValueToUse) {
/*
* (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();
Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null");
for (Field field : fields) {
nestedObject.put(field.getName(), context.getReference(field.getTarget()).toString());
}
if (!(fieldNameOrValueToUse instanceof Fields)) {
return null;
return new BasicDBObject(name, nestedObject);
}
}
}
private static abstract class Projection {
DBObject fieldsObject = new BasicDBObject();
for (Map.Entry<String, Object> fieldsEntry : ((Fields) fieldNameOrValueToUse).getValues().entrySet()) {
private final ExposedField field;
Object fieldsEntryFieldNameOrValueToUse = fieldsEntry.getValue();
if (fieldsEntryFieldNameOrValueToUse instanceof String && inputAggregateOperationContext != null) {
String fieldName = inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr((String) fieldsEntryFieldNameOrValueToUse);
fieldsEntryFieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName);
public Projection(Field name) {
Assert.notNull(name, "Field must not be null!");
this.field = new ExposedField(name, true);
}
fieldsObject.put(fieldsEntry.getKey(), fieldsEntryFieldNameOrValueToUse);
public ExposedField getField() {
return field;
}
return fieldsObject;
public abstract DBObject toDBObject(AggregationOperationContext context);
}
}

121
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java

@ -1,121 +0,0 @@ @@ -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 <code>$</code>
*
* @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);
}
/**
* <pre>
* $("a") -> $a
* </pre>
*
* @param fieldName
* @return the field name prefixed with {@literal $}
* @see #safeReference(String)
*/
public static String $(String fieldName) {
return safeReference(fieldName);
}
/**
* <pre>
* $id("a") -> $_id.a
* </pre>
*
* @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;
}
/**
* <pre>
* a: $a -> true
* </pre>
*
* @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));
}
}

22
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SkipOperation.java

@ -15,26 +15,40 @@ @@ -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);
}
}

45
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortOperation.java

@ -16,6 +16,9 @@ @@ -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; @@ -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);
}
}

102
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java

@ -0,0 +1,102 @@ @@ -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<? extends MongoPersistentEntity<?>, 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<? extends MongoPersistentEntity<?>, 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<MongoPersistentProperty> 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));
}
}

22
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypedAggregation.java

@ -15,14 +15,17 @@ @@ -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<I, O> extends Aggregation<I, O> {
public class TypedAggregation<I> extends Aggregation {
private Class<I> inputType;
private final Class<I> inputType;
/**
* Creates a new {@link TypedAggregation} from the given {@link AggregationOperation}s.
@ -30,20 +33,19 @@ public class TypedAggregation<I, O> extends Aggregation<I, O> { @@ -30,20 +33,19 @@ public class TypedAggregation<I, O> extends Aggregation<I, O> {
* @param operations must not be {@literal null} or empty.
*/
public TypedAggregation(Class<I> 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<I> getInputType() {
return inputType;
}
protected AggregateOperationContext createInitialAggregateOperationContext() {
// TODO construct initial aggregate operation context from input type.
return super.createInitialAggregateOperationContext();
}
}

40
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java

@ -15,30 +15,50 @@ @@ -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());
}
}

18
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java

@ -1,19 +1,5 @@ @@ -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;

6
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

@ -356,7 +356,7 @@ public class QueryMapper { @@ -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 { @@ -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 { @@ -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
*/

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java

@ -51,7 +51,7 @@ import org.springframework.util.StringUtils; @@ -51,7 +51,7 @@ import org.springframework.util.StringUtils;
public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty> implements
MongoPersistentEntity<T>, 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;

2
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; @@ -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

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java

@ -73,7 +73,7 @@ public class Field { @@ -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;

95
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UnwrapAndReadDbObjectCallbackUnitTests.java

@ -0,0 +1,95 @@ @@ -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<Target> callback;
@Before
public void setUp() {
MongoTemplate template = new MongoTemplate(factory);
MappingMongoConverter converter = new MappingMongoConverter(factory, new MongoMappingContext());
this.callback = template.new UnwrapAndReadDbObjectCallback<Target>(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;
}
}

92
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationPipelineTests.java

@ -1,92 +0,0 @@ @@ -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));
}
}

181
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java

@ -55,26 +55,24 @@ import com.mongodb.util.JSON; @@ -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 { @@ -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<Void>() {
@ -133,17 +132,17 @@ public class AggregationTests { @@ -133,17 +132,17 @@ public class AggregationTests {
@Test(expected = IllegalArgumentException.class)
public void shouldHandleMissingInputCollection() {
mongoTemplate.aggregate((String) null, new Aggregation<Object, TagCount>(), 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<Object, TagCount>(), null);
mongoTemplate.aggregate(newAggregation(), INPUT_COLLECTION, null);
}
@Test
@ -151,20 +150,22 @@ public class AggregationTests { @@ -151,20 +150,22 @@ public class AggregationTests {
createTagDocuments();
Aggregation<Object, TagCount> 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<TagCount> results = mongoTemplate.aggregate(INPUT_COLLECTION, agg, TagCount.class);
AggregationResults<TagCount> results = mongoTemplate.aggregate(agg, INPUT_COLLECTION, TagCount.class);
assertThat(results, is(notNullValue()));
assertThat(results.getServerUsed(), is("/127.0.0.1:27017"));
List<TagCount> tagCount = results.getAggregationResult();
List<TagCount> tagCount = results.getMappedResults();
assertThat(tagCount, is(notNullValue()));
assertThat(tagCount.size(), is(3));
@ -177,20 +178,22 @@ public class AggregationTests { @@ -177,20 +178,22 @@ public class AggregationTests {
@Test
public void shouldAggregateEmptyCollection() {
Aggregation<Object, TagCount> 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<TagCount> results = mongoTemplate.aggregate(INPUT_COLLECTION, agg, TagCount.class);
AggregationResults<TagCount> results = mongoTemplate.aggregate(aggregation, INPUT_COLLECTION, TagCount.class);
assertThat(results, is(notNullValue()));
assertThat(results.getServerUsed(), is("/127.0.0.1:27017"));
List<TagCount> tagCount = results.getAggregationResult();
List<TagCount> tagCount = results.getMappedResults();
assertThat(tagCount, is(notNullValue()));
assertThat(tagCount.size(), is(0));
@ -200,19 +203,21 @@ public class AggregationTests { @@ -200,19 +203,21 @@ public class AggregationTests {
public void shouldDetectResultMismatch() {
createTagDocuments();
Aggregation<Object, TagCount> agg = newAggregation( //
Aggregation aggregation = newAggregation( //
project("tags"), //
unwind("tags"), //
group("tags").count("count"), //
group("tags") //
.and("count").count(), //
limit(2) //
);
AggregationResults<TagCount> results = mongoTemplate.aggregate(INPUT_COLLECTION, agg, TagCount.class);
AggregationResults<TagCount> results = mongoTemplate.aggregate(aggregation, INPUT_COLLECTION, TagCount.class);
assertThat(results, is(notNullValue()));
assertThat(results.getServerUsed(), is("/127.0.0.1:27017"));
List<TagCount> tagCount = results.getAggregationResult();
List<TagCount> tagCount = results.getMappedResults();
assertThat(tagCount, is(notNullValue()));
assertThat(tagCount.size(), is(2));
@ -220,61 +225,6 @@ public class AggregationTests { @@ -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 { @@ -337,31 +287,31 @@ public class AggregationTests {
)
*/
TypedAggregation<ZipInfo, ZipInfoStats> agg = newAggregation(ZipInfo.class, //
group("state", "city").sum("pop"), //
TypedAggregation<ZipInfo> 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<ZipInfoStats> result = mongoTemplate.aggregate(agg, ZipInfoStats.class);
AggregationResults<ZipInfoStats> 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 { @@ -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 { @@ -408,9 +358,10 @@ public class AggregationTests {
)
*/
TypedAggregation<ZipInfo, StateStats> agg = newAggregation(ZipInfo.class, //
group("state").sum("totalPop", "pop"), //
sort(ASC, id(), "totalPop"), //
TypedAggregation<ZipInfo> 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 { @@ -419,10 +370,10 @@ public class AggregationTests {
AggregationResults<StateStats> 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 { @@ -447,12 +398,12 @@ public class AggregationTests {
*/
TypedAggregation<UserWithLikes, LikeStats> agg = newAggregation(UserWithLikes.class, //
TypedAggregation<UserWithLikes> 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 { @@ -460,14 +411,14 @@ public class AggregationTests {
AggregationResults<LikeStats> 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) {

46
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java

@ -0,0 +1,46 @@ @@ -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]);
}
}

55
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java

@ -0,0 +1,55 @@ @@ -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));
}
}

115
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java

@ -0,0 +1,115 @@ @@ -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.<Field> 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.<Field> 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));
}
}

32
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextConsumingAggregateOperation.java → spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java

@ -15,22 +15,30 @@ @@ -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 {
/**
* 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);
@Test
public void rendersNearQueryAsAggregationOperation() {
NearQuery query = NearQuery.near(10.0, 10.0);
GeoNearOperation operation = new GeoNearOperation(query);
DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
DBObject nearClause = DBObjectUtils.getAsDBObject(dbObject, "$geoNear");
assertThat(nearClause, is(query.toDBObject()));
}
}

87
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java

@ -0,0 +1,87 @@ @@ -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")));
}
}

62
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java

@ -0,0 +1,62 @@ @@ -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"));
}
}

159
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionTests.java

@ -1,159 +0,0 @@ @@ -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<Object> summands = (List<Object>) 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<Object> summands = (List<Object>) 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));
}
}

31
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ContextProducingAggregateOperation.java → spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SkipOperationUnitTests.java

@ -15,12 +15,33 @@ @@ -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));
}
}

56
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java

@ -0,0 +1,56 @@ @@ -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)));
}
}

2
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/FieldUnitTests.java

@ -21,7 +21,7 @@ import static org.junit.Assert.*; @@ -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
*/

Loading…
Cancel
Save