DATAMONGO-774 - A round of Jürgenization for SpEL support in aggregation framework support.

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.
This commit is contained in:
Oliver Gierke
2013-10-14 11:01:16 +02:00
parent 7e471e2301
commit dd59cdc59a
21 changed files with 1740 additions and 981 deletions

View File

@@ -1,153 +1,176 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<context version="7.1.7.187">
<scope name="spring-data-mongodb" type="Project">
<element name="Filter" type="TypeFilterReferenceOverridden">
<element name="org.springframework.data.mongodb.**" type="IncludeTypePattern"/>
<context version="7.1.9.205">
<scope type="Project" name="spring-data-mongodb">
<element type="TypeFilterReferenceOverridden" name="Filter">
<element type="IncludeTypePattern" name="org.springframework.data.mongodb.**"/>
</element>
<architecture>
<element name="Config" type="Layer">
<element name="Assignment" type="TypeFilter">
<element name="**.config.**" type="WeakTypePattern"/>
<element type="Layer" name="Config">
<element type="TypeFilter" name="Assignment">
<element type="WeakTypePattern" name="**.config.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|GridFS"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Monitoring"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Repositories"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|GridFS" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Monitoring" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Repositories" type="AllowedDependency"/>
</element>
<element name="Repositories" type="Layer">
<element name="Assignment" type="TypeFilter">
<element name="**.repository.**" type="IncludeTypePattern"/>
<element type="Layer" name="Repositories">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.repository.**"/>
</element>
<element name="API" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.repository.*" type="IncludeTypePattern"/>
<element type="Subsystem" name="API">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.repository.*"/>
</element>
</element>
<element name="Query" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.query.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Query">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.query.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|API"/>
<dependency toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|API" type="AllowedDependency"/>
</element>
<element name="Implementation" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.support.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Implementation">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.support.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|API"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|Query"/>
<dependency toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|API" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|Query" type="AllowedDependency"/>
</element>
<element name="Config" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.config.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Config">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.config.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|Implementation"/>
<dependency toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|Implementation" type="AllowedDependency"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core" type="AllowedDependency"/>
</element>
<element name="Monitoring" type="Layer">
<element name="Assignment" type="TypeFilter">
<element name="**.monitor.**" type="IncludeTypePattern"/>
<element type="Layer" name="Monitoring">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.monitor.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core" type="AllowedDependency"/>
</element>
<element name="GridFS" type="Layer">
<element name="Assignment" type="TypeFilter">
<element name="**.gridfs.**" type="IncludeTypePattern"/>
<element type="Layer" name="GridFS">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.gridfs.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core" type="AllowedDependency"/>
</element>
<element name="Core" type="Layer">
<element name="Assignment" type="TypeFilter">
<element name="**.core.**" type="IncludeTypePattern"/>
<element type="Layer" name="Core">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.core.**"/>
</element>
<element name="Mapping" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.mapping.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Mapping">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.mapping.**"/>
</element>
</element>
<element name="Geospatial" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.geo.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Geospatial">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.geo.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping" type="AllowedDependency"/>
</element>
<element name="Query" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.query.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Query">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.query.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Geospatial"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Geospatial" type="AllowedDependency"/>
</element>
<element name="Index" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.index.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Conversion">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.convert.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Query"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Query" type="AllowedDependency"/>
</element>
<element name="Core" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="**.core.**" type="WeakTypePattern"/>
<element type="Subsystem" name="SpEL">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.spel.**"/>
</element>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Geospatial"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Index"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping"/>
<dependency type="AllowedDependency" toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Query"/>
</element>
<element type="Subsystem" name="Aggregation">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.aggregation.**"/>
</element>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Conversion" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Query" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|SpEL" type="AllowedDependency"/>
</element>
<element type="Subsystem" name="Index">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.index.**"/>
</element>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Query" type="AllowedDependency"/>
</element>
<element type="Subsystem" name="Core">
<element type="TypeFilter" name="Assignment">
<element type="WeakTypePattern" name="**.core.**"/>
</element>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Aggregation" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Conversion" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Geospatial" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Index" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Query" type="AllowedDependency"/>
</element>
</element>
<element name="API" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="org.springframework.data.mongodb.*" type="IncludeTypePattern"/>
<element type="Subsystem" name="API">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="org.springframework.data.mongodb.*"/>
</element>
<stereotype name="Public"/>
</element>
</architecture>
<workspace>
<element name="src/main/java" type="JavaRootDirectory">
<element type="JavaRootDirectory" name="src/main/java">
<reference name="Project|spring-data-mongodb::BuildUnit|spring-data-mongodb"/>
</element>
<element name="target/classes" type="JavaRootDirectory">
<element type="JavaRootDirectory" name="target/classes">
<reference name="Project|spring-data-mongodb::BuildUnit|spring-data-mongodb"/>
</element>
</workspace>
<physical>
<element name="spring-data-mongodb" type="BuildUnit"/>
<element type="BuildUnit" name="spring-data-mongodb"/>
</physical>
</scope>
<scope name="External" type="External">
<element name="Filter" type="TypeFilter">
<element name="**" type="IncludeTypePattern"/>
<element name="java.**" type="ExcludeTypePattern"/>
<element name="javax.**" type="ExcludeTypePattern"/>
<scope type="External" name="External">
<element type="TypeFilter" name="Filter">
<element type="IncludeTypePattern" name="**"/>
<element type="ExcludeTypePattern" name="java.**"/>
<element type="ExcludeTypePattern" name="javax.**"/>
</element>
<architecture>
<element name="Spring" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="org.springframework.**" type="IncludeTypePattern"/>
<element name="org.springframework.data.**" type="ExcludeTypePattern"/>
<element type="Subsystem" name="Spring">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="org.springframework.**"/>
<element type="ExcludeTypePattern" name="org.springframework.data.**"/>
</element>
</element>
<element name="Spring Data Core" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="org.springframework.data.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Spring Data Core">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="org.springframework.data.**"/>
</element>
</element>
<element name="Mongo Java Driver" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="com.mongodb.**" type="IncludeTypePattern"/>
<element name="org.bson.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Mongo Java Driver">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="com.mongodb.**"/>
<element type="IncludeTypePattern" name="org.bson.**"/>
</element>
</element>
<element name="Querydsl" type="Subsystem">
<element name="Assignment" type="TypeFilter">
<element name="com.mysema.query.**" type="IncludeTypePattern"/>
<element type="Subsystem" name="Querydsl">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="com.mysema.query.**"/>
</element>
</element>
</architecture>
</scope>
<scope name="Global" type="Global">
<element name="Configuration" type="Configuration"/>
<element name="Filter" type="TypeFilter">
<element name="**" type="IncludeTypePattern"/>
<scope type="Global" name="Global">
<element type="Configuration" name="Configuration"/>
<element type="TypeFilter" name="Filter">
<element type="IncludeTypePattern" name="**"/>
</element>
</scope>
</context>

