/*
* Copyright 2002-2016 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.expression.spel;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import static org.junit.Assert.*;
///CLOVER:OFF
/**
* Testcases showing the common scenarios/use-cases for picking up the expression language support.
* The first test shows very basic usage, just drop it in and go. By 'standard infrastructure', it means:<br>
* <ul>
* <li>The context classloader is used (so, the default classpath)
* <li>Some basic type converters are included
* <li>properties/methods/constructors are discovered and invoked using reflection
* </ul>
* The scenarios after that then how to plug in extensions:<br>
* <ul>
* <li>Adding entries to the classpath that will be used to load types and define well known 'imports'
* <li>Defining variables that are then accessible in the expression
* <li>Changing the root context object against which non-qualified references are resolved
* <li>Registering java methods as functions callable from the expression
* <li>Adding a basic property resolver
* <li>Adding an advanced (better performing) property resolver
* <li>Adding your own type converter to support conversion between any types you like
* </ul>
*
* @author Andy Clement
*/
public class ExpressionLanguageScenarioTests extends AbstractExpressionTests {
/**
* Scenario: using the standard infrastructure and running simple expression evaluation.
*/
@Test
public void testScenario_UsingStandardInfrastructure() {
try {
// Create a parser
SpelExpressionParser parser = new SpelExpressionParser();
// Parse an expression
Expression expr = parser.parseRaw("new String('hello world')");
// Evaluate it using a 'standard' context
Object value = expr.getValue();
// They are reusable
value = expr.getValue();
assertEquals("hello world", value);
assertEquals(String.class, value.getClass());
}
catch (EvaluationException ee) {
ee.printStackTrace();
fail("Unexpected Exception: " + ee.getMessage());
}
catch (ParseException pe) {
pe.printStackTrace();
fail("Unexpected Exception: " + pe.getMessage());
}
}
/**
* Scenario: using the standard context but adding your own variables
*/
@Test
public void testScenario_DefiningVariablesThatWillBeAccessibleInExpressions() throws Exception {
// Create a parser
SpelExpressionParser parser = new SpelExpressionParser();
// Use the standard evaluation context
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("favouriteColour","blue");
List<Integer> primes = new ArrayList<>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));
ctx.setVariable("primes",primes);
Expression expr = parser.parseRaw("#favouriteColour");
Object value = expr.getValue(ctx);
assertEquals("blue", value);
expr = parser.parseRaw("#primes.get(1)");
value = expr.getValue(ctx);
assertEquals(3, value);
// all prime numbers > 10 from the list (using selection ?{...})
expr = parser.parseRaw("#primes.?[#this>10]");
value = expr.getValue(ctx);
assertEquals("[11, 13, 17]", value.toString());
}
static class TestClass {
public String str;
private int property;
public int getProperty() { return property; }
public void setProperty(int i) { property = i; }
}
/**
* Scenario: using your own root context object
*/
@Test
public void testScenario_UsingADifferentRootContextObject() throws Exception {
// Create a parser
SpelExpressionParser parser = new SpelExpressionParser();
// Use the standard evaluation context
StandardEvaluationContext ctx = new StandardEvaluationContext();
TestClass tc = new TestClass();
tc.setProperty(42);
tc.str = "wibble";
ctx.setRootObject(tc);
// read it, set it, read it again
Expression expr = parser.parseRaw("str");
Object value = expr.getValue(ctx);
assertEquals("wibble", value);
expr = parser.parseRaw("str");
expr.setValue(ctx, "wobble");
expr = parser.parseRaw("str");
value = expr.getValue(ctx);
assertEquals("wobble", value);
// or using assignment within the expression
expr = parser.parseRaw("str='wabble'");
value = expr.getValue(ctx);
expr = parser.parseRaw("str");
value = expr.getValue(ctx);
assertEquals("wabble", value);
// private property will be accessed through getter()
expr = parser.parseRaw("property");
value = expr.getValue(ctx);
assertEquals(42, value);
// ... and set through setter
expr = parser.parseRaw("property=4");
value = expr.getValue(ctx);
expr = parser.parseRaw("property");
value = expr.getValue(ctx);
assertEquals(4,value);
}
public static String repeat(String s) { return s+s; }
/**
* Scenario: using your own java methods and calling them from the expression
*/
@Test
public void testScenario_RegisteringJavaMethodsAsFunctionsAndCallingThem() throws SecurityException, NoSuchMethodException {
try {
// Create a parser
SpelExpressionParser parser = new SpelExpressionParser();
// Use the standard evaluation context
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.registerFunction("repeat",ExpressionLanguageScenarioTests.class.getDeclaredMethod("repeat",String.class));
Expression expr = parser.parseRaw("#repeat('hello')");
Object value = expr.getValue(ctx);
assertEquals("hellohello", value);
}
catch (EvaluationException ee) {
ee.printStackTrace();
fail("Unexpected Exception: " + ee.getMessage());
}
catch (ParseException pe) {
pe.printStackTrace();
fail("Unexpected Exception: " + pe.getMessage());
}
}
/**
* Scenario: add a property resolver that will get called in the resolver chain, this one only supports reading.
*/
@Test
public void testScenario_AddingYourOwnPropertyResolvers_1() throws Exception {
// Create a parser
SpelExpressionParser parser = new SpelExpressionParser();
// Use the standard evaluation context
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.addPropertyAccessor(new FruitColourAccessor());
Expression expr = parser.parseRaw("orange");
Object value = expr.getValue(ctx);
assertEquals(Color.orange, value);
try {
expr.setValue(ctx, Color.blue);
fail("Should not be allowed to set oranges to be blue !");
}
catch (SpelEvaluationException ee) {
assertEquals(ee.getMessageCode(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL);
}
}
@Test
public void testScenario_AddingYourOwnPropertyResolvers_2() throws Exception {
// Create a parser
SpelExpressionParser parser = new SpelExpressionParser();
// Use the standard evaluation context
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.addPropertyAccessor(new VegetableColourAccessor());
Expression expr = parser.parseRaw("pea");
Object value = expr.getValue(ctx);
assertEquals(Color.green, value);
try {
expr.setValue(ctx, Color.blue);
fail("Should not be allowed to set peas to be blue !");
}
catch (SpelEvaluationException ee) {
assertEquals(ee.getMessageCode(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL);
}
}
/**
* Regardless of the current context object, or root context object, this resolver can tell you what colour a fruit is !
* It only supports property reading, not writing. To support writing it would need to override canWrite() and write()
*/
private static class FruitColourAccessor implements PropertyAccessor {
private static Map<String,Color> propertyMap = new HashMap<>();
static {
propertyMap.put("banana",Color.yellow);
propertyMap.put("apple",Color.red);
propertyMap.put("orange",Color.orange);
}
/**
* Null means you might be able to read any property, if an earlier property resolver hasn't beaten you to it
*/
@Override
public Class<?>[] getSpecificTargetClasses() {
return null;
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
return propertyMap.containsKey(name);
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
return new TypedValue(propertyMap.get(name));
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
return false;
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException {
}
}
/**
* Regardless of the current context object, or root context object, this resolver can tell you what colour a vegetable is !
* It only supports property reading, not writing.
*/
private static class VegetableColourAccessor implements PropertyAccessor {
private static Map<String,Color> propertyMap = new HashMap<>();
static {
propertyMap.put("carrot",Color.orange);
propertyMap.put("pea",Color.green);
}
/**
* Null means you might be able to read any property, if an earlier property resolver hasn't beaten you to it
*/
@Override
public Class<?>[] getSpecificTargetClasses() {
return null;
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
return propertyMap.containsKey(name);
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
return new TypedValue(propertyMap.get(name));
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
return false;
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
}
}
}