/*
* Copyright 2002-2017 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.support;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ParseException;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.AbstractExpressionTests;
import org.springframework.expression.spel.SpelUtilities;
import org.springframework.expression.spel.ast.FormatHelper;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind;
import static org.junit.Assert.*;
/**
* Tests for any helper code.
*
* @author Andy Clement
*/
public class ReflectionHelperTests extends AbstractExpressionTests {
@Test
public void testFormatHelperForClassName() {
assertEquals("java.lang.String",FormatHelper.formatClassNameForMessage(String.class));
assertEquals("java.lang.String[]",FormatHelper.formatClassNameForMessage(new String[1].getClass()));
assertEquals("java.lang.String[][]",FormatHelper.formatClassNameForMessage(new String[1][1].getClass()));
assertEquals("int[]",FormatHelper.formatClassNameForMessage(new int[1].getClass()));
assertEquals("int[][]",FormatHelper.formatClassNameForMessage(new int[1][2].getClass()));
assertEquals("null",FormatHelper.formatClassNameForMessage(null));
}
/*
@Test
public void testFormatHelperForMethod() {
assertEquals("foo(java.lang.String)",FormatHelper.formatMethodForMessage("foo", String.class));
assertEquals("goo(java.lang.String,int[])",FormatHelper.formatMethodForMessage("goo", String.class, new int[1].getClass()));
assertEquals("boo()",FormatHelper.formatMethodForMessage("boo"));
}
*/
@Test
public void testUtilities() throws ParseException {
SpelExpression expr = (SpelExpression)parser.parseExpression("3+4+5+6+7-2");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
SpelUtilities.printAbstractSyntaxTree(ps, expr);
ps.flush();
String s = baos.toString();
// ===> Expression '3+4+5+6+7-2' - AST start
// OperatorMinus value:(((((3 + 4) + 5) + 6) + 7) - 2) #children:2
// OperatorPlus value:((((3 + 4) + 5) + 6) + 7) #children:2
// OperatorPlus value:(((3 + 4) + 5) + 6) #children:2
// OperatorPlus value:((3 + 4) + 5) #children:2
// OperatorPlus value:(3 + 4) #children:2
// CompoundExpression value:3
// IntLiteral value:3
// CompoundExpression value:4
// IntLiteral value:4
// CompoundExpression value:5
// IntLiteral value:5
// CompoundExpression value:6
// IntLiteral value:6
// CompoundExpression value:7
// IntLiteral value:7
// CompoundExpression value:2
// IntLiteral value:2
// ===> Expression '3+4+5+6+7-2' - AST end
assertTrue(s.contains("===> Expression '3+4+5+6+7-2' - AST start"));
assertTrue(s.contains(" OpPlus value:((((3 + 4) + 5) + 6) + 7) #children:2"));
}
@Test
public void testTypedValue() {
TypedValue tv1 = new TypedValue("hello");
TypedValue tv2 = new TypedValue("hello");
TypedValue tv3 = new TypedValue("bye");
assertEquals(String.class, tv1.getTypeDescriptor().getType());
assertEquals("TypedValue: 'hello' of [java.lang.String]", tv1.toString());
assertEquals(tv1, tv2);
assertEquals(tv2, tv1);
assertNotEquals(tv1, tv3);
assertNotEquals(tv2, tv3);
assertNotEquals(tv3, tv1);
assertNotEquals(tv3, tv2);
assertEquals(tv1.hashCode(), tv2.hashCode());
assertNotEquals(tv1.hashCode(), tv3.hashCode());
assertNotEquals(tv2.hashCode(), tv3.hashCode());
}
@Test
public void testReflectionHelperCompareArguments_ExactMatching() {
StandardTypeConverter tc = new StandardTypeConverter();
// Calling foo(String) with (String) is exact match
checkMatch(new Class[] {String.class}, new Class[] {String.class}, tc, ReflectionHelper.ArgumentsMatchKind.EXACT);
// Calling foo(String,Integer) with (String,Integer) is exact match
checkMatch(new Class[] {String.class, Integer.class}, new Class[] {String.class, Integer.class}, tc, ArgumentsMatchKind.EXACT);
}
@Test
public void testReflectionHelperCompareArguments_CloseMatching() {
StandardTypeConverter tc = new StandardTypeConverter();
// Calling foo(List) with (ArrayList) is close match (no conversion required)
checkMatch(new Class[] {ArrayList.class}, new Class[] {List.class}, tc, ArgumentsMatchKind.CLOSE);
// Passing (Sub,String) on call to foo(Super,String) is close match
checkMatch(new Class[] {Sub.class, String.class}, new Class[] {Super.class, String.class}, tc, ArgumentsMatchKind.CLOSE);
// Passing (String,Sub) on call to foo(String,Super) is close match
checkMatch(new Class[] {String.class, Sub.class}, new Class[] {String.class, Super.class}, tc, ArgumentsMatchKind.CLOSE);
}
@Test
public void testReflectionHelperCompareArguments_RequiresConversionMatching() {
StandardTypeConverter tc = new StandardTypeConverter();
// Calling foo(String,int) with (String,Integer) requires boxing conversion of argument one
checkMatch(new Class[] {String.class, Integer.TYPE}, new Class[] {String.class,Integer.class},tc, ArgumentsMatchKind.CLOSE);
// Passing (int,String) on call to foo(Integer,String) requires boxing conversion of argument zero
checkMatch(new Class[] {Integer.TYPE, String.class}, new Class[] {Integer.class, String.class},tc, ArgumentsMatchKind.CLOSE);
// Passing (int,Sub) on call to foo(Integer,Super) requires boxing conversion of argument zero
checkMatch(new Class[] {Integer.TYPE, Sub.class}, new Class[] {Integer.class, Super.class}, tc, ArgumentsMatchKind.CLOSE);
// Passing (int,Sub,boolean) on call to foo(Integer,Super,Boolean) requires boxing conversion of arguments zero and two
// TODO checkMatch(new Class[] {Integer.TYPE, Sub.class, Boolean.TYPE}, new Class[] {Integer.class, Super.class, Boolean.class}, tc, ArgsMatchKind.REQUIRES_CONVERSION);
}
@Test
public void testReflectionHelperCompareArguments_NotAMatch() {
StandardTypeConverter typeConverter = new StandardTypeConverter();
// Passing (Super,String) on call to foo(Sub,String) is not a match
checkMatch(new Class[] {Super.class,String.class}, new Class[] {Sub.class,String.class},typeConverter,null);
}
@Test
public void testReflectionHelperCompareArguments_Varargs_ExactMatching() {
StandardTypeConverter tc = new StandardTypeConverter();
Class<?> stringArrayClass = new String[0].getClass();
Class<?> integerArrayClass = new Integer[0].getClass();
// Passing (String[]) on call to (String[]) is exact match
checkMatch2(new Class[] {stringArrayClass}, new Class[] {stringArrayClass}, tc, ArgumentsMatchKind.EXACT);
// Passing (Integer, String[]) on call to (Integer, String[]) is exact match
checkMatch2(new Class[] {Integer.class, stringArrayClass}, new Class[] {Integer.class, stringArrayClass}, tc, ArgumentsMatchKind.EXACT);
// Passing (String, Integer, String[]) on call to (String, String, String[]) is exact match
checkMatch2(new Class[] {String.class, Integer.class, stringArrayClass}, new Class[] {String.class,Integer.class, stringArrayClass}, tc, ArgumentsMatchKind.EXACT);
// Passing (Sub, String[]) on call to (Super, String[]) is exact match
checkMatch2(new Class[] {Sub.class, stringArrayClass}, new Class[] {Super.class,stringArrayClass}, tc, ArgumentsMatchKind.CLOSE);
// Passing (Integer, String[]) on call to (String, String[]) is exact match
checkMatch2(new Class[] {Integer.class, stringArrayClass}, new Class[] {String.class, stringArrayClass}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);
// Passing (Integer, Sub, String[]) on call to (String, Super, String[]) is exact match
checkMatch2(new Class[] {Integer.class, Sub.class, String[].class}, new Class[] {String.class,Super .class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);
// Passing (String) on call to (String[]) is exact match
checkMatch2(new Class[] {String.class}, new Class[] {stringArrayClass}, tc, ArgumentsMatchKind.EXACT);
// Passing (Integer,String) on call to (Integer,String[]) is exact match
checkMatch2(new Class[] {Integer.class, String.class}, new Class[] {Integer.class, stringArrayClass}, tc, ArgumentsMatchKind.EXACT);
// Passing (String) on call to (Integer[]) is conversion match (String to Integer)
checkMatch2(new Class[] {String.class}, new Class[] {integerArrayClass}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);
// Passing (Sub) on call to (Super[]) is close match
checkMatch2(new Class[] {Sub.class}, new Class[] {new Super[0].getClass()}, tc, ArgumentsMatchKind.CLOSE);
// Passing (Super) on call to (Sub[]) is not a match
checkMatch2(new Class[] {Super.class}, new Class[] {new Sub[0].getClass()}, tc, null);
checkMatch2(new Class[] {Unconvertable.class, String.class}, new Class[] {Sub.class, Super[].class}, tc, null);
checkMatch2(new Class[] {Integer.class, Integer.class, String.class}, new Class[] {String.class, String.class, Super[].class}, tc, null);
checkMatch2(new Class[] {Unconvertable.class, String.class}, new Class[] {Sub.class, Super[].class}, tc, null);
checkMatch2(new Class[] {Integer.class, Integer.class, String.class}, new Class[] {String.class, String.class, Super[].class}, tc, null);
checkMatch2(new Class[] {Integer.class, Integer.class, Sub.class}, new Class[] {String.class, String.class, Super[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);
checkMatch2(new Class[] {Integer.class, Integer.class, Integer.class}, new Class[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);
// what happens on (Integer,String) passed to (Integer[]) ?
}
@Test
public void testConvertArguments() throws Exception {
StandardTypeConverter tc = new StandardTypeConverter();
Method oneArg = TestInterface.class.getMethod("oneArg", String.class);
Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class);
// basic conversion int>String
Object[] args = new Object[] {3};
ReflectionHelper.convertArguments(tc, args, oneArg, null);
checkArguments(args, "3");
// varargs but nothing to convert
args = new Object[] {3};
ReflectionHelper.convertArguments(tc, args, twoArg, 1);
checkArguments(args, "3");
// varargs with nothing needing conversion
args = new Object[] {3, "abc", "abc"};
ReflectionHelper.convertArguments(tc, args, twoArg, 1);
checkArguments(args, "3","abc","abc");
// varargs with conversion required
args = new Object[] {3, false ,3.0d};
ReflectionHelper.convertArguments(tc, args, twoArg, 1);
checkArguments(args, "3","false","3.0");
}
@Test
public void testConvertArguments2() throws Exception {
StandardTypeConverter tc = new StandardTypeConverter();
Method oneArg = TestInterface.class.getMethod("oneArg", String.class);
Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class);
// Simple conversion: int to string
Object[] args = new Object[] {3};
ReflectionHelper.convertAllArguments(tc, args, oneArg);
checkArguments(args,"3");
// varargs conversion
args = new Object[] {3, false, 3.0f};
ReflectionHelper.convertAllArguments(tc, args, twoArg);
checkArguments(args,"3","false","3.0");
// varargs conversion but no varargs
args = new Object[] {3};
ReflectionHelper.convertAllArguments(tc, args, twoArg);
checkArguments(args,"3");
// null value
args = new Object[] {3, null, 3.0f};
ReflectionHelper.convertAllArguments(tc, args, twoArg);
checkArguments(args,"3",null,"3.0");
}
@Test
public void testSetupArguments() {
Object[] newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(new Class[] {new String[0].getClass()},"a","b","c");
assertEquals(1, newArray.length);
Object firstParam = newArray[0];
assertEquals(String.class,firstParam.getClass().getComponentType());
Object[] firstParamArray = (Object[])firstParam;
assertEquals(3,firstParamArray.length);
assertEquals("a",firstParamArray[0]);
assertEquals("b",firstParamArray[1]);
assertEquals("c",firstParamArray[2]);
}
@Test
public void testReflectivePropertyResolver() throws Exception {
ReflectivePropertyAccessor rpr = new ReflectivePropertyAccessor();
Tester t = new Tester();
t.setProperty("hello");
EvaluationContext ctx = new StandardEvaluationContext(t);
assertTrue(rpr.canRead(ctx, t, "property"));
assertEquals("hello",rpr.read(ctx, t, "property").getValue());
assertEquals("hello",rpr.read(ctx, t, "property").getValue()); // cached accessor used
assertTrue(rpr.canRead(ctx, t, "field"));
assertEquals(3,rpr.read(ctx, t, "field").getValue());
assertEquals(3,rpr.read(ctx, t, "field").getValue()); // cached accessor used
assertTrue(rpr.canWrite(ctx, t, "property"));
rpr.write(ctx, t, "property","goodbye");
rpr.write(ctx, t, "property","goodbye"); // cached accessor used
assertTrue(rpr.canWrite(ctx, t, "field"));
rpr.write(ctx, t, "field",12);
rpr.write(ctx, t, "field",12);
// Attempted write as first activity on this field and property to drive testing
// of populating type descriptor cache
rpr.write(ctx,t,"field2",3);
rpr.write(ctx, t, "property2","doodoo");
assertEquals(3,rpr.read(ctx,t,"field2").getValue());
// Attempted read as first activity on this field and property (no canRead before them)
assertEquals(0,rpr.read(ctx,t,"field3").getValue());
assertEquals("doodoo",rpr.read(ctx,t,"property3").getValue());
// Access through is method
// assertEquals(0,rpr.read(ctx,t,"field3").getValue());
assertEquals(false,rpr.read(ctx,t,"property4").getValue());
assertTrue(rpr.canRead(ctx,t,"property4"));
// repro SPR-9123, ReflectivePropertyAccessor JavaBean property names compliance tests
assertEquals("iD",rpr.read(ctx,t,"iD").getValue());
assertTrue(rpr.canRead(ctx,t,"iD"));
assertEquals("id",rpr.read(ctx,t,"id").getValue());
assertTrue(rpr.canRead(ctx,t,"id"));
assertEquals("ID",rpr.read(ctx,t,"ID").getValue());
assertTrue(rpr.canRead(ctx,t,"ID"));
// note: "Id" is not a valid JavaBean name, nevertheless it is treated as "id"
assertEquals("id",rpr.read(ctx,t,"Id").getValue());
assertTrue(rpr.canRead(ctx,t,"Id"));
// repro SPR-10994
assertEquals("xyZ",rpr.read(ctx,t,"xyZ").getValue());
assertTrue(rpr.canRead(ctx,t,"xyZ"));
assertEquals("xY",rpr.read(ctx,t,"xY").getValue());
assertTrue(rpr.canRead(ctx,t,"xY"));
// SPR-10122, ReflectivePropertyAccessor JavaBean property names compliance tests - setters
rpr.write(ctx, t, "pEBS","Test String");
assertEquals("Test String",rpr.read(ctx,t,"pEBS").getValue());
}
@Test
public void testOptimalReflectivePropertyResolver() throws Exception {
ReflectivePropertyAccessor rpr = new ReflectivePropertyAccessor();
Tester t = new Tester();
t.setProperty("hello");
EvaluationContext ctx = new StandardEvaluationContext(t);
// assertTrue(rpr.canRead(ctx, t, "property"));
// assertEquals("hello",rpr.read(ctx, t, "property").getValue());
// assertEquals("hello",rpr.read(ctx, t, "property").getValue()); // cached accessor used
PropertyAccessor optA = rpr.createOptimalAccessor(ctx, t, "property");
assertTrue(optA.canRead(ctx, t, "property"));
assertFalse(optA.canRead(ctx, t, "property2"));
try {
optA.canWrite(ctx, t, "property");
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
try {
optA.canWrite(ctx, t, "property2");
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
assertEquals("hello",optA.read(ctx, t, "property").getValue());
assertEquals("hello",optA.read(ctx, t, "property").getValue()); // cached accessor used
try {
optA.getSpecificTargetClasses();
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
try {
optA.write(ctx,t,"property",null);
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
optA = rpr.createOptimalAccessor(ctx, t, "field");
assertTrue(optA.canRead(ctx, t, "field"));
assertFalse(optA.canRead(ctx, t, "field2"));
try {
optA.canWrite(ctx, t, "field");
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
try {
optA.canWrite(ctx, t, "field2");
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
assertEquals(3,optA.read(ctx, t, "field").getValue());
assertEquals(3,optA.read(ctx, t, "field").getValue()); // cached accessor used
try {
optA.getSpecificTargetClasses();
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
try {
optA.write(ctx,t,"field",null);
fail();
}
catch (UnsupportedOperationException uoe) {
// success
}
}
// test classes
static class Tester {
String property;
public int field = 3;
public int field2;
public int field3 = 0;
String property2;
String property3 = "doodoo";
boolean property4 = false;
String iD = "iD";
String id = "id";
String ID = "ID";
String pEBS = "pEBS";
String xY = "xY";
String xyZ = "xyZ";
public String getProperty() { return property; }
public void setProperty(String value) { property = value; }
public void setProperty2(String value) { property2 = value; }
public String getProperty3() { return property3; }
public boolean isProperty4() { return property4; }
public String getiD() { return iD; }
public String getId() { return id; }
public String getID() { return ID; }
public String getXY() { return xY; }
public String getXyZ() { return xyZ; }
public String getpEBS() {
return pEBS;
}
public void setpEBS(String pEBS) {
this.pEBS = pEBS;
}
}
static class Super {
}
static class Sub extends Super {
}
static class Unconvertable {}
// ---
/**
* Used to validate the match returned from a compareArguments call.
*/
private void checkMatch(Class<?>[] inputTypes, Class<?>[] expectedTypes, StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) {
ReflectionHelper.ArgumentsMatchInfo matchInfo = ReflectionHelper.compareArguments(getTypeDescriptors(expectedTypes), getTypeDescriptors(inputTypes), typeConverter);
if (expectedMatchKind == null) {
assertNull("Did not expect them to match in any way", matchInfo);
}
else {
assertNotNull("Should not be a null match", matchInfo);
}
if (expectedMatchKind == ArgumentsMatchKind.EXACT) {
assertTrue(matchInfo.isExactMatch());
}
else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) {
assertTrue(matchInfo.isCloseMatch());
}
else if (expectedMatchKind == ArgumentsMatchKind.REQUIRES_CONVERSION) {
assertTrue("expected to be a match requiring conversion, but was " + matchInfo, matchInfo.isMatchRequiringConversion());
}
}
/**
* Used to validate the match returned from a compareArguments call.
*/
private void checkMatch2(Class<?>[] inputTypes, Class<?>[] expectedTypes, StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) {
ReflectionHelper.ArgumentsMatchInfo matchInfo = ReflectionHelper.compareArgumentsVarargs(getTypeDescriptors(expectedTypes), getTypeDescriptors(inputTypes), typeConverter);
if (expectedMatchKind == null) {
assertNull("Did not expect them to match in any way: " + matchInfo, matchInfo);
}
else {
assertNotNull("Should not be a null match", matchInfo);
}
if (expectedMatchKind == ArgumentsMatchKind.EXACT) {
assertTrue(matchInfo.isExactMatch());
}
else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) {
assertTrue(matchInfo.isCloseMatch());
}
else if (expectedMatchKind == ArgumentsMatchKind.REQUIRES_CONVERSION) {
assertTrue("expected to be a match requiring conversion, but was " + matchInfo, matchInfo.isMatchRequiringConversion());
}
}
private void checkArguments(Object[] args, Object... expected) {
assertEquals(expected.length,args.length);
for (int i = 0; i < expected.length; i++) {
checkArgument(expected[i],args[i]);
}
}
private void checkArgument(Object expected, Object actual) {
assertEquals(expected,actual);
}
private List<TypeDescriptor> getTypeDescriptors(Class<?>... types) {
List<TypeDescriptor> typeDescriptors = new ArrayList<>(types.length);
for (Class<?> type : types) {
typeDescriptors.add(TypeDescriptor.valueOf(type));
}
return typeDescriptors;
}
public interface TestInterface {
void oneArg(String arg1);
void twoArg(String arg1, String... arg2);
}
}