From dd59cdc59a044f739b4019f9c9e5ae463dac3358 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 14 Oct 2013 11:01:16 +0200 Subject: [PATCH] =?UTF-8?q?DATAMONGO-774=20-=20A=20round=20of=20J=C3=BCrge?= =?UTF-8?q?nization=20for=20SpEL=20support=20in=20aggregation=20framework?= =?UTF-8?q?=20support.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced dedicated spel package and extracted value objects to encapsulate and express information about the node transformation in a more semantical way. Moved a lot of the logic contained in the SpelExpressionTransformer into the value objects for cohesiveness and testability. Updated Sonargraph architecture model to reflect the new packages we've introduced. Original pull request: #81. --- .../Spring Data MongoDB.sonargraph | 211 +++--- .../AggregationExpressionTransformer.java | 82 +++ .../core/aggregation/ExposedFields.java | 4 +- .../data/mongodb/core/aggregation/Fields.java | 23 +- .../core/aggregation/ProjectionOperation.java | 81 ++- ...xpressionToMongoExpressionTransformer.java | 668 ------------------ .../SpelExpressionTransformer.java | 510 +++++++++++++ .../mongodb/core/spel/ExpressionNode.java | 217 ++++++ ...xpressionTransformationContextSupport.java | 126 ++++ .../core/spel/ExpressionTransformer.java | 33 + .../data/mongodb/core/spel/LiteralNode.java | 72 ++ .../core/spel/MethodReferenceNode.java | 75 ++ .../data/mongodb/core/spel/OperatorNode.java | 120 ++++ .../data/mongodb/core/spel/package-info.java | 5 + .../core/aggregation/AggregationTests.java | 14 +- .../aggregation/ExposedFieldsUnitTests.java | 8 - .../core/aggregation/FieldsUnitTests.java | 28 + ...sionToMongoExpressionTransformerTests.java | 179 ----- ...xpressionTransformerIntegrationTests.java} | 7 +- .../SpelExpressionTransformerUnitTests.java | 192 +++++ .../core/spel/ExpressionNodeUnitTests.java | 66 ++ 21 files changed, 1740 insertions(+), 981 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformer.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerTests.java rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/{SpelExpressionToMongoExpressionTransformerIntegrationTests.java => SpelExpressionTransformerIntegrationTests.java} (91%) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/spel/ExpressionNodeUnitTests.java diff --git a/spring-data-mongodb/Spring Data MongoDB.sonargraph b/spring-data-mongodb/Spring Data MongoDB.sonargraph index a9c16718e..9c3390d73 100644 --- a/spring-data-mongodb/Spring Data MongoDB.sonargraph +++ b/spring-data-mongodb/Spring Data MongoDB.sonargraph @@ -1,153 +1,176 @@ - - - - + + + + - - - + + + - - - - + + + + - - - + + + - - - + + + - - - + + + - + - - - + + + - - + + - - - + + + - + - + - - - + + + - + - - - + + + - + - - - + + + - - - + + + - - - + + + - + - - - + + + - + - - - + + + - - + + - - - + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - + - + - + - - - - - + + + + + - - - - + + + + - - - + + + - - - - + + + + - - - + + + - - - - + + + + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java new file mode 100644 index 000000000..0d1ce4996 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import org.springframework.data.mongodb.core.aggregation.AggregationExpressionTransformer.AggregationExpressionTransformationContext; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.spel.ExpressionNode; +import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport; +import org.springframework.data.mongodb.core.spel.ExpressionTransformer; +import org.springframework.util.Assert; + +import com.mongodb.DBObject; + +/** + * Interface to type an {@link ExpressionTransformer} to the contained + * {@link AggregationExpressionTransformationContext}. + * + * @author Oliver Gierke + */ +interface AggregationExpressionTransformer extends + ExpressionTransformer> { + + /** + * A special {@link ExpressionTransformationContextSupport} to be aware of the {@link AggregationOperationContext}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ + public static class AggregationExpressionTransformationContext extends + ExpressionTransformationContextSupport { + + private final AggregationOperationContext aggregationContext; + + /** + * Creates an {@link AggregationExpressionTransformationContext}. + * + * @param currentNode must not be {@literal null}. + * @param parentNode + * @param previousOperationObject + * @param aggregationContext must not be {@literal null}. + */ + public AggregationExpressionTransformationContext(T currentNode, ExpressionNode parentNode, + DBObject previousOperationObject, AggregationOperationContext context) { + + super(currentNode, parentNode, previousOperationObject); + + Assert.notNull(context, "AggregationOperationContext must not be null!"); + this.aggregationContext = context; + } + + /** + * Returns the underlying {@link AggregationOperationContext}. + * + * @return + */ + public AggregationOperationContext getAggregationContext() { + return aggregationContext; + } + + /** + * Returns the {@link FieldReference} for the current {@link ExpressionNode}. + * + * @return + */ + public FieldReference getFieldReference() { + return aggregationContext.getReference(getCurrentNode().getName()); + } + } +} 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 b8195159d..b3ac433d3 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 @@ -342,9 +342,7 @@ public class ExposedFields implements Iterable { 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/Fields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java index 050d6be60..15fb4ec13 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java @@ -197,17 +197,30 @@ public class Fields implements Iterable { public AggregationField(String name, String target) { - Assert.hasText(name, "AggregationField name must not be null or empty!"); + String nameToSet = cleanUp(name); + String targetToSet = cleanUp(target); + + Assert.hasText(nameToSet, "AggregationField name must not be null or empty!"); if (target == null && name.contains(".")) { - this.name = name.substring(name.indexOf(".") + 1); - this.target = name; + this.name = nameToSet.substring(nameToSet.indexOf(".") + 1); + this.target = nameToSet; } else { - this.name = name; - this.target = target; + this.name = nameToSet; + this.target = targetToSet; } } + private static final String cleanUp(String source) { + + if (source == null) { + return source; + } + + int dollarIndex = source.lastIndexOf('$'); + return dollarIndex == -1 ? source : source.substring(dollarIndex + 1); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.Field#getKey() 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 d766b088d..3d7b25ed6 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 @@ -193,13 +193,21 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { } /** + * Base class for {@link ProjectionOperationBuilder}s. + * * @author Thomas Darimont */ - public static abstract class AbstractProjectionOperationBuilder implements AggregationOperation { + private static abstract class AbstractProjectionOperationBuilder implements AggregationOperation { protected final Object value; protected final ProjectionOperation operation; + /** + * Creates a new {@link AbstractProjectionOperationBuilder} fot the given value and {@link ProjectionOperation}. + * + * @param value must not be {@literal null}. + * @param operation must not be {@literal null}. + */ public AbstractProjectionOperationBuilder(Object value, ProjectionOperation operation) { Assert.notNull(value, "value must not be null or empty!"); @@ -209,15 +217,22 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { this.operation = operation; } - public abstract ProjectionOperation as(String alias); - - /* (non-Javadoc) + /* + * (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); } + + /** + * Returns the finally to be applied {@link ProjectionOperation} with the given alias. + * + * @param alias will never be {@literal null} or empty. + * @return + */ + public abstract ProjectionOperation as(String alias); } /** @@ -225,36 +240,71 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { */ public static class ExpressionProjectionOperationBuilder extends AbstractProjectionOperationBuilder { - private Object[] params; + private final Object[] params; + + /** + * Creates a new {@link ExpressionProjectionOperationBuilder} for the given value, {@link ProjectionOperation} and + * parameters. + * + * @param value must not be {@literal null}. + * @param operation must not be {@literal null}. + * @param parameters + */ + public ExpressionProjectionOperationBuilder(Object value, ProjectionOperation operation, Object[] parameters) { - public ExpressionProjectionOperationBuilder(Object value, ProjectionOperation operation, Object[] params) { super(value, operation); - this.params = params; + this.params = parameters; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#as(java.lang.String) + */ + @Override public ProjectionOperation as(String alias) { - return this.operation.and(new ExpressionProjection(Fields.field(alias, "expr"), this.value.toString(), params)); + Field expressionField = Fields.field(alias, "expr"); + return this.operation.and(new ExpressionProjection(expressionField, this.value.toString(), params)); } + /** + * A {@link Projection} based on a SpEL expression. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ static class ExpressionProjection extends Projection { - private String expression; - private Object[] params; + private static final SpelExpressionTransformer TRANSFORMER = new SpelExpressionTransformer(); + + private final String expression; + private final Object[] params; + + /** + * Creates a new {@link ExpressionProjection} for the given field, SpEL expression and parameters. + * + * @param field must not be {@literal null}. + * @param expression must not be {@literal null} or empty. + * @param parameters must not be {@literal null}. + */ + public ExpressionProjection(Field field, String expression, Object[] parameters) { - public ExpressionProjection(Field field, String expression, Object[] params) { super(field); + + Assert.hasText(expression, "Expression must not be null!"); + Assert.notNull(parameters, "Parameters must not be null!"); + this.expression = expression; - this.params = params; + this.params = parameters; } - /* (non-Javadoc) + /* + * (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)); + return new BasicDBObject(getExposedField().getName(), TRANSFORMER.transform(expression, context, params)); } } } @@ -315,6 +365,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { * @param string * @return */ + @Override public ProjectionOperation as(String alias) { if (this.previousProjection != 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 deleted file mode 100644 index 19dcb487a..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformer.java +++ /dev/null @@ -1,668 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.core.aggregation; - -import java.util.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/core/aggregation/SpelExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java new file mode 100644 index 000000000..958d4291f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java @@ -0,0 +1,510 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import static org.springframework.data.mongodb.util.DBObjectUtils.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.data.mongodb.core.spel.ExpressionNode; +import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport; +import org.springframework.data.mongodb.core.spel.LiteralNode; +import org.springframework.data.mongodb.core.spel.MethodReferenceNode; +import org.springframework.data.mongodb.core.spel.OperatorNode; +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.Indexer; +import org.springframework.expression.spel.ast.InlineList; +import org.springframework.expression.spel.ast.PropertyOrFieldReference; +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 + */ +class SpelExpressionTransformer implements AggregationExpressionTransformer { + + private final SpelExpressionParser parser = new SpelExpressionParser(); + private final List> conversions; + + /** + * Creates a new {@link SpelExpressionTransformer}. + */ + public SpelExpressionTransformer() { + + List> conversions = new ArrayList>(); + 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(this)); + + this.conversions = Collections.unmodifiableList(conversions); + } + + /** + * 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(String expression, AggregationOperationContext context, Object... params) { + + Assert.notNull(expression, "Expression must not be null!"); + Assert.notNull(context, "AggregationOperationContext must not be null!"); + Assert.notNull(params, "Parameters must not be null!"); + + SpelExpression spelExpression = (SpelExpression) parser.parseExpression(expression); + ExpressionState state = new ExpressionState(new StandardEvaluationContext(params)); + ExpressionNode node = ExpressionNode.from(spelExpression.getAST(), state); + + return transform(new AggregationExpressionTransformationContext(node, null, null, context)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.spel.ExpressionTransformer#transform(org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport) + */ + public Object transform(AggregationExpressionTransformationContext context) { + return lookupConversionFor(context.getCurrentNode()).convert(context); + } + + /** + * Returns an appropriate {@link ExpressionNodeConversion} for the given {@code node}. Throws an + * {@link IllegalArgumentException} if no conversion could be found. + * + * @param node + * @return the appropriate {@link ExpressionNodeConversion} for the given {@link ExpressionNode}. + */ + @SuppressWarnings("unchecked") + private ExpressionNodeConversion lookupConversionFor(ExpressionNode node) { + + for (ExpressionNodeConversion candidate : conversions) { + if (candidate.supports(node)) { + return (ExpressionNodeConversion) candidate; + } + } + + throw new IllegalArgumentException("Unsupported Element: " + node + " Type: " + node.getClass() + + " You probably have a syntax error in your SpEL expression!"); + } + + /** + * Abstract base class for {@link SpelNode} to (Db)-object conversions. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private static abstract class ExpressionNodeConversion implements + AggregationExpressionTransformer { + + private final AggregationExpressionTransformer transformer; + private final Class nodeType; + + /** + * Creates a new {@link ExpressionNodeConversion}. + * + * @param transformer must not be {@literal null}. + */ + @SuppressWarnings("unchecked") + public ExpressionNodeConversion(AggregationExpressionTransformer transformer) { + + Assert.notNull(transformer, "Transformer must not be null!"); + + this.nodeType = (Class) GenericTypeResolver.resolveTypeArgument(this.getClass(), + ExpressionNodeConversion.class); + this.transformer = transformer; + } + + /** + * Returns whether the current conversion supports the given {@link ExpressionNode}. By default we will match the + * node type against the genric type the subclass types the type parameter to. + * + * @param node will never be {@literal null}. + * @return true if {@literal this} conversion can be applied to the given {@code node}. + */ + protected boolean supports(ExpressionNode node) { + return nodeType.equals(node.getClass()); + } + + /** + * Triggers the transformation for the given {@link ExpressionNode} and the given current context. + * + * @param node must not be {@literal null}. + * @param context must not be {@literal null}. + * @return + */ + protected Object transform(ExpressionNode node, AggregationExpressionTransformationContext context) { + + Assert.notNull(node, "ExpressionNode must not be null!"); + Assert.notNull(context, "AggregationExpressionTransformationContext must not be null!"); + + return transform(node, context.getParentNode(), null, context); + } + + /** + * Triggers the transformation with the given new {@link ExpressionNode}, new parent node, the current operation and + * the previous context. + * + * @param node must not be {@literal null}. + * @param parent + * @param operation + * @param context must not be {@literal null}. + * @return + */ + protected Object transform(ExpressionNode node, ExpressionNode parent, DBObject operation, + AggregationExpressionTransformationContext context) { + + Assert.notNull(node, "ExpressionNode must not be null!"); + Assert.notNull(context, "AggregationExpressionTransformationContext must not be null!"); + + return transform(new AggregationExpressionTransformationContext(node, parent, operation, + context.getAggregationContext())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#transform(org.springframework.data.mongodb.core.aggregation.AggregationExpressionTransformer.AggregationExpressionTransformationContext) + */ + @Override + public Object transform(AggregationExpressionTransformationContext context) { + return transformer.transform(context); + } + + /** + * Performs the actual conversion from {@link SpelNode} to the corresponding representation for MongoDB. + * + * @param context + * @return + */ + protected abstract Object convert(AggregationExpressionTransformationContext context); + } + + /** + * A {@link ExpressionNodeConversion} that converts arithmetic operations. + * + * @author Thomas Darimont + */ + private static class OperatorNodeConversion extends ExpressionNodeConversion { + + public OperatorNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + + OperatorNode currentNode = context.getCurrentNode(); + + DBObject operationObject = createOperationObjectAndAddToPreviousArgumentsIfNecessary(context, currentNode); + Object leftResult = transform(currentNode.getLeft(), currentNode, operationObject, context); + + if (currentNode.isUnaryMinus()) { + return convertUnaryMinusOp(context, leftResult); + } + + // we deliberately ignore the RHS result + transform(currentNode.getRight(), currentNode, operationObject, context); + + return operationObject; + } + + private DBObject createOperationObjectAndAddToPreviousArgumentsIfNecessary( + AggregationExpressionTransformationContext context, OperatorNode currentNode) { + + DBObject nextDbObject = new BasicDBObject(currentNode.getMongoOperator(), new BasicDBList()); + + if (!context.hasPreviousOperation()) { + return nextDbObject; + } + + if (context.parentIsSameOperation()) { + + // 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 (!currentNode.isUnaryOperator()) { + + // different operator -> add context object for next level to list if arguments of previous expression + context.addToPreviousOperation(nextDbObject); + } + + return nextDbObject; + } + + private Object convertUnaryMinusOp(ExpressionTransformationContextSupport context, Object leftResult) { + + Object result = leftResult instanceof Number ? leftResult + : new BasicDBObject("$multiply", dbList(-1, leftResult)); + + if (leftResult != null && context.hasPreviousOperation()) { + context.addToPreviousOperation(result); + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#supports(java.lang.Class) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isMathematicalOperation(); + } + } + + /** + * A {@link ExpressionNodeConversion} that converts indexed expressions. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private static class IndexerNodeConversion extends ExpressionNodeConversion { + + public IndexerNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + return context.addToPreviousOrReturn(context.getCurrentNode().getValue()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#supports(org.springframework.data.mongodb.core.spel.ExpressionNode) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isOfType(Indexer.class); + } + } + + /** + * A {@link ExpressionNodeConversion} that converts in-line list expressions. + * + * @author Thomas Darimont + */ + private static class InlineListNodeConversion extends ExpressionNodeConversion { + + public InlineListNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + + ExpressionNode currentNode = context.getCurrentNode(); + + if (!currentNode.hasChildren()) { + return null; + } + + // just take the first item + return transform(currentNode.getChild(0), currentNode, null, context); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#supports(org.springframework.data.mongodb.core.spel.ExpressionNode) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isOfType(InlineList.class); + } + } + + /** + * A {@link ExpressionNodeConversion} that converts property or field reference expressions. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private static class PropertyOrFieldReferenceNodeConversion extends ExpressionNodeConversion { + + public PropertyOrFieldReferenceNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#convert(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionTransformationContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + + String fieldReference = context.getFieldReference().toString(); + return context.addToPreviousOrReturn(fieldReference); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#supports(org.springframework.data.mongodb.core.spel.ExpressionNode) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isOfType(PropertyOrFieldReference.class); + } + } + + /** + * A {@link ExpressionNodeConversion} that converts literal expressions. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private static class LiteralNodeConversion extends ExpressionNodeConversion { + + public LiteralNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + @SuppressWarnings("unchecked") + protected Object convert(AggregationExpressionTransformationContext context) { + + LiteralNode node = context.getCurrentNode(); + Object value = node.getValue(); + + if (context.hasPreviousOperation()) { + + if (node.isUnaryMinus(context.getParentNode())) { + // unary minus operator + return NumberUtils.convertNumberToTargetClass(((Number) value).doubleValue() * -1, + (Class) value.getClass()); // retain type, e.g. int to -int + } + + return context.addToPreviousOperation(value); + } + + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#supports(org.springframework.expression.spel.SpelNode) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isLiteral(); + } + } + + /** + * A {@link ExpressionNodeConversion} that converts method reference expressions. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private static class MethodReferenceNodeConversion extends ExpressionNodeConversion { + + public MethodReferenceNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + + MethodReferenceNode node = context.getCurrentNode(); + List args = new ArrayList(); + + for (ExpressionNode childNode : node) { + args.add(transform(childNode, context)); + } + + return context.addToPreviousOrReturn(new BasicDBObject(node.getMethodName(), dbList(args.toArray()))); + } + } + + /** + * A {@link ExpressionNodeConversion} that converts method compound expressions. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private static class CompoundExpressionNodeConversion extends ExpressionNodeConversion { + + public CompoundExpressionNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + + ExpressionNode currentNode = context.getCurrentNode(); + + if (currentNode.hasfirstChildNotOfType(Indexer.class)) { + // we have a property path expression like: foo.bar -> render as reference + return context.getFieldReference().toString(); + } + + return context.addToPreviousOrReturn(currentNode.getValue()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#supports(org.springframework.data.mongodb.core.spel.ExpressionNode) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isOfType(CompoundExpression.class); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java new file mode 100644 index 000000000..b323b9cf3 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java @@ -0,0 +1,217 @@ +/* + * 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.spel; + +import java.util.Collections; +import java.util.Iterator; + +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.ast.Literal; +import org.springframework.expression.spel.ast.MethodReference; +import org.springframework.expression.spel.ast.Operator; +import org.springframework.util.Assert; + +/** + * A value object for nodes in an expression. Allows iterating ove potentially available child {@link ExpressionNode}s. + * + * @author Oliver Gierke + */ +public class ExpressionNode implements Iterable { + + private static final Iterator EMPTY_ITERATOR = Collections. emptySet().iterator(); + + private final SpelNode node; + private final ExpressionState state; + + /** + * Creates a new {@link ExpressionNode} from the given {@link SpelNode} and {@link ExpressionState}. + * + * @param node must not be {@literal null}. + * @param state must not be {@literal null}. + */ + protected ExpressionNode(SpelNode node, ExpressionState state) { + + Assert.notNull(node, "SpelNode must not be null!"); + Assert.notNull(state, "ExpressionState must not be null!"); + + this.node = node; + this.state = state; + } + + /** + * Factory method to create {@link ExpressionNode}'s according to the given {@link SpelNode} and + * {@link ExpressionState}. + * + * @param node + * @param state must not be {@literal null}. + * @return an {@link ExpressionNode} for the given {@link SpelNode} or {@literal null} if {@literal null} was given + * for the {@link SpelNode}. + */ + public static ExpressionNode from(SpelNode node, ExpressionState state) { + + if (node == null) { + return null; + } + + if (node instanceof Operator) { + return new OperatorNode((Operator) node, state); + } + + if (node instanceof MethodReference) { + return new MethodReferenceNode((MethodReference) node, state); + } + + if (node instanceof Literal) { + return new LiteralNode((Literal) node, state); + } + + return new ExpressionNode(node, state); + } + + /** + * Returns the name of the {@link ExpressionNode}. + * + * @return + */ + public String getName() { + return node.toStringAST(); + } + + /** + * Returns whether the current {@link ExpressionNode} is backed by the given type. + * + * @param type must not be {@literal null}. + * @return + */ + public boolean isOfType(Class type) { + + Assert.notNull(type, "Type must not be empty!"); + return type.isAssignableFrom(node.getClass()); + } + + /** + * Returns whether the given {@link ExpressionNode} is representing the same backing node type as the current one. + * + * @param node + * @return + */ + boolean isOfSameTypeAs(ExpressionNode node) { + return node == null ? false : this.node.getClass().equals(node.node.getClass()); + } + + /** + * Returns whether the {@link ExpressionNode} is a mathematical operation. + * + * @return + */ + public boolean isMathematicalOperation() { + return false; + } + + /** + * Returns whether the {@link ExpressionNode} is a literal. + * + * @return + */ + public boolean isLiteral() { + return false; + } + + /** + * Returns the value of the current node. + * + * @return + */ + public Object getValue() { + return node.getValue(state); + } + + /** + * Returns whether the current node has child nodes. + * + * @return + */ + public boolean hasChildren() { + return node.getChildCount() != 0; + } + + /** + * Returns the child {@link ExpressionNode} with the given index. + * + * @param index must not be negative. + * @return + */ + public ExpressionNode getChild(int index) { + + Assert.isTrue(index >= 0); + return from(node.getChild(index), state); + } + + /** + * Returns whether the {@link ExpressionNode} has a first child node that is not of the given type. + * + * @param type must not be {@literal null}. + * @return + */ + public boolean hasfirstChildNotOfType(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return hasChildren() && !node.getChild(0).getClass().equals(type); + } + + /** + * Creates a new {@link ExpressionNode} from the given {@link SpelNode}. + * + * @param node + * @return + */ + protected ExpressionNode from(SpelNode node) { + return from(node, state); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + + if (!hasChildren()) { + return EMPTY_ITERATOR; + } + + return new Iterator() { + + int index = 0; + + @Override + public boolean hasNext() { + return index < node.getChildCount(); + } + + @Override + public ExpressionNode next() { + return from(node.getChild(index++)); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java new file mode 100644 index 000000000..50a8a2dd9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java @@ -0,0 +1,126 @@ +/* + * 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.spel; + +import org.springframework.util.Assert; + +import com.mongodb.BasicDBList; +import com.mongodb.DBObject; + +/** + * The context for an {@link ExpressionNode} transformation. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ +public class ExpressionTransformationContextSupport { + + private final T currentNode; + private final ExpressionNode parentNode; + private final DBObject previousOperationObject; + + /** + * Creates a new {@link ExpressionTransformationContextSupport} for the given {@link ExpressionNode}s and an optional + * previous operation. + * + * @param currentNode must not be {@literal null}. + * @param parentNode + * @param previousOperationObject + */ + public ExpressionTransformationContextSupport(T currentNode, ExpressionNode parentNode, + DBObject previousOperationObject) { + + Assert.notNull(currentNode, "currentNode must not be null!"); + + this.currentNode = currentNode; + this.parentNode = parentNode; + this.previousOperationObject = previousOperationObject; + } + + /** + * Returns the current {@link ExpressionNode}. + * + * @return + */ + public T getCurrentNode() { + return currentNode; + } + + /** + * Returns the parent {@link ExpressionNode} or {@literal null} if none available. + * + * @return + */ + public ExpressionNode getParentNode() { + return parentNode; + } + + /** + * Returns the previously accumulated operaton object or {@literal null} if none available. Rather than manually + * adding stuff to the object prefer using {@link #addToPreviousOrReturn(Object)} to transparently do if one is + * present. + * + * @see #hasPreviousOperation() + * @see #addToPreviousOrReturn(Object) + * @return + */ + public DBObject getPreviousOperationObject() { + return previousOperationObject; + } + + /** + * Returns whether a previous operation is present. + * + * @return + */ + public boolean hasPreviousOperation() { + return getPreviousOperationObject() != null; + } + + /** + * Returns whether the parent node is of the same operation as the current node. + * + * @return + */ + public boolean parentIsSameOperation() { + return parentNode == null ? false : currentNode.isOfSameTypeAs(parentNode); + } + + /** + * Adds the given value to the previous operation and returns it. + * + * @param value + * @return + */ + public DBObject addToPreviousOperation(Object value) { + extractArgumentListFrom(previousOperationObject).add(value); + return previousOperationObject; + } + + /** + * Adds the given value to the previous operation if one is present or returns the value to add as is. + * + * @param value + * @return + */ + public Object addToPreviousOrReturn(Object value) { + return hasPreviousOperation() ? addToPreviousOperation(value) : value; + } + + private BasicDBList extractArgumentListFrom(DBObject context) { + return (BasicDBList) context.get(context.keySet().iterator().next()); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java new file mode 100644 index 000000000..a19239095 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.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.core.spel; + +/** + * SPI interface to implement components that can transfrom an {@link ExpressionTransformationContextSupport} into an + * object. + * + * @author Oliver Gierke + */ +public interface ExpressionTransformer> { + + /** + * Transforms the given {@link ExpressionTransformationContextSupport} into an Object. + * + * @param context will never be {@literal null}. + * @return + */ + Object transform(T context); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java new file mode 100644 index 000000000..68c53860f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java @@ -0,0 +1,72 @@ +/* + * 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.spel; + +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.ast.FloatLiteral; +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.NullLiteral; +import org.springframework.expression.spel.ast.RealLiteral; +import org.springframework.expression.spel.ast.StringLiteral; + +/** + * A node representing a literal in an expression. + * + * @author Oliver Gierke + */ +public class LiteralNode extends ExpressionNode { + + private final Literal literal; + + /** + * Creates a new {@link LiteralNode} from the given {@link Literal} and {@link ExpressionState}. + * + * @param node must not be {@literal null}. + * @param state must not be {@literal null}. + */ + LiteralNode(Literal node, ExpressionState state) { + super(node, state); + this.literal = node; + } + + /** + * Returns whether the given {@link ExpressionNode} is a unary minus. + * + * @param parent + * @return + */ + public boolean isUnaryMinus(ExpressionNode parent) { + + if (!(parent instanceof OperatorNode)) { + return false; + } + + OperatorNode operator = (OperatorNode) parent; + return operator.isUnaryMinus() && operator.getRight() == null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.spel.ExpressionNode#isLiteral() + */ + @Override + public boolean isLiteral() { + return literal instanceof FloatLiteral || literal instanceof RealLiteral || literal instanceof IntLiteral + || literal instanceof LongLiteral || literal instanceof StringLiteral || literal instanceof NullLiteral; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java new file mode 100644 index 000000000..a32c49c3c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -0,0 +1,75 @@ +/* + * 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.spel; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.ast.MethodReference; + +/** + * An {@link ExpressionNode} representing a method reference. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class MethodReferenceNode extends ExpressionNode { + + private static final Map FUNCTIONS; + + static { + + Map map = new HashMap(); + + map.put("concat", "$concat"); // Concatenates two strings. + map.put("strcasecmp", "$strcasecmp"); // Compares two strings and returns an integer that reflects the comparison. + map.put("substr", "$substr"); // Takes a string and returns portion of that string. + map.put("toLower", "$toLower"); // Converts a string to lowercase. + map.put("toUpper", "$toUpper"); // Converts a string to uppercase. + + map.put("dayOfYear", "$dayOfYear"); // Converts a date to a number between 1 and 366. + map.put("dayOfMonth", "$dayOfMonth"); // Converts a date to a number between 1 and 31. + map.put("dayOfWeek", "$dayOfWeek"); // Converts a date to a number between 1 and 7. + map.put("year", "$year"); // Converts a date to the full year. + map.put("month", "$month"); // Converts a date into a number between 1 and 12. + map.put("week", "$week"); // Converts a date into a number between 0 and 53 + map.put("hour", "$hour"); // Converts a date into a number between 0 and 23. + map.put("minute", "$minute"); // Converts a date into a number between 0 and 59. + map.put("second", "$second"); // Converts a date into a number between 0 and 59. May be 60 to account for leap + // seconds. + map.put("millisecond", "$millisecond"); // Returns the millisecond portion of a date as an integer between 0 and + + FUNCTIONS = Collections.unmodifiableMap(map); + } + + MethodReferenceNode(MethodReference reference, ExpressionState state) { + super(reference, state); + } + + /** + * Returns the name of the method. + * + * @return + */ + public String getMethodName() { + + String name = getName(); + String methodName = name.substring(0, name.indexOf('(')); + return FUNCTIONS.get(methodName); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java new file mode 100644 index 000000000..55a11bd7e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java @@ -0,0 +1,120 @@ +/* + * 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.spel; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.expression.spel.ExpressionState; +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; + +/** + * An {@link ExpressionNode} representing an operator. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class OperatorNode extends ExpressionNode { + + private static final Map OPERATORS; + + static { + + Map map = new HashMap(6); + + map.put("+", "$add"); + map.put("-", "$subtract"); + map.put("*", "$multiply"); + map.put("/", "$divide"); + map.put("%", "$mod"); + + OPERATORS = Collections.unmodifiableMap(map); + } + + private final Operator operator; + + /** + * Creates a new {@link OperatorNode} from the given {@link Operator} and {@link ExpressionState}. + * + * @param node must not be {@literal null}. + * @param state must not be {@literal null}. + */ + OperatorNode(Operator node, ExpressionState state) { + super(node, state); + this.operator = node; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.spel.ExpressionNode#isMathematicalOperation() + */ + @Override + public boolean isMathematicalOperation() { + return operator instanceof OpMinus || operator instanceof OpPlus || operator instanceof OpMultiply + || operator instanceof OpDivide || operator instanceof OpModulus; + } + + /** + * Returns whether the operator is unary. + * + * @return + */ + public boolean isUnaryOperator() { + return operator.getRightOperand() == null; + } + + /** + * Returns the Mongo expression of the operator. + * + * @return + */ + public String getMongoOperator() { + return OPERATORS.get(operator.getOperatorName()); + } + + /** + * Returns whether the operator is a unary minus, e.g. -1. + * + * @return + */ + public boolean isUnaryMinus() { + return isUnaryOperator() && operator instanceof OpMinus; + } + + /** + * Returns the left operand as {@link ExpressionNode}. + * + * @return + */ + public ExpressionNode getLeft() { + return from(operator.getLeftOperand()); + } + + /** + * Returns the right operand as {@link ExpressionNode}. + * + * @return + */ + public ExpressionNode getRight() { + return from(operator.getRightOperand()); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java new file mode 100644 index 000000000..f703ec9e4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/package-info.java @@ -0,0 +1,5 @@ +/** + * Support classes to transform SpEL expressions into MongoDB expressions. + * @since 1.4 + */ +package org.springframework.data.mongodb.core.spel; \ No newline at end of file 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 1f918b6e6..8fa8b777a 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 @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.aggregation; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.junit.Assume.*; import static org.springframework.data.domain.Sort.Direction.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.core.query.Criteria.*; @@ -30,7 +31,6 @@ 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; @@ -45,6 +45,7 @@ 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; +import org.springframework.data.util.Version; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -69,13 +70,14 @@ public class AggregationTests { private static final String INPUT_COLLECTION = "aggregation_test_collection"; private static final Logger LOGGER = LoggerFactory.getLogger(AggregationTests.class); + private static final Version TWO_DOT_FOUR = new Version(2, 4); private static boolean initialized = false; @Autowired MongoTemplate mongoTemplate; @Rule public ExpectedException exception = ExpectedException.none(); - private static String mongoVersion; + private static Version mongoVersion; @Before public void setUp() { @@ -89,7 +91,7 @@ public class AggregationTests { if (mongoVersion == null) { CommandResult result = mongoTemplate.executeCommand("{ buildInfo: 1 }"); - mongoVersion = result.get("version").toString(); + mongoVersion = Version.parse(result.get("version").toString()); } } @@ -521,7 +523,7 @@ public class AggregationTests { @Test public void stringExpressionsInProjectionExample() { - Assume.assumeTrue(mongoVersion.startsWith("2.4")); + assumeTrue(mongoVersion.isGreaterThanOrEqualTo(TWO_DOT_FOUR)); Product product = new Product("P1", "A", 1.99, 3, 0.05, 0.19); mongoTemplate.insert(product); @@ -645,7 +647,7 @@ public class AggregationTests { @Test public void shouldPerformDateProjectionOperatorsCorrectly() throws ParseException { - Assume.assumeTrue(mongoVersion.startsWith("2.4")); + assumeTrue(mongoVersion.isGreaterThanOrEqualTo(TWO_DOT_FOUR)); Data data = new Data(); data.stringValue = "ABC"; @@ -676,7 +678,7 @@ public class AggregationTests { @Test public void shouldPerformStringProjectionOperatorsCorrectly() throws ParseException { - Assume.assumeTrue(mongoVersion.startsWith("2.4")); + assumeTrue(mongoVersion.isGreaterThanOrEqualTo(TWO_DOT_FOUR)); Data data = new Data(); data.dateValue = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SSSZ").parse("29.08.1983 12:34:56.789+0000"); 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 4b0bba6b9..8672235a1 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 @@ -44,14 +44,6 @@ 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/FieldsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java index 54a3fffd9..77cd20020 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FieldsUnitTests.java @@ -106,6 +106,34 @@ public class FieldsUnitTests { fields("b", "a.b"); } + /** + * @see DATAMONGO-774 + */ + @Test + public void stripsLeadingDollarsFromName() { + + assertThat(Fields.field("$name").getName(), is("name")); + assertThat(Fields.field("$$$$name").getName(), is("name")); + } + + /** + * @see DATAMONGO-774 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNameConsistingOfDollarOnly() { + Fields.field("$"); + } + + /** + * @see DATAMONGO-774 + */ + @Test + public void stripsLeadingDollarsFromTarget() { + + assertThat(Fields.field("$target").getTarget(), is("target")); + assertThat(Fields.field("$$$$target").getTarget(), is("target")); + } + private static void verify(Field field, String name, String target) { assertThat(field, is(notNullValue())); 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 deleted file mode 100644 index 3f50f9176..000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerTests.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.core.aggregation; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; - -import 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/aggregation/SpelExpressionToMongoExpressionTransformerIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerIntegrationTests.java similarity index 91% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionToMongoExpressionTransformerIntegrationTests.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerIntegrationTests.java index 7fb875591..5b19ef5b2 100644 --- 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/SpelExpressionTransformerIntegrationTests.java @@ -32,19 +32,20 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** - * Integration tests for {@link SpelExpressionToMongoExpressionTransformer}. + * Integration tests for {@link SpelExpressionTransformer}. * + * @see DATAMONGO-774 * @author Thomas Darimont */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") -public class SpelExpressionToMongoExpressionTransformerIntegrationTests { +public class SpelExpressionTransformerIntegrationTests { @Autowired MongoDbFactory mongoDbFactory; @Rule public ExpectedException exception = ExpectedException.none(); - SpelExpressionToMongoExpressionTransformer transformer = SpelExpressionToMongoExpressionTransformer.INSTANCE; + SpelExpressionTransformer transformer = new SpelExpressionTransformer(); @Test public void shouldConvertCompoundExpressionToPropertyPath() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java new file mode 100644 index 000000000..9050b6e90 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -0,0 +1,192 @@ +/* + * 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 SpelExpressionTransformer}. + * + * @see DATAMONGO-774 + * @author Thomas Darimont + * @author Oliver Gierke + */ +public class SpelExpressionTransformerUnitTests { + + SpelExpressionTransformer transformer = new SpelExpressionTransformer(); + + 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(transform("1"), is("1")); + assertThat(transform("-1"), is("-1")); + assertThat(transform("1.0"), is("1.0")); + assertThat(transform("-1.0"), is("-1.0")); + assertThat(transform("null"), is(nullValue())); + } + + @Test + public void shouldSupportKnownOperands() { + + assertThat(transform("a + b"), is("{ \"$add\" : [ \"$a\" , \"$b\"]}")); + assertThat(transform("a - b"), is("{ \"$subtract\" : [ \"$a\" , \"$b\"]}")); + assertThat(transform("a * b"), is("{ \"$multiply\" : [ \"$a\" , \"$b\"]}")); + assertThat(transform("a / b"), is("{ \"$divide\" : [ \"$a\" , \"$b\"]}")); + assertThat(transform("a % b"), is("{ \"$mod\" : [ \"$a\" , \"$b\"]}")); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionOnUnknownOperand() { + transform("a ^ 1"); + } + + @Test + public void shouldRenderSumExpression() { + assertThat(transform("a + 1"), is("{ \"$add\" : [ \"$a\" , 1]}")); + } + + @Test + public void shouldRenderFormula() { + + assertThat( + transform("(netPrice + surCharge) * taxrate + 42"), + is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); + } + + @Test + public void shouldRenderFormulaInCurlyBrackets() { + + assertThat( + transform("{(netPrice + surCharge) * taxrate + 42}"), + is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); + } + + @Test + public void shouldRenderFieldReference() { + + assertThat(transform("foo"), is("$foo")); + assertThat(transform("$foo"), is("$foo")); + } + + @Test + public void shouldRenderNestedFieldReference() { + + assertThat(transform("foo.bar"), is("$foo.bar")); + assertThat(transform("$foo.bar"), is("$foo.bar")); + } + + @Test + @Ignore + public void shouldRenderNestedIndexedFieldReference() { + + // TODO add support for rendering nested indexed field references + assertThat(transform("foo[3].bar"), is("$foo[3].bar")); + } + + @Test + public void shouldRenderConsecutiveOperation() { + assertThat(transform("1 + 1 + 1"), is("{ \"$add\" : [ 1 , 1 , 1]}")); + } + + @Test + public void shouldRenderComplexExpression0() { + + assertThat(transform("-(1 + q)"), is("{ \"$multiply\" : [ -1 , { \"$add\" : [ 1 , \"$q\"]}]}")); + } + + @Test + public void shouldRenderComplexExpression1() { + + assertThat(transform("1 + (q + 1) / (q - 1)"), + is("{ \"$add\" : [ 1 , { \"$divide\" : [ { \"$add\" : [ \"$q\" , 1]} , { \"$subtract\" : [ \"$q\" , 1]}]}]}")); + } + + @Test + public void shouldRenderComplexExpression2() { + + assertThat( + transform("(q + 1 + 4 - 5) / (q + 1 + 3 + 4)"), + is("{ \"$divide\" : [ { \"$subtract\" : [ { \"$add\" : [ \"$q\" , 1 , 4]} , 5]} , { \"$add\" : [ \"$q\" , 1 , 3 , 4]}]}")); + } + + @Test + public void shouldRenderBinaryExpressionWithMixedSignsCorrectly() { + + assertThat(transform("-4 + 1"), is("{ \"$add\" : [ -4 , 1]}")); + assertThat(transform("1 + -4"), is("{ \"$add\" : [ 1 , -4]}")); + } + + @Test + public void shouldRenderConsecutiveOperationsInComplexExpression() { + + assertThat(transform("1 + 1 + (1 + 1 + 1) / q"), + is("{ \"$add\" : [ 1 , 1 , { \"$divide\" : [ { \"$add\" : [ 1 , 1 , 1]} , \"$q\"]}]}")); + } + + @Test + public void shouldRenderParameterExpressionResults() { + assertThat(transform("[0] + [1] + [2]", 1, 2, 3), is("{ \"$add\" : [ 1 , 2 , 3]}")); + } + + @Test + public void shouldRenderNestedParameterExpressionResults() { + + assertThat(transform("[0].primitiveLongValue + [0].primitiveDoubleValue + [0].doubleValue.longValue()", data), + is("{ \"$add\" : [ 42 , 1.2345 , 23]}")); + } + + @Test + public void shouldRenderNestedParameterExpressionResultsInNestedExpressions() { + + assertThat( + transform("((1 + [0].primitiveLongValue) + [0].primitiveDoubleValue) * [0].doubleValue.longValue()", data), + is("{ \"$multiply\" : [ { \"$add\" : [ 1 , 42 , 1.2345]} , 23]}")); + } + + @Test + public void shouldRenderStringFunctions() { + + assertThat(transform("concat(a, b)"), is("{ \"$concat\" : [ \"$a\" , \"$b\"]}")); + assertThat(transform("substr(a, 1, 2)"), is("{ \"$substr\" : [ \"$a\" , 1 , 2]}")); + assertThat(transform("strcasecmp(a, b)"), is("{ \"$strcasecmp\" : [ \"$a\" , \"$b\"]}")); + assertThat(transform("toLower(a)"), is("{ \"$toLower\" : [ \"$a\"]}")); + assertThat(transform("toUpper(a)"), is("{ \"$toUpper\" : [ \"$a\"]}")); + assertThat(transform("toUpper(toLower(a))"), is("{ \"$toUpper\" : [ { \"$toLower\" : [ \"$a\"]}]}")); + } + + private String transform(String expression, Object... params) { + Object result = transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); + return result == null ? null : result.toString(); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/spel/ExpressionNodeUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/spel/ExpressionNodeUnitTests.java new file mode 100644 index 000000000..0f6d4b9b0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/spel/ExpressionNodeUnitTests.java @@ -0,0 +1,66 @@ +/* + * 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.spel; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.ast.OpDivide; +import org.springframework.expression.spel.ast.OpMinus; +import org.springframework.expression.spel.ast.OpMultiply; +import org.springframework.expression.spel.ast.OpPlus; + +/** + * Unit tests for {@link ExpressionNode}. + * + * @see DATAMONGO-774 + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class ExpressionNodeUnitTests { + + @Mock ExpressionState state; + + @Mock OpMinus minus; + @Mock OpPlus plus; + @Mock OpDivide divide; + @Mock OpMultiply multiply; + + Collection operators; + + @Before + public void setUp() { + this.operators = Arrays.asList(minus, plus, divide, multiply); + } + + @Test + public void createsOperatorNodeForOperations() { + + for (SpelNode operator : operators) { + assertThat(ExpressionNode.from(operator, state), is(instanceOf(OperatorNode.class))); + } + } +}