/* * Copyright 2002-2018 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.standard.SpelExpression; import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind; import static org.junit.Assert.*; /** * Tests for reflection helper code. * * @author Andy Clement */ public class ReflectionHelperTests extends AbstractExpressionTests { @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(); // Passing (String[]) on call to (String[]) is exact match checkMatch2(new Class<?>[] {String[].class}, new Class<?>[] {String[].class}, 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, String[].class}, tc, ArgumentsMatchKind.EXACT); // Passing (String, Integer, String[]) on call to (String, String, String[]) is exact match checkMatch2(new Class<?>[] {String.class, Integer.class, String[].class}, new Class<?>[] {String.class,Integer.class, String[].class}, tc, ArgumentsMatchKind.EXACT); // Passing (Sub, String[]) on call to (Super, String[]) is exact match checkMatch2(new Class<?>[] {Sub.class, String[].class}, new Class<?>[] {Super.class,String[].class}, tc, ArgumentsMatchKind.CLOSE); // Passing (Integer, String[]) on call to (String, String[]) is exact match checkMatch2(new Class<?>[] {Integer.class, String[].class}, new Class<?>[] {String.class, String[].class}, 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<?>[] {String[].class}, 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, String[].class}, tc, ArgumentsMatchKind.EXACT); // Passing (String) on call to (Integer[]) is conversion match (String to Integer) checkMatch2(new Class<?>[] {String.class}, new Class<?>[] {Integer[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); // Passing (Sub) on call to (Super[]) is close match checkMatch2(new Class<?>[] {Sub.class}, new Class<?>[] {Super[].class}, tc, ArgumentsMatchKind.CLOSE); // Passing (Super) on call to (Sub[]) is not a match checkMatch2(new Class<?>[] {Super.class}, new Class<?>[] {Sub[].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<?>[] {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<?>[] {String[].class}, "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 testReflectivePropertyAccessor() throws Exception { ReflectivePropertyAccessor rpa = new ReflectivePropertyAccessor(); Tester t = new Tester(); t.setProperty("hello"); EvaluationContext ctx = new StandardEvaluationContext(t); assertTrue(rpa.canRead(ctx, t, "property")); assertEquals("hello",rpa.read(ctx, t, "property").getValue()); assertEquals("hello",rpa.read(ctx, t, "property").getValue()); // cached accessor used assertTrue(rpa.canRead(ctx, t, "field")); assertEquals(3,rpa.read(ctx, t, "field").getValue()); assertEquals(3,rpa.read(ctx, t, "field").getValue()); // cached accessor used assertTrue(rpa.canWrite(ctx, t, "property")); rpa.write(ctx, t, "property", "goodbye"); rpa.write(ctx, t, "property", "goodbye"); // cached accessor used assertTrue(rpa.canWrite(ctx, t, "field")); rpa.write(ctx, t, "field", 12); rpa.write(ctx, t, "field", 12); // Attempted write as first activity on this field and property to drive testing // of populating type descriptor cache rpa.write(ctx, t, "field2", 3); rpa.write(ctx, t, "property2", "doodoo"); assertEquals(3,rpa.read(ctx, t, "field2").getValue()); // Attempted read as first activity on this field and property (no canRead before them) assertEquals(0,rpa.read(ctx, t, "field3").getValue()); assertEquals("doodoo",rpa.read(ctx, t, "property3").getValue()); // Access through is method assertEquals(0,rpa .read(ctx, t, "field3").getValue()); assertEquals(false,rpa.read(ctx, t, "property4").getValue()); assertTrue(rpa.canRead(ctx, t, "property4")); // repro SPR-9123, ReflectivePropertyAccessor JavaBean property names compliance tests assertEquals("iD",rpa.read(ctx, t, "iD").getValue()); assertTrue(rpa.canRead(ctx, t, "iD")); assertEquals("id",rpa.read(ctx, t, "id").getValue()); assertTrue(rpa.canRead(ctx, t, "id")); assertEquals("ID",rpa.read(ctx, t, "ID").getValue()); assertTrue(rpa.canRead(ctx, t, "ID")); // note: "Id" is not a valid JavaBean name, nevertheless it is treated as "id" assertEquals("id",rpa.read(ctx, t, "Id").getValue()); assertTrue(rpa.canRead(ctx, t, "Id")); // repro SPR-10994 assertEquals("xyZ",rpa.read(ctx, t, "xyZ").getValue()); assertTrue(rpa.canRead(ctx, t, "xyZ")); assertEquals("xY",rpa.read(ctx, t, "xY").getValue()); assertTrue(rpa.canRead(ctx, t, "xY")); // SPR-10122, ReflectivePropertyAccessor JavaBean property names compliance tests - setters rpa.write(ctx, t, "pEBS", "Test String"); assertEquals("Test String",rpa.read(ctx, t, "pEBS").getValue()); } @Test public void testOptimalReflectivePropertyAccessor() throws Exception { ReflectivePropertyAccessor rpa = new ReflectivePropertyAccessor(); Tester t = new Tester(); t.setProperty("hello"); EvaluationContext ctx = new StandardEvaluationContext(t); assertTrue(rpa.canRead(ctx, t, "property")); assertEquals("hello", rpa.read(ctx, t, "property").getValue()); assertEquals("hello", rpa.read(ctx, t, "property").getValue()); // cached accessor used PropertyAccessor optA = rpa.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 = rpa.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 } } /** * 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); } static class Super { } static class Sub extends Super { } static class Unconvertable { } 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; } } }