diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java index 0529c5110..b8195159d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java @@ -340,7 +340,11 @@ public class ExposedFields implements Iterable { * @return */ public String getRaw() { + String target = field.getTarget(); + if (target.startsWith("$")) { + target = target.substring(1); + } return field.synthetic ? target : String.format("%s.%s", Fields.UNDERSCORE_ID, target); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java index 16ed45afd..d766b088d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java @@ -116,6 +116,10 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { return new ProjectionOperationBuilder(name, this, null); } + public ExpressionProjectionOperationBuilder andExpression(String expression, Object... params) { + return new ExpressionProjectionOperationBuilder(expression, this, params); + } + /** * Excludes the given fields from the projection. * @@ -188,13 +192,80 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { return new BasicDBObject("$project", fieldObject); } + /** + * @author Thomas Darimont + */ + public static abstract class AbstractProjectionOperationBuilder implements AggregationOperation { + + protected final Object value; + protected final ProjectionOperation operation; + + public AbstractProjectionOperationBuilder(Object value, ProjectionOperation operation) { + + Assert.notNull(value, "value must not be null or empty!"); + Assert.notNull(operation, "ProjectionOperation must not be null!"); + + this.value = value; + this.operation = operation; + } + + public abstract ProjectionOperation as(String alias); + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDBObject(AggregationOperationContext context) { + return this.operation.toDBObject(context); + } + } + + /** + * @author Thomas Darimont + */ + public static class ExpressionProjectionOperationBuilder extends AbstractProjectionOperationBuilder { + + private Object[] params; + + public ExpressionProjectionOperationBuilder(Object value, ProjectionOperation operation, Object[] params) { + super(value, operation); + this.params = params; + } + + public ProjectionOperation as(String alias) { + + return this.operation.and(new ExpressionProjection(Fields.field(alias, "expr"), this.value.toString(), params)); + } + + static class ExpressionProjection extends Projection { + + private String expression; + private Object[] params; + + public ExpressionProjection(Field field, String expression, Object[] params) { + super(field); + this.expression = expression; + this.params = params; + } + + /* (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(getExposedField().getName(), + SpelExpressionToMongoExpressionTransformer.INSTANCE.transform(expression, context, params)); + } + } + } + /** * Builder for {@link ProjectionOperation}s on a field. * * @author Oliver Gierke * @author Thomas Darimont */ - public static class ProjectionOperationBuilder implements AggregationOperation { + public static class ProjectionOperationBuilder extends AbstractProjectionOperationBuilder { private final String name; private final ProjectionOperation operation; @@ -209,9 +280,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { * @param previousProjection the previous operation projection, may be {@literal null}. */ public ProjectionOperationBuilder(String name, ProjectionOperation operation, OperationProjection previousProjection) { - - Assert.hasText(name, "Field name must not be null or empty!"); - Assert.notNull(operation, "ProjectionOperation must not be null!"); + super(name, operation); this.name = name; this.operation = operation; @@ -248,8 +317,8 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { */ public ProjectionOperation as(String alias) { - if (previousProjection != null) { - return this.operation.andReplaceLastOneWith(previousProjection.withAlias(alias)); + if (this.previousProjection != null) { + return this.operation.andReplaceLastOneWith(this.previousProjection.withAlias(alias)); } else { return this.operation.and(new FieldProjection(Fields.field(alias, name), null)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformer.java new file mode 100644 index 000000000..19dcb487a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformer.java @@ -0,0 +1,668 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.util.DBObjectUtils; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.ast.CompoundExpression; +import org.springframework.expression.spel.ast.FloatLiteral; +import org.springframework.expression.spel.ast.Indexer; +import org.springframework.expression.spel.ast.InlineList; +import org.springframework.expression.spel.ast.IntLiteral; +import org.springframework.expression.spel.ast.Literal; +import org.springframework.expression.spel.ast.LongLiteral; +import org.springframework.expression.spel.ast.MethodReference; +import org.springframework.expression.spel.ast.NullLiteral; +import org.springframework.expression.spel.ast.OpDivide; +import org.springframework.expression.spel.ast.OpMinus; +import org.springframework.expression.spel.ast.OpModulus; +import org.springframework.expression.spel.ast.OpMultiply; +import org.springframework.expression.spel.ast.OpPlus; +import org.springframework.expression.spel.ast.Operator; +import org.springframework.expression.spel.ast.PropertyOrFieldReference; +import org.springframework.expression.spel.ast.RealLiteral; +import org.springframework.expression.spel.ast.StringLiteral; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Renders the AST of a SpEL expression as a MongoDB Aggregation Framework projection expression. + * + * @author Thomas Darimont + */ +enum SpelExpressionToMongoExpressionTransformer { + + INSTANCE; + + private List> conversions; + + /** + * Creates a new {@link SpelExpressionToMongoExpressionTransformer}. + */ + private SpelExpressionToMongoExpressionTransformer() { + + this.conversions = new ArrayList>(); + this.conversions.add(new OperatorNodeConversion()); + this.conversions.add(new LiteralNodeConversion()); + this.conversions.add(new IndexerNodeConversion()); + this.conversions.add(new InlineListNodeConversion()); + this.conversions.add(new PropertyOrFieldReferenceNodeConversion()); + this.conversions.add(new CompoundExpressionNodeConversion()); + this.conversions.add(new MethodReferenceNodeConversion()); + } + + /** + * Transforms the given SpEL expression string to a corresponding MongoDB Expression. + * + * @param expression must be a SpEL expression. + * @return + */ + public Object transform(String expression) { + return transform(expression, Aggregation.DEFAULT_CONTEXT, new Object[0]); + } + + /** + * Transforms the given SpEL expression string to a corresponding MongoDB expression against the + * {@link Aggregation#DEFAULT_CONTEXT}. + *

+ * Exposes the given @{code params} as [0] ... [n]. + * + * @param expression must be a SpEL expression. + * @param params must not be {@literal null} + * @return + */ + public Object transform(String expression, Object... params) { + + return transform(expression, Aggregation.DEFAULT_CONTEXT, params); + } + + /** + * Transforms the given SpEL expression string to a corresponding MongoDB expression against the given + * {@link AggregationOperationContext} {@code context}. + *

+ * Exposes the given @{code params} as [0] ... [n]. + * + * @param expression must not be {@literal null} + * @param context must not be {@literal null} + * @param params must not be {@literal null} + * @return + */ + public Object transform(String expression, AggregationOperationContext context, Object[] params) { + + Assert.notNull(expression, "expression must not be null!"); + + return transform((SpelExpression) new SpelExpressionParser().parseExpression(expression), context, params); + } + + /** + * Transforms the given SpEL expression to a corresponding MongoDB expression against the given + * {@link AggregationOperationContext} {@code context}. + *

+ * Exposes the given @{code params} as [0] ... [n]. + * + * @param expression must not be {@literal null} + * @param context must not be {@literal null} + * @param params must not be {@literal null} + * @return + */ + public Object transform(SpelExpression expression, AggregationOperationContext context, Object[] params) { + + Assert.notNull(params, "params must not be null!"); + + return transform(expression, context, new ExpressionState(new StandardEvaluationContext(params))); + } + + /** + * Transforms the given SpEL expression to a corresponding MongoDB expression against the given + * {@link AggregationOperationContext} {@code context} and the given {@link ExpressionState}. + * + * @param expression + * @param aggregationContext + * @param expressionState + * @return + */ + public Object transform(SpelExpression expression, AggregationOperationContext aggregationContext, + ExpressionState expressionState) { + + Assert.notNull(expression, "expression must not be null!"); + Assert.notNull(aggregationContext, "aggregationContext must not be null!"); + Assert.notNull(expressionState, "expressionState must not be null!"); + + ExpressionTransformationContext expressionContext = new ExpressionTransformationContext(expression.getAST(), null, + null, aggregationContext, expressionState); + return doTransform(expressionContext); + } + + /** + * @param spelNode + * @param context + * @return + */ + private Object doTransform(ExpressionTransformationContext context) { + + return lookupConversionFor(context.getCurrentNode()).convert(context); + } + + /** + * Returns an appropriate {@link SpelNodeConversion} for the given {@code node}. Throws an + * {@link IllegalArgumentException} if no conversion could be found. + * + * @param node + * @return the appropriate {@link SpelNodeConversion} for the given {@link SpelNode}. + */ + private SpelNodeConversion lookupConversionFor(SpelNode node) { + + for (SpelNodeConversion candidate : conversions) { + if (candidate.supports(node)) { + return candidate; + } + } + + throw new IllegalArgumentException("Unsupported Element: " + node + " Type: " + node.getClass() + + " You probably have a syntax error in your SpEL expression!"); + } + + /** + * Holds information about the current transformation context. + * + * @author Thomas Darimont + */ + private static class ExpressionTransformationContext { + + private final SpelNode currentNode; + + private final SpelNode parentNode; + + private final Object previousOperationObject; + + private final AggregationOperationContext aggregationContext; + + private final ExpressionState expressionState; + + /** + * Creates a ExpressionConversionContext + * + * @param currentNode, must not be {@literal null} + * @param parentNode + * @param previousOperationObject + * @param aggregationContext, must not be {@literal null} + * @param expressionState, must not be {@literal null} + */ + public ExpressionTransformationContext(SpelNode currentNode, SpelNode parentNode, Object previousOperationObject, + AggregationOperationContext aggregationContext, ExpressionState expressionState) { + + Assert.notNull(currentNode, "currentNode must not be null!"); + Assert.notNull(aggregationContext, "aggregationContext must not be null!"); + Assert.notNull(expressionState, "expressionState must not be null!"); + + this.currentNode = currentNode; + this.parentNode = parentNode; + this.previousOperationObject = previousOperationObject; + this.aggregationContext = aggregationContext; + this.expressionState = expressionState; + } + + /** + * Creates a {@link ExpressionTransformationContext}. + * + * @param child, must not be {@literal null} + * @param context, must not be {@literal null} + */ + public ExpressionTransformationContext(SpelNode currentNode, ExpressionTransformationContext context) { + this(currentNode, context.getParentNode(), context.getPreviousOperationObject(), context.getAggregationContext(), + context.getExpressionState()); + } + + public SpelNode getCurrentNode() { + return currentNode; + } + + public SpelNode getParentNode() { + return parentNode; + } + + public Object getPreviousOperationObject() { + return previousOperationObject; + } + + public AggregationOperationContext getAggregationContext() { + return aggregationContext; + } + + public ExpressionState getExpressionState() { + return expressionState; + } + + public boolean isPreviousOperationPresent() { + return getPreviousOperationObject() != null; + } + + /** + * Returns a {@link FieldReference} for the given {@code fieldName}. Checks whether a field with the given + * {@code fieldName} can be found in the {@link AggregationOperationContext}. + * + * @param fieldName + * @return + */ + private FieldReference getFieldReference(String fieldName) { + + if (aggregationContext == null) { + return null; + } + + return aggregationContext.getReference(fieldName); + } + } + + /** + * Abstract base class for {@link SpelNode} to (Db)-object conversions. + * + * @author Thomas Darimont + */ + static abstract class SpelNodeConversion { + + protected final Class nodeType; + + public SpelNodeConversion(Class nodeType) { + this.nodeType = nodeType; + } + + /** + * @param node + * @return true if {@literal this} conversion can be applied to the given {@code node}. + */ + protected boolean supports(SpelNode node) { + return nodeType.isAssignableFrom(node.getClass()); + } + + /** + * Performs the actual conversion from {@link SpelNode} to the corresponding representation for MongoDB. + * + * @param context + * @return + */ + abstract Object convert(ExpressionTransformationContext context); + + /** + * Extracts the argument list from the given {@code context}. + * + * @param context + * @return + */ + protected static BasicDBList extractArgumentListFrom(DBObject context) { + return (BasicDBList) context.get(context.keySet().iterator().next()); + } + + protected SpelExpressionToMongoExpressionTransformer getTransformer() { + return INSTANCE; + } + } + + /** + * A {@link SpelNodeConversion} that converts arithmetic operations. + * + * @author Thomas Darimont + */ + static class OperatorNodeConversion extends SpelNodeConversion { + + private Map arithmeticOperatorsSpelToMongoConversion = new HashMap() { + private static final long serialVersionUID = 1L; + + { + put("+", "$add"); + put("-", "$subtract"); + put("*", "$multiply"); + put("/", "$divide"); + put("%", "$mod"); + } + }; + + public OperatorNodeConversion() { + super(Operator.class); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.ExpressionConversionContext) + */ + @Override + Object convert(ExpressionTransformationContext context) { + + Operator currentNode = Operator.class.cast(context.getCurrentNode()); + boolean unaryOperator = currentNode.getRightOperand() == null; + + Object operationObject = createOperationObjectAndAddToPreviousArgumentsIfNecessary(context, currentNode, + unaryOperator); + + Object leftResult = convertPart(currentNode.getLeftOperand(), context, currentNode, operationObject); + + if (unaryOperator && currentNode instanceof OpMinus) { + + return convertUnaryMinusOp(context, leftResult); + } + + // we deliberately ignore the RHS result + convertPart(currentNode.getRightOperand(), context, currentNode, operationObject); + + return operationObject; + } + + private Object convertPart(SpelNode currentNode, ExpressionTransformationContext context, Operator parentNode, + Object operationObject) { + + return getTransformer().doTransform( + new ExpressionTransformationContext(currentNode, parentNode, operationObject, + context.getAggregationContext(), context.getExpressionState())); + } + + private Object createOperationObjectAndAddToPreviousArgumentsIfNecessary(ExpressionTransformationContext context, + Operator currentNode, boolean unaryOperator) { + + Object nextDbObject = new BasicDBObject(getOp(currentNode), new BasicDBList()); + + if (context.isPreviousOperationPresent()) { + + if (currentNode.getClass().equals(context.getParentNode().getClass())) { + + // same operator applied in a row e.g. 1 + 2 + 3 carry on with the operation and render as $add: [1, 2 ,3] + nextDbObject = context.getPreviousOperationObject(); + } else if (!unaryOperator) { + + // different operator -> add context object for next level to list if arguments of previous expression + extractArgumentListFrom((DBObject) context.getPreviousOperationObject()).add(nextDbObject); + } + } + + return nextDbObject; + } + + private Object convertUnaryMinusOp(ExpressionTransformationContext context, Object leftResult) { + + Object result = leftResult instanceof Number ? leftResult : new BasicDBObject("$multiply", DBObjectUtils.dbList( + -1, leftResult)); + + if (leftResult != null && context.getPreviousOperationObject() != null) { + extractArgumentListFrom((DBObject) context.getPreviousOperationObject()).add(result); + } + + return result; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#supports(java.lang.Class) + */ + @Override + protected boolean supports(SpelNode node) { + return node instanceof OpMinus || node instanceof OpPlus || node instanceof OpMultiply + || node instanceof OpDivide || node instanceof OpModulus; + } + + private String getOp(SpelNode node) { + return supports(node) ? toMongoOperator((Operator) node) : null; + } + + private String toMongoOperator(Operator operator) { + return arithmeticOperatorsSpelToMongoConversion.get(operator.getOperatorName()); + } + } + + /** + * A {@link SpelNodeConversion} that converts indexed expressions. + * + * @author Thomas Darimont + */ + static class IndexerNodeConversion extends SpelNodeConversion { + + public IndexerNodeConversion() { + super(Indexer.class); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.ExpressionConversionContext) + */ + @Override + Object convert(ExpressionTransformationContext context) { + + Indexer currentNode = Indexer.class.cast(context.getCurrentNode()); + Object value = currentNode.getValue(context.getExpressionState()); + + if (context.isPreviousOperationPresent()) { + + extractArgumentListFrom((DBObject) context.getPreviousOperationObject()).add(value); + return context.getPreviousOperationObject(); + } + + return value; + } + } + + /** + * A {@link SpelNodeConversion} that converts in-line list expressions. + * + * @author Thomas Darimont + */ + static class InlineListNodeConversion extends SpelNodeConversion { + + public InlineListNodeConversion() { + super(InlineList.class); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.ExpressionConversionContext) + */ + @Override + Object convert(ExpressionTransformationContext context) { + + InlineList currentNode = InlineList.class.cast(context.getCurrentNode()); + + if (currentNode.getChildCount() == 0) { + return null; + } + + // just take the first item + ExpressionTransformationContext nestedExpressionContext = new ExpressionTransformationContext( + currentNode.getChild(0), currentNode, null, context.getAggregationContext(), context.getExpressionState()); + return INSTANCE.doTransform(nestedExpressionContext); + } + + } + + /** + * A {@link SpelNodeConversion} that converts property or field reference expressions. + * + * @author Thomas Darimont + */ + static class PropertyOrFieldReferenceNodeConversion extends SpelNodeConversion { + + public PropertyOrFieldReferenceNodeConversion() { + super(PropertyOrFieldReference.class); + } + + @Override + Object convert(ExpressionTransformationContext context) { + + PropertyOrFieldReference currentNode = PropertyOrFieldReference.class.cast(context.getCurrentNode()); + FieldReference fieldReference = context.getFieldReference(currentNode.getName()); + + if (context.isPreviousOperationPresent()) { + extractArgumentListFrom((DBObject) context.getPreviousOperationObject()).add(fieldReference.toString()); + return context.getPreviousOperationObject(); + } + + return fieldReference.toString(); + } + } + + /** + * A {@link SpelNodeConversion} that converts literal expressions. + * + * @author Thomas Darimont + */ + static class LiteralNodeConversion extends SpelNodeConversion { + + public LiteralNodeConversion() { + super(Literal.class); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.ExpressionConversionContext) + */ + @Override + Object convert(ExpressionTransformationContext context) { + + Literal currentNode = Literal.class.cast(context.getCurrentNode()); + Object value = currentNode.getLiteralValue().getValue(); + + if (context.isPreviousOperationPresent()) { + + if (context.getParentNode() instanceof OpMinus && ((OpMinus) context.getParentNode()).getRightOperand() == null) { + // unary minus operator + return NumberUtils.convertNumberToTargetClass(((Number) value).doubleValue() * -1, + (Class) value.getClass()); // retain type, e.g. int to -int + } + + extractArgumentListFrom((DBObject) context.getPreviousOperationObject()).add(value); + return context.getPreviousOperationObject(); + } + + return value; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#supports(org.springframework.expression.spel.SpelNode) + */ + @Override + protected boolean supports(SpelNode node) { + return node instanceof FloatLiteral || node instanceof RealLiteral || node instanceof IntLiteral + || node instanceof LongLiteral || node instanceof StringLiteral || node instanceof NullLiteral; + } + } + + /** + * A {@link SpelNodeConversion} that converts method reference expressions. + * + * @author Thomas Darimont + */ + static class MethodReferenceNodeConversion extends SpelNodeConversion { + + private Map namedFunctionToMongoExpressionMap = new HashMap() { + private static final long serialVersionUID = 1L; + + { + put("concat", "$concat"); // Concatenates two strings. + put("strcasecmp", "$strcasecmp"); // Compares two strings and returns an integer that reflects the comparison. + put("substr", "$substr"); // Takes a string and returns portion of that string. + put("toLower", "$toLower"); // Converts a string to lowercase. + put("toUpper", "$toUpper"); // Converts a string to uppercase. + + put("dayOfYear", "$dayOfYear"); // Converts a date to a number between 1 and 366. + put("dayOfMonth", "$dayOfMonth"); // Converts a date to a number between 1 and 31. + put("dayOfWeek", "$dayOfWeek"); // Converts a date to a number between 1 and 7. + put("year", "$year"); // Converts a date to the full year. + put("month", "$month"); // Converts a date into a number between 1 and 12. + put("week", "$week"); // Converts a date into a number between 0 and 53 + put("hour", "$hour"); // Converts a date into a number between 0 and 23. + put("minute", "$minute"); // Converts a date into a number between 0 and 59. + put("second", "$second"); // Converts a date into a number between 0 and 59. May be 60 to account for leap + // seconds. + put("millisecond", "$millisecond"); // Returns the millisecond portion of a date as an integer between 0 and + // 999. + } + }; + + public MethodReferenceNodeConversion() { + super(MethodReference.class); + } + + private String getMongoFunctionFor(String methodName) { + return namedFunctionToMongoExpressionMap.get(methodName); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.ExpressionConversionContext) + */ + @Override + Object convert(ExpressionTransformationContext context) { + + MethodReference currentNode = MethodReference.class.cast(context.getCurrentNode()); + String stringAST = currentNode.toStringAST(); + String methodName = stringAST.substring(0, stringAST.indexOf('(')); + String mongoFunction = getMongoFunctionFor(methodName); + + List args = new ArrayList(); + for (int i = 0; i < currentNode.getChildCount(); i++) { + args.add(getTransformer().doTransform(new ExpressionTransformationContext(currentNode.getChild(i), context))); + } + + BasicDBObject functionObject = new BasicDBObject(mongoFunction, DBObjectUtils.dbList(args.toArray())); + + if (context.isPreviousOperationPresent()) { + extractArgumentListFrom((DBObject) context.getPreviousOperationObject()).add(functionObject); + return context.getPreviousOperationObject(); + } + + return functionObject; + } + } + + /** + * A {@link SpelNodeConversion} that converts method compound expressions. + * + * @author Thomas Darimont + */ + static class CompoundExpressionNodeConversion extends SpelNodeConversion { + + public CompoundExpressionNodeConversion() { + super(CompoundExpression.class); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionToMongoExpressionTransformer.ExpressionConversionContext) + */ + @Override + Object convert(ExpressionTransformationContext context) { + + CompoundExpression currentNode = CompoundExpression.class.cast(context.getCurrentNode()); + + if (currentNode.getChildCount() > 0 && !(currentNode.getChild(0) instanceof Indexer)) { + // we have a property path expression like: foo.bar -> render as reference + return context.getFieldReference(currentNode.toStringAST()).toString(); + } + + Object value = currentNode.getValue(context.getExpressionState()); + + if (context.isPreviousOperationPresent()) { + extractArgumentListFrom((DBObject) context.getPreviousOperationObject()).add(value); + return context.getPreviousOperationObject(); + } + + return value; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DBObjectUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DBObjectUtils.java new file mode 100644 index 000000000..1e334782b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DBObjectUtils.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +import com.mongodb.BasicDBList; + +/** + * @author Thomas Darimont + */ +public class DBObjectUtils { + + public static BasicDBList dbList(Object... items) { + + BasicDBList list = new BasicDBList(); + for (Object item : items) { + list.add(item); + } + return list; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java new file mode 100644 index 000000000..bfee8e660 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java @@ -0,0 +1,20 @@ +/* + * 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 + */ +package org.springframework.data.mongodb.util; \ No newline at end of file diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java similarity index 97% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectUtils.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java index a9be3ba3a..b53623fc6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectUtils.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java @@ -26,9 +26,9 @@ import com.mongodb.DBObject; * * @author Oliver Gierke */ -public abstract class DBObjectUtils { +public abstract class DBObjectTestUtils { - private DBObjectUtils() { + private DBObjectTestUtils() { } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index b7534f5f6..1f918b6e6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -22,20 +22,26 @@ import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import java.io.BufferedInputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Scanner; import org.junit.After; +import org.junit.Assume; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.dao.DataAccessException; +import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Query; @@ -68,13 +74,25 @@ public class AggregationTests { @Autowired MongoTemplate mongoTemplate; + @Rule public ExpectedException exception = ExpectedException.none(); + private static String mongoVersion; + @Before public void setUp() { + queryMongoVersionIfNecessary(); cleanDb(); initSampleDataIfNecessary(); } + private void queryMongoVersionIfNecessary() { + + if (mongoVersion == null) { + CommandResult result = mongoTemplate.executeCommand("{ buildInfo: 1 }"); + mongoVersion = result.get("version").toString(); + } + } + @After public void cleanUp() { cleanDb(); @@ -85,6 +103,7 @@ public class AggregationTests { mongoTemplate.dropCollection(Product.class); mongoTemplate.dropCollection(UserWithLikes.class); mongoTemplate.dropCollection(DATAMONGO753.class); + mongoTemplate.dropCollection(Data.class); } /** @@ -97,9 +116,7 @@ public class AggregationTests { if (!initialized) { - CommandResult result = mongoTemplate.executeCommand("{ buildInfo: 1 }"); - Object version = result.get("version"); - LOGGER.debug("Server uses MongoDB Version: {}", version); + LOGGER.debug("Server uses MongoDB Version: {}", mongoVersion); mongoTemplate.dropCollection(ZipInfo.class); mongoTemplate.execute(ZipInfo.class, new CollectionCallback() { @@ -425,13 +442,8 @@ public class AggregationTests { @Test public void arithmenticOperatorsInProjectionExample() { - double taxRate = 0.19; - double netPrice = 1.99; - double discountRate = 0.05; - int spaceUnits = 3; - String productId = "P1"; - String productName = "A"; - mongoTemplate.insert(new Product(productId, productName, netPrice, spaceUnits, discountRate, taxRate)); + Product product = new Product("P1", "A", 1.99, 3, 0.05, 0.19); + mongoTemplate.insert(product); TypedAggregation agg = newAggregation(Product.class, // project("name", "netPrice") // @@ -451,18 +463,125 @@ public class AggregationTests { List resultList = result.getMappedResults(); assertThat(resultList, is(notNullValue())); - assertThat((String) resultList.get(0).get("_id"), is(productId)); - assertThat((String) resultList.get(0).get("name"), is(productName)); - assertThat((Double) resultList.get(0).get("netPricePlus1"), is(netPrice + 1)); - assertThat((Double) resultList.get(0).get("netPriceMinus1"), is(netPrice - 1)); - assertThat((Double) resultList.get(0).get("netPriceMul2"), is(netPrice * 2)); - assertThat((Double) resultList.get(0).get("netPriceDiv119"), is(netPrice / 1.19)); - assertThat((Integer) resultList.get(0).get("spaceUnitsMod2"), is(spaceUnits % 2)); - assertThat((Integer) resultList.get(0).get("spaceUnitsPlusSpaceUnits"), is(spaceUnits + spaceUnits)); - assertThat((Integer) resultList.get(0).get("spaceUnitsMinusSpaceUnits"), is(spaceUnits - spaceUnits)); - assertThat((Integer) resultList.get(0).get("spaceUnitsMultiplySpaceUnits"), is(spaceUnits * spaceUnits)); - assertThat((Double) resultList.get(0).get("spaceUnitsDivideSpaceUnits"), is((double) (spaceUnits / spaceUnits))); - assertThat((Integer) resultList.get(0).get("spaceUnitsModSpaceUnits"), is(spaceUnits % spaceUnits)); + assertThat((String) resultList.get(0).get("_id"), is(product.id)); + assertThat((String) resultList.get(0).get("name"), is(product.name)); + assertThat((Double) resultList.get(0).get("netPricePlus1"), is(product.netPrice + 1)); + assertThat((Double) resultList.get(0).get("netPriceMinus1"), is(product.netPrice - 1)); + assertThat((Double) resultList.get(0).get("netPriceMul2"), is(product.netPrice * 2)); + assertThat((Double) resultList.get(0).get("netPriceDiv119"), is(product.netPrice / 1.19)); + assertThat((Integer) resultList.get(0).get("spaceUnitsMod2"), is(product.spaceUnits % 2)); + assertThat((Integer) resultList.get(0).get("spaceUnitsPlusSpaceUnits"), is(product.spaceUnits + product.spaceUnits)); + assertThat((Integer) resultList.get(0).get("spaceUnitsMinusSpaceUnits"), + is(product.spaceUnits - product.spaceUnits)); + assertThat((Integer) resultList.get(0).get("spaceUnitsMultiplySpaceUnits"), is(product.spaceUnits + * product.spaceUnits)); + assertThat((Double) resultList.get(0).get("spaceUnitsDivideSpaceUnits"), + is((double) (product.spaceUnits / product.spaceUnits))); + assertThat((Integer) resultList.get(0).get("spaceUnitsModSpaceUnits"), is(product.spaceUnits % product.spaceUnits)); + } + + /** + * @see DATAMONGO-774 + */ + @Test + public void expressionsInProjectionExample() { + + Product product = new Product("P1", "A", 1.99, 3, 0.05, 0.19); + mongoTemplate.insert(product); + + TypedAggregation agg = newAggregation(Product.class, // + project("name", "netPrice") // + .andExpression("netPrice + 1").as("netPricePlus1") // + .andExpression("netPrice - 1").as("netPriceMinus1") // + .andExpression("netPrice / 2").as("netPriceDiv2") // + .andExpression("netPrice * 1.19").as("grossPrice") // + .andExpression("spaceUnits % 2").as("spaceUnitsMod2") // + .andExpression("(netPrice * 0.8 + 1.2) * 1.19").as("grossPriceIncludingDiscountAndCharge") // + + ); + + AggregationResults result = mongoTemplate.aggregate(agg, DBObject.class); + List resultList = result.getMappedResults(); + + assertThat(resultList, is(notNullValue())); + assertThat((String) resultList.get(0).get("_id"), is(product.id)); + assertThat((String) resultList.get(0).get("name"), is(product.name)); + assertThat((Double) resultList.get(0).get("netPricePlus1"), is(product.netPrice + 1)); + assertThat((Double) resultList.get(0).get("netPriceMinus1"), is(product.netPrice - 1)); + assertThat((Double) resultList.get(0).get("netPriceDiv2"), is(product.netPrice / 2)); + assertThat((Double) resultList.get(0).get("grossPrice"), is(product.netPrice * 1.19)); + assertThat((Integer) resultList.get(0).get("spaceUnitsMod2"), is(product.spaceUnits % 2)); + assertThat((Double) resultList.get(0).get("grossPriceIncludingDiscountAndCharge"), + is((product.netPrice * 0.8 + 1.2) * 1.19)); + } + + /** + * @see DATAMONGO-774 + */ + @Test + public void stringExpressionsInProjectionExample() { + + Assume.assumeTrue(mongoVersion.startsWith("2.4")); + + Product product = new Product("P1", "A", 1.99, 3, 0.05, 0.19); + mongoTemplate.insert(product); + + TypedAggregation agg = newAggregation(Product.class, // + project("name", "netPrice") // + .andExpression("concat(name, '_bubu')").as("name_bubu") // + ); + + AggregationResults result = mongoTemplate.aggregate(agg, DBObject.class); + List resultList = result.getMappedResults(); + + assertThat(resultList, is(notNullValue())); + assertThat((String) resultList.get(0).get("_id"), is(product.id)); + assertThat((String) resultList.get(0).get("name"), is(product.name)); + assertThat((String) resultList.get(0).get("name_bubu"), is(product.name + "_bubu")); + } + + /** + * @see DATAMONGO-774 + */ + @Test + public void expressionsInProjectionExampleShowcase() { + + Product product = new Product("P1", "A", 1.99, 3, 0.05, 0.19); + mongoTemplate.insert(product); + + double shippingCosts = 1.2; + + TypedAggregation agg = newAggregation(Product.class, // + project("name", "netPrice") // + .andExpression("(netPrice * (1-discountRate) + [0]) * (1+taxRate)", shippingCosts).as("salesPrice") // + ); + + AggregationResults result = mongoTemplate.aggregate(agg, DBObject.class); + List resultList = result.getMappedResults(); + + assertThat(resultList, is(notNullValue())); + DBObject firstItem = resultList.get(0); + assertThat((String) firstItem.get("_id"), is(product.id)); + assertThat((String) firstItem.get("name"), is(product.name)); + assertThat((Double) firstItem.get("salesPrice"), is((product.netPrice * (1 - product.discountRate) + shippingCosts) + * (1 + product.taxRate))); + } + + @Test + public void shouldThrowExceptionIfUnknownFieldIsReferencedInArithmenticExpressionsInProjection() { + + exception.expect(MappingException.class); + exception.expectMessage("unknown"); + + Product product = new Product("P1", "A", 1.99, 3, 0.05, 0.19); + mongoTemplate.insert(product); + + TypedAggregation agg = newAggregation(Product.class, // + project("name", "netPrice") // + .andExpression("unknown + 1").as("netPricePlus1") // + ); + + mongoTemplate.aggregate(agg, DBObject.class); } /** @@ -520,6 +639,78 @@ public class AggregationTests { } } + /** + * @DATAMONGO-774 + */ + @Test + public void shouldPerformDateProjectionOperatorsCorrectly() throws ParseException { + + Assume.assumeTrue(mongoVersion.startsWith("2.4")); + + Data data = new Data(); + data.stringValue = "ABC"; + mongoTemplate.insert(data); + + TypedAggregation agg = newAggregation(Data.class, project() // + .andExpression("concat(stringValue, 'DE')").as("concat") // + .andExpression("strcasecmp(stringValue,'XYZ')").as("strcasecmp") // + .andExpression("substr(stringValue,1,1)").as("substr") // + .andExpression("toLower(stringValue)").as("toLower") // + .andExpression("toUpper(toLower(stringValue))").as("toUpper") // + ); + + AggregationResults results = mongoTemplate.aggregate(agg, DBObject.class); + DBObject dbo = results.getUniqueMappedResult(); + + assertThat(dbo, is(notNullValue())); + assertThat((String) dbo.get("concat"), is("ABCDE")); + assertThat((Integer) dbo.get("strcasecmp"), is(-1)); + assertThat((String) dbo.get("substr"), is("B")); + assertThat((String) dbo.get("toLower"), is("abc")); + assertThat((String) dbo.get("toUpper"), is("ABC")); + } + + /** + * @DATAMONGO-774 + */ + @Test + public void shouldPerformStringProjectionOperatorsCorrectly() throws ParseException { + + Assume.assumeTrue(mongoVersion.startsWith("2.4")); + + Data data = new Data(); + data.dateValue = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SSSZ").parse("29.08.1983 12:34:56.789+0000"); + mongoTemplate.insert(data); + + TypedAggregation agg = newAggregation(Data.class, project() // + .andExpression("dayOfYear(dateValue)").as("dayOfYear") // + .andExpression("dayOfMonth(dateValue)").as("dayOfMonth") // + .andExpression("dayOfWeek(dateValue)").as("dayOfWeek") // + .andExpression("year(dateValue)").as("year") // + .andExpression("month(dateValue)").as("month") // + .andExpression("week(dateValue)").as("week") // + .andExpression("hour(dateValue)").as("hour") // + .andExpression("minute(dateValue)").as("minute") // + .andExpression("second(dateValue)").as("second") // + .andExpression("millisecond(dateValue)").as("millisecond") // + ); + + AggregationResults results = mongoTemplate.aggregate(agg, DBObject.class); + DBObject dbo = results.getUniqueMappedResult(); + + assertThat(dbo, is(notNullValue())); + assertThat((Integer) dbo.get("dayOfYear"), is(241)); + assertThat((Integer) dbo.get("dayOfMonth"), is(29)); + assertThat((Integer) dbo.get("dayOfWeek"), is(2)); + assertThat((Integer) dbo.get("year"), is(1983)); + assertThat((Integer) dbo.get("month"), is(8)); + assertThat((Integer) dbo.get("week"), is(35)); + assertThat((Integer) dbo.get("hour"), is(12)); + assertThat((Integer) dbo.get("minute"), is(34)); + assertThat((Integer) dbo.get("second"), is(56)); + assertThat((Integer) dbo.get("millisecond"), is(789)); + } + private void assertLikeStats(LikeStats like, String id, long count) { assertThat(like, is(notNullValue())); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Data.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Data.java new file mode 100644 index 000000000..1ea143d58 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Data.java @@ -0,0 +1,31 @@ +/* + * 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.Date; + +/** + * @author Thomas Darimont + */ +class Data { + public long primitiveLongValue; + public double primitiveDoubleValue; + public Double doubleValue; + public Date dateValue; + public String stringValue; + + public DataItem item; +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DataItem.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DataItem.java new file mode 100644 index 000000000..0ed5e1563 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DataItem.java @@ -0,0 +1,23 @@ +/* + * 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 + */ +class DataItem { + int primitiveIntValue; +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java index 50ef25076..4b0bba6b9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsUnitTests.java @@ -25,6 +25,7 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedFi * Unit tests for {@link ExposedFields}. * * @author Oliver Gierke + * @author Thomas Darimont */ public class ExposedFieldsUnitTests { @@ -43,6 +44,14 @@ public class ExposedFieldsUnitTests { ExposedFields.nonSynthetic(null); } + @Test + public void mitigateLeadingDollarSignInFieldName() { + + ExposedFields fields = ExposedFields.synthetic(Fields.fields("$foo")); + assertThat(fields.iterator().next().getName(), is("$foo")); + assertThat(fields.iterator().next().getTarget(), is("$foo")); + } + @Test public void exposesSingleField() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java index b1eb43bd2..16d4e145c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java @@ -19,7 +19,7 @@ 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.DBObjectTestUtils; import org.springframework.data.mongodb.core.query.NearQuery; import com.mongodb.DBObject; @@ -38,7 +38,7 @@ public class GeoNearOperationUnitTests { GeoNearOperation operation = new GeoNearOperation(query); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject nearClause = DBObjectUtils.getAsDBObject(dbObject, "$geoNear"); + DBObject nearClause = DBObjectTestUtils.getAsDBObject(dbObject, "$geoNear"); assertThat(nearClause, is(query.toDBObject())); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java index d5f71cc3a..57e30ebee 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java @@ -20,7 +20,7 @@ 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 org.springframework.data.mongodb.core.DBObjectTestUtils; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -85,7 +85,7 @@ public class GroupOperationUnitTests { GroupOperation operation = new GroupOperation(fields("a").and("b", "c")); DBObject groupClause = extractDbObjectFromGroupOperation(operation); - DBObject idClause = DBObjectUtils.getAsDBObject(groupClause, UNDERSCORE_ID); + DBObject idClause = DBObjectTestUtils.getAsDBObject(groupClause, UNDERSCORE_ID); assertThat(idClause.get("a"), is((Object) "$a")); assertThat(idClause.get("b"), is((Object) "$c")); @@ -98,7 +98,7 @@ public class GroupOperationUnitTests { .sum("e").as("e"); DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject eOp = DBObjectUtils.getAsDBObject(groupClause, "e"); + DBObject eOp = DBObjectTestUtils.getAsDBObject(groupClause, "e"); assertThat(eOp, is((DBObject) new BasicDBObject("$sum", "$e"))); } @@ -109,7 +109,7 @@ public class GroupOperationUnitTests { .sum("e").as("ee"); DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject eOp = DBObjectUtils.getAsDBObject(groupClause, "ee"); + DBObject eOp = DBObjectTestUtils.getAsDBObject(groupClause, "ee"); assertThat(eOp, is((DBObject) new BasicDBObject("$sum", "$e"))); } @@ -120,7 +120,7 @@ public class GroupOperationUnitTests { .count().as("count"); DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject eOp = DBObjectUtils.getAsDBObject(groupClause, "count"); + DBObject eOp = DBObjectTestUtils.getAsDBObject(groupClause, "count"); assertThat(eOp, is((DBObject) new BasicDBObject("$sum", 1))); } @@ -132,10 +132,10 @@ public class GroupOperationUnitTests { .min("e").as("min"); // DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject sum = DBObjectUtils.getAsDBObject(groupClause, "sum"); + DBObject sum = DBObjectTestUtils.getAsDBObject(groupClause, "sum"); assertThat(sum, is((DBObject) new BasicDBObject("$sum", "$e"))); - DBObject min = DBObjectUtils.getAsDBObject(groupClause, "min"); + DBObject min = DBObjectTestUtils.getAsDBObject(groupClause, "min"); assertThat(min, is((DBObject) new BasicDBObject("$min", "$e"))); } @@ -145,7 +145,7 @@ public class GroupOperationUnitTests { GroupOperation groupOperation = Aggregation.group("a", "b").push(1).as("x"); DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject push = DBObjectUtils.getAsDBObject(groupClause, "x"); + DBObject push = DBObjectTestUtils.getAsDBObject(groupClause, "x"); assertThat(push, is((DBObject) new BasicDBObject("$push", 1))); } @@ -156,7 +156,7 @@ public class GroupOperationUnitTests { GroupOperation groupOperation = Aggregation.group("a", "b").push("ref").as("x"); DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject push = DBObjectUtils.getAsDBObject(groupClause, "x"); + DBObject push = DBObjectTestUtils.getAsDBObject(groupClause, "x"); assertThat(push, is((DBObject) new BasicDBObject("$push", "$ref"))); } @@ -167,7 +167,7 @@ public class GroupOperationUnitTests { GroupOperation groupOperation = Aggregation.group("a", "b").addToSet("ref").as("x"); DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject push = DBObjectUtils.getAsDBObject(groupClause, "x"); + DBObject push = DBObjectTestUtils.getAsDBObject(groupClause, "x"); assertThat(push, is((DBObject) new BasicDBObject("$addToSet", "$ref"))); } @@ -178,14 +178,14 @@ public class GroupOperationUnitTests { GroupOperation groupOperation = Aggregation.group("a", "b").addToSet(42).as("x"); DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); - DBObject push = DBObjectUtils.getAsDBObject(groupClause, "x"); + DBObject push = DBObjectTestUtils.getAsDBObject(groupClause, "x"); assertThat(push, is((DBObject) new BasicDBObject("$addToSet", 42))); } private DBObject extractDbObjectFromGroupOperation(GroupOperation groupOperation) { DBObject dbObject = groupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject groupClause = DBObjectUtils.getAsDBObject(dbObject, "$group"); + DBObject groupClause = DBObjectTestUtils.getAsDBObject(dbObject, "$group"); return groupClause; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java index 44944c99e..d9c49475d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java @@ -17,11 +17,12 @@ package org.springframework.data.mongodb.core.aggregation; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.springframework.data.mongodb.util.DBObjectUtils.*; import java.util.Arrays; import org.junit.Test; -import org.springframework.data.mongodb.core.DBObjectUtils; +import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; import com.mongodb.BasicDBList; @@ -55,7 +56,7 @@ public class ProjectionOperationUnitTests { operation = operation.and("prop").previousOperation(); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); assertThat(projectClause.get("prop"), is((Object) Fields.UNDERSCORE_ID_REF)); } @@ -65,7 +66,7 @@ public class ProjectionOperationUnitTests { ProjectionOperation operation = new ProjectionOperation(Fields.fields("foo").and("bar", "foobar")); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); assertThat((Integer) projectClause.get("foo"), is(1)); assertThat(projectClause.get("bar"), is((Object) "$foobar")); @@ -77,7 +78,7 @@ public class ProjectionOperationUnitTests { ProjectionOperation operation = new ProjectionOperation(); DBObject dbObject = operation.and("foo").as("bar").toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); assertThat(projectClause.get("bar"), is((Object) "$foo")); } @@ -88,9 +89,9 @@ public class ProjectionOperationUnitTests { ProjectionOperation operation = new ProjectionOperation(); DBObject dbObject = operation.and("foo").plus(41).as("bar").toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); - DBObject barClause = DBObjectUtils.getAsDBObject(projectClause, "bar"); - BasicDBList addClause = DBObjectUtils.getAsDBList(barClause, "$add"); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); + DBObject barClause = DBObjectTestUtils.getAsDBObject(projectClause, "bar"); + BasicDBList addClause = DBObjectTestUtils.getAsDBList(barClause, "$add"); assertThat(addClause, hasSize(2)); assertThat(addClause.get(0), is((Object) "$foo")); @@ -102,7 +103,7 @@ public class ProjectionOperationUnitTests { String fieldName = "a"; ProjectionOperationBuilder operation = new ProjectionOperation().and(fieldName).plus(1); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject oper = exctractOperation(fieldName, projectClause); assertThat(oper.containsField(ADD), is(true)); @@ -116,7 +117,7 @@ public class ProjectionOperationUnitTests { String fieldAlias = "b"; ProjectionOperation operation = new ProjectionOperation().and(fieldName).plus(1).as(fieldAlias); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject oper = exctractOperation(fieldAlias, projectClause); assertThat(oper.containsField(ADD), is(true)); @@ -130,7 +131,7 @@ public class ProjectionOperationUnitTests { String fieldAlias = "b"; ProjectionOperation operation = new ProjectionOperation().and(fieldName).minus(1).as(fieldAlias); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject oper = exctractOperation(fieldAlias, projectClause); assertThat(oper.containsField(SUBTRACT), is(true)); @@ -144,7 +145,7 @@ public class ProjectionOperationUnitTests { String fieldAlias = "b"; ProjectionOperation operation = new ProjectionOperation().and(fieldName).multiply(1).as(fieldAlias); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject oper = exctractOperation(fieldAlias, projectClause); assertThat(oper.containsField(MULTIPLY), is(true)); @@ -158,7 +159,7 @@ public class ProjectionOperationUnitTests { String fieldAlias = "b"; ProjectionOperation operation = new ProjectionOperation().and(fieldName).divide(1).as(fieldAlias); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject oper = exctractOperation(fieldAlias, projectClause); assertThat(oper.containsField(DIVIDE), is(true)); @@ -178,7 +179,7 @@ public class ProjectionOperationUnitTests { String fieldAlias = "b"; ProjectionOperation operation = new ProjectionOperation().and(fieldName).mod(3).as(fieldAlias); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); DBObject oper = exctractOperation(fieldAlias, projectClause); assertThat(oper.containsField(MOD), is(true)); @@ -202,7 +203,7 @@ public class ProjectionOperationUnitTests { ProjectionOperation projectionOp = new ProjectionOperation().andExclude(Fields.UNDERSCORE_ID); DBObject dbObject = projectionOp.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); assertThat((Integer) projectClause.get(Fields.UNDERSCORE_ID), is(0)); } @@ -216,7 +217,7 @@ public class ProjectionOperationUnitTests { .andExclude("_id"); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); assertThat(projectClause.get("foo"), is((Object) 1)); // implicit assertThat(projectClause.get("bar"), is((Object) "$foobar")); // explicit @@ -245,7 +246,7 @@ public class ProjectionOperationUnitTests { .and("foo").mod("bar").as("fooModBar"); DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); - DBObject projectClause = DBObjectUtils.getAsDBObject(dbObject, PROJECT); + DBObject projectClause = DBObjectTestUtils.getAsDBObject(dbObject, PROJECT); assertThat((BasicDBObject) projectClause.get("fooPlusBar"), // is(new BasicDBObject("$add", dbList("$foo", "$bar")))); @@ -259,13 +260,20 @@ public class ProjectionOperationUnitTests { is(new BasicDBObject("$mod", dbList("$foo", "$bar")))); } - public static BasicDBList dbList(Object... items) { + /** + * @see DATAMONGO-774 + */ + @Test + public void projectionExpressions() { - BasicDBList list = new BasicDBList(); - for (Object item : items) { - list.add(item); - } - return list; + ProjectionOperation operation = Aggregation.project() // + .andExpression("(netPrice + surCharge) * taxrate * [0]", 2).as("grossSalesPrice") // + .and("foo").as("bar"); // + + DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT); + assertThat( + dbObject.toString(), + is("{ \"$project\" : { \"grossSalesPrice\" : { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\" , 2]} , \"bar\" : \"$foo\"}}")); } private static DBObject exctractOperation(String field, DBObject fromProjectClause) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java index b1c760a0d..aa23d222a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortOperationUnitTests.java @@ -17,7 +17,7 @@ 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 static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import org.junit.Test; import org.springframework.data.domain.Sort; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerIntegrationTests.java new file mode 100644 index 000000000..7fb875591 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerIntegrationTests.java @@ -0,0 +1,70 @@ +/* + * 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.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests for {@link SpelExpressionToMongoExpressionTransformer}. + * + * @author Thomas Darimont + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:infrastructure.xml") +public class SpelExpressionToMongoExpressionTransformerIntegrationTests { + + @Autowired MongoDbFactory mongoDbFactory; + + @Rule public ExpectedException exception = ExpectedException.none(); + + SpelExpressionToMongoExpressionTransformer transformer = SpelExpressionToMongoExpressionTransformer.INSTANCE; + + @Test + public void shouldConvertCompoundExpressionToPropertyPath() { + + MappingMongoConverter converter = new MappingMongoConverter(mongoDbFactory, new MongoMappingContext()); + TypeBasedAggregationOperationContext ctxt = new TypeBasedAggregationOperationContext(Data.class, + new MongoMappingContext(), new QueryMapper(converter)); + assertThat(transformer.transform("item.primitiveIntValue", ctxt, new Object[0]).toString(), + is("$item.primitiveIntValue")); + } + + @Test + public void shouldThrowExceptionIfNestedPropertyCannotBeFound() { + + exception.expect(MappingException.class); + exception.expectMessage("value2"); + + MappingMongoConverter converter = new MappingMongoConverter(mongoDbFactory, new MongoMappingContext()); + TypeBasedAggregationOperationContext ctxt = new TypeBasedAggregationOperationContext(Data.class, + new MongoMappingContext(), new QueryMapper(converter)); + assertThat(transformer.transform("item.value2", ctxt, new Object[0]).toString(), is("$item.value2")); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerTests.java new file mode 100644 index 000000000..3f50f9176 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerTests.java @@ -0,0 +1,179 @@ +/* + * 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.Before; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Unit tests for {@link SpelExpressionToMongoExpressionTransformer}. + * + * @author Thomas Darimont + */ +public class SpelExpressionToMongoExpressionTransformerTests { + + SpelExpressionToMongoExpressionTransformer transformer = SpelExpressionToMongoExpressionTransformer.INSTANCE; + + Data data; + + @Before + public void setup() { + + this.data = new Data(); + this.data.primitiveLongValue = 42; + this.data.primitiveDoubleValue = 1.2345; + this.data.doubleValue = 23.0; + this.data.item = new DataItem(); + this.data.item.primitiveIntValue = 21; + } + + @Test + public void shouldRenderConstantExpression() { + assertThat(transformer.transform("1").toString(), is("1")); + assertThat(transformer.transform("-1").toString(), is("-1")); + assertThat(transformer.transform("1.0").toString(), is("1.0")); + assertThat(transformer.transform("-1.0").toString(), is("-1.0")); + assertThat(String.valueOf(transformer.transform("null")), is("null")); + } + + @Test + public void shouldSupportKnownOperands() { + assertThat(transformer.transform("a + b").toString(), is("{ \"$add\" : [ \"$a\" , \"$b\"]}")); + assertThat(transformer.transform("a - b").toString(), is("{ \"$subtract\" : [ \"$a\" , \"$b\"]}")); + assertThat(transformer.transform("a * b").toString(), is("{ \"$multiply\" : [ \"$a\" , \"$b\"]}")); + assertThat(transformer.transform("a / b").toString(), is("{ \"$divide\" : [ \"$a\" , \"$b\"]}")); + assertThat(transformer.transform("a % b").toString(), is("{ \"$mod\" : [ \"$a\" , \"$b\"]}")); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionOnUnknownOperand() { + transformer.transform("a ^ 1"); + } + + @Test + public void shouldRenderSumExpression() { + assertThat(transformer.transform("a + 1").toString(), is("{ \"$add\" : [ \"$a\" , 1]}")); + } + + @Test + public void shouldRenderFormula() { + assertThat( + transformer.transform("(netPrice + surCharge) * taxrate + 42").toString(), + is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); + } + + @Test + public void shouldRenderFormulaInCurlyBrackets() { + assertThat( + transformer.transform("{(netPrice + surCharge) * taxrate + 42}").toString(), + is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); + } + + @Test + public void shouldRenderFieldReference() { + assertThat(transformer.transform("foo").toString(), is("$foo")); + assertThat(transformer.transform("$foo").toString(), is("$foo")); + } + + @Test + public void shouldRenderNestedFieldReference() { + assertThat(transformer.transform("foo.bar").toString(), is("$foo.bar")); + assertThat(transformer.transform("$foo.bar").toString(), is("$foo.bar")); + } + + @Test + @Ignore + public void shouldRenderNestedIndexedFieldReference() { + + // TODO add support for rendering nested indexed field references + assertThat(transformer.transform("foo[3].bar").toString(), is("$foo[3].bar")); + } + + @Test + public void shouldRenderConsecutiveOperation() { + assertThat(transformer.transform("1 + 1 + 1").toString(), is("{ \"$add\" : [ 1 , 1 , 1]}")); + } + + @Test + public void shouldRenderComplexExpression0() { + assertThat(transformer.transform("-(1 + q)").toString(), + is("{ \"$multiply\" : [ -1 , { \"$add\" : [ 1 , \"$q\"]}]}")); + } + + @Test + public void shouldRenderComplexExpression1() { + assertThat(transformer.transform("1 + (q + 1) / (q - 1)").toString(), + is("{ \"$add\" : [ 1 , { \"$divide\" : [ { \"$add\" : [ \"$q\" , 1]} , { \"$subtract\" : [ \"$q\" , 1]}]}]}")); + } + + @Test + public void shouldRenderComplexExpression2() { + assertThat( + transformer.transform("(q + 1 + 4 - 5) / (q + 1 + 3 + 4)").toString(), + is("{ \"$divide\" : [ { \"$subtract\" : [ { \"$add\" : [ \"$q\" , 1 , 4]} , 5]} , { \"$add\" : [ \"$q\" , 1 , 3 , 4]}]}")); + } + + @Test + public void shouldRenderBinaryExpressionWithMixedSignsCorrectly() { + assertThat(transformer.transform("-4 + 1").toString(), is("{ \"$add\" : [ -4 , 1]}")); + assertThat(transformer.transform("1 + -4").toString(), is("{ \"$add\" : [ 1 , -4]}")); + } + + @Test + public void shouldRenderConsecutiveOperationsInComplexExpression() { + assertThat(transformer.transform("1 + 1 + (1 + 1 + 1) / q").toString(), + is("{ \"$add\" : [ 1 , 1 , { \"$divide\" : [ { \"$add\" : [ 1 , 1 , 1]} , \"$q\"]}]}")); + } + + @Test + public void shouldRenderParameterExpressionResults() { + + assertThat(transformer.transform("[0] + [1] + [2]", 1, 2, 3).toString(), is("{ \"$add\" : [ 1 , 2 , 3]}")); + } + + @Test + public void shouldRenderNestedParameterExpressionResults() { + + assertThat( + transformer.transform("[0].primitiveLongValue + [0].primitiveDoubleValue + [0].doubleValue.longValue()", data) + .toString(), is("{ \"$add\" : [ 42 , 1.2345 , 23]}")); + } + + @Test + public void shouldRenderNestedParameterExpressionResultsInNestedExpressions() { + + assertThat( + transformer.transform( + "((1 + [0].primitiveLongValue) + [0].primitiveDoubleValue) * [0].doubleValue.longValue()", data).toString(), + is("{ \"$multiply\" : [ { \"$add\" : [ 1 , 42 , 1.2345]} , 23]}")); + } + + @Test + public void shouldRenderStringFunctions() { + + assertThat(transformer.transform("concat(a, b)").toString(), is("{ \"$concat\" : [ \"$a\" , \"$b\"]}")); + assertThat(transformer.transform("substr(a, 1, 2)").toString(), is("{ \"$substr\" : [ \"$a\" , 1 , 2]}")); + assertThat(transformer.transform("strcasecmp(a, b)").toString(), is("{ \"$strcasecmp\" : [ \"$a\" , \"$b\"]}")); + assertThat(transformer.transform("toLower(a)").toString(), is("{ \"$toLower\" : [ \"$a\"]}")); + assertThat(transformer.transform("toUpper(a)").toString(), is("{ \"$toUpper\" : [ \"$a\"]}")); + assertThat(transformer.transform("toUpper(toLower(a))").toString(), + is("{ \"$toUpper\" : [ { \"$toLower\" : [ \"$a\"]}]}")); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java index 6501d4ff8..e476d7e1d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java @@ -19,7 +19,7 @@ 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.DBObjectTestUtils; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; @@ -49,7 +49,7 @@ public class DBObjectAccessorUnitTests { DBObjectAccessor accessor = new DBObjectAccessor(dbObject); accessor.put(fooProperty, "FooBar"); - DBObject aDbObject = DBObjectUtils.getAsDBObject(dbObject, "a"); + DBObject aDbObject = DBObjectTestUtils.getAsDBObject(dbObject, "a"); assertThat(aDbObject.get("b"), is((Object) "FooBar")); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapperUnitTests.java index 644fc7913..83ee16e77 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapperUnitTests.java @@ -26,7 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.data.convert.ConfigurableTypeInformationMapper; import org.springframework.data.convert.SimpleTypeInformationMapper; -import org.springframework.data.mongodb.core.DBObjectUtils; +import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.util.TypeInformation; import com.mongodb.BasicDBList; @@ -119,8 +119,8 @@ public class DefaultMongoTypeMapperUnitTests { typeMapper = new DefaultMongoTypeMapper(); typeMapper.writeTypeRestrictions(result, Collections.> singleton(String.class)); - DBObject typeInfo = DBObjectUtils.getAsDBObject(result, DefaultMongoTypeMapper.DEFAULT_TYPE_KEY); - List aliases = DBObjectUtils.getAsDBList(typeInfo, "$in"); + DBObject typeInfo = DBObjectTestUtils.getAsDBObject(result, DefaultMongoTypeMapper.DEFAULT_TYPE_KEY); + List aliases = DBObjectTestUtils.getAsDBList(typeInfo, "$in"); assertThat(aliases, hasSize(1)); assertThat(aliases.get(0), is((Object) String.class.getName())); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 57f709d94..f10f81eda 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -53,7 +53,7 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mapping.model.MappingInstantiationException; import org.springframework.data.mongodb.MongoDbFactory; -import org.springframework.data.mongodb.core.DBObjectUtils; +import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.NestedType; import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.ProjectingType; import org.springframework.data.mongodb.core.mapping.Document; @@ -1446,7 +1446,7 @@ public class MappingMongoConverterUnitTests { converter.write(type, result); assertThat(result.get("name"), is((Object) "name")); - DBObject aValue = DBObjectUtils.getAsDBObject(result, "a"); + DBObject aValue = DBObjectTestUtils.getAsDBObject(result, "a"); assertThat(aValue.get("b"), is((Object) "bar")); assertThat(aValue.get("c"), is((Object) "C")); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 372bd3975..a7c07cc90 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -17,7 +17,7 @@ package org.springframework.data.mongodb.core.convert; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.springframework.data.mongodb.core.DBObjectUtils.*; +import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; @@ -35,7 +35,7 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.MongoDbFactory; -import org.springframework.data.mongodb.core.DBObjectUtils; +import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.Person; import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.DBRef; @@ -333,7 +333,7 @@ public class QueryMapperUnitTests { Query query = query(where("reference").in(first, second)); DBObject result = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDBRef.class)); - DBObject reference = DBObjectUtils.getAsDBObject(result, "reference"); + DBObject reference = DBObjectTestUtils.getAsDBObject(result, "reference"); BasicDBList inClause = getAsDBList(reference, "$in"); assertThat(inClause, hasSize(2)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java index 9c5f74bce..68d794059 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java @@ -26,7 +26,7 @@ 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.DBObjectUtils; +import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Update; @@ -62,8 +62,8 @@ public class UpdateMapperUnitTests { DBObject mappedObject = mapper.getMappedObject(update.getUpdateObject(), context.getPersistentEntity(ParentClass.class)); - DBObject push = DBObjectUtils.getAsDBObject(mappedObject, "$push"); - DBObject list = DBObjectUtils.getAsDBObject(push, "list"); + DBObject push = DBObjectTestUtils.getAsDBObject(mappedObject, "$push"); + DBObject list = DBObjectTestUtils.getAsDBObject(push, "list"); assertThat(list.get("_class"), is((Object) ConcreteChildClass.class.getName())); }