View File

@@ -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<AggregationExpressionTransformationContext<ExpressionNode>> {
/**
* A special {@link ExpressionTransformationContextSupport} to be aware of the {@link AggregationOperationContext}.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
public static class AggregationExpressionTransformationContext<T extends ExpressionNode> extends
ExpressionTransformationContextSupport<T> {
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());
}
}
}

View File

@@ -342,9 +342,7 @@ public class ExposedFields implements Iterable<ExposedField> {
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);
}

View File

@@ -197,17 +197,30 @@ public class Fields implements Iterable<Field> {
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()

View File

@@ -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) {

View File

@@ -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<SpelNodeConversion<? extends SpelNode>> conversions;
/**
* Creates a new {@link SpelExpressionToMongoExpressionTransformer}.
*/
private SpelExpressionToMongoExpressionTransformer() {
this.conversions = new ArrayList<SpelNodeConversion<? extends SpelNode>>();
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}.
* <p>
* Exposes the given @{code params} as <code>[0] ... [n]</code>.
*
* @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}.
* <p>
* Exposes the given @{code params} as <code>[0] ... [n]</code>.
*
* @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}.
* <p>
* Exposes the given @{code params} as <code>[0] ... [n]</code>.
*
* @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<? extends SpelNode> lookupConversionFor(SpelNode node) {
for (SpelNodeConversion<? extends SpelNode> 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 <code>ExpressionConversionContext<code>
*
* @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<T extends SpelNode> {
protected final Class<T> nodeType;
public SpelNodeConversion(Class<T> 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<Operator> {
private Map<String, String> arithmeticOperatorsSpelToMongoConversion = new HashMap<String, String>() {
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<Indexer> {
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<InlineList> {
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<PropertyOrFieldReference> {
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<Literal> {
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<Number>) 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<MethodReference> {
private Map<String, String> namedFunctionToMongoExpressionMap = new HashMap<String, String>() {
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<Object> args = new ArrayList<Object>();
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<CompoundExpression> {
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;
}
}
}

View File

@@ -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<ExpressionNodeConversion<? extends ExpressionNode>> conversions;
/**
* Creates a new {@link SpelExpressionTransformer}.
*/
public SpelExpressionTransformer() {
List<ExpressionNodeConversion<? extends ExpressionNode>> conversions = new ArrayList<ExpressionNodeConversion<? extends ExpressionNode>>();
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}.
* <p>
* Exposes the given @{code params} as <code>[0] ... [n]</code>.
*
* @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<ExpressionNode>(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<ExpressionNode> 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<ExpressionNode> lookupConversionFor(ExpressionNode node) {
for (ExpressionNodeConversion<? extends ExpressionNode> candidate : conversions) {
if (candidate.supports(node)) {
return (ExpressionNodeConversion<ExpressionNode>) 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<T extends ExpressionNode> implements
AggregationExpressionTransformer {
private final AggregationExpressionTransformer transformer;
private final Class<? extends ExpressionNode> 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<? extends ExpressionNode>) 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<ExpressionNode>(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<ExpressionNode> 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<T> context);
}
/**
* A {@link ExpressionNodeConversion} that converts arithmetic operations.
*
* @author Thomas Darimont
*/
private static class OperatorNodeConversion extends ExpressionNodeConversion<OperatorNode> {
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<OperatorNode> 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<OperatorNode> 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<OperatorNode> 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<ExpressionNode> {
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<ExpressionNode> 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<ExpressionNode> {
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<ExpressionNode> 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<ExpressionNode> {
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<ExpressionNode> 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<LiteralNode> {
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<LiteralNode> 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<Number>) 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<MethodReferenceNode> {
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<MethodReferenceNode> context) {
MethodReferenceNode node = context.getCurrentNode();
List<Object> args = new ArrayList<Object>();
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<ExpressionNode> {
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<ExpressionNode> 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);
}
}
}

View File

@@ -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<ExpressionNode> {
private static final Iterator<ExpressionNode> EMPTY_ITERATOR = Collections.<ExpressionNode> 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<ExpressionNode> iterator() {
if (!hasChildren()) {
return EMPTY_ITERATOR;
}
return new Iterator<ExpressionNode>() {
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();
}
};
}
}

View File

@@ -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<T extends ExpressionNode> {
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());
}
}

View File

@@ -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<T extends ExpressionTransformationContextSupport<?>> {
/**
* Transforms the given {@link ExpressionTransformationContextSupport} into an Object.
*
* @param context will never be {@literal null}.
* @return
*/
Object transform(T context);
}

View File

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

View File

@@ -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<String, String> FUNCTIONS;
static {
Map<String, String> map = new HashMap<String, String>();
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);
}
}

View File

@@ -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<String, String> OPERATORS;
static {
Map<String, String> map = new HashMap<String, String>(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());
}
}

View File

@@ -0,0 +1,5 @@
/**
* Support classes to transform SpEL expressions into MongoDB expressions.
* @since 1.4
*/
package org.springframework.data.mongodb.core.spel;

View File

@@ -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");

View File

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

View File

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

View File

@@ -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\"]}]}"));
}
}

View File

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

View File

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

View File

@@ -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<? extends SpelNode> 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)));
}
}
}