Skip to content

Commit 7cdfaf3

Browse files
bedlacbeams
authored andcommitted
Convert SpEL plus operands using reg'd converters
Prior to this commit, SpEL's OpPlus ('+' operator) would convert its left and right operands to String directly via #toString calls. This change ensures that operand values are delegated to any registered converters, allowing for customization of the stringified output. Note that the OpPlus constructor now throws IllegalArgumentException if zero operands are supplied. Issue: SPR-8308
1 parent 3514cc5 commit 7cdfaf3

File tree

2 files changed

+272
-16
lines changed
  • spring-expression/src

2 files changed

+272
-16
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2009 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
1616

1717
package org.springframework.expression.spel.ast;
1818

19+
import org.springframework.core.convert.TypeDescriptor;
1920
import org.springframework.expression.EvaluationException;
2021
import org.springframework.expression.Operation;
22+
import org.springframework.expression.TypeConverter;
2123
import org.springframework.expression.TypedValue;
2224
import org.springframework.expression.spel.ExpressionState;
25+
import org.springframework.util.Assert;
2326

2427
/**
2528
* The plus operator will:
@@ -31,14 +34,16 @@
3134
* </ul>
3235
* It can be used as a unary operator for numbers (double/long/int). The standard promotions are performed
3336
* when the operand types vary (double+int=double). For other options it defers to the registered overloader.
34-
*
37+
*
3538
* @author Andy Clement
39+
* @author Ivo Smid
3640
* @since 3.0
3741
*/
3842
public class OpPlus extends Operator {
3943

4044
public OpPlus(int pos, SpelNodeImpl... operands) {
4145
super("+", pos, operands);
46+
Assert.notEmpty(operands);
4247
}
4348

4449
@Override
@@ -48,19 +53,21 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
4853
if (rightOp == null) { // If only one operand, then this is unary plus
4954
Object operandOne = leftOp.getValueInternal(state).getValue();
5055
if (operandOne instanceof Number) {
51-
if (operandOne instanceof Double) {
52-
return new TypedValue(((Double) operandOne).doubleValue());
53-
} else if (operandOne instanceof Long) {
54-
return new TypedValue(((Long) operandOne).longValue());
56+
if (operandOne instanceof Double || operandOne instanceof Long) {
57+
return new TypedValue(operandOne);
5558
} else {
56-
return new TypedValue(((Integer) operandOne).intValue());
59+
return new TypedValue(((Number) operandOne).intValue());
5760
}
5861
}
5962
return state.operate(Operation.ADD, operandOne, null);
6063
}
6164
else {
62-
Object operandOne = leftOp.getValueInternal(state).getValue();
63-
Object operandTwo = rightOp.getValueInternal(state).getValue();
65+
final TypedValue operandOneValue = leftOp.getValueInternal(state);
66+
final Object operandOne = operandOneValue.getValue();
67+
68+
final TypedValue operandTwoValue = rightOp.getValueInternal(state);
69+
final Object operandTwo = operandTwoValue.getValue();
70+
6471
if (operandOne instanceof Number && operandTwo instanceof Number) {
6572
Number op1 = (Number) operandOne;
6673
Number op2 = (Number) operandTwo;
@@ -74,13 +81,14 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
7481
} else if (operandOne instanceof String && operandTwo instanceof String) {
7582
return new TypedValue(new StringBuilder((String) operandOne).append((String) operandTwo).toString());
7683
} else if (operandOne instanceof String) {
77-
StringBuilder result = new StringBuilder((String)operandOne);
78-
result.append((operandTwo==null?"null":operandTwo.toString()));
79-
return new TypedValue(result.toString());
84+
StringBuilder result = new StringBuilder((String) operandOne);
85+
result.append((operandTwo == null ? "null" : convertTypedValueToString(operandTwoValue, state)));
86+
return new TypedValue(result.toString());
8087
} else if (operandTwo instanceof String) {
81-
StringBuilder result = new StringBuilder((operandOne==null?"null":operandOne.toString()));
82-
result.append((String)operandTwo);
83-
return new TypedValue(result.toString());
88+
StringBuilder result = new StringBuilder((operandOne == null ? "null" : convertTypedValueToString(
89+
operandOneValue, state)));
90+
result.append((String) operandTwo);
91+
return new TypedValue(result.toString());
8492
}
8593
return state.operate(Operation.ADD, operandOne, operandTwo);
8694
}
@@ -94,10 +102,32 @@ public String toStringAST() {
94102
return super.toStringAST();
95103
}
96104

105+
@Override
97106
public SpelNodeImpl getRightOperand() {
98-
if (children.length<2) {return null;}
107+
if (children.length < 2) {
108+
return null;
109+
}
99110
return children[1];
100111
}
101112

113+
/**
114+
* Convert operand value to string using registered converter or using
115+
* {@code toString} method.
116+
*
117+
* @param value typed value to be converted
118+
* @param state expression state
119+
* @return {@code TypedValue} instance converted to {@code String}
120+
*/
121+
private static String convertTypedValueToString(TypedValue value, ExpressionState state) {
122+
final TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter();
123+
final TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(String.class);
124+
125+
if (typeConverter.canConvert(value.getTypeDescriptor(), typeDescriptor)) {
126+
final Object obj = typeConverter.convertValue(value.getValue(), value.getTypeDescriptor(), typeDescriptor);
127+
return String.valueOf(obj);
128+
} else {
129+
return String.valueOf(value.getValue());
130+
}
131+
}
102132

103133
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.expression.spel.ast;
18+
19+
import java.sql.Time;
20+
import java.text.SimpleDateFormat;
21+
import java.util.Date;
22+
import java.util.Locale;
23+
24+
import org.junit.Test;
25+
26+
import org.springframework.core.convert.converter.Converter;
27+
import org.springframework.core.convert.support.GenericConversionService;
28+
import org.springframework.expression.TypedValue;
29+
import org.springframework.expression.spel.ExpressionState;
30+
import org.springframework.expression.spel.SpelEvaluationException;
31+
import org.springframework.expression.spel.support.StandardEvaluationContext;
32+
import org.springframework.expression.spel.support.StandardTypeConverter;
33+
34+
import static org.junit.Assert.*;
35+
36+
/**
37+
* Unit tests for SpEL's plus operator.
38+
*
39+
* @author Ivo Smid
40+
* @author Chris Beams
41+
* @since 3.2
42+
* @see OpPlus
43+
*/
44+
public class OpPlusTests {
45+
46+
@Test(expected = IllegalArgumentException.class)
47+
public void test_emptyOperands() {
48+
new OpPlus(-1);
49+
}
50+
51+
@Test(expected = SpelEvaluationException.class)
52+
public void test_unaryPlusWithStringLiteral() {
53+
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
54+
55+
StringLiteral str = new StringLiteral("word", -1, "word");
56+
57+
OpPlus o = new OpPlus(-1, str);
58+
o.getValueInternal(expressionState);
59+
}
60+
61+
@Test
62+
public void test_unaryPlusWithNumberOperand() {
63+
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
64+
65+
{
66+
RealLiteral realLiteral = new RealLiteral("123.00", -1, 123.0);
67+
OpPlus o = new OpPlus(-1, realLiteral);
68+
TypedValue value = o.getValueInternal(expressionState);
69+
70+
assertEquals(Double.class, value.getTypeDescriptor().getObjectType());
71+
assertEquals(Double.class, value.getTypeDescriptor().getType());
72+
assertEquals(realLiteral.getLiteralValue().getValue(), value.getValue());
73+
}
74+
75+
{
76+
IntLiteral intLiteral = new IntLiteral("123", -1, 123);
77+
OpPlus o = new OpPlus(-1, intLiteral);
78+
TypedValue value = o.getValueInternal(expressionState);
79+
80+
assertEquals(Integer.class, value.getTypeDescriptor().getObjectType());
81+
assertEquals(Integer.class, value.getTypeDescriptor().getType());
82+
assertEquals(intLiteral.getLiteralValue().getValue(), value.getValue());
83+
}
84+
85+
{
86+
LongLiteral longLiteral = new LongLiteral("123", -1, 123L);
87+
OpPlus o = new OpPlus(-1, longLiteral);
88+
TypedValue value = o.getValueInternal(expressionState);
89+
90+
assertEquals(Long.class, value.getTypeDescriptor().getObjectType());
91+
assertEquals(Long.class, value.getTypeDescriptor().getType());
92+
assertEquals(longLiteral.getLiteralValue().getValue(), value.getValue());
93+
}
94+
}
95+
96+
@Test
97+
public void test_binaryPlusWithNumberOperands() {
98+
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
99+
100+
{
101+
RealLiteral n1 = new RealLiteral("123.00", -1, 123.0);
102+
RealLiteral n2 = new RealLiteral("456.00", -1, 456.0);
103+
OpPlus o = new OpPlus(-1, n1, n2);
104+
TypedValue value = o.getValueInternal(expressionState);
105+
106+
assertEquals(Double.class, value.getTypeDescriptor().getObjectType());
107+
assertEquals(Double.class, value.getTypeDescriptor().getType());
108+
assertEquals(Double.valueOf(123.0 + 456.0), value.getValue());
109+
}
110+
111+
{
112+
LongLiteral n1 = new LongLiteral("123", -1, 123L);
113+
LongLiteral n2 = new LongLiteral("456", -1, 456L);
114+
OpPlus o = new OpPlus(-1, n1, n2);
115+
TypedValue value = o.getValueInternal(expressionState);
116+
117+
assertEquals(Long.class, value.getTypeDescriptor().getObjectType());
118+
assertEquals(Long.class, value.getTypeDescriptor().getType());
119+
assertEquals(Long.valueOf(123L + 456L), value.getValue());
120+
}
121+
122+
{
123+
IntLiteral n1 = new IntLiteral("123", -1, 123);
124+
IntLiteral n2 = new IntLiteral("456", -1, 456);
125+
OpPlus o = new OpPlus(-1, n1, n2);
126+
TypedValue value = o.getValueInternal(expressionState);
127+
128+
assertEquals(Integer.class, value.getTypeDescriptor().getObjectType());
129+
assertEquals(Integer.class, value.getTypeDescriptor().getType());
130+
assertEquals(Integer.valueOf(123 + 456), value.getValue());
131+
}
132+
}
133+
134+
@Test
135+
public void test_binaryPlusWithStringOperands() {
136+
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
137+
138+
StringLiteral n1 = new StringLiteral("\"foo\"", -1, "\"foo\"");
139+
StringLiteral n2 = new StringLiteral("\"bar\"", -1, "\"bar\"");
140+
OpPlus o = new OpPlus(-1, n1, n2);
141+
TypedValue value = o.getValueInternal(expressionState);
142+
143+
assertEquals(String.class, value.getTypeDescriptor().getObjectType());
144+
assertEquals(String.class, value.getTypeDescriptor().getType());
145+
assertEquals("foobar", value.getValue());
146+
}
147+
148+
@Test
149+
public void test_binaryPlusWithLeftStringOperand() {
150+
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
151+
152+
StringLiteral n1 = new StringLiteral("\"number is \"", -1, "\"number is \"");
153+
LongLiteral n2 = new LongLiteral("123", -1, 123);
154+
OpPlus o = new OpPlus(-1, n1, n2);
155+
TypedValue value = o.getValueInternal(expressionState);
156+
157+
assertEquals(String.class, value.getTypeDescriptor().getObjectType());
158+
assertEquals(String.class, value.getTypeDescriptor().getType());
159+
assertEquals("number is 123", value.getValue());
160+
}
161+
162+
@Test
163+
public void test_binaryPlusWithRightStringOperand() {
164+
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
165+
166+
LongLiteral n1 = new LongLiteral("123", -1, 123);
167+
StringLiteral n2 = new StringLiteral("\" is a number\"", -1, "\" is a number\"");
168+
OpPlus o = new OpPlus(-1, n1, n2);
169+
TypedValue value = o.getValueInternal(expressionState);
170+
171+
assertEquals(String.class, value.getTypeDescriptor().getObjectType());
172+
assertEquals(String.class, value.getTypeDescriptor().getType());
173+
assertEquals("123 is a number", value.getValue());
174+
}
175+
176+
@Test
177+
public void test_binaryPlusWithTime_ToString() {
178+
179+
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
180+
181+
Time time = new Time(new Date().getTime());
182+
183+
VariableReference var = new VariableReference("timeVar", -1);
184+
var.setValue(expressionState, time);
185+
186+
StringLiteral n2 = new StringLiteral("\" is now\"", -1, "\" is now\"");
187+
OpPlus o = new OpPlus(-1, var, n2);
188+
TypedValue value = o.getValueInternal(expressionState);
189+
190+
assertEquals(String.class, value.getTypeDescriptor().getObjectType());
191+
assertEquals(String.class, value.getTypeDescriptor().getType());
192+
assertEquals(time + " is now", value.getValue());
193+
}
194+
195+
@Test
196+
public void test_binaryPlusWithTimeConverted() {
197+
198+
final SimpleDateFormat format = new SimpleDateFormat("hh :--: mm :--: ss", Locale.ENGLISH);
199+
200+
GenericConversionService conversionService = new GenericConversionService();
201+
conversionService.addConverter(new Converter<Time, String>() {
202+
public String convert(Time source) {
203+
return format.format(source);
204+
}
205+
});
206+
207+
StandardEvaluationContext evaluationContextConverter = new StandardEvaluationContext();
208+
evaluationContextConverter.setTypeConverter(new StandardTypeConverter(conversionService));
209+
210+
ExpressionState expressionState = new ExpressionState(evaluationContextConverter);
211+
212+
Time time = new Time(new Date().getTime());
213+
214+
VariableReference var = new VariableReference("timeVar", -1);
215+
var.setValue(expressionState, time);
216+
217+
StringLiteral n2 = new StringLiteral("\" is now\"", -1, "\" is now\"");
218+
OpPlus o = new OpPlus(-1, var, n2);
219+
TypedValue value = o.getValueInternal(expressionState);
220+
221+
assertEquals(String.class, value.getTypeDescriptor().getObjectType());
222+
assertEquals(String.class, value.getTypeDescriptor().getType());
223+
assertEquals(format.format(time) + " is now", value.getValue());
224+
}
225+
226+
}

0 commit comments

Comments
 (0)