DATAMONGO-586 - Initial support for automatic field reference resolution.

Added automatic field reference resolution which removes the need to have in depth knowledge on how aggregation steps structures the output.
Introduced AggregateOperationContext abstraction to hold the information of available fields for an aggregation step.
Introduced ContextConsumingAggregateOperation and ContextProducingAggregateOperation abstractions to be able to distinguish operations.
Updates test cases to reflect the API changes.
This commit is contained in:
Thomas Darimont
2013-07-19 16:35:21 +02:00
committed by Oliver Gierke
parent 966f971bee
commit 7dd94949d5
24 changed files with 702 additions and 283 deletions

View File

@@ -31,9 +31,6 @@ abstract class AbstractAggregateOperation implements AggregationOperation {
this.operationName = operationName;
}
/**
* @return the operationName
*/
public String getOperationName() {
return operationName;
}
@@ -42,19 +39,18 @@ abstract class AbstractAggregateOperation implements AggregationOperation {
return OPERATOR_PREFIX + getOperationName();
}
/**
* @return the argument for the operation
*/
public abstract Object getOperationArgument();
public Object getOperationArgument() {
return new BasicDBObject();
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDbObject()
*/
@Override
public DBObject toDbObject() {
return new BasicDBObject(getOperationCommand(), getOperationArgument());
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.valueOf(toDbObject());

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* @author Thomas Darimont
*/
abstract class AbstractContextAwareAggregateOperation extends AbstractAggregateOperation implements
ContextConsumingAggregateOperation {
public AbstractContextAwareAggregateOperation(String operationName) {
super(operationName);
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
*/
@Override
public Object getOperationArgument() {
throw new UnsupportedOperationException(String.format("This is not supported on an instance of %s",
ContextConsumingAggregateOperation.class.getName()));
}
/**
* Creates the argument for the aggregation operation from the given {@code inputAggregateOperationContext}
*
* @param inputAggregateOperationContext
* @return the argument for the operation
*/
public abstract Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext);
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#toDbObject()
*/
@Override
public DBObject toDbObject() {
throw new UnsupportedOperationException(String.format("This is not supported on an instance of %s",
ContextConsumingAggregateOperation.class.getName()));
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ContextAwareAggregateOperation#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregateOperationContext)
*/
public DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext) {
return new BasicDBObject(getOperationCommand(), getOperationArgument(inputAggregateOperationContext));
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
/**
* @author Thomas Darimont
*/
abstract class AbstractContextProducingAggregateOperation extends AbstractContextAwareAggregateOperation implements
ContextProducingAggregateOperation {
private final AggregateOperationContext outputAggregateOperationContext;
public AbstractContextProducingAggregateOperation(String operationName) {
super(operationName);
this.outputAggregateOperationContext = createAggregateContext();
}
private AggregateOperationContext createAggregateContext() {
return new BasicAggregateOperationContext();
}
public AggregateOperationContext getOutputAggregateOperationContext() {
return this.outputAggregateOperationContext;
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.Map;
/**
* A {@code AggregateOperationContext} holds information about available fields for the aggregation steps.
*
* @author Thomas Darimont
*/
interface AggregateOperationContext {
Map<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);
}

View File

@@ -23,7 +23,6 @@ import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -73,40 +72,53 @@ public class Aggregation<I, O> {
private List<DBObject> getOperationObjects() {
AggregateOperationContext aggregateOperationContext = createInitialAggregateOperationContext();
List<DBObject> operationObjects = new ArrayList<DBObject>();
for (AggregationOperation operation : operations) {
operationObjects.add(operation.toDbObject());
if (operation instanceof NoopAggreationOperation) {
continue;
}
operationObjects.add(toOperationObject(operation, aggregateOperationContext));
if (operation instanceof ContextProducingAggregateOperation) {
aggregateOperationContext = ((ContextProducingAggregateOperation) operation)
.getOutputAggregateOperationContext();
}
}
return operationObjects;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
/**
* @param aggregateOperationContext
* @param operation
* @return the {@link DBObject} representation of the given {@link AggregationOperation}
*/
@Override
public String toString() {
return StringUtils.collectionToCommaDelimitedString(operations);
protected DBObject toOperationObject(AggregationOperation operation,
AggregateOperationContext aggregateOperationContext) {
DBObject operationObject;
if (operation instanceof ContextConsumingAggregateOperation) {
operationObject = ((ContextConsumingAggregateOperation) operation).toDbObject(aggregateOperationContext);
} else {
operationObject = operation.toDbObject();
}
return operationObject;
}
/**
* Factory method to create a new {@link GroupOperation} for the given {@code id}.
*
* @param id, must not be {@literal null}
* @return
*/
public static GroupOperation group(DBObject id) {
return new GroupOperation(id);
protected AggregateOperationContext createInitialAggregateOperationContext() {
return new BasicAggregateOperationContext();
}
/**
* Factory method to create a new {@link GroupOperation} for the given {@code idFields}.
*
* @param idField the first idField to use, must not be {@literal null}.
* @param moreIdFields more id fields to use, can be {@literal null}.
* @param additionalIdFields more id fields to use, can be {@literal null}.
* @return
*/
public static GroupOperation group(String idField, String... moreIdFields) {
return new GroupOperation(idField, moreIdFields);
public static GroupOperation group(String idField, String... additionalIdFields) {
return new GroupOperation(fields(idField, additionalIdFields));
}
/**
@@ -221,15 +233,6 @@ public class Aggregation<I, O> {
return new SortOperation(sort);
}
/**
* Factory method to create a new empty {@link Fields} container for key-value pairs.
*
* @return
*/
public static Fields fields() {
return fields(new String[0]);
}
/**
* Factory method to create a new {@link Fields} container for key-value pairs from the given {@code fieldNames}.
* <p>
@@ -246,8 +249,16 @@ public class Aggregation<I, O> {
*
* @return
*/
public static Fields fields(String... fieldNames) {
return new BackendFields(fieldNames);
public static Fields fields(String fieldName, String... additionalFieldNames) {
return new BackendFields(additionalFieldNames).and(fieldName);
}
public static Fields fields() {
return new BackendFields();
}
public static Fields pick(String fieldName, Object fieldNameOrValue) {
return fields().and(fieldName, fieldNameOrValue);
}
/**

View File

@@ -26,7 +26,7 @@ class BackendFields implements Fields {
* @return
*/
public Fields and(String name) {
return and(name, ReferenceUtil.safeReference(name));
return and(name, name);
}
/**

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Map based implementation of {@link AggregateOperationContext}.
*
* @author Thomas Darimont
*/
public class BasicAggregateOperationContext implements AggregateOperationContext {
private Map<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);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import com.mongodb.DBObject;
/**
* Represents one single operation in an aggregation pipeline that is aware of an {@link AggregateOperationContext}. The
* {@code AggregateOperationContext} can be used to resolve the correct field reference expression for field references.
*
* @author Thomas Darimont
*/
public interface ContextConsumingAggregateOperation extends AggregationOperation {
/**
* Creates a {@link DBObject} representation backing this object and considers the field references from the given
* {@link AggregateOperationContext}.
*
* @param inputAggregateOperationContext
* @return
*/
DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext);
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
/**
* Represents one single operation in an aggregation pipeline that is aware of an {@link AggregateOperationContext} that
* produces an {@link AggregateOperationContext} as output.
*
* @author Thomas Darimont
*/
public interface ContextProducingAggregateOperation extends AggregationOperation {
AggregateOperationContext getOutputAggregateOperationContext();
}

View File

@@ -31,12 +31,8 @@ public class GeoNearOperation extends AbstractAggregateOperation {
this.nearQuery = nearQuery;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
*/
@Override
public Object getOperationArgument() {
return nearQuery.toDBObject();
}
}

View File

@@ -15,9 +15,9 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.util.Assert;
@@ -32,15 +32,10 @@ import com.mongodb.DBObject;
* @author Thomas Darimont
* @since 1.3
*/
public class GroupOperation extends AbstractAggregateOperation {
public class GroupOperation extends AbstractContextProducingAggregateOperation {
final Object id;
final Map<String, DBObject> fields = new HashMap<String, DBObject>();
public GroupOperation(Object id) {
super("group");
this.id = id;
}
final List<GroupingOperation> ops = new ArrayList<GroupOperation.GroupingOperation>();
/**
* Creates a <code>$group</code> operation with <code>_id</code> referencing to a field of the document. The returned
@@ -53,72 +48,129 @@ public class GroupOperation extends AbstractAggregateOperation {
* @param id
* @param moreIdFields
*/
public GroupOperation(String idField, String... moreIdFields) {
this(createGroupIdFrom(idField, moreIdFields));
public GroupOperation(Fields fields) {
super("group");
this.id = createGroupIdFrom(fields);
}
/**
* @param idField
* @param moreIdFields
* @param fields
* @return
*/
private static Object createGroupIdFrom(String idField, String[] moreIdFields) {
private Object createGroupIdFrom(Fields fields) {
Assert.notNull(idField, "idField must not be null!");
Object result = ReferenceUtil.safeReference(idField);
Assert.notNull(fields, "fields must not be null!");
Map<String, Object> values = fields.getValues();
Assert.notEmpty(values, "fields.values must not be empty!");
if (moreIdFields != null && moreIdFields.length > 0) {
DBObject idReferences = new BasicDBObject(moreIdFields.length + 1);
idReferences.put(ReferenceUtil.safeNonReference(idField), ReferenceUtil.safeReference(idField));
for (String additionalIdField : moreIdFields) {
idReferences.put(ReferenceUtil.safeNonReference(additionalIdField),
ReferenceUtil.safeReference(additionalIdField));
}
result = idReferences;
DBObject idReferences = new BasicDBObject(values.size());
for (Map.Entry<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 result;
}
public GroupOperation(Fields idFields) {
this((Object) idFields.getValues());
return idReferences;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
*/
@Override
public Object getOperationArgument() {
return createProjection();
}
public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) {
/**
* @return
*/
private DBObject createProjection() {
DBObject projection = new BasicDBObject(ReferenceUtil.ID_KEY, id);
DBObject projection = new BasicDBObject();
for (Entry<String, DBObject> entry : fields.entrySet()) {
projection.put(entry.getKey(), entry.getValue());
Object idToUse = id;
if (idToUse instanceof DBObject) {
idToUse = createGroupIdObject((DBObject) idToUse, inputAggregateOperationContext);
}
projection.put(ReferenceUtil.ID_KEY, idToUse);
for (GroupingOperation op : ops) {
projection.put(op.alias, op.toDbObject(inputAggregateOperationContext));
}
return projection;
}
public GroupOperation addField(String key, DBObject value) {
/**
* @param idCandidate
* @param inputAggregateOperationContext
* @return
*/
private Object createGroupIdObject(DBObject groupIdObject, AggregateOperationContext inputAggregateOperationContext) {
Assert.hasText(key, "Key is empty");
Assert.notNull(value, "Value is null");
String trimmedKey = key.trim();
if (ReferenceUtil.ID_KEY.equals(trimmedKey)) {
throw new IllegalArgumentException("_id field can only be set in constructor");
Object simpleIdOrNull = returnIfGroupIdIsSingleFieldReference(inputAggregateOperationContext, groupIdObject);
if (simpleIdOrNull != null) {
return simpleIdOrNull;
}
fields.put(key, value);
return this;
DBObject idObject = new BasicDBObject();
for (String idFieldName : groupIdObject.keySet()) {
Object idFieldValue = groupIdObject.get(idFieldName);
Object idFieldValueOrNull = returnIfFieldValueReferencesAvailableField(inputAggregateOperationContext,
idFieldName, idFieldValue);
if (idFieldValueOrNull != null) {
idFieldValue = idFieldValueOrNull;
}
getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName));
idObject.put(idFieldName, idFieldValue);
}
return idObject;
}
private Object returnIfGroupIdIsSingleFieldReference(AggregateOperationContext inputAggregateOperationContext,
DBObject idObject) {
if (idObject.keySet().size() != 1) {
return null;
}
return returnIfFieldNameIsSimpleReference(inputAggregateOperationContext, idObject, idObject.keySet().iterator()
.next());
}
private Object returnIfFieldValueReferencesAvailableField(AggregateOperationContext inputAggregateOperationContext,
String idFieldName, Object idFieldValue) {
Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null");
if (!ReferenceUtil.isValueFieldReference(idFieldName, idFieldValue)) {
return null;
}
if (!inputAggregateOperationContext.isFieldAvailable(idFieldName)) {
return null;
}
String idFieldNameToUse = inputAggregateOperationContext.returnFieldNameAliasIfAvailableOr(idFieldName);
return ReferenceUtil.safeReference(inputAggregateOperationContext instanceof GroupOperation ? ReferenceUtil
.id(idFieldNameToUse) : idFieldNameToUse);
}
private Object returnIfFieldNameIsSimpleReference(AggregateOperationContext inputAggregateOperationContext,
DBObject idObject, String idFieldName) {
Object idFieldValue = idObject.get(idFieldName);
if (!idFieldValueIsSimpleIdFieldExpression(idFieldName, idFieldValue)) {
return null;
}
getOutputAggregateOperationContext().registerAvailableField(idFieldName, ReferenceUtil.id(idFieldName));
idFieldValue = ReferenceUtil.safeReference(inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr(idFieldName));
return idFieldValue;
}
private static boolean idFieldValueIsSimpleIdFieldExpression(String idFieldName, Object idFieldValue) {
return idFieldValue instanceof String
&& idFieldName.equals(ReferenceUtil.safeNonReference(idFieldValue.toString()));
}
/**
@@ -271,7 +323,7 @@ public class GroupOperation extends AbstractAggregateOperation {
* @return
*/
public GroupOperation count(String name, double increment) {
return addField(name, new BasicDBObject("$sum", increment));
return sum(name, increment);
}
/**
@@ -292,6 +344,10 @@ public class GroupOperation extends AbstractAggregateOperation {
return count(name, 1);
}
private GroupOperation sum(String name, Object field) {
return addOperation("$sum", name, field);
}
/**
* Adds a field with the <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum
* operation</a>.
@@ -308,7 +364,7 @@ public class GroupOperation extends AbstractAggregateOperation {
* @return
*/
public GroupOperation sum(String name, String field) {
return addOperation("$sum", name, field);
return sum(name, (Object) field);
}
/**
@@ -329,106 +385,37 @@ public class GroupOperation extends AbstractAggregateOperation {
return sum(field, field);
}
protected GroupOperation addOperation(String operation, String name, String field) {
return addField(name, new BasicDBObject(operation, ReferenceUtil.safeReference(field)));
protected GroupOperation addOperation(String operation, String name, Object field) {
getOutputAggregateOperationContext().registerAvailableField(name);
this.ops.add(new GroupingOperation(operation, name, field));
return this;
}
/**
* Creates a <code>$group</code> operation with a id that consists of multiple fields. Using
* {@link IdField#idField(String)} or {@link IdField#idField(String, String)} you can easily create complex id fields
* like:
*
* <pre>
*
* group(idField(&quot;path&quot;), idField(&quot;pageView&quot;, &quot;page.views&quot;), idField(&quot;field3&quot;))
*
* </pre>
*
* which would result in:
*
* <pre>
*
* {$group: {_id: {path: "$path", pageView: "$page.views", field3: "$field3"}}}
*
* </pre>
*
* @param idFields
* @return
*/
// TODO still relevant? IdField is probably a better abstraction than Fields?!
public static GroupOperation group(IdField... idFields) {
Assert.notNull(idFields, "Combined id is null");
static class GroupingOperation {
final String operation;
final String alias;
final Object fieldNameOrValue;
BasicDBObject id = new BasicDBObject();
for (IdField idField : idFields) {
id.put(idField.getKey(), idField.getValue());
public GroupingOperation(String operation, String alias, Object fieldNameOrValue) {
this.operation = operation;
this.alias = alias;
this.fieldNameOrValue = fieldNameOrValue;
}
return new GroupOperation(id);
}
public DBObject toDbObject(AggregateOperationContext inputAggregateOperationContext) {
/**
* Represents a single field in a complex id of a <code>$group</code> operation. For example:
*
* <pre>
* {$group: {_id: {key: "$value"}}}
* </pre>
*/
public static class IdField {
Object fieldNameOrValueToUse = fieldNameOrValue;
private final String key;
private final String value;
if (fieldNameOrValue instanceof String) {
if (inputAggregateOperationContext != null) {
fieldNameOrValueToUse = inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse);
}
fieldNameOrValueToUse = ReferenceUtil.safeReference((String) fieldNameOrValueToUse);
}
/**
* Creates a new {@link IdField} with the given key and value.
*
* @param key must not be {@literal null} or empty.
* @param value must not be {@literal null} or empty.
*/
public IdField(String key, String value) {
Assert.hasText(key, "Key must not be null or empty");
Assert.hasText(value, "Value must not be null or empty");
this.key = ReferenceUtil.safeNonReference(key);
this.value = ReferenceUtil.safeReference(value);
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
/**
* Creates an id field with the name of the referenced field:
*
* <pre>
* _id : { field : "$field" }
* </pre>
*
* @param field reference to a field of the document
* @return the id field
*/
public static IdField idField(String field) {
return new IdField(field, field);
}
/**
* Creates an id field with key and reference.
*
* <pre>
* _id: {key: "$field"}
* </pre>
*
* @param key the key
* @param field reference to a field of the document
* @return the id field
*/
public static IdField idField(String key, String field) {
return new IdField(key, field);
return new BasicDBObject(operation, fieldNameOrValueToUse);
}
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.data.mongodb.core.aggregation;
/**
* Encapsulates the {@code $limit}-operation
*
* @see http://docs.mongodb.org/manual/reference/aggregation/limit/
* @author Thomas Darimont
*/
class LimitOperation extends AbstractAggregateOperation {
@@ -30,9 +33,6 @@ class LimitOperation extends AbstractAggregateOperation {
this.maxElements = maxElements;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
*/
@Override
public Object getOperationArgument() {
return maxElements;

View File

@@ -23,6 +23,7 @@ import com.mongodb.DBObject;
/**
* Encapsulates the {@code $match}-operation
*
* @see http://docs.mongodb.org/manual/reference/aggregation/match/
* @author Sebastian Herold
* @author Thomas Darimont
* @since 1.3

View File

@@ -19,9 +19,12 @@ import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Represents a skippable AggregationOperation that is not considered for execution.
*
* @author Thomas Darimont
*/
public class NoopAggreationOperation implements AggregationOperation {
public DBObject toDbObject() {
return new BasicDBObject();
}

View File

@@ -18,7 +18,9 @@ package org.springframework.data.mongodb.core.aggregation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import org.springframework.dao.InvalidDataAccessApiUsageException;
@@ -29,40 +31,35 @@ import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Projection of field to be used in an {@link Aggregation}.
* Encapsulates the aggregation framework {@code $project}-operation.
* <p>
* A projection is similar to a {@link Field} inclusion/exclusion but more powerful. It can generate new fields, change
* values of given field etc.
* Projection of field to be used in an {@link Aggregation}. A projection is similar to a {@link Field}
* inclusion/exclusion but more powerful. It can generate new fields, change values of given field etc.
* <p>
*
* @see http://docs.mongodb.org/manual/reference/aggregation/project/
* @author Tobias Trelle
* @author Thomas Darimont
* @since 1.3
*/
public class ProjectionOperation extends AbstractAggregateOperation {
public class ProjectionOperation extends AbstractContextProducingAggregateOperation {
/** Stack of key names. Size is 0 or 1. */
private final Stack<String> reference = new Stack<String>();
private final DBObject projection = new BasicDBObject();
private final Map<String, Object> projection = new HashMap<String, Object>();
private DBObject rightHandExpression;
/**
* Create an empty projection.
*/
public ProjectionOperation() {
super("project");
}
/**
* This convenience constructor excludes the field {@code _id} and includes the given fields.
*
* @param includes Keys of field to include, must not be {@literal null} or empty.
*/
public ProjectionOperation(String... includes) {
this();
Assert.notEmpty(includes);
super("project");
Assert.notNull(includes, "includes must not be null");
exclude("_id");
for (String key : includes) {
@@ -76,7 +73,15 @@ public class ProjectionOperation extends AbstractAggregateOperation {
* @param targetClass
*/
public ProjectionOperation(Class<?> targetClass) {
this();
this(extractFieldsFrom(targetClass));
}
/**
* @param targetClass
* @return
*/
private static String[] extractFieldsFrom(Class<?> targetClass) {
return new String[0];
}
/**
@@ -87,6 +92,8 @@ public class ProjectionOperation extends AbstractAggregateOperation {
public final ProjectionOperation exclude(String key) {
Assert.hasText(key, "Missing key");
getOutputAggregateOperationContext().unregisterAvailableField(ReferenceUtil.safeNonReference(key));
projection.put(key, 0);
return this;
}
@@ -102,6 +109,7 @@ public class ProjectionOperation extends AbstractAggregateOperation {
safePop();
reference.push(key);
getOutputAggregateOperationContext().registerAvailableField(key);
return this;
}
@@ -116,7 +124,10 @@ public class ProjectionOperation extends AbstractAggregateOperation {
Assert.hasText(key, "Missing key");
try {
projection.put(key, rightHandSide(ReferenceUtil.safeReference(reference.pop())));
String rhsFieldName = reference.pop();
getOutputAggregateOperationContext().unregisterAvailableField(ReferenceUtil.safeNonReference(rhsFieldName));
getOutputAggregateOperationContext().registerAvailableField(ReferenceUtil.safeNonReference(key));
projection.put(key, rightHandSide(ReferenceUtil.safeReference(rhsFieldName)));
} catch (EmptyStackException e) {
throw new InvalidDataAccessApiUsageException("Invalid use of as()", e);
}
@@ -152,7 +163,6 @@ public class ProjectionOperation extends AbstractAggregateOperation {
private ProjectionOperation arithmeticOperation(String op, Number n) {
Assert.notNull(n, "Missing number");
rightHandExpression = createArrayObject(op, ReferenceUtil.safeReference(reference.peek()), n);
return this;
}
@@ -187,10 +197,26 @@ public class ProjectionOperation extends AbstractAggregateOperation {
Assert.notNull(key, "Missing Key");
Assert.notNull(value);
this.projection.put(key, value instanceof Fields ? ((Fields) value).getValues() : value);
getOutputAggregateOperationContext().registerAvailableField(key);
registerAvailableFieldsRecursive(key, value);
this.projection.put(key, value);
return this;
}
private void registerAvailableFieldsRecursive(String outerKey, Object value) {
if (value instanceof Fields) {
Map<String, Object> values = ((Fields) value).getValues();
for (String key : values.keySet()) {
String innerKey = outerKey + "." + key;
getOutputAggregateOperationContext().registerAvailableField(innerKey);
registerAvailableFieldsRecursive(innerKey, values.get(key));
}
}
}
/**
* @param name
* @param value
@@ -205,9 +231,53 @@ public class ProjectionOperation extends AbstractAggregateOperation {
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
*/
@Override
public Object getOperationArgument() {
public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) {
Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null");
safePop();
return projection;
DBObject projectionObject = new BasicDBObject();
for (Map.Entry<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;
}
if (fieldNameOrValueToUse instanceof String) {
String fieldName = inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr((String) fieldNameOrValueToUse);
fieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName);
}
projectionObject.put(entry.getKey(), fieldNameOrValueToUse);
}
return projectionObject;
}
private DBObject returnIfValueIsIdFields(AggregateOperationContext inputAggregateOperationContext,
Object fieldNameOrValueToUse) {
Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null");
if (!(fieldNameOrValueToUse instanceof Fields)) {
return null;
}
DBObject fieldsObject = new BasicDBObject();
for (Map.Entry<String, Object> fieldsEntry : ((Fields) fieldNameOrValueToUse).getValues().entrySet()) {
Object fieldsEntryFieldNameOrValueToUse = fieldsEntry.getValue();
if (fieldsEntryFieldNameOrValueToUse instanceof String && inputAggregateOperationContext != null) {
String fieldName = inputAggregateOperationContext
.returnFieldNameAliasIfAvailableOr((String) fieldsEntryFieldNameOrValueToUse);
fieldsEntryFieldNameOrValueToUse = ReferenceUtil.safeReference(fieldName);
}
fieldsObject.put(fieldsEntry.getKey(), fieldsEntryFieldNameOrValueToUse);
}
return fieldsObject;
}
}

View File

@@ -105,4 +105,17 @@ class ReferenceUtil {
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));
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.data.mongodb.core.aggregation;
/**
* Encapsulates the aggregation framework {@code $skip}-operation.
*
* @see http://docs.mongodb.org/manual/reference/aggregation/skip/
* @author Thomas Darimont
*/
public class SkipOperation extends AbstractAggregateOperation {
@@ -30,9 +33,6 @@ public class SkipOperation extends AbstractAggregateOperation {
this.skipCount = skipCount;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
*/
@Override
public Object getOperationArgument() {
return skipCount;

View File

@@ -22,9 +22,12 @@ import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Encapsulates the aggregation framework {@code $sort}-operation.
*
* @see http://docs.mongodb.org/manual/reference/aggregation/sort/#pipe._S_sort
* @author Thomas Darimont
*/
public class SortOperation extends AbstractAggregateOperation {
public class SortOperation extends AbstractContextAwareAggregateOperation implements ContextConsumingAggregateOperation {
private Sort sort;
@@ -50,17 +53,14 @@ public class SortOperation extends AbstractAggregateOperation {
* @see org.springframework.data.mongodb.core.aggregation.AbstractAggregateOperation#getOperationArgument()
*/
@Override
public Object getOperationArgument() {
return createSortProperties();
}
public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) {
Assert.notNull(inputAggregateOperationContext, "inputAggregateOperationContext must not be null!");
/**
* @return
*/
private DBObject createSortProperties() {
DBObject sortProperties = new BasicDBObject();
for (org.springframework.data.domain.Sort.Order order : sort) {
sortProperties.put(order.getProperty(), order.isAscending() ? 1 : -1);
String fieldName = inputAggregateOperationContext.returnFieldNameAliasIfAvailableOr(order.getProperty());
sortProperties.put(fieldName, order.isAscending() ? 1 : -1);
}
return sortProperties;
}

View File

@@ -41,4 +41,9 @@ public class TypedAggregation<I, O> extends Aggregation<I, O> {
return inputType;
}
protected AggregateOperationContext createInitialAggregateOperationContext() {
// TODO construct initial aggregate operation context from input type.
return super.createInitialAggregateOperationContext();
}
}

View File

@@ -18,20 +18,27 @@ package org.springframework.data.mongodb.core.aggregation;
import org.springframework.util.Assert;
/**
* Encapsulates the aggregation framework {@code $unwind}-operation.
*
* @see http://docs.mongodb.org/manual/reference/aggregation/unwind/#pipe._S_unwind
* @author Thomas Darimont
*/
public class UnwindOperation extends AbstractAggregateOperation {
public class UnwindOperation extends AbstractContextProducingAggregateOperation implements
ContextConsumingAggregateOperation {
private final String fieldName;
public UnwindOperation(String fieldName) {
super("unwind");
Assert.notNull(fieldName);
this.fieldName = fieldName;
getOutputAggregateOperationContext().registerAvailableField(fieldName, fieldName);
}
@Override
public Object getOperationArgument() {
public Object getOperationArgument(AggregateOperationContext inputAggregateOperationContext) {
return ReferenceUtil.safeReference(fieldName);
}
}

View File

@@ -50,13 +50,13 @@ public class AggregationPipelineTests {
@Test
public void unwindOperation() {
assertSingleDBObject("$unwind", "$field", unwind("$field").toDbObject());
assertSingleDBObject("$unwind", "$field", unwind("$field").toDbObject(new BasicAggregateOperationContext()));
}
@Test
public void unwindOperationWithAddedPrefix() {
assertSingleDBObject("$unwind", "$field", unwind("field").toDbObject());
assertSingleDBObject("$unwind", "$field", unwind("field").toDbObject(new BasicAggregateOperationContext()));
}
@Test
@@ -71,7 +71,7 @@ public class AggregationPipelineTests {
@Test
public void sortOperation() {
DBObject sortDoc = sort(ASC, "n").toDbObject();
DBObject sortDoc = sort(ASC, "n").toDbObject(new BasicAggregateOperationContext());
DBObject orderDoc = getAsDBObject(sortDoc, "$sort");
assertThat(orderDoc, is(notNullValue()));
assertSingleDBObject("n", 1, orderDoc);
@@ -80,7 +80,7 @@ public class AggregationPipelineTests {
@Test
public void projectOperation() {
DBObject projectionDoc = project("a").toDbObject();
DBObject projectionDoc = project("a").toDbObject(new BasicAggregateOperationContext());
DBObject fields = getAsDBObject(projectionDoc, "$project");
assertThat(fields, is(notNullValue()));
assertSingleDBObject("a", 1, fields);

View File

@@ -70,6 +70,11 @@ public class AggregationTests {
public void setUp() {
cleanDb();
initSampleDataIfNecessary();
CommandResult result = mongoTemplate.executeCommand("{ buildInfo: 1 }");
Object version = result.get("version");
LOGGER.debug("Server uses MongoDB Version: {}", version);
}
@After
@@ -149,7 +154,7 @@ public class AggregationTests {
Aggregation<Object, TagCount> agg = newAggregation( //
project("tags"), //
unwind("tags"), //
group($("tags")).count("n"), //
group("tags").count("n"), //
project().field("tag", $id()).field("n", 1), //
sort(DESC, "n") //
);
@@ -175,7 +180,7 @@ public class AggregationTests {
Aggregation<Object, TagCount> agg = newAggregation(//
project("tags"), //
unwind("tags"), //
group($("tags")).count("n"), //
group("tags").count("n"), //
project().field("tag", $id()).field("n", 1), //
sort(DESC, "n") //
);
@@ -198,7 +203,7 @@ public class AggregationTests {
Aggregation<Object, TagCount> agg = newAggregation( //
project("tags"), //
unwind("tags"), //
group($("tags")).count("count"), //
group("tags").count("count"), //
limit(2) //
);
@@ -222,30 +227,54 @@ public class AggregationTests {
assertThat(fields, is(notNullValue()));
assertThat(fields.getValues(), is(notNullValue()));
assertThat(fields.getValues().size(), is(4));
assertThat(fields.getValues().get("a"), is((Object) "$a"));
assertThat(fields.getValues().get("b"), is((Object) "$b"));
assertThat(fields.getValues().get("c"), is((Object) "$c"));
assertThat(fields.getValues().get("a"), is((Object) "a"));
assertThat(fields.getValues().get("b"), is((Object) "b"));
assertThat(fields.getValues().get("c"), is((Object) "c"));
assertThat(fields.getValues().get("d"), is((Object) 42));
}
@Test
public void groupFactoryMethodWithFieldsAndSumOperation() {
public void shouldCreateSimpleIdForGroupOperationWithSingleSimpleIdField() {
Fields fields = fields("a", "b").and("c").and("d", 42);
GroupOperation groupOperation = group(fields).sum("e");
Fields fields = fields("a");
GroupOperation groupOperation = new GroupOperation(fields);
assertThat(groupOperation, is(notNullValue()));
assertThat(groupOperation.toDbObject(), is(notNullValue()));
assertThat(groupOperation.id, is(notNullValue()));
assertThat(groupOperation.id, is((Object) fields.getValues()));
assertThat(groupOperation.fields, is(notNullValue()));
assertThat(groupOperation.fields.size(), is(1));
assertThat(groupOperation.fields.containsKey("e"), is(true));
assertThat(groupOperation.fields.get("e"), is(notNullValue()));
assertThat(groupOperation.fields.get("e").get("$sum"), is(notNullValue()));
assertThat(groupOperation.fields.get("e").get("$sum"), is((Object) "$e"));
DBObject dbObject = groupOperation.toDbObject(new BasicAggregateOperationContext());
assertThat(dbObject, is(notNullValue()));
assertThat(dbObject.get("$group"), is(notNullValue()));
assertThat(((DBObject) dbObject.get("$group")).get(id()), is(notNullValue()));
assertThat(((DBObject) dbObject.get("$group")).get(id()), is((Object) "$a"));
}
@Test
public void shouldCreateComplexIdForGroupOperationWithSingleComplexIdField() {
Fields fields = fields().and("a", 42);
GroupOperation groupOperation = new GroupOperation(fields);
assertThat(groupOperation.toDbObject(new BasicAggregateOperationContext()), is(notNullValue()));
assertThat(groupOperation.id, is(notNullValue()));
assertThat(groupOperation.id, is((Object) new BasicDBObject("a", 42)));
}
// @Test
// public void groupFactoryMethodWithMultipleFieldsAndSumOperation() {
//
// Fields fields = fields("a", "b").pick("c").pick("d", 42);
// GroupOperation groupOperation = group(fields).sum("e");
//
// assertThat(groupOperation, is(notNullValue()));
// assertThat(groupOperation.toDbObject(null), is(notNullValue()));
// assertThat(groupOperation.id, is(notNullValue()));
// assertThat(groupOperation.id, is((Object) new BasicDBObject(fields.getValues())));
// assertThat(groupOperation.fields, is(notNullValue()));
// assertThat(groupOperation.fields.size(), is(1));
// assertThat(groupOperation.fields.containsKey("e"), is(true));
// assertThat(groupOperation.fields.get("e"), is(notNullValue()));
// assertThat(groupOperation.fields.get("e").get("$sum"), is(notNullValue()));
// assertThat(groupOperation.fields.get("e").get("$sum"), is((Object) "$e"));
// }
@Test
public void complexAggregationFrameworkUsageLargestAndSmallestCitiesByState() {
/*
@@ -308,20 +337,19 @@ public class AggregationTests {
)
*/
TypedAggregation<ZipInfo, ZipInfoStats> agg = newAggregation(
ZipInfo.class, //
group("state", "city").sum("pop"), // group("state", "city") -> _id: {state: $state, city: $city}
sort(ASC, "pop", id("state"), id("city")), //
group($id("state")) // $id("state") -> _id : $_id.state
.last("biggestCity", $id("city")) //
.last("biggestPop", $("pop")) //
.first("smallestCity", $id("city")) //
.first("smallestPop", $("pop")), //
TypedAggregation<ZipInfo, ZipInfoStats> agg = newAggregation(ZipInfo.class, //
group("state", "city").sum("pop"), //
sort(ASC, "pop", "state", "city"), //
group("state") //
.last("biggestCity", "city") //
.last("biggestPop", "pop") //
.first("smallestCity", "city") //
.first("smallestPop", "pop"), //
project(ZipInfoStats.class) //
.field("_id", 0) //
.field("state", $id()) // $id() -> $_id
.field("biggestCity", fields().and("name", $("biggestCity")).and("population", $("biggestPop"))) //
.field("smallestCity", fields().and("name", $("smallestCity")).and("population", $("smallestPop"))),
.field("state", id()) //
.field("biggestCity", pick("name", "biggestCity").and("population", "biggestPop")) //
.field("smallestCity", pick("name", "smallestCity").and("population", "smallestPop")), //
sort(ASC, "state") //
);
@@ -359,10 +387,12 @@ public class AggregationTests {
@Test
public void findStatesWithPopulationOver10MillionAggregationExample() {
/*
//complex mongodb aggregation framework example from http://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state
//complex mongodb aggregation framework example from
http://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state
db.zipcodes.aggregate(
{
$group:{
$group: {
_id:"$state",
totalPop:{ $sum:"$pop"}
}
@@ -371,7 +401,7 @@ public class AggregationTests {
$sort: { _id: 1, "totalPop": 1 }
},
{
$match:{
$match: {
totalPop: { $gte:10*1000*1000 }
}
}
@@ -379,7 +409,7 @@ public class AggregationTests {
*/
TypedAggregation<ZipInfo, StateStats> agg = newAggregation(ZipInfo.class, //
group("state").sum("totalPop", $("pop")), // fields("state", "city") -> state: $state, city: $city
group("state").sum("totalPop", "pop"), //
sort(ASC, id(), "totalPop"), //
match(where("totalPop").gte(10 * 1000 * 1000)) //
);
@@ -407,6 +437,16 @@ public class AggregationTests {
createUserWithLikesDocuments();
/*
...
$group: {
_id:"$like",
number:{ $sum:1}
}
...
*/
TypedAggregation<UserWithLikes, LikeStats> agg = newAggregation(UserWithLikes.class, //
unwind("likes"), //
group("likes").count("number"), //

View File

@@ -38,8 +38,8 @@ public class ProjectionTests {
public void emptyProjection() {
DBObject raw = safeExtractDbObjectFromProjection(project());
assertThat(raw, is(notNullValue()));
assertThat(raw.toMap().isEmpty(), is(true));
assertThat(raw.toMap().size(), is(1));
assertThat((Integer) raw.get("_id"), is(0));
}
@Test(expected = IllegalArgumentException.class)
@@ -69,14 +69,19 @@ public class ProjectionTests {
public void exclude() {
DBObject raw = safeExtractDbObjectFromProjection(project().exclude("a"));
assertSingleDBObject("a", 0, raw);
assertThat(raw.toMap().size(), is(2));
assertThat((Integer) raw.get("_id"), is(0));
assertThat((Integer) raw.get("a"), is(0));
}
@Test
public void includeAlias() {
DBObject raw = safeExtractDbObjectFromProjection(project().include("a").as("b"));
assertSingleDBObject("b", "$a", raw);
assertThat(raw.toMap().size(), is(2));
assertThat((Integer) raw.get("_id"), is(0));
assertThat((String) raw.get("b"), is("$a"));
}
@Test(expected = InvalidDataAccessApiUsageException.class)
@@ -94,10 +99,10 @@ public class ProjectionTests {
public void plus() {
DBObject raw = safeExtractDbObjectFromProjection(project().include("a").plus(10));
assertNotNullDBObject(raw);
assertThat(raw, is(notNullValue()));
DBObject addition = (DBObject) raw.get("a");
assertNotNullDBObject(addition);
assertThat(addition, is(notNullValue()));
List<Object> summands = (List<Object>) addition.get("$add");
assertThat(summands, is(notNullValue()));
@@ -111,10 +116,10 @@ public class ProjectionTests {
public void plusWithAlias() {
DBObject raw = safeExtractDbObjectFromProjection(project().include("a").plus(10).as("b"));
assertNotNullDBObject(raw);
assertThat(raw, is(notNullValue()));
DBObject addition = (DBObject) raw.get("b");
assertNotNullDBObject(addition);
assertThat(addition, is(notNullValue()));
List<Object> summands = (List<Object>) addition.get("$add");
assertThat(summands, is(notNullValue()));
@@ -137,8 +142,8 @@ public class ProjectionTests {
private static DBObject safeExtractDbObjectFromProjection(ProjectionOperation projectionOperation) {
assertThat(projectionOperation, is(notNullValue()));
DBObject dbObject = projectionOperation.toDbObject();
assertNotNullDBObject(dbObject);
DBObject dbObject = projectionOperation.toDbObject(new BasicAggregateOperationContext());
assertThat(dbObject, is(notNullValue()));
Object projection = dbObject.get("$project");
assertThat("Expected non null value for key $project ", projection, is(notNullValue()));
assertTrue("projection contents should be a " + DBObject.class.getSimpleName(), projection instanceof DBObject);
@@ -148,13 +153,7 @@ public class ProjectionTests {
private static void assertSingleDBObject(String key, Object value, DBObject doc) {
assertNotNullDBObject(doc);
assertThat(doc, is(notNullValue()));
assertThat(doc.get(key), is(value));
}
private static void assertNotNullDBObject(DBObject doc) {
assertThat(doc, is(notNullValue()));
assertThat(doc.toMap().size(), is(1));
}
}

View File

@@ -10,6 +10,8 @@
<!--
<logger name="org.springframework" level="debug" />
-->
<logger name="org.springframework.data.mongodb.core.aggregation" level="debug" />
<root level="error">
<appender-ref ref="console" />