/* * 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.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; 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.AccessException; import org.springframework.expression.BeanResolver; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionInvocationTargetException; import org.springframework.expression.MethodExecutor; import org.springframework.expression.MethodFilter; import org.springframework.expression.MethodResolver; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.testresources.PlaceOfBirth; import static org.junit.Assert.*; /** * Tests invocation of methods. * * @author Andy Clement * @author Phillip Webb */ public class MethodInvocationTests extends AbstractExpressionTests { @Test public void testSimpleAccess01() { evaluate("getPlaceOfBirth().getCity()", "SmilJan", String.class); } @Test public void testStringClass() { evaluate("new java.lang.String('hello').charAt(2)", 'l', Character.class); evaluate("new java.lang.String('hello').charAt(2).equals('l'.charAt(0))", true, Boolean.class); evaluate("'HELLO'.toLowerCase()", "hello", String.class); evaluate("' abcba '.trim()", "abcba", String.class); } @Test public void testNonExistentMethods() { // name is ok but madeup() does not exist evaluateAndCheckError("name.madeup()", SpelMessage.METHOD_NOT_FOUND, 5); } @Test public void testWidening01() { // widening of int 3 to double 3 is OK evaluate("new Double(3.0d).compareTo(8)", -1, Integer.class); evaluate("new Double(3.0d).compareTo(3)", 0, Integer.class); evaluate("new Double(3.0d).compareTo(2)", 1, Integer.class); } @Test public void testArgumentConversion01() { // Rely on Double>String conversion for calling startsWith() evaluate("new String('hello 2.0 to you').startsWith(7.0d)", false, Boolean.class); evaluate("new String('7.0 foobar').startsWith(7.0d)", true, Boolean.class); } @Test public void testMethodThrowingException_SPR6760() { // Test method on inventor: throwException() // On 1 it will throw an IllegalArgumentException // On 2 it will throw a RuntimeException // On 3 it will exit normally // In each case it increments the Inventor field 'counter' when invoked SpelExpressionParser parser = new SpelExpressionParser(); Expression expr = parser.parseExpression("throwException(#bar)"); // Normal exit StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); eContext.setVariable("bar", 3); Object o = expr.getValue(eContext); assertEquals(o, 3); assertEquals(1, parser.parseExpression("counter").getValue(eContext)); // Now the expression has cached that throwException(int) is the right thing to call // Let's change 'bar' to be a PlaceOfBirth which indicates the cached reference is // out of date. eContext.setVariable("bar", new PlaceOfBirth("London")); o = expr.getValue(eContext); assertEquals("London", o); // That confirms the logic to mark the cached reference stale and retry is working // Now let's cause the method to exit via exception and ensure it doesn't cause a retry. // First, switch back to throwException(int) eContext.setVariable("bar", 3); o = expr.getValue(eContext); assertEquals(3, o); assertEquals(2, parser.parseExpression("counter").getValue(eContext)); // Now cause it to throw an exception: eContext.setVariable("bar", 1); try { o = expr.getValue(eContext); fail(); } catch (Exception ex) { if (ex instanceof SpelEvaluationException) { fail("Should not be a SpelEvaluationException: " + ex); } // normal } // If counter is 4 then the method got called twice! assertEquals(3, parser.parseExpression("counter").getValue(eContext)); eContext.setVariable("bar", 4); try { o = expr.getValue(eContext); fail(); } catch (Exception ex) { // 4 means it will throw a checked exception - this will be wrapped if (!(ex instanceof ExpressionInvocationTargetException)) { fail("Should have been wrapped: " + ex); } // normal } // If counter is 5 then the method got called twice! assertEquals(4, parser.parseExpression("counter").getValue(eContext)); } /** * Check on first usage (when the cachedExecutor in MethodReference is null) that the exception is not wrapped. */ @Test public void testMethodThrowingException_SPR6941() { // Test method on inventor: throwException() // On 1 it will throw an IllegalArgumentException // On 2 it will throw a RuntimeException // On 3 it will exit normally // In each case it increments the Inventor field 'counter' when invoked SpelExpressionParser parser = new SpelExpressionParser(); Expression expr = parser.parseExpression("throwException(#bar)"); eContext.setVariable("bar", 2); try { expr.getValue(eContext); fail(); } catch (Exception ex) { if (ex instanceof SpelEvaluationException) { fail("Should not be a SpelEvaluationException: " + ex); } // normal } } @Test public void testMethodThrowingException_SPR6941_2() { // Test method on inventor: throwException() // On 1 it will throw an IllegalArgumentException // On 2 it will throw a RuntimeException // On 3 it will exit normally // In each case it increments the Inventor field 'counter' when invoked SpelExpressionParser parser = new SpelExpressionParser(); Expression expr = parser.parseExpression("throwException(#bar)"); eContext.setVariable("bar", 4); try { expr.getValue(eContext); fail(); } catch (ExpressionInvocationTargetException ex) { Throwable cause = ex.getCause(); assertEquals("org.springframework.expression.spel.testresources.Inventor$TestException", cause.getClass().getName()); } } @Test public void testMethodFiltering_SPR6764() { SpelExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setRootObject(new TestObject()); LocalFilter filter = new LocalFilter(); context.registerMethodFilter(TestObject.class,filter); // Filter will be called but not do anything, so first doit() will be invoked SpelExpression expr = (SpelExpression) parser.parseExpression("doit(1)"); String result = expr.getValue(context, String.class); assertEquals("1", result); assertTrue(filter.filterCalled); // Filter will now remove non @Anno annotated methods filter.removeIfNotAnnotated = true; filter.filterCalled = false; expr = (SpelExpression) parser.parseExpression("doit(1)"); result = expr.getValue(context, String.class); assertEquals("double 1.0", result); assertTrue(filter.filterCalled); // check not called for other types filter.filterCalled = false; context.setRootObject(new String("abc")); expr = (SpelExpression) parser.parseExpression("charAt(0)"); result = expr.getValue(context, String.class); assertEquals("a", result); assertFalse(filter.filterCalled); // check de-registration works filter.filterCalled = false; context.registerMethodFilter(TestObject.class,null);//clear filter context.setRootObject(new TestObject()); expr = (SpelExpression) parser.parseExpression("doit(1)"); result = expr.getValue(context, String.class); assertEquals("1", result); assertFalse(filter.filterCalled); } @Test public void testAddingMethodResolvers() { StandardEvaluationContext ctx = new StandardEvaluationContext(); // reflective method accessor is the only one by default List<MethodResolver> methodResolvers = ctx.getMethodResolvers(); assertEquals(1, methodResolvers.size()); MethodResolver dummy = new DummyMethodResolver(); ctx.addMethodResolver(dummy); assertEquals(2, ctx.getMethodResolvers().size()); List<MethodResolver> copy = new ArrayList<>(); copy.addAll(ctx.getMethodResolvers()); assertTrue(ctx.removeMethodResolver(dummy)); assertFalse(ctx.removeMethodResolver(dummy)); assertEquals(1, ctx.getMethodResolvers().size()); ctx.setMethodResolvers(copy); assertEquals(2, ctx.getMethodResolvers().size()); } @Test public void testVarargsInvocation01() { // Calling 'public int aVarargsMethod(String... strings)' //evaluate("aVarargsMethod('a','b','c')", 3, Integer.class); //evaluate("aVarargsMethod('a')", 1, Integer.class); evaluate("aVarargsMethod()", 0, Integer.class); evaluate("aVarargsMethod(1,2,3)", 3, Integer.class); // all need converting to strings evaluate("aVarargsMethod(1)", 1, Integer.class); // needs string conversion evaluate("aVarargsMethod(1,'a',3.0d)", 3, Integer.class); // first and last need conversion // evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class); } @Test public void testVarargsInvocation02() { // Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int+length_of_strings evaluate("aVarargsMethod2(5,'a','b','c')", 8, Integer.class); evaluate("aVarargsMethod2(2,'a')", 3, Integer.class); evaluate("aVarargsMethod2(4)", 4, Integer.class); evaluate("aVarargsMethod2(8,2,3)", 10, Integer.class); evaluate("aVarargsMethod2(9)", 9, Integer.class); evaluate("aVarargsMethod2(2,'a',3.0d)", 4, Integer.class); // evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class); } @Test public void testInvocationOnNullContextObject() { evaluateAndCheckError("null.toString()",SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED); } @Test public void testMethodOfClass() throws Exception { Expression expression = parser.parseExpression("getName()"); Object value = expression.getValue(new StandardEvaluationContext(String.class)); assertEquals(value, "java.lang.String"); } @Test public void invokeMethodWithoutConversion() throws Exception { final BytesService service = new BytesService(); byte[] bytes = new byte[100]; StandardEvaluationContext context = new StandardEvaluationContext(bytes); context.setBeanResolver(new BeanResolver() { @Override public Object resolve(EvaluationContext context, String beanName) throws AccessException { if ("service".equals(beanName)) { return service; } return null; } }); Expression expression = parser.parseExpression("@service.handleBytes(#root)"); byte[] outBytes = expression.getValue(context, byte[].class); assertSame(bytes, outBytes); } // Simple filter static class LocalFilter implements MethodFilter { public boolean removeIfNotAnnotated = false; public boolean filterCalled = false; private boolean isAnnotated(Method method) { Annotation[] anns = method.getAnnotations(); if (anns == null) { return false; } for (Annotation ann : anns) { String name = ann.annotationType().getName(); if (name.endsWith("Anno")) { return true; } } return false; } @Override public List<Method> filter(List<Method> methods) { filterCalled = true; List<Method> forRemoval = new ArrayList<>(); for (Method method: methods) { if (removeIfNotAnnotated && !isAnnotated(method)) { forRemoval.add(method); } } for (Method method: forRemoval) { methods.remove(method); } return methods; } } @Retention(RetentionPolicy.RUNTIME) @interface Anno { } class TestObject { public int doit(int i) { return i; } @Anno public String doit(double d) { return "double "+d; } } static class DummyMethodResolver implements MethodResolver { @Override public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, List<TypeDescriptor> argumentTypes) throws AccessException { throw new UnsupportedOperationException(); } } public static class BytesService { public byte[] handleBytes(byte[] bytes) { return bytes; } } }