diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc index c133af0f36..ca5d9c28d0 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc @@ -9,7 +9,7 @@ SpEL supports the following types of literal expressions. - boolean values: `true` or `false` - null -Strings can delimited by single quotation marks (`'`) or double quotation marks (`"`). To +Strings can be delimited by single quotation marks (`'`) or double quotation marks (`"`). To include a single quotation mark within a string literal enclosed in single quotation marks, use two adjacent single quotation mark characters. Similarly, to include a double quotation mark within a string literal enclosed in double quotation marks, use two diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java index c7e0fd355c..5af02af1ee 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 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. @@ -16,32 +16,37 @@ package org.springframework.expression; - -// TODO Is the resolver/executor model too pervasive in this package? /** - * Executors are built by resolvers and can be cached by the infrastructure to repeat an - * operation quickly without going back to the resolvers. For example, the particular - * constructor to run on a class may be discovered by the reflection constructor resolver - * - it will then build a ConstructorExecutor that executes that constructor and the - * ConstructorExecutor can be reused without needing to go back to the resolver to - * discover the constructor again. + * A {@code ConstructorExecutor} is built by a {@link ConstructorResolver} and + * can be cached by the infrastructure to repeat an operation quickly without + * going back to the resolvers. * - *

They can become stale, and in that case should throw an AccessException - this will - * cause the infrastructure to go back to the resolvers to ask for a new one. + *

For example, the particular constructor to execute on a class may be discovered + * by a {@code ConstructorResolver} which then builds a {@code ConstructorExecutor} + * that executes that constructor, and the resolved {@code ConstructorExecutor} + * can be reused without needing to go back to the resolvers to discover the + * constructor again. + * + *

If a {@code ConstructorExecutor} becomes stale, it should throw an + * {@link AccessException} which signals to the infrastructure to go back to the + * resolvers to ask for a new one. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see ConstructorResolver + * @see MethodExecutor */ public interface ConstructorExecutor { /** * Execute a constructor in the specified context using the specified arguments. - * @param context the evaluation context in which the command is being executed - * @param arguments the arguments to the constructor call, should match (in terms - * of number and type) whatever the command will need to run + * @param context the evaluation context in which the constructor is being executed + * @param arguments the arguments to the constructor; should match (in terms + * of number and type) whatever the constructor will need to run * @return the new object - * @throws AccessException if there is a problem executing the command or the - * CommandExecutor is no longer valid + * @throws AccessException if there is a problem executing the constructor or + * if this {@code ConstructorExecutor} has become stale */ TypedValue execute(EvaluationContext context, Object... arguments) throws AccessException; diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java index 821a46c1bd..3c9c232c3e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -22,24 +22,33 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; /** - * A constructor resolver attempts to locate a constructor and returns a ConstructorExecutor - * that can be used to invoke that constructor. The ConstructorExecutor will be cached but - * if it 'goes stale' the resolvers will be called again. + * A constructor resolver attempts to locate a constructor and returns a + * {@link ConstructorExecutor} that can be used to invoke that constructor. + * + *

The {@code ConstructorExecutor} will be cached, but if it becomes stale the + * resolvers will be called again. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see ConstructorExecutor + * @see MethodResolver */ @FunctionalInterface public interface ConstructorResolver { /** - * Within the supplied context determine a suitable constructor on the supplied type - * that can handle the specified arguments. Return a ConstructorExecutor that can be - * used to invoke that constructor (or {@code null} if no constructor could be found). + * Within the supplied context, resolve a suitable constructor on the + * supplied type that can handle the specified arguments. + *

Returns a {@link ConstructorExecutor} that can be used to invoke that + * constructor (or {@code null} if no constructor could be found). * @param context the current evaluation context - * @param typeName the type upon which to look for the constructor - * @param argumentTypes the arguments that the constructor must be able to handle - * @return a ConstructorExecutor that can invoke the constructor, or null if non found + * @param typeName the fully-qualified name of the type upon which to look + * for the constructor + * @param argumentTypes the types of arguments that the constructor must be + * able to handle + * @return a {@code ConstructorExecutor} that can invoke the constructor, + * or {@code null} if the constructor cannot be found */ @Nullable ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java index 3c0b44d9d9..170914e456 100644 --- a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 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. @@ -17,30 +17,37 @@ package org.springframework.expression; /** - * MethodExecutors are built by the resolvers and can be cached by the infrastructure to - * repeat an operation quickly without going back to the resolvers. For example, the - * particular method to run on an object may be discovered by the reflection method - * resolver - it will then build a MethodExecutor that executes that method and the - * MethodExecutor can be reused without needing to go back to the resolver to discover - * the method again. + * A {@code MethodExecutor} is built by a {@link MethodResolver} and can be cached + * by the infrastructure to repeat an operation quickly without going back to the + * resolvers. * - *

They can become stale, and in that case should throw an AccessException: - * This will cause the infrastructure to go back to the resolvers to ask for a new one. + *

For example, the particular method to execute on an object may be discovered + * by a {@code MethodResolver} which then builds a {@code MethodExecutor} that + * executes that method, and the resolved {@code MethodExecutor} can be reused + * without needing to go back to the resolvers to discover the method again. + * + *

If a {@code MethodExecutor} becomes stale, it should throw an + * {@link AccessException} which signals to the infrastructure to go back to the + * resolvers to ask for a new one. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see MethodResolver + * @see ConstructorExecutor */ public interface MethodExecutor { /** - * Execute a command using the specified arguments, and using the specified expression state. - * @param context the evaluation context in which the command is being executed - * @param target the target object of the call - null for static methods - * @param arguments the arguments to the executor, should match (in terms of number - * and type) whatever the command will need to run - * @return the value returned from execution - * @throws AccessException if there is a problem executing the command or the - * MethodExecutor is no longer valid + * Execute a method in the specified context using the specified arguments. + * @param context the evaluation context in which the method is being executed + * @param target the target of the method invocation; may be {@code null} for + * {@code static} methods + * @param arguments the arguments to the method; should match (in terms of + * number and type) whatever the method will need to run + * @return the value returned from the method + * @throws AccessException if there is a problem executing the method or + * if this {@code MethodExecutor} has become stale */ TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException; diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java index d4216d2a55..9d8c0bbaf0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -22,23 +22,32 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; /** - * A method resolver attempts to locate a method and returns a command executor that can be - * used to invoke that method. The command executor will be cached, but if it 'goes stale' - * the resolvers will be called again. + * A method resolver attempts to locate a method and returns a + * {@link MethodExecutor} that can be used to invoke that method. + * + *

The {@code MethodExecutor} will be cached, but if it becomes stale the + * resolvers will be called again. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see MethodExecutor + * @see ConstructorResolver */ public interface MethodResolver { /** - * Within the supplied context determine a suitable method on the supplied object that - * can handle the specified arguments. Return a {@link MethodExecutor} that can be used - * to invoke that method, or {@code null} if no method could be found. + * Within the supplied context, resolve a suitable method on the supplied + * object that can handle the specified arguments. + *

Returns a {@link MethodExecutor} that can be used to invoke that method, + * or {@code null} if no method could be found. * @param context the current evaluation context * @param targetObject the object upon which the method is being called - * @param argumentTypes the arguments that the constructor must be able to handle - * @return a MethodExecutor that can invoke the method, or null if the method cannot be found + * @param name the name of the method + * @param argumentTypes the types of arguments that the method must be able + * to handle + * @return a {@code MethodExecutor} that can invoke the method, or {@code null} + * if the method cannot be found */ @Nullable MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java index 72aa836588..abc80b0370 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java @@ -21,13 +21,12 @@ import java.util.List; import org.junit.jupiter.api.Test; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.expression.ConstructorExecutor; import org.springframework.expression.ConstructorResolver; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.expression.spel.testresources.Fruit; import org.springframework.expression.spel.testresources.PlaceOfBirth; import static org.assertj.core.api.Assertions.assertThat; @@ -41,19 +40,145 @@ import static org.assertj.core.api.Assertions.assertThatException; class ConstructorInvocationTests extends AbstractExpressionTests { @Test - void testTypeConstructors() { + void constructorWithArgument() { evaluate("new String('hello world')", "hello world", String.class); } @Test - void testNonExistentType() { + void nonExistentType() { evaluateAndCheckError("new FooBar()", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM); } + @Test + void constructorThrowingException() { + // Test ctor on inventor: + // 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 Tester field 'counter' when invoked + + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("new org.springframework.expression.spel.ConstructorInvocationTests$Tester(#bar).i"); + + // Normal exit + StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + context.setRootObject(new Tester()); + context.setVariable("bar", 3); + Object o = expr.getValue(context); + assertThat(o).isEqualTo(3); + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(1); + + // 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. + context.setVariable("bar", new PlaceOfBirth("London")); + o = expr.getValue(context); + assertThat(o).isEqualTo(0); + // 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) + context.setVariable("bar", 3); + o = expr.getValue(context); + assertThat(o).isEqualTo(3); + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(2); + + // 4 will make it throw a checked exception - this will be wrapped by spel on the + // way out + context.setVariable("bar", 4); + assertThatException() + .isThrownBy(() -> expr.getValue(context)) + .withMessageContaining("Tester"); + // A problem occurred whilst attempting to construct an object of type + // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' + // using arguments '(java.lang.Integer)' + + // If counter is 4 then the method got called twice! + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(3); + + // 1 will make it throw a RuntimeException - SpEL will let this through + context.setVariable("bar", 1); + assertThatException() + .isThrownBy(() -> expr.getValue(context)) + .isNotInstanceOf(SpelEvaluationException.class); + // A problem occurred whilst attempting to construct an object of type + // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' + // using arguments '(java.lang.Integer)' + + // If counter is 5 then the method got called twice! + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(4); + } + + @Test + void constructorResolvers() { + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + // reflective constructor accessor is the only one by default + List constructorResolvers = ctx.getConstructorResolvers(); + assertThat(constructorResolvers).hasSize(1); + + ConstructorResolver dummy = (context, typeName, argumentTypes) -> { + throw new UnsupportedOperationException(); + }; + ctx.addConstructorResolver(dummy); + assertThat(ctx.getConstructorResolvers()).hasSize(2); + + List copy = new ArrayList<>(ctx.getConstructorResolvers()); + assertThat(ctx.removeConstructorResolver(dummy)).isTrue(); + assertThat(ctx.removeConstructorResolver(dummy)).isFalse(); + assertThat(ctx.getConstructorResolvers()).hasSize(1); + + ctx.setConstructorResolvers(copy); + assertThat(ctx.getConstructorResolvers()).hasSize(2); + } + + @Test + void varargsConstructors() { + ((StandardTypeLocator) super.context.getTypeLocator()).registerImport(Fruit.class.getPackageName()); + + // Calling 'Fruit(String... strings)' - returns length_of_strings + evaluate("new Fruit('a','b','c').stringscount()", 3, Integer.class); + evaluate("new Fruit('a').stringscount()", 1, Integer.class); + evaluate("new Fruit().stringscount()", 0, Integer.class); + // all need converting to strings + evaluate("new Fruit(1,2,3).stringscount()", 3, Integer.class); + // needs string conversion + evaluate("new Fruit(1).stringscount()", 1, Integer.class); + // first and last need conversion + evaluate("new Fruit(1,'a',3.0d).stringscount()", 3, Integer.class); + + // Calling 'Fruit(int i, String... strings)' - returns int + length_of_strings + evaluate("new Fruit(5,'a','b','c').stringscount()", 8, Integer.class); + evaluate("new Fruit(2,'a').stringscount()", 3, Integer.class); + evaluate("new Fruit(4).stringscount()", 4, Integer.class); + evaluate("new Fruit(8,2,3).stringscount()", 10, Integer.class); + evaluate("new Fruit(9).stringscount()", 9, Integer.class); + evaluate("new Fruit(2,'a',3.0d).stringscount()", 4, Integer.class); + evaluate("new Fruit(8,stringArrayOfThreeItems).stringscount()", 11, Integer.class); + } + + /* + * These tests are attempting to call constructors where we need to widen or convert + * the argument in order to satisfy a suitable constructor. + */ + @Test + void widening() { + // widening of int 3 to double 3 is OK + evaluate("new Double(3)", 3.0d, Double.class); + // widening of int 3 to long 3 is OK + evaluate("new Long(3)", 3L, Long.class); + } + + @Test + void argumentConversion() { + evaluate("new String(3.0d)", "3.0", String.class); + } + @SuppressWarnings("serial") static class TestException extends Exception { - } static class Tester { @@ -80,154 +205,7 @@ class ConstructorInvocationTests extends AbstractExpressionTests { } public Tester(PlaceOfBirth pob) { - } - - } - - - @Test - void testConstructorThrowingException_SPR6760() { - // Test ctor on inventor: - // 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 Tester field 'counter' when invoked - - SpelExpressionParser parser = new SpelExpressionParser(); - Expression expr = parser.parseExpression("new org.springframework.expression.spel.ConstructorInvocationTests$Tester(#bar).i"); - - // Normal exit - StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); - eContext.setRootObject(new Tester()); - eContext.setVariable("bar", 3); - Object o = expr.getValue(eContext); - assertThat(o).isEqualTo(3); - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(1); - - // 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); - assertThat(o).isEqualTo(0); - // 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); - assertThat(o).isEqualTo(3); - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(2); - - // 4 will make it throw a checked exception - this will be wrapped by spel on the - // way out - eContext.setVariable("bar", 4); - assertThatException() - .isThrownBy(() -> expr.getValue(eContext)) - .withMessageContaining("Tester"); - // A problem occurred whilst attempting to construct an object of type - // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' - // using arguments '(java.lang.Integer)' - - // If counter is 4 then the method got called twice! - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(3); - - // 1 will make it throw a RuntimeException - SpEL will let this through - eContext.setVariable("bar", 1); - assertThatException() - .isThrownBy(() -> expr.getValue(eContext)) - .isNotInstanceOf(SpelEvaluationException.class); - // A problem occurred whilst attempting to construct an object of type - // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' - // using arguments '(java.lang.Integer)' - - // If counter is 5 then the method got called twice! - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(4); - } - - @Test - void testAddingConstructorResolvers() { - StandardEvaluationContext ctx = new StandardEvaluationContext(); - - // reflective constructor accessor is the only one by default - List constructorResolvers = ctx.getConstructorResolvers(); - assertThat(constructorResolvers).hasSize(1); - - ConstructorResolver dummy = new DummyConstructorResolver(); - ctx.addConstructorResolver(dummy); - assertThat(ctx.getConstructorResolvers()).hasSize(2); - - List copy = new ArrayList<>(ctx.getConstructorResolvers()); - assertThat(ctx.removeConstructorResolver(dummy)).isTrue(); - assertThat(ctx.removeConstructorResolver(dummy)).isFalse(); - assertThat(ctx.getConstructorResolvers()).hasSize(1); - - ctx.setConstructorResolvers(copy); - assertThat(ctx.getConstructorResolvers()).hasSize(2); - } - - - static class DummyConstructorResolver implements ConstructorResolver { - - @Override - public ConstructorExecutor resolve(EvaluationContext context, String typeName, - List argumentTypes) { - throw new UnsupportedOperationException("Auto-generated method stub"); - } - - } - - - @Test - void testVarargsInvocation01() { - // Calling 'Fruit(String... strings)' - evaluate("new org.springframework.expression.spel.testresources.Fruit('a','b','c').stringscount()", 3, - Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit('a').stringscount()", 1, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit().stringscount()", 0, Integer.class); - // all need converting to strings - evaluate("new org.springframework.expression.spel.testresources.Fruit(1,2,3).stringscount()", 3, Integer.class); - // needs string conversion - evaluate("new org.springframework.expression.spel.testresources.Fruit(1).stringscount()", 1, Integer.class); - // first and last need conversion - evaluate("new org.springframework.expression.spel.testresources.Fruit(1,'a',3.0d).stringscount()", 3, - Integer.class); - } - - @Test - void testVarargsInvocation02() { - // Calling 'Fruit(int i, String... strings)' - returns int+length_of_strings - evaluate("new org.springframework.expression.spel.testresources.Fruit(5,'a','b','c').stringscount()", 8, - Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a').stringscount()", 3, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(4).stringscount()", 4, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(8,2,3).stringscount()", 10, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(9).stringscount()", 9, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a',3.0d).stringscount()", 4, - Integer.class); - evaluate( - "new org.springframework.expression.spel.testresources.Fruit(8,stringArrayOfThreeItems).stringscount()", - 11, Integer.class); - } - - /* - * These tests are attempting to call constructors where we need to widen or convert - * the argument in order to satisfy a suitable constructor. - */ - @Test - void testWidening01() { - // widening of int 3 to double 3 is OK - evaluate("new Double(3)", 3.0d, Double.class); - // widening of int 3 to long 3 is OK - evaluate("new Long(3)", 3L, Long.class); - } - - @Test - void testArgumentConversion01() { - evaluate("new String(3.0d)", "3.0", String.class); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/LiteralTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralTests.java index 729c09aa9d..abbfd936df 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/LiteralTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralTests.java @@ -19,83 +19,78 @@ package org.springframework.expression.spel; import org.junit.jupiter.api.Test; import org.springframework.expression.spel.standard.SpelExpression; -import org.springframework.expression.spel.support.StandardEvaluationContext; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests the evaluation of basic literals: boolean, integer, hex integer, long, real, null, date + * Tests the evaluation of basic literals: boolean, string, integer, long, + * hex integer, hex long, float, double, null. * * @author Andy Clement + * @author Sam Brannen */ class LiteralTests extends AbstractExpressionTests { @Test - void testLiteralBoolean01() { - evaluate("false", "false", Boolean.class); + void booleans() { + evaluate("false", false, Boolean.class); + evaluate("true", true, Boolean.class); } @Test - void testLiteralBoolean02() { - evaluate("true", "true", Boolean.class); - } - - @Test - void testLiteralInteger01() { - evaluate("1", "1", Integer.class); - } - - @Test - void testLiteralInteger02() { - evaluate("1415", "1415", Integer.class); - } - - @Test - void testLiteralString01() { + void strings() { + evaluate("'hello'", "hello", String.class); + evaluate("'joe bloggs'", "joe bloggs", String.class); evaluate("'Hello World'", "Hello World", String.class); } @Test - void testLiteralString02() { - evaluate("'joe bloggs'", "joe bloggs", String.class); - } - - @Test - void testLiteralString03() { - evaluate("'hello'", "hello", String.class); - } - - @Test - void testLiteralString04() { + void stringsContainingQuotes() { evaluate("'Tony''s Pizza'", "Tony's Pizza", String.class); evaluate("'Tony\\r''s Pizza'", "Tony\\r's Pizza", String.class); - } - - @Test - void testLiteralString05() { evaluate("\"Hello World\"", "Hello World", String.class); - } - - @Test - void testLiteralString06() { evaluate("\"Hello ' World\"", "Hello ' World", String.class); } @Test - void testHexIntLiteral01() { - evaluate("0x7FFFF", "524287", Integer.class); - evaluate("0x7FFFFL", 524287L, Long.class); - evaluate("0X7FFFF", "524287", Integer.class); - evaluate("0X7FFFFl", 524287L, Long.class); + void integers() { + evaluate("1", 1, Integer.class); + evaluate("1415", 1415, Integer.class); } @Test - void testLongIntLiteral01() { + void longs() { + evaluate("1L", 1L, Long.class); + evaluate("1415L", 1415L, Long.class); + } + + @Test + void signedIntegers() { + evaluate("-1", -1, Integer.class); + evaluate("-0xa", -10, Integer.class); + } + + @Test + void signedLongs() { + evaluate("-1L", -1L, Long.class); + evaluate("-0x20l", -32L, Long.class); + } + + @Test + void hexIntegers() { + evaluate("0x7FFFF", 524287, Integer.class); + evaluate("0X7FFFF", 524287, Integer.class); + } + + @Test + void hexLongs() { + evaluate("0X7FFFFl", 524287L, Long.class); + evaluate("0x7FFFFL", 524287L, Long.class); evaluate("0xCAFEBABEL", 3405691582L, Long.class); } @Test - void testLongIntInteractions01() { + void hexLongAndIntInteractions() { evaluate("0x20 * 2L", 64L, Long.class); // ask for the result to be made into an Integer evaluateAndAskForReturnType("0x20 * 2L", 64, Integer.class); @@ -104,29 +99,8 @@ class LiteralTests extends AbstractExpressionTests { } @Test - void testSignedIntLiterals() { - evaluate("-1", -1, Integer.class); - evaluate("-0xa", -10, Integer.class); - evaluate("-1L", -1L, Long.class); - evaluate("-0x20l", -32L, Long.class); - } - - @Test - void testLiteralReal01_CreatingDoubles() { - evaluate("1.25", 1.25d, Double.class); - evaluate("2.99", 2.99d, Double.class); - evaluate("-3.141", -3.141d, Double.class); - evaluate("1.25d", 1.25d, Double.class); - evaluate("2.99d", 2.99d, Double.class); - evaluate("-3.141d", -3.141d, Double.class); - evaluate("1.25D", 1.25d, Double.class); - evaluate("2.99D", 2.99d, Double.class); - evaluate("-3.141D", -3.141d, Double.class); - } - - @Test - void testLiteralReal02_CreatingFloats() { - // For now, everything becomes a double... + void floats() { + // "f" or "F" must be explicitly specified. evaluate("1.25f", 1.25f, Float.class); evaluate("2.5f", 2.5f, Float.class); evaluate("-3.5f", -3.5f, Float.class); @@ -136,7 +110,23 @@ class LiteralTests extends AbstractExpressionTests { } @Test - void testLiteralReal03_UsingExponents() { + void doubles() { + // Real numbers are Doubles by default + evaluate("1.25", 1.25d, Double.class); + evaluate("2.99", 2.99d, Double.class); + evaluate("-3.141", -3.141d, Double.class); + + // But "d" or "D" can also be explicitly specified. + evaluate("1.25d", 1.25d, Double.class); + evaluate("2.99d", 2.99d, Double.class); + evaluate("-3.141d", -3.141d, Double.class); + evaluate("1.25D", 1.25d, Double.class); + evaluate("2.99D", 2.99d, Double.class); + evaluate("-3.141D", -3.141d, Double.class); + } + + @Test + void doublesUsingExponents() { evaluate("6.0221415E+23", "6.0221415E23", Double.class); evaluate("6.0221415e+23", "6.0221415E23", Double.class); evaluate("6.0221415E+23d", "6.0221415E23", Double.class); @@ -145,30 +135,33 @@ class LiteralTests extends AbstractExpressionTests { } @Test - void testLiteralReal04_BadExpressions() { + void doublesUsingExponentsWithInvalidInput() { parseAndCheckError("6.1e23e22", SpelMessage.MORE_INPUT, 6, "e22"); parseAndCheckError("6.1f23e22", SpelMessage.MORE_INPUT, 4, "23e22"); } @Test - void testLiteralNull01() { + void nullLiteral() { evaluate("null", null, null); } @Test - void testConversions() { + void conversions() { // getting the expression type to be what we want - either: - evaluate("new Integer(37).byteValue()", (byte) 37, Byte.class); // calling byteValue() on Integer.class - evaluateAndAskForReturnType("new Integer(37)", (byte) 37, Byte.class); // relying on registered type converters + evaluate("37.byteValue", (byte) 37, Byte.class); // calling byteValue() on Integer.class + evaluateAndAskForReturnType("37", (byte) 37, Byte.class); // relying on registered type converters } @Test - void testNotWritable() { - SpelExpression expr = (SpelExpression)parser.parseExpression("37"); - assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse(); - expr = (SpelExpression)parser.parseExpression("37L"); - assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse(); - expr = (SpelExpression)parser.parseExpression("true"); - assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse(); + void notWritable() { + SpelExpression expr = (SpelExpression) parser.parseExpression("37"); + assertThat(expr.isWritable(context)).isFalse(); + + expr = (SpelExpression) parser.parseExpression("37L"); + assertThat(expr.isWritable(context)).isFalse(); + + expr = (SpelExpression) parser.parseExpression("true"); + assertThat(expr.isWritable(context)).isFalse(); } + } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index 5bd15b113d..975e036676 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -99,7 +99,7 @@ class ParsingTests { } @Test - void literalNull() { + void nullLiteral() { parseCheck("null"); } @@ -399,12 +399,12 @@ class ParsingTests { @Test void mathOperatorsAddStrings() { - parseCheck("'a'+'b'", "('a' + 'b')"); + parseCheck("'a' + 'b'", "('a' + 'b')"); } @Test void mathOperatorsAddMultipleStrings() { - parseCheck("'hello'+' '+'world'", "(('hello' + ' ') + 'world')"); + parseCheck("'hello' + ' ' + 'world'", "(('hello' + ' ') + 'world')"); } @Test @@ -429,7 +429,7 @@ class ParsingTests { } @Nested - class References { + class BeanReferences { @Test void references() {