From 4571626839a00c924d7e38b8d0b02cb2518345b5 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 17 Feb 2022 07:45:38 +0000 Subject: [PATCH 001/131] Next development version (v5.3.17-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ce5199c44137..c51960841a5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.16-SNAPSHOT +version=5.3.17-SNAPSHOT org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true From 2ffefbb21116dda66973b920218c79ffe13cbac8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 17 Feb 2022 09:44:22 +0100 Subject: [PATCH 002/131] Downgrade to concourse-release-scripts 0.3.2 This commit reverts partially "0ab054c7b943d65bb9034d1d7987f556e9d54d05" as 0.3.3 is breaking promition. --- ci/images/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/setup.sh b/ci/images/setup.sh index 3e1e0bc05c2d..6c02f65ef665 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -14,7 +14,7 @@ rm -rf /var/lib/apt/lists/* curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh -curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.3.3/concourse-release-scripts-0.3.3.jar +curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.3.2/concourse-release-scripts-0.3.2.jar ########################################################### # JAVA From ff20a06876209e6dafd519e732801f292230f9d9 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 17 Feb 2022 11:51:14 +0100 Subject: [PATCH 003/131] Added .sdkmanrc file This commit adds a .sdkmanrc file, so that we can automatically switch to JDK 8 when building the 5.3. branch. --- .sdkmanrc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .sdkmanrc diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 000000000000..a59545673245 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=8.0.322-librca From 94af2ca06bf711de03286999b1c64a703d6de552 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 18 Feb 2022 15:31:59 +0100 Subject: [PATCH 004/131] Recover from error during SpEL MIXED mode compilation Prior to this commit, SpEL was able to recover from an error that occurred while running a CompiledExpression; however, SpEL was not able to recover from an error that occurred while compiling the expression (such as a java.lang.VerifyError). The latter can occur when multiple threads concurrently change types involved in the expression, such as the concrete type of a custom variable registered via EvaluationContext.setVariable(...), which can result in SpEL generating invalid bytecode. This commit addresses this issue by catching exceptions thrown while compiling an expression and updating the `failedAttempts` and `interpretedCount` counters accordingly. If an exception is caught while operating in SpelCompilerMode.IMMEDIATE mode, the exception will be propagated via a SpelEvaluationException with a new SpelMessage.EXCEPTION_COMPILING_EXPRESSION error category. Closes gh-28043 --- .../expression/spel/SpelMessage.java | 11 ++++-- .../spel/standard/SpelCompiler.java | 5 ++- .../spel/standard/SpelExpression.java | 36 ++++++++++++----- .../spel/standard/SpelCompilerTests.java | 39 +++++++++++++++++++ 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index 3a03cfd9a1a4..b8f5f92d0554 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -27,10 +27,11 @@ *

When a message is formatted, it will have this kind of form, capturing the prefix * and the error kind: * - *

EL1004E: Type cannot be found 'String'
+ *
EL1005E: Type cannot be found 'String'
* * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public enum SpelMessage { @@ -255,7 +256,11 @@ public enum SpelMessage { /** @since 4.3.17 */ FLAWED_PATTERN(Kind.ERROR, 1073, - "Failed to efficiently evaluate pattern ''{0}'': consider redesigning it"); + "Failed to efficiently evaluate pattern ''{0}'': consider redesigning it"), + + /** @since 5.3.16 */ + EXCEPTION_COMPILING_EXPRESSION(Kind.ERROR, 1074, + "An exception occurred while compiling an expression"); private final Kind kind; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java index f2a225952a0d..d94c18ea5c7f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -110,7 +110,8 @@ public CompiledExpression compile(SpelNodeImpl expression) { return ReflectionUtils.accessibleConstructor(clazz).newInstance(); } catch (Throwable ex) { - throw new IllegalStateException("Failed to instantiate CompiledExpression", ex); + throw new IllegalStateException("Failed to instantiate CompiledExpression for expression: " + + expression.toStringAST(), ex); } } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java index 86ed383c3892..660fb23ddc11 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -44,6 +44,7 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class SpelExpression implements Expression { @@ -522,17 +523,34 @@ public boolean compileExpression() { // Compiled by another thread before this thread got into the sync block return true; } - SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader()); - compiledAst = compiler.compile(this.ast); - if (compiledAst != null) { - // Successfully compiled - this.compiledAst = compiledAst; - return true; + try { + SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader()); + compiledAst = compiler.compile(this.ast); + if (compiledAst != null) { + // Successfully compiled + this.compiledAst = compiledAst; + return true; + } + else { + // Failed to compile + this.failedAttempts.incrementAndGet(); + return false; + } } - else { + catch (Exception ex) { // Failed to compile this.failedAttempts.incrementAndGet(); - return false; + + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + return false; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_COMPILING_EXPRESSION); + } } } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java index 5046f58189aa..a3ee9cb08355 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java @@ -76,6 +76,21 @@ void defaultMethodInvocation() { assertThat(expression.getValue(context)).asInstanceOf(BOOLEAN).isTrue(); } + @Test // gh-28043 + void changingRegisteredVariableTypeDoesNotResultInFailureInMixedMode() { + SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.MIXED, null); + SpelExpressionParser parser = new SpelExpressionParser(config); + Expression sharedExpression = parser.parseExpression("#bean.value"); + StandardEvaluationContext context = new StandardEvaluationContext(); + + Object[] beans = new Object[] {new Bean1(), new Bean2(), new Bean3(), new Bean4()}; + + IntStream.rangeClosed(1, 1_000_000).parallel().forEach(count -> { + context.setVariable("bean", beans[count % 4]); + assertThat(sharedExpression.getValue(context)).asString().startsWith("1"); + }); + } + static class OrderedComponent implements Ordered { @@ -121,4 +136,28 @@ default boolean isEditable2() { boolean hasSomeProperty(); } + public static class Bean1 { + public String getValue() { + return "11"; + } + } + + public static class Bean2 { + public Integer getValue() { + return 111; + } + } + + public static class Bean3 { + public Float getValue() { + return 1.23f; + } + } + + public static class Bean4 { + public Character getValue() { + return '1'; + } + } + } From 071c2988d55b1ad823b79fd4be8908666fc466a8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 18 Feb 2022 16:18:13 +0100 Subject: [PATCH 005/131] Suppress deprecation warnings in tests in build --- .../orm/jpa/support/OpenEntityManagerInViewTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java index 70360c26dea6..6d8c3157bf46 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -419,6 +419,7 @@ private static class TestTaskExecutor extends SimpleAsyncTaskExecutor { private final CountDownLatch latch = new CountDownLatch(1); @Override + @SuppressWarnings("deprecation") public void execute(Runnable task, long startTimeout) { Runnable decoratedTask = () -> { try { From 5689395678f57fe967a3b21ed7d9087cfec7b622 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 19 Feb 2022 16:51:00 +0100 Subject: [PATCH 006/131] Deprecate "enclosing classes" search strategy for MergedAnnotations The TYPE_HIERARCHY_AND_ENCLOSING_CLASSES search strategy for MergedAnnotations was originally introduced to support @Nested test classes in JUnit Jupiter (see #23378). However, while implementing #19930, we determined that the TYPE_HIERARCHY_AND_ENCLOSING_CLASSES search strategy unfortunately could not be used since it does not allow the user to control when to recurse up the enclosing class hierarchy. For example, this search strategy will automatically search on enclosing classes for static nested classes as well as for inner classes, when the user probably only wants one such category of "enclosing class" to be searched. Consequently, TestContextAnnotationUtils was introduced in the Spring TestContext Framework to address the shortcomings of the TYPE_HIERARCHY_AND_ENCLOSING_CLASSES search strategy. Since this search strategy is unlikely to be useful to general users, the team has decided to deprecate this search strategy in Spring Framework 5.3.x and remove it in 6.0. Closes gh-28079 --- .../springframework/core/annotation/AnnotationsScanner.java | 5 ++++- .../springframework/core/annotation/MergedAnnotations.java | 2 ++ .../core/annotation/AnnotationsScannerTests.java | 5 ++++- .../core/annotation/MergedAnnotationsTests.java | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index 626413838c9b..70efaeea41c7 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -92,6 +92,7 @@ private static R process(C context, AnnotatedElement source, } @Nullable + @SuppressWarnings("deprecation") private static R processClass(C context, Class source, SearchStrategy searchStrategy, AnnotationsProcessor processor) { @@ -235,6 +236,7 @@ private static R processClassHierarchy(C context, int[] aggregateIndex, C } @Nullable + @SuppressWarnings("deprecation") private static R processMethod(C context, Method source, SearchStrategy searchStrategy, AnnotationsProcessor processor) { @@ -510,6 +512,7 @@ static boolean hasPlainJavaAnnotationsOnly(Class type) { return (type.getName().startsWith("java.") || type == Ordered.class); } + @SuppressWarnings("deprecation") private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy) { if (source == Object.class) { return true; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java index 55dff9086f74..28f7cf009a99 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -482,7 +482,9 @@ enum SearchStrategy { * need to be meta-annotated with {@link Inherited @Inherited}. When * searching a {@link Method} source, this strategy is identical to * {@link #TYPE_HIERARCHY}. + * @deprecated as of Spring Framework 5.3.17; to be removed in Spring Framework 6.0 */ + @Deprecated TYPE_HIERARCHY_AND_ENCLOSING_CLASSES } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java index 83518d9b3846..e848090b1241 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -421,6 +421,7 @@ void typeHierarchyStrategyOnMethodWithGenericParameterNonOverrideScansAnnotation } @Test + @SuppressWarnings("deprecation") void typeHierarchyWithEnclosedStrategyOnEnclosedStaticClassScansAnnotations() { Class source = AnnotationEnclosingClassSample.EnclosedStatic.EnclosedStaticStatic.class; assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)) @@ -428,6 +429,7 @@ void typeHierarchyWithEnclosedStrategyOnEnclosedStaticClassScansAnnotations() { } @Test + @SuppressWarnings("deprecation") void typeHierarchyWithEnclosedStrategyOnEnclosedInnerClassScansAnnotations() { Class source = AnnotationEnclosingClassSample.EnclosedInner.EnclosedInnerInner.class; assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)) @@ -435,6 +437,7 @@ void typeHierarchyWithEnclosedStrategyOnEnclosedInnerClassScansAnnotations() { } @Test + @SuppressWarnings("deprecation") void typeHierarchyWithEnclosedStrategyOnMethodHierarchyUsesTypeHierarchyScan() { Method source = methodFrom(WithHierarchy.class); assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)).containsExactly( diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 02ec8f0980e9..e175e6e030da 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -712,6 +712,7 @@ void streamTypeHierarchyFromClassWithInterface() throws Exception { } @Test + @SuppressWarnings("deprecation") void streamTypeHierarchyAndEnclosingClassesFromNonAnnotatedInnerClassWithAnnotatedEnclosingClass() { Stream> classes = MergedAnnotations.from(AnnotatedClass.NonAnnotatedInnerClass.class, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES).stream().map(MergedAnnotation::getType); @@ -719,6 +720,7 @@ void streamTypeHierarchyAndEnclosingClassesFromNonAnnotatedInnerClassWithAnnotat } @Test + @SuppressWarnings("deprecation") void streamTypeHierarchyAndEnclosingClassesFromNonAnnotatedStaticNestedClassWithAnnotatedEnclosingClass() { Stream> classes = MergedAnnotations.from(AnnotatedClass.NonAnnotatedStaticNestedClass.class, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES).stream().map(MergedAnnotation::getType); From 84b4cebb3913ddd4a803939fdc8dde1b0401ff35 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 19 Feb 2022 16:54:16 +0100 Subject: [PATCH 007/131] Fix (@)since tag in SpelMessage See gh-28043 --- .../java/org/springframework/expression/spel/SpelMessage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index b8f5f92d0554..9c08841158ed 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -258,7 +258,7 @@ public enum SpelMessage { FLAWED_PATTERN(Kind.ERROR, 1073, "Failed to efficiently evaluate pattern ''{0}'': consider redesigning it"), - /** @since 5.3.16 */ + /** @since 5.3.17 */ EXCEPTION_COMPILING_EXPRESSION(Kind.ERROR, 1074, "An exception occurred while compiling an expression"); From 453c6d41f71acc54bb3928f9c7b9787d29e84a43 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 24 Feb 2022 10:54:52 +0100 Subject: [PATCH 008/131] Fix Objenesis version See gh-28100 --- src/docs/dist/license.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/dist/license.txt b/src/docs/dist/license.txt index c68e1154b170..0eb8edb06324 100644 --- a/src/docs/dist/license.txt +++ b/src/docs/dist/license.txt @@ -255,10 +255,10 @@ CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which is included above. ->>> Objenesis 3.1 (org.objenesis:objenesis:3.1): +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.1 is licensed under the +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the Apache License, version 2.0, the text of which is included above. Per the NOTICE file in the Objenesis ZIP distribution downloaded from From 7e2106b850ea65866dc97d24d108c7fe1ea64c8c Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 24 Feb 2022 14:46:26 +0100 Subject: [PATCH 009/131] Refactor roll forward in CronField Before this commit, CronField.Type::rollForward added temporal units to reach the higher order field. This caused issues with DST, where the added amount of hours was either too small or too large. This commit refactors the implementation so that it now adds one to the higher order field, and reset the current field to the minimum value. Closes gh-28095 --- .../scheduling/support/CronField.java | 34 ++++++++----------- .../support/CronExpressionTests.java | 8 +++++ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index adc2c2ffe5d6..f794645d654f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -18,6 +18,7 @@ import java.time.DateTimeException; import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; import java.time.temporal.ValueRange; import java.util.function.BiFunction; @@ -168,22 +169,25 @@ protected static > T cast(Temporal te * day-of-month, month, day-of-week. */ protected enum Type { - NANO(ChronoField.NANO_OF_SECOND), - SECOND(ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - HOUR(ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - DAY_OF_MONTH(ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - MONTH(ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); + NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS), + SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND), + MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + HOUR(ChronoField.HOUR_OF_DAY, ChronoUnit.DAYS, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + DAY_OF_MONTH(ChronoField.DAY_OF_MONTH, ChronoUnit.MONTHS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + MONTH(ChronoField.MONTH_OF_YEAR, ChronoUnit.YEARS, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoUnit.WEEKS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); private final ChronoField field; + private final ChronoUnit higherOrder; + private final ChronoField[] lowerOrders; - Type(ChronoField field, ChronoField... lowerOrders) { + Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) { this.field = field; + this.higherOrder = higherOrder; this.lowerOrders = lowerOrders; } @@ -266,17 +270,9 @@ public > T elapseUntil(T temporal, in * @return the rolled forward temporal */ public > T rollForward(T temporal) { - int current = get(temporal); - ValueRange range = temporal.range(this.field); - long amount = range.getMaximum() - current + 1; - T result = this.field.getBaseUnit().addTo(temporal, amount); - current = get(result); - range = result.range(this.field); - // adjust for daylight savings - if (current != range.getMinimum()) { - result = this.field.adjustInto(result, range.getMinimum()); - } - return result; + T result = this.higherOrder.addTo(temporal, 1); + ValueRange range = result.range(this.field); + return this.field.adjustInto(result, range.getMinimum()); } /** diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index 5de5362fc1dd..5dd837e0379f 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -1344,6 +1344,14 @@ public void daylightSaving() { actual = cronExpression.next(last); assertThat(actual).isNotNull(); assertThat(actual).isEqualTo(expected); + + cronExpression = CronExpression.parse("0 5 0 * * *"); + + last = ZonedDateTime.parse("2019-10-27T01:05+02:00[Europe/Amsterdam]"); + expected = ZonedDateTime.parse("2019-10-28T00:05+01:00[Europe/Amsterdam]"); + actual = cronExpression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); } @Test From 6f41180cc5e2f96b11950d483a41454d384defc9 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 28 Feb 2022 16:37:07 +0100 Subject: [PATCH 010/131] Align AsyncRestTemplate error logging with RestTemplate Prior to this commit, `AsyncRestTemplate` would log errors (including simple 404s) with WARN level. Such errors are quite common and should not clutter logs. This commit aligns the logging strategy with RestTemplate, using the DEBUG level for such cases. Fixes gh-28049 --- .../org/springframework/web/client/AsyncRestTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java index f94359de4aeb..fcdc0e483232 100644 --- a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java @@ -559,9 +559,9 @@ private void logResponseStatus(HttpMethod method, URI url, ClientHttpResponse re } private void handleResponseError(HttpMethod method, URI url, ClientHttpResponse response) throws IOException { - if (logger.isWarnEnabled()) { + if (logger.isDebugEnabled()) { try { - logger.warn("Async " + method.name() + " request for \"" + url + "\" resulted in " + + logger.debug("Async " + method.name() + " request for \"" + url + "\" resulted in " + response.getRawStatusCode() + " (" + response.getStatusText() + "); invoking error handler"); } catch (IOException ex) { From beab8ab4e752fa7da27a1df3904fe4f628236e06 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 28 Feb 2022 14:19:29 +0100 Subject: [PATCH 011/131] Test claims regarding SpEL support for T(Character) See gh-28112 --- .../springframework/expression/spel/EvaluationTests.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index 98b9704ea336..a1c1a0eedaee 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -420,7 +420,11 @@ public void testIndexerError() { @Test public void testStaticRef02() { - evaluate("T(java.awt.Color).green.getRGB()!=0", "true", Boolean.class); + evaluate("T(java.awt.Color).green.getRGB() != 0", true, Boolean.class); + evaluate("(T(java.lang.Math).random() * 100.0 ) > 0", true, Boolean.class); + evaluate("(T(Math).random() * 100.0) > 0", true, Boolean.class); + evaluate("T(Character).isUpperCase('Test'.charAt(0)) ? 'uppercase' : 'lowercase'", "uppercase", String.class); + evaluate("T(Character).isUpperCase('Test'.charAt(1)) ? 'uppercase' : 'lowercase'", "lowercase", String.class); } // variables and functions From 84de100fc64cbb96719d7ac18e977e7aa668074a Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 28 Feb 2022 17:15:35 +0100 Subject: [PATCH 012/131] Polishing --- .../expression/MethodResolver.java | 6 +- .../expression/spel/ast/MethodReference.java | 3 +- .../expression/spel/EvaluationTests.java | 2390 +++++++++-------- .../spel/SpelDocumentationTests.java | 130 +- .../expression/spel/TestScenarioCreator.java | 2 +- 5 files changed, 1270 insertions(+), 1261 deletions(-) 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 db555ce7b238..d4216d2a55c3 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-2013 the original author or authors. + * Copyright 2002-2021 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,8 +22,8 @@ import org.springframework.lang.Nullable; /** - * A method resolver attempts 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' + * 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. * * @author Andy Clement diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index cbaf1c8d1add..1ecb6187001a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -200,8 +200,7 @@ private MethodExecutor findAccessorForMethod(List argumentTypes, EvaluationContext evaluationContext) throws SpelEvaluationException { AccessException accessException = null; - List methodResolvers = evaluationContext.getMethodResolvers(); - for (MethodResolver methodResolver : methodResolvers) { + for (MethodResolver methodResolver : evaluationContext.getMethodResolvers()) { try { MethodExecutor methodExecutor = methodResolver.resolve( evaluationContext, targetObject, this.name, argumentTypes); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index a1c1a0eedaee..097daf526da6 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -16,25 +16,23 @@ package org.springframework.expression.spel; -import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.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.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; -import org.springframework.expression.MethodExecutor; import org.springframework.expression.MethodFilter; -import org.springframework.expression.MethodResolver; import org.springframework.expression.ParseException; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -57,1305 +55,1333 @@ * @author Giovanni Dall'Oglio Risso * @since 3.0 */ -public class EvaluationTests extends AbstractExpressionTests { - - @Test - public void testCreateListsOnAttemptToIndexNull01() throws EvaluationException, ParseException { - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e = parser.parseExpression("list[0]"); - TestClass testClass = new TestClass(); - - Object o = e.getValue(new StandardEvaluationContext(testClass)); - assertThat(o).isEqualTo(""); - o = parser.parseExpression("list[3]").getValue(new StandardEvaluationContext(testClass)); - assertThat(o).isEqualTo(""); - assertThat(testClass.list.size()).isEqualTo(4); - - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - parser.parseExpression("list2[3]").getValue(new StandardEvaluationContext(testClass))); - - o = parser.parseExpression("foo[3]").getValue(new StandardEvaluationContext(testClass)); - assertThat(o).isEqualTo(""); - assertThat(testClass.getFoo().size()).isEqualTo(4); - } +class EvaluationTests extends AbstractExpressionTests { - @Test - public void testCreateMapsOnAttemptToIndexNull01() { - TestClass testClass = new TestClass(); - StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + @Nested + class MiscellaneousTests { - Object o = parser.parseExpression("map['a']").getValue(ctx); - assertThat(o).isNull(); - o = parser.parseExpression("map").getValue(ctx); - assertThat(o).isNotNull(); + @Test + void createListsOnAttemptToIndexNull01() throws EvaluationException, ParseException { + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("list[0]"); + TestClass testClass = new TestClass(); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("map2['a']").getValue(ctx)); - // map2 should be null, there is no setter - } + Object o = e.getValue(new StandardEvaluationContext(testClass)); + assertThat(o).isEqualTo(""); + o = parser.parseExpression("list[3]").getValue(new StandardEvaluationContext(testClass)); + assertThat(o).isEqualTo(""); + assertThat(testClass.list.size()).isEqualTo(4); - // wibble2 should be null (cannot be initialized dynamically), there is no setter - @Test - public void testCreateObjectsOnAttemptToReferenceNull() { - TestClass testClass = new TestClass(); - StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + parser.parseExpression("list2[3]").getValue(new StandardEvaluationContext(testClass))); - Object o = parser.parseExpression("wibble.bar").getValue(ctx); - assertThat(o).isEqualTo("hello"); - o = parser.parseExpression("wibble").getValue(ctx); - assertThat(o).isNotNull(); + o = parser.parseExpression("foo[3]").getValue(new StandardEvaluationContext(testClass)); + assertThat(o).isEqualTo(""); + assertThat(testClass.getFoo().size()).isEqualTo(4); + } - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("wibble2.bar").getValue(ctx)); - } + @Test + void createMapsOnAttemptToIndexNull() { + TestClass testClass = new TestClass(); + StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - @Test - public void testElvis01() { - evaluate("'Andy'?:'Dave'", "Andy", String.class); - evaluate("null?:'Dave'", "Dave", String.class); - } + Object o = parser.parseExpression("map['a']").getValue(ctx); + assertThat(o).isNull(); + o = parser.parseExpression("map").getValue(ctx); + assertThat(o).isNotNull(); - @Test - public void testSafeNavigation() { - evaluate("null?.null?.null", null, null); - } + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("map2['a']").getValue(ctx)); + // map2 should be null, there is no setter + } - @Test - public void testRelOperatorGT01() { - evaluate("3 > 6", "false", Boolean.class); - } + // wibble2 should be null (cannot be initialized dynamically), there is no setter + @Test + void createObjectsOnAttemptToReferenceNull() { + TestClass testClass = new TestClass(); + StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - @Test - public void testRelOperatorLT01() { - evaluate("3 < 6", "true", Boolean.class); - } + Object o = parser.parseExpression("wibble.bar").getValue(ctx); + assertThat(o).isEqualTo("hello"); + o = parser.parseExpression("wibble").getValue(ctx); + assertThat(o).isNotNull(); - @Test - public void testRelOperatorLE01() { - evaluate("3 <= 6", "true", Boolean.class); - } + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("wibble2.bar").getValue(ctx)); + } - @Test - public void testRelOperatorGE01() { - evaluate("3 >= 6", "false", Boolean.class); - } + @Test + void elvisOperator() { + evaluate("'Andy'?:'Dave'", "Andy", String.class); + evaluate("null?:'Dave'", "Dave", String.class); + } - @Test - public void testRelOperatorGE02() { - evaluate("3 >= 3", "true", Boolean.class); - } + @Test + void safeNavigation() { + evaluate("null?.null?.null", null, null); + } - @Test - public void testRelOperatorsInstanceof01() { - evaluate("'xyz' instanceof T(int)", "false", Boolean.class); - } + @Test // SPR-16731 + void matchesWithPatternAccessThreshold() { + String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$"; + String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches \'" + pattern + "\'"; + Expression expr = parser.parseExpression(expression); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(expr::getValue) + .withCauseInstanceOf(IllegalStateException.class) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.FLAWED_PATTERN)); + } - @Test - public void testRelOperatorsInstanceof04() { - evaluate("null instanceof T(String)", "false", Boolean.class); - } + // mixing operators + @Test + void mixingOperators() { + evaluate("true and 5>3", "true", Boolean.class); + } - @Test - public void testRelOperatorsInstanceof05() { - evaluate("null instanceof T(Integer)", "false", Boolean.class); - } + // assignment + @Test + void assignmentToVariables() { + evaluate("#var1='value1'", "value1", String.class); + } - @Test - public void testRelOperatorsInstanceof06() { - evaluateAndCheckError("'A' instanceof null", SpelMessage.INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND, 15, "null"); - } + @Test + void operatorVariants() { + SpelExpression e = (SpelExpression) parser.parseExpression("#a < #b"); + EvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("a", (short) 3); + ctx.setVariable("b", (short) 6); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("b", (byte) 6); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 9); + ctx.setVariable("b", (byte) 6); + assertThat(e.getValue(ctx, Boolean.class)).isFalse(); + ctx.setVariable("a", 10L); + ctx.setVariable("b", (short) 30); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 3); + ctx.setVariable("b", (short) 30); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 3); + ctx.setVariable("b", 30L); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 3); + ctx.setVariable("b", 30f); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", new BigInteger("10")); + ctx.setVariable("b", new BigInteger("20")); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + } - @Test - public void testRelOperatorsMatches01() { - evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class); - } + @Test + void indexer03() { + evaluate("'christian'[8]", "n", String.class); + } - @Test - public void testRelOperatorsMatches02() { - evaluate("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "true", Boolean.class); - } + @Test + void indexerError() { + evaluateAndCheckError("new org.springframework.expression.spel.testresources.Inventor().inventions[1]", + SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); + } - @Test - public void testRelOperatorsMatches03() { - evaluateAndCheckError("null matches '^.*$'", SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, 0, null); - } + @Test + void stringType() { + evaluateAndAskForReturnType("getPlaceOfBirth().getCity()", "SmilJan", String.class); + } - @Test - public void testRelOperatorsMatches04() { - evaluateAndCheckError("'abc' matches null", SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, 14, null); - } + @Test + void numbers() { + evaluateAndAskForReturnType("3*4+5", 17, Integer.class); + evaluateAndAskForReturnType("3*4+5", 17L, Long.class); + evaluateAndAskForReturnType("65", 'A', Character.class); + evaluateAndAskForReturnType("3*4+5", (short) 17, Short.class); + evaluateAndAskForReturnType("3*4+5", "17", String.class); + } - @Test - public void testRelOperatorsMatches05() { - evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int>string - } + @Test + void advancedNumerics() { + int twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Integer.class); + assertThat(twentyFour).isEqualTo(24); + double one = parser.parseExpression("8.0 / 5e0 % 2").getValue(Double.class); + assertThat((float) one).isCloseTo((float) 1.6d, within((float) 0d)); + int o = parser.parseExpression("8.0 / 5e0 % 2").getValue(Integer.class); + assertThat(o).isEqualTo(1); + int sixteen = parser.parseExpression("-2 ^ 4").getValue(Integer.class); + assertThat(sixteen).isEqualTo(16); + int minusFortyFive = parser.parseExpression("1+2-3*8^2/2/2").getValue(Integer.class); + assertThat(minusFortyFive).isEqualTo(-45); + } - @Test // SPR-16731 - public void testMatchesWithPatternAccessThreshold() { - String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$"; - String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches \'" + pattern + "\'"; - Expression expr = parser.parseExpression(expression); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy( - expr::getValue) - .withCauseInstanceOf(IllegalStateException.class) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.FLAWED_PATTERN)); - } + @Test + void comparison() { + EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + boolean trueValue = parser.parseExpression("T(java.util.Date) == Birthdate.Class").getValue( + context, Boolean.class); + assertThat(trueValue).isTrue(); + } - // mixing operators - @Test - public void testMixingOperators01() { - evaluate("true and 5>3", "true", Boolean.class); - } + @Test + void resolvingList() { + StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); + ((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util"); + assertThat(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)).isTrue(); + } + + @Test + void resolvingString() { + Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); + assertThat(stringClass).isEqualTo(String.class); + } + + /** + * SPR-6984: attempting to index a collection on write using an index that + * doesn't currently exist in the collection (address.crossStreets[0] below) + */ + @Test + void initializingCollectionElementsOnWrite() { + TestPerson person = new TestPerson(); + EvaluationContext context = new StandardEvaluationContext(person); + SpelParserConfiguration config = new SpelParserConfiguration(true, true); + ExpressionParser parser = new SpelExpressionParser(config); + Expression e = parser.parseExpression("name"); + e.setValue(context, "Oleg"); + assertThat(person.getName()).isEqualTo("Oleg"); + + e = parser.parseExpression("address.street"); + e.setValue(context, "123 High St"); + assertThat(person.getAddress().getStreet()).isEqualTo("123 High St"); + + e = parser.parseExpression("address.crossStreets[0]"); + e.setValue(context, "Blah"); + assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); + + e = parser.parseExpression("address.crossStreets[3]"); + e.setValue(context, "Wibble"); + assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); + assertThat(person.getAddress().getCrossStreets().get(3)).isEqualTo("Wibble"); + } + + /** + * Verifies behavior requested in SPR-9613. + */ + @Test + void caseInsensitiveNullLiterals() { + ExpressionParser parser = new SpelExpressionParser(); + + Expression e = parser.parseExpression("null"); + assertThat(e.getValue()).isNull(); + + e = parser.parseExpression("NULL"); + assertThat(e.getValue()).isNull(); + + e = parser.parseExpression("NuLl"); + assertThat(e.getValue()).isNull(); + } + + /** + * Verifies behavior requested in SPR-9621. + */ + @Test + void customMethodFilter() { + StandardEvaluationContext context = new StandardEvaluationContext(); + + // Register a custom MethodResolver... + context.setMethodResolvers(Arrays.asList((evaluationContext, targetObject, name, argumentTypes) -> null)); + + // or simply... + // context.setMethodResolvers(new ArrayList()); + + // Register a custom MethodFilter... + MethodFilter methodFilter = methods -> null; + assertThatIllegalStateException() + .isThrownBy(() -> context.registerMethodFilter(String.class, methodFilter)) + .withMessage("Method filter cannot be set as the reflective method resolver is not in use"); + } + + /** + * This test is checking that with the changes for 9751 that the refactoring in Indexer is + * coping correctly for references beyond collection boundaries. + */ + @Test + void collectionGrowingViaIndexer() { + Spr9751 instance = new Spr9751(); + + // Add a new element to the list + StandardEvaluationContext ctx = new StandardEvaluationContext(instance); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("listOfStrings[++index3]='def'"); + e.getValue(ctx); + assertThat(instance.listOfStrings.size()).isEqualTo(2); + assertThat(instance.listOfStrings.get(1)).isEqualTo("def"); + + // Check reference beyond end of collection + ctx = new StandardEvaluationContext(instance); + parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + e = parser.parseExpression("listOfStrings[0]"); + String value = e.getValue(ctx, String.class); + assertThat(value).isEqualTo("abc"); + e = parser.parseExpression("listOfStrings[1]"); + value = e.getValue(ctx, String.class); + assertThat(value).isEqualTo("def"); + e = parser.parseExpression("listOfStrings[2]"); + value = e.getValue(ctx, String.class); + assertThat(value).isEqualTo(""); + + // Now turn off growing and reference off the end + StandardEvaluationContext failCtx = new StandardEvaluationContext(instance); + parser = new SpelExpressionParser(new SpelParserConfiguration(false, false)); + Expression failExp = parser.parseExpression("listOfStrings[3]"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + failExp.getValue(failCtx, String.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS)); + } + + @Test + void limitCollectionGrowing() { + TestClass instance = new TestClass(); + StandardEvaluationContext ctx = new StandardEvaluationContext(instance); + SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(true, true, 3)); + Expression e = parser.parseExpression("foo[2]"); + e.setValue(ctx, "2"); + assertThat(instance.getFoo().size()).isEqualTo(3); + e = parser.parseExpression("foo[3]"); + try { + e.setValue(ctx, "3"); + } + catch (SpelEvaluationException see) { + assertThat(see.getMessageCode()).isEqualTo(SpelMessage.UNABLE_TO_GROW_COLLECTION); + assertThat(instance.getFoo().size()).isEqualTo(3); + } + } - // property access - @Test - public void testPropertyField01() { - evaluate("name", "Nikola Tesla", String.class, false); - // not writable because (1) name is private (2) there is no setter, only a getter - evaluateAndCheckError("madeup", SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, 0, "madeup", - "org.springframework.expression.spel.testresources.Inventor"); } - @Test - public void testPropertyField02_SPR7100() { - evaluate("_name", "Nikola Tesla", String.class); - evaluate("_name_", "Nikola Tesla", String.class); + @Nested + class RelationalOperatorTests { + + @Test + void relOperatorGT() { + evaluate("3 > 6", "false", Boolean.class); + } + + @Test + void relOperatorLT() { + evaluate("3 < 6", "true", Boolean.class); + } + + @Test + void relOperatorLE() { + evaluate("3 <= 6", "true", Boolean.class); + } + + @Test + void relOperatorGE01() { + evaluate("3 >= 6", "false", Boolean.class); + } + + @Test + void relOperatorGE02() { + evaluate("3 >= 3", "true", Boolean.class); + } + + @Test + void relOperatorsInstanceof01() { + evaluate("'xyz' instanceof T(int)", "false", Boolean.class); + } + + @Test + void relOperatorsInstanceof04() { + evaluate("null instanceof T(String)", "false", Boolean.class); + } + + @Test + void relOperatorsInstanceof05() { + evaluate("null instanceof T(Integer)", "false", Boolean.class); + } + + @Test + void relOperatorsInstanceof06() { + evaluateAndCheckError("'A' instanceof null", SpelMessage.INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND, 15, "null"); + } + + @Test + void relOperatorsMatches01() { + evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class); + } + + @Test + void relOperatorsMatches02() { + evaluate("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "true", Boolean.class); + } + + @Test + void relOperatorsMatches03() { + evaluateAndCheckError("null matches '^.*$'", SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, 0, null); + } + + @Test + void relOperatorsMatches04() { + evaluateAndCheckError("'abc' matches null", SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, 14, null); + } + + @Test + void relOperatorsMatches05() { + evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int>string + } + } - @Test - public void testRogueTrailingDotCausesNPE_SPR6866() { - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> - new SpelExpressionParser().parseExpression("placeOfBirth.foo.")) + @Nested + class PropertyAccessTests { + + @Test + void propertyField() { + evaluate("name", "Nikola Tesla", String.class, false); + // not writable because (1) name is private (2) there is no setter, only a getter + evaluateAndCheckError("madeup", SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, 0, "madeup", + "org.springframework.expression.spel.testresources.Inventor"); + } + + @Test + void propertyField_SPR7100() { + evaluate("_name", "Nikola Tesla", String.class); + evaluate("_name_", "Nikola Tesla", String.class); + } + + @Test + void rogueTrailingDotCausesNPE_SPR6866() { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseExpression("placeOfBirth.foo.")) .satisfies(ex -> { assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OOD); assertThat(ex.getPosition()).isEqualTo(16); }); - } + } - // nested properties - @Test - public void testPropertiesNested01() { - evaluate("placeOfBirth.city", "SmilJan", String.class, true); - } + @Nested + class NestedPropertiesTests { - @Test - public void testPropertiesNested02() { - evaluate("placeOfBirth.doubleIt(12)", "24", Integer.class); - } - - @Test - public void testPropertiesNested03() throws ParseException { - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> - new SpelExpressionParser().parseRaw("placeOfBirth.23")) - .satisfies(ex -> { - assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.UNEXPECTED_DATA_AFTER_DOT); - assertThat(ex.getInserts()[0]).isEqualTo("23"); - }); - } + // nested properties + @Test + void propertiesNested01() { + evaluate("placeOfBirth.city", "SmilJan", String.class, true); + } - // methods - @Test - public void testMethods01() { - evaluate("echo(12)", "12", String.class); - } + @Test + void propertiesNested02() { + evaluate("placeOfBirth.doubleIt(12)", "24", Integer.class); + } - @Test - public void testMethods02() { - evaluate("echo(name)", "Nikola Tesla", String.class); - } + @Test + void propertiesNested03() throws ParseException { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseRaw("placeOfBirth.23")) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.UNEXPECTED_DATA_AFTER_DOT); + assertThat(ex.getInserts()[0]).isEqualTo("23"); + }); + } - // constructors - @Test - public void testConstructorInvocation01() { - evaluate("new String('hello')", "hello", String.class); - } + } - @Test - public void testConstructorInvocation05() { - evaluate("new java.lang.String('foobar')", "foobar", String.class); } - @Test - public void testConstructorInvocation06() { - // repeated evaluation to drive use of cached executor - SpelExpression e = (SpelExpression) parser.parseExpression("new String('wibble')"); - String newString = e.getValue(String.class); - assertThat(newString).isEqualTo("wibble"); - newString = e.getValue(String.class); - assertThat(newString).isEqualTo("wibble"); - - // not writable - assertThat(e.isWritable(new StandardEvaluationContext())).isFalse(); + @Nested + class MethodAndConstructorTests { - // ast - assertThat(e.toStringAST()).isEqualTo("new String('wibble')"); - } + @Test + void methods01() { + evaluate("echo(12)", "12", String.class); + } - // unary expressions - @Test - public void testUnaryMinus01() { - evaluate("-5", "-5", Integer.class); - } + @Test + void methods02() { + evaluate("echo(name)", "Nikola Tesla", String.class); + } - @Test - public void testUnaryPlus01() { - evaluate("+5", "5", Integer.class); - } + @Test + void constructorInvocation01() { + evaluate("new String('hello')", "hello", String.class); + } - @Test - public void testUnaryNot01() { - evaluate("!true", "false", Boolean.class); - } + @Test + void constructorInvocation05() { + evaluate("new java.lang.String('foobar')", "foobar", String.class); + } - @Test - public void testUnaryNot02() { - evaluate("!false", "true", Boolean.class); - } + @Test + void constructorInvocation06() { + // repeated evaluation to drive use of cached executor + SpelExpression e = (SpelExpression) parser.parseExpression("new String('wibble')"); + String newString = e.getValue(String.class); + assertThat(newString).isEqualTo("wibble"); + newString = e.getValue(String.class); + assertThat(newString).isEqualTo("wibble"); - @Test - public void testUnaryNotWithNullValue() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("!null")::getValue); - } + // not writable + assertThat(e.isWritable(new StandardEvaluationContext())).isFalse(); - @Test - public void testAndWithNullValueOnLeft() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("null and true")::getValue); - } + // ast + assertThat(e.toStringAST()).isEqualTo("new String('wibble')"); + } - @Test - public void testAndWithNullValueOnRight() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("true and null")::getValue); } - @Test - public void testOrWithNullValueOnLeft() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("null or false")::getValue); - } + @Nested + class UnaryOperatorTests { - @Test - public void testOrWithNullValueOnRight() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("false or null")::getValue); - } + @Test + void unaryMinus() { + evaluate("-5", "-5", Integer.class); + } - // assignment - @Test - public void testAssignmentToVariables01() { - evaluate("#var1='value1'", "value1", String.class); - } + @Test + void unaryPlus() { + evaluate("+5", "5", Integer.class); + } - @Test - public void testTernaryOperator01() { - evaluate("2>4?1:2", 2, Integer.class); - } + @Test + void unaryNot01() { + evaluate("!true", "false", Boolean.class); + } - @Test - public void testTernaryOperator02() { - evaluate("'abc'=='abc'?1:2", 1, Integer.class); - } + @Test + void unaryNot02() { + evaluate("!false", "true", Boolean.class); + } - @Test - public void testTernaryOperator03() { - // cannot convert String to boolean - evaluateAndCheckError("'hello'?1:2", SpelMessage.TYPE_CONVERSION_ERROR); - } + @Test + void unaryNotWithNullValue() { + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(parser.parseExpression("!null")::getValue); + } - @Test - public void testTernaryOperator04() { - Expression e = parser.parseExpression("1>2?3:4"); - assertThat(e.isWritable(context)).isFalse(); } - @Test - public void testTernaryOperator05() { - evaluate("1>2?#var=4:#var=5", 5, Integer.class); - evaluate("3?:#var=5", 3, Integer.class); - evaluate("null?:#var=5", 5, Integer.class); - evaluate("2>4?(3>2?true:false):(5<3?true:false)", false, Boolean.class); - } + @Nested + class BinaryOperatorTests { - @Test - public void testTernaryOperatorWithNullValue() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("null ? 0 : 1")::getValue); - } + @Test + void andWithNullValueOnLeft() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("null and true")::getValue); + } - @Test - public void methodCallWithRootReferenceThroughParameter() { - evaluate("placeOfBirth.doubleIt(inventions.length)", 18, Integer.class); - } + @Test + void andWithNullValueOnRight() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("true and null")::getValue); + } - @Test - public void ctorCallWithRootReferenceThroughParameter() { - evaluate("new org.springframework.expression.spel.testresources.PlaceOfBirth(inventions[0].toString()).city", - "Telephone repeater", String.class); - } + @Test + void orWithNullValueOnLeft() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("null or false")::getValue); + } - @Test - public void fnCallWithRootReferenceThroughParameter() { - evaluate("#reverseInt(inventions.length, inventions.length, inventions.length)", "int[3]{9,9,9}", int[].class); - } + @Test + void orWithNullValueOnRight() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("false or null")::getValue); + } - @Test - public void methodCallWithRootReferenceThroughParameterThatIsAFunctionCall() { - evaluate("placeOfBirth.doubleIt(#reverseInt(inventions.length,2,3)[2])", 18, Integer.class); } - @Test - public void testIndexer03() { - evaluate("'christian'[8]", "n", String.class); - } + @Nested + class TernaryOperatorTests { - @Test - public void testIndexerError() { - evaluateAndCheckError("new org.springframework.expression.spel.testresources.Inventor().inventions[1]", - SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); - } + @Test + void ternaryOperator01() { + evaluate("2>4?1:2", 2, Integer.class); + } - @Test - public void testStaticRef02() { - evaluate("T(java.awt.Color).green.getRGB() != 0", true, Boolean.class); - evaluate("(T(java.lang.Math).random() * 100.0 ) > 0", true, Boolean.class); - evaluate("(T(Math).random() * 100.0) > 0", true, Boolean.class); - evaluate("T(Character).isUpperCase('Test'.charAt(0)) ? 'uppercase' : 'lowercase'", "uppercase", String.class); - evaluate("T(Character).isUpperCase('Test'.charAt(1)) ? 'uppercase' : 'lowercase'", "lowercase", String.class); - } + @Test + void ternaryOperator02() { + evaluate("'abc'=='abc'?1:2", 1, Integer.class); + } - // variables and functions - @Test - public void testVariableAccess01() { - evaluate("#answer", "42", Integer.class, true); - } + @Test + void ternaryOperator03() { + // cannot convert String to boolean + evaluateAndCheckError("'hello'?1:2", SpelMessage.TYPE_CONVERSION_ERROR); + } - @Test - public void testFunctionAccess01() { - evaluate("#reverseInt(1,2,3)", "int[3]{3,2,1}", int[].class); - } + @Test + void ternaryOperator04() { + Expression e = parser.parseExpression("1>2?3:4"); + assertThat(e.isWritable(context)).isFalse(); + } - @Test - public void testFunctionAccess02() { - evaluate("#reverseString('hello')", "olleh", String.class); - } + @Test + void ternaryOperator05() { + evaluate("1>2?#var=4:#var=5", 5, Integer.class); + evaluate("3?:#var=5", 3, Integer.class); + evaluate("null?:#var=5", 5, Integer.class); + evaluate("2>4?(3>2?true:false):(5<3?true:false)", false, Boolean.class); + } - // type references - @Test - public void testTypeReferences01() { - evaluate("T(java.lang.String)", "class java.lang.String", Class.class); - } + @Test + void ternaryOperatorWithNullValue() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("null ? 0 : 1")::getValue); + } - @Test - public void testTypeReferencesAndQualifiedIdentifierCaching() { - SpelExpression e = (SpelExpression) parser.parseExpression("T(java.lang.String)"); - assertThat(e.isWritable(new StandardEvaluationContext())).isFalse(); - assertThat(e.toStringAST()).isEqualTo("T(java.lang.String)"); - assertThat(e.getValue(Class.class)).isEqualTo(String.class); - // use cached QualifiedIdentifier: - assertThat(e.toStringAST()).isEqualTo("T(java.lang.String)"); - assertThat(e.getValue(Class.class)).isEqualTo(String.class); } - @Test - public void operatorVariants() { - SpelExpression e = (SpelExpression)parser.parseExpression("#a < #b"); - EvaluationContext ctx = new StandardEvaluationContext(); - ctx.setVariable("a", (short) 3); - ctx.setVariable("b", (short) 6); - assertThat(e.getValue(ctx, Boolean.class)).isTrue(); - ctx.setVariable("b", (byte) 6); - assertThat(e.getValue(ctx, Boolean.class)).isTrue(); - ctx.setVariable("a", (byte) 9); - ctx.setVariable("b", (byte) 6); - assertThat(e.getValue(ctx, Boolean.class)).isFalse(); - ctx.setVariable("a", 10L); - ctx.setVariable("b", (short) 30); - assertThat(e.getValue(ctx, Boolean.class)).isTrue(); - ctx.setVariable("a", (byte) 3); - ctx.setVariable("b", (short) 30); - assertThat(e.getValue(ctx, Boolean.class)).isTrue(); - ctx.setVariable("a", (byte) 3); - ctx.setVariable("b", 30L); - assertThat(e.getValue(ctx, Boolean.class)).isTrue(); - ctx.setVariable("a", (byte) 3); - ctx.setVariable("b", 30f); - assertThat(e.getValue(ctx, Boolean.class)).isTrue(); - ctx.setVariable("a", new BigInteger("10")); - ctx.setVariable("b", new BigInteger("20")); - assertThat(e.getValue(ctx, Boolean.class)).isTrue(); - } + @Nested + class MethodConstructorAndFunctionInvocationTests { - @Test - public void testTypeReferencesPrimitive() { - evaluate("T(int)", "int", Class.class); - evaluate("T(byte)", "byte", Class.class); - evaluate("T(char)", "char", Class.class); - evaluate("T(boolean)", "boolean", Class.class); - evaluate("T(long)", "long", Class.class); - evaluate("T(short)", "short", Class.class); - evaluate("T(double)", "double", Class.class); - evaluate("T(float)", "float", Class.class); - } + @Test + void methodCallWithRootReferenceThroughParameter() { + evaluate("placeOfBirth.doubleIt(inventions.length)", 18, Integer.class); + } - @Test - public void testTypeReferences02() { - evaluate("T(String)", "class java.lang.String", Class.class); - } + @Test + void ctorCallWithRootReferenceThroughParameter() { + evaluate("new org.springframework.expression.spel.testresources.PlaceOfBirth(inventions[0].toString()).city", + "Telephone repeater", String.class); + } - @Test - public void testStringType() { - evaluateAndAskForReturnType("getPlaceOfBirth().getCity()", "SmilJan", String.class); - } + @Test + void fnCallWithRootReferenceThroughParameter() { + evaluate("#reverseInt(inventions.length, inventions.length, inventions.length)", "int[3]{9,9,9}", int[].class); + } - @Test - public void testNumbers01() { - evaluateAndAskForReturnType("3*4+5", 17, Integer.class); - evaluateAndAskForReturnType("3*4+5", 17L, Long.class); - evaluateAndAskForReturnType("65", 'A', Character.class); - evaluateAndAskForReturnType("3*4+5", (short) 17, Short.class); - evaluateAndAskForReturnType("3*4+5", "17", String.class); - } + @Test + void methodCallWithRootReferenceThroughParameterThatIsAFunctionCall() { + evaluate("placeOfBirth.doubleIt(#reverseInt(inventions.length,2,3)[2])", 18, Integer.class); + } - @Test - public void testAdvancedNumerics() { - int twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Integer.class); - assertThat(twentyFour).isEqualTo(24); - double one = parser.parseExpression("8.0 / 5e0 % 2").getValue(Double.class); - assertThat((float) one).isCloseTo((float) 1.6d, within((float) 0d)); - int o = parser.parseExpression("8.0 / 5e0 % 2").getValue(Integer.class); - assertThat(o).isEqualTo(1); - int sixteen = parser.parseExpression("-2 ^ 4").getValue(Integer.class); - assertThat(sixteen).isEqualTo(16); - int minusFortyFive = parser.parseExpression("1+2-3*8^2/2/2").getValue(Integer.class); - assertThat(minusFortyFive).isEqualTo(-45); } - @Test - public void testComparison() { - EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); - boolean trueValue = parser.parseExpression("T(java.util.Date) == Birthdate.Class").getValue( - context, Boolean.class); - assertThat(trueValue).isTrue(); - } + @Nested + class VariableAndFunctionAccessTests { - @Test - public void testResolvingList() { - StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); - ((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util"); - assertThat(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)).isTrue(); - } + @Test + void variableAccess() { + evaluate("#answer", "42", Integer.class, true); + } - @Test - public void testResolvingString() { - Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); - assertThat(stringClass).isEqualTo(String.class); - } + @Test + void functionAccess() { + evaluate("#reverseInt(1,2,3)", "int[3]{3,2,1}", int[].class); + evaluate("#reverseString('hello')", "olleh", String.class); + } - /** - * SPR-6984: attempting to index a collection on write using an index that - * doesn't currently exist in the collection (address.crossStreets[0] below) - */ - @Test - public void initializingCollectionElementsOnWrite() { - TestPerson person = new TestPerson(); - EvaluationContext context = new StandardEvaluationContext(person); - SpelParserConfiguration config = new SpelParserConfiguration(true, true); - ExpressionParser parser = new SpelExpressionParser(config); - Expression e = parser.parseExpression("name"); - e.setValue(context, "Oleg"); - assertThat(person.getName()).isEqualTo("Oleg"); - - e = parser.parseExpression("address.street"); - e.setValue(context, "123 High St"); - assertThat(person.getAddress().getStreet()).isEqualTo("123 High St"); - - e = parser.parseExpression("address.crossStreets[0]"); - e.setValue(context, "Blah"); - assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); - - e = parser.parseExpression("address.crossStreets[3]"); - e.setValue(context, "Wibble"); - assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); - assertThat(person.getAddress().getCrossStreets().get(3)).isEqualTo("Wibble"); } - /** - * Verifies behavior requested in SPR-9613. - */ - @Test - public void caseInsensitiveNullLiterals() { - ExpressionParser parser = new SpelExpressionParser(); + @Nested + class TypeReferenceTests { - Expression e = parser.parseExpression("null"); - assertThat(e.getValue()).isNull(); + @Test + void typeReferences() { + evaluate("T(java.lang.String)", "class java.lang.String", Class.class); + evaluate("T(String)", "class java.lang.String", Class.class); + } - e = parser.parseExpression("NULL"); - assertThat(e.getValue()).isNull(); + @Test + void typeReferencesAndQualifiedIdentifierCaching() { + SpelExpression e = (SpelExpression) parser.parseExpression("T(java.lang.String)"); + assertThat(e.isWritable(new StandardEvaluationContext())).isFalse(); + assertThat(e.toStringAST()).isEqualTo("T(java.lang.String)"); + assertThat(e.getValue(Class.class)).isEqualTo(String.class); + // use cached QualifiedIdentifier: + assertThat(e.toStringAST()).isEqualTo("T(java.lang.String)"); + assertThat(e.getValue(Class.class)).isEqualTo(String.class); + } - e = parser.parseExpression("NuLl"); - assertThat(e.getValue()).isNull(); - } + @Test + void typeReferencesPrimitive() { + evaluate("T(int)", "int", Class.class); + evaluate("T(byte)", "byte", Class.class); + evaluate("T(char)", "char", Class.class); + evaluate("T(boolean)", "boolean", Class.class); + evaluate("T(long)", "long", Class.class); + evaluate("T(short)", "short", Class.class); + evaluate("T(double)", "double", Class.class); + evaluate("T(float)", "float", Class.class); + } - /** - * Verifies behavior requested in SPR-9621. - */ - @Test - public void customMethodFilter() { - StandardEvaluationContext context = new StandardEvaluationContext(); - - // Register a custom MethodResolver... - List customResolvers = new ArrayList<>(); - customResolvers.add(new CustomMethodResolver()); - context.setMethodResolvers(customResolvers); - - // or simply... - // context.setMethodResolvers(new ArrayList()); - - // Register a custom MethodFilter... - MethodFilter filter = new CustomMethodFilter(); - assertThatIllegalStateException().isThrownBy(() -> - context.registerMethodFilter(String.class, filter)) - .withMessage("Method filter cannot be set as the reflective method resolver is not in use"); - } + @Test + void staticMethodReferences() { + evaluate("T(java.awt.Color).green.getRGB() != 0", true, Boolean.class); + evaluate("(T(java.lang.Math).random() * 100.0 ) > 0", true, Boolean.class); + evaluate("(T(Math).random() * 100.0) > 0", true, Boolean.class); + evaluate("T(Character).isUpperCase('Test'.charAt(0)) ? 'uppercase' : 'lowercase'", "uppercase", String.class); + evaluate("T(Character).isUpperCase('Test'.charAt(1)) ? 'uppercase' : 'lowercase'", "lowercase", String.class); + } - /** - * This test is checking that with the changes for 9751 that the refactoring in Indexer is - * coping correctly for references beyond collection boundaries. - */ - @Test - public void collectionGrowingViaIndexer() { - Spr9751 instance = new Spr9751(); - - // Add a new element to the list - StandardEvaluationContext ctx = new StandardEvaluationContext(instance); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e = parser.parseExpression("listOfStrings[++index3]='def'"); - e.getValue(ctx); - assertThat(instance.listOfStrings.size()).isEqualTo(2); - assertThat(instance.listOfStrings.get(1)).isEqualTo("def"); - - // Check reference beyond end of collection - ctx = new StandardEvaluationContext(instance); - parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - e = parser.parseExpression("listOfStrings[0]"); - String value = e.getValue(ctx, String.class); - assertThat(value).isEqualTo("abc"); - e = parser.parseExpression("listOfStrings[1]"); - value = e.getValue(ctx, String.class); - assertThat(value).isEqualTo("def"); - e = parser.parseExpression("listOfStrings[2]"); - value = e.getValue(ctx, String.class); - assertThat(value).isEqualTo(""); - - // Now turn off growing and reference off the end - StandardEvaluationContext failCtx = new StandardEvaluationContext(instance); - parser = new SpelExpressionParser(new SpelParserConfiguration(false, false)); - Expression failExp = parser.parseExpression("listOfStrings[3]"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - failExp.getValue(failCtx, String.class)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS)); } - @Test - public void limitCollectionGrowing() { - TestClass instance = new TestClass(); - StandardEvaluationContext ctx = new StandardEvaluationContext(instance); - SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(true, true, 3)); - Expression e = parser.parseExpression("foo[2]"); - e.setValue(ctx, "2"); - assertThat(instance.getFoo().size()).isEqualTo(3); - e = parser.parseExpression("foo[3]"); - try { - e.setValue(ctx, "3"); - } - catch (SpelEvaluationException see) { - assertThat(see.getMessageCode()).isEqualTo(SpelMessage.UNABLE_TO_GROW_COLLECTION); - assertThat(instance.getFoo().size()).isEqualTo(3); - } - } + @Nested + class IncrementAndDecrementTests { - // For now I am making #this not assignable - @Test - public void increment01root() { - Integer i = 42; - StandardEvaluationContext ctx = new StandardEvaluationContext(i); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e = parser.parseExpression("#this++"); - assertThat(i.intValue()).isEqualTo(42); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e.getValue(ctx, Integer.class)) + // For now I am making #this not assignable + @Test + void increment01root() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("#this++"); + assertThat(i.intValue()).isEqualTo(42); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e.getValue(ctx, Integer.class)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); - } + } - @Test - public void increment02postfix() { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e; - - // BigDecimal - e = parser.parseExpression("bd++"); - assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); - BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); - assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); - assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); - - // double - e = parser.parseExpression("ddd++"); - assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); - assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); - assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); - - // float - e = parser.parseExpression("fff++"); - assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); - assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); - assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); - - // long - e = parser.parseExpression("lll++"); - assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); - assertThat(return_lll).isEqualTo(66666L); - assertThat(helper.lll).isEqualTo(66667L); - - // int - e = parser.parseExpression("iii++"); - assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(42); - assertThat(helper.iii).isEqualTo(43); - return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(43); - assertThat(helper.iii).isEqualTo(44); - - // short - e = parser.parseExpression("sss++"); - assertThat(helper.sss).isEqualTo((short) 15); - short return_sss = e.getValue(ctx, Short.TYPE); - assertThat(return_sss).isEqualTo((short) 15); - assertThat(helper.sss).isEqualTo((short) 16); - } + @Test + void increment02postfix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BigDecimal + e = parser.parseExpression("bd++"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); + assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("ddd++"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); + + // float + e = parser.parseExpression("fff++"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); + + // long + e = parser.parseExpression("lll++"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66666L); + assertThat(helper.lll).isEqualTo(66667L); + + // int + e = parser.parseExpression("iii++"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(43); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(43); + assertThat(helper.iii).isEqualTo(44); + + // short + e = parser.parseExpression("sss++"); + assertThat(helper.sss).isEqualTo((short) 15); + short return_sss = e.getValue(ctx, Short.TYPE); + assertThat(return_sss).isEqualTo((short) 15); + assertThat(helper.sss).isEqualTo((short) 16); + } - @Test - public void increment02prefix() { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e; - - - // BigDecimal - e = parser.parseExpression("++bd"); - assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); - BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); - assertThat(new BigDecimal("3").equals(return_bd)).isTrue(); - assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); - - // double - e = parser.parseExpression("++ddd"); - assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); - assertThat((float) return_ddd).isCloseTo((float) 3.0d, within((float) 0d)); - assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); - - // float - e = parser.parseExpression("++fff"); - assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); - assertThat(return_fff).isCloseTo(4.0f, within((float) 0d)); - assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); - - // long - e = parser.parseExpression("++lll"); - assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); - assertThat(return_lll).isEqualTo(66667L); - assertThat(helper.lll).isEqualTo(66667L); - - // int - e = parser.parseExpression("++iii"); - assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(43); - assertThat(helper.iii).isEqualTo(43); - return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(44); - assertThat(helper.iii).isEqualTo(44); - - // short - e = parser.parseExpression("++sss"); - assertThat(helper.sss).isEqualTo((short) 15); - int return_sss = (Integer) e.getValue(ctx); - assertThat(return_sss).isEqualTo((short) 16); - assertThat(helper.sss).isEqualTo((short) 16); - } + @Test + void increment02prefix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BigDecimal + e = parser.parseExpression("++bd"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); + assertThat(new BigDecimal("3").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("++ddd"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 3.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); + + // float + e = parser.parseExpression("++fff"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(4.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); + + // long + e = parser.parseExpression("++lll"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66667L); + assertThat(helper.lll).isEqualTo(66667L); + + // int + e = parser.parseExpression("++iii"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(43); + assertThat(helper.iii).isEqualTo(43); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(44); + assertThat(helper.iii).isEqualTo(44); + + // short + e = parser.parseExpression("++sss"); + assertThat(helper.sss).isEqualTo((short) 15); + int return_sss = (Integer) e.getValue(ctx); + assertThat(return_sss).isEqualTo((short) 16); + assertThat(helper.sss).isEqualTo((short) 16); + } - @Test - public void increment03() { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + @Test + void increment03() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e1 = parser.parseExpression("m()++"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Double.TYPE)) + Expression e1 = parser.parseExpression("m()++"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Double.TYPE)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); - Expression e2 = parser.parseExpression("++m()"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Double.TYPE)) + Expression e2 = parser.parseExpression("++m()"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Double.TYPE)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); - } + } - @Test - public void increment04() { - Integer i = 42; - StandardEvaluationContext ctx = new StandardEvaluationContext(i); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e1 = parser.parseExpression("++1"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Double.TYPE)) + @Test + void increment04() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e1 = parser.parseExpression("++1"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Double.TYPE)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); - Expression e2 = parser.parseExpression("1++"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Double.TYPE)) + Expression e2 = parser.parseExpression("1++"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Double.TYPE)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); - } + } - @Test - public void decrement01root() { - Integer i = 42; - StandardEvaluationContext ctx = new StandardEvaluationContext(i); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e = parser.parseExpression("#this--"); - assertThat(i.intValue()).isEqualTo(42); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e.getValue(ctx, Integer.class)) + @Test + void decrement01root() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("#this--"); + assertThat(i.intValue()).isEqualTo(42); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e.getValue(ctx, Integer.class)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); - } + } - @Test - public void decrement02postfix() { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e; - - // BigDecimal - e = parser.parseExpression("bd--"); - assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); - BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); - assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); - assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); - - // double - e = parser.parseExpression("ddd--"); - assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); - assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); - assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); - - // float - e = parser.parseExpression("fff--"); - assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); - assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); - assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); - - // long - e = parser.parseExpression("lll--"); - assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); - assertThat(return_lll).isEqualTo(66666L); - assertThat(helper.lll).isEqualTo(66665L); - - // int - e = parser.parseExpression("iii--"); - assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(42); - assertThat(helper.iii).isEqualTo(41); - return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(41); - assertThat(helper.iii).isEqualTo(40); - - // short - e = parser.parseExpression("sss--"); - assertThat(helper.sss).isEqualTo((short) 15); - short return_sss = e.getValue(ctx, Short.TYPE); - assertThat(return_sss).isEqualTo((short) 15); - assertThat(helper.sss).isEqualTo((short) 14); - } + @Test + void decrement02postfix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BigDecimal + e = parser.parseExpression("bd--"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); + assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("ddd--"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); + + // float + e = parser.parseExpression("fff--"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); + + // long + e = parser.parseExpression("lll--"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66666L); + assertThat(helper.lll).isEqualTo(66665L); + + // int + e = parser.parseExpression("iii--"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(41); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(41); + assertThat(helper.iii).isEqualTo(40); + + // short + e = parser.parseExpression("sss--"); + assertThat(helper.sss).isEqualTo((short) 15); + short return_sss = e.getValue(ctx, Short.TYPE); + assertThat(return_sss).isEqualTo((short) 15); + assertThat(helper.sss).isEqualTo((short) 14); + } - @Test - public void decrement02prefix() { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e; - - // BigDecimal - e = parser.parseExpression("--bd"); - assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); - BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); - assertThat(new BigDecimal("1").equals(return_bd)).isTrue(); - assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); - - // double - e = parser.parseExpression("--ddd"); - assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); - assertThat((float) return_ddd).isCloseTo((float) 1.0d, within((float) 0d)); - assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); - - // float - e = parser.parseExpression("--fff"); - assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); - assertThat(return_fff).isCloseTo(2.0f, within((float) 0d)); - assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); - - // long - e = parser.parseExpression("--lll"); - assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); - assertThat(return_lll).isEqualTo(66665L); - assertThat(helper.lll).isEqualTo(66665L); - - // int - e = parser.parseExpression("--iii"); - assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(41); - assertThat(helper.iii).isEqualTo(41); - return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(40); - assertThat(helper.iii).isEqualTo(40); - - // short - e = parser.parseExpression("--sss"); - assertThat(helper.sss).isEqualTo((short) 15); - int return_sss = (Integer)e.getValue(ctx); - assertThat(return_sss).isEqualTo(14); - assertThat(helper.sss).isEqualTo((short) 14); - } + @Test + void decrement02prefix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BigDecimal + e = parser.parseExpression("--bd"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); + assertThat(new BigDecimal("1").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("--ddd"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 1.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); + + // float + e = parser.parseExpression("--fff"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(2.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); + + // long + e = parser.parseExpression("--lll"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66665L); + assertThat(helper.lll).isEqualTo(66665L); + + // int + e = parser.parseExpression("--iii"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(41); + assertThat(helper.iii).isEqualTo(41); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(40); + assertThat(helper.iii).isEqualTo(40); + + // short + e = parser.parseExpression("--sss"); + assertThat(helper.sss).isEqualTo((short) 15); + int return_sss = (Integer)e.getValue(ctx); + assertThat(return_sss).isEqualTo(14); + assertThat(helper.sss).isEqualTo((short) 14); + } - @Test - public void decrement03() { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + @Test + void decrement03() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e1 = parser.parseExpression("m()--"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Double.TYPE)) + Expression e1 = parser.parseExpression("m()--"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Double.TYPE)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); - Expression e2 = parser.parseExpression("--m()"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Double.TYPE)) + Expression e2 = parser.parseExpression("--m()"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Double.TYPE)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); - } - + } - @Test - public void decrement04() { - Integer i = 42; - StandardEvaluationContext ctx = new StandardEvaluationContext(i); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e1 = parser.parseExpression("--1"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Integer.class)) + @Test + void decrement04() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e1 = parser.parseExpression("--1"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Integer.class)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); - Expression e2 = parser.parseExpression("1--"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Integer.class)) + Expression e2 = parser.parseExpression("1--"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Integer.class)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); - } - - @Test - public void incdecTogether() { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e; - - // index1 is 2 at the start - the 'intArray[#root.index1++]' should not be evaluated twice! - // intArray[2] is 3 - e = parser.parseExpression("intArray[#root.index1++]++"); - e.getValue(ctx, Integer.class); - assertThat(helper.index1).isEqualTo(3); - assertThat(helper.intArray[2]).isEqualTo(4); - - // index1 is 3 intArray[3] is 4 - e = parser.parseExpression("intArray[#root.index1++]--"); - assertThat(e.getValue(ctx, Integer.class).intValue()).isEqualTo(4); - assertThat(helper.index1).isEqualTo(4); - assertThat(helper.intArray[3]).isEqualTo(3); - - // index1 is 4, intArray[3] is 3 - e = parser.parseExpression("intArray[--#root.index1]++"); - assertThat(e.getValue(ctx, Integer.class).intValue()).isEqualTo(3); - assertThat(helper.index1).isEqualTo(3); - assertThat(helper.intArray[3]).isEqualTo(4); - } - - - // Verify how all the nodes behave with assignment (++, --, =) - @Test - public void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { - Spr9751 helper = new Spr9751(); - StandardEvaluationContext ctx = new StandardEvaluationContext(helper); - ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e; - - // BooleanLiteral - expectFailNotAssignable(parser, ctx, "true++"); - expectFailNotAssignable(parser, ctx, "--false"); - expectFailSetValueNotSupported(parser, ctx, "true=false"); - - // IntLiteral - expectFailNotAssignable(parser, ctx, "12++"); - expectFailNotAssignable(parser, ctx, "--1222"); - expectFailSetValueNotSupported(parser, ctx, "12=16"); - - // LongLiteral - expectFailNotAssignable(parser, ctx, "1.0d++"); - expectFailNotAssignable(parser, ctx, "--3.4d"); - expectFailSetValueNotSupported(parser, ctx, "1.0d=3.2d"); - - // NullLiteral - expectFailNotAssignable(parser, ctx, "null++"); - expectFailNotAssignable(parser, ctx, "--null"); - expectFailSetValueNotSupported(parser, ctx, "null=null"); - expectFailSetValueNotSupported(parser, ctx, "null=123"); - - // OpAnd - expectFailNotAssignable(parser, ctx, "(true && false)++"); - expectFailNotAssignable(parser, ctx, "--(false AND true)"); - expectFailSetValueNotSupported(parser, ctx, "(true && false)=(false && true)"); - - // OpDivide - expectFailNotAssignable(parser, ctx, "(3/4)++"); - expectFailNotAssignable(parser, ctx, "--(2/5)"); - expectFailSetValueNotSupported(parser, ctx, "(1/2)=(3/4)"); - - // OpEq - expectFailNotAssignable(parser, ctx, "(3==4)++"); - expectFailNotAssignable(parser, ctx, "--(2==5)"); - expectFailSetValueNotSupported(parser, ctx, "(1==2)=(3==4)"); - - // OpGE - expectFailNotAssignable(parser, ctx, "(3>=4)++"); - expectFailNotAssignable(parser, ctx, "--(2>=5)"); - expectFailSetValueNotSupported(parser, ctx, "(1>=2)=(3>=4)"); - - // OpGT - expectFailNotAssignable(parser, ctx, "(3>4)++"); - expectFailNotAssignable(parser, ctx, "--(2>5)"); - expectFailSetValueNotSupported(parser, ctx, "(1>2)=(3>4)"); - - // OpLE - expectFailNotAssignable(parser, ctx, "(3<=4)++"); - expectFailNotAssignable(parser, ctx, "--(2<=5)"); - expectFailSetValueNotSupported(parser, ctx, "(1<=2)=(3<=4)"); - - // OpLT - expectFailNotAssignable(parser, ctx, "(3<4)++"); - expectFailNotAssignable(parser, ctx, "--(2<5)"); - expectFailSetValueNotSupported(parser, ctx, "(1<2)=(3<4)"); - - // OpMinus - expectFailNotAssignable(parser, ctx, "(3-4)++"); - expectFailNotAssignable(parser, ctx, "--(2-5)"); - expectFailSetValueNotSupported(parser, ctx, "(1-2)=(3-4)"); - - // OpModulus - expectFailNotAssignable(parser, ctx, "(3%4)++"); - expectFailNotAssignable(parser, ctx, "--(2%5)"); - expectFailSetValueNotSupported(parser, ctx, "(1%2)=(3%4)"); - - // OpMultiply - expectFailNotAssignable(parser, ctx, "(3*4)++"); - expectFailNotAssignable(parser, ctx, "--(2*5)"); - expectFailSetValueNotSupported(parser, ctx, "(1*2)=(3*4)"); - - // OpNE - expectFailNotAssignable(parser, ctx, "(3!=4)++"); - expectFailNotAssignable(parser, ctx, "--(2!=5)"); - expectFailSetValueNotSupported(parser, ctx, "(1!=2)=(3!=4)"); - - // OpOr - expectFailNotAssignable(parser, ctx, "(true || false)++"); - expectFailNotAssignable(parser, ctx, "--(false OR true)"); - expectFailSetValueNotSupported(parser, ctx, "(true || false)=(false OR true)"); - - // OpPlus - expectFailNotAssignable(parser, ctx, "(3+4)++"); - expectFailNotAssignable(parser, ctx, "--(2+5)"); - expectFailSetValueNotSupported(parser, ctx, "(1+2)=(3+4)"); - - // RealLiteral - expectFailNotAssignable(parser, ctx, "1.0d++"); - expectFailNotAssignable(parser, ctx, "--2.0d"); - expectFailSetValueNotSupported(parser, ctx, "(1.0d)=(3.0d)"); - expectFailNotAssignable(parser, ctx, "1.0f++"); - expectFailNotAssignable(parser, ctx, "--2.0f"); - expectFailSetValueNotSupported(parser, ctx, "(1.0f)=(3.0f)"); - - // StringLiteral - expectFailNotAssignable(parser, ctx, "'abc'++"); - expectFailNotAssignable(parser, ctx, "--'def'"); - expectFailSetValueNotSupported(parser, ctx, "'abc'='def'"); - - // Ternary - expectFailNotAssignable(parser, ctx, "(true?true:false)++"); - expectFailNotAssignable(parser, ctx, "--(true?true:false)"); - expectFailSetValueNotSupported(parser, ctx, "(true?true:false)=(true?true:false)"); - - // TypeReference - expectFailNotAssignable(parser, ctx, "T(String)++"); - expectFailNotAssignable(parser, ctx, "--T(Integer)"); - expectFailSetValueNotSupported(parser, ctx, "T(String)=T(Integer)"); - - // OperatorBetween - expectFailNotAssignable(parser, ctx, "(3 between {1,5})++"); - expectFailNotAssignable(parser, ctx, "--(3 between {1,5})"); - expectFailSetValueNotSupported(parser, ctx, "(3 between {1,5})=(3 between {1,5})"); - - // OperatorInstanceOf - expectFailNotAssignable(parser, ctx, "(type instanceof T(String))++"); - expectFailNotAssignable(parser, ctx, "--(type instanceof T(String))"); - expectFailSetValueNotSupported(parser, ctx, "(type instanceof T(String))=(type instanceof T(String))"); - - // Elvis - expectFailNotAssignable(parser, ctx, "(true?:false)++"); - expectFailNotAssignable(parser, ctx, "--(true?:false)"); - expectFailSetValueNotSupported(parser, ctx, "(true?:false)=(true?:false)"); - - // OpInc - expectFailNotAssignable(parser, ctx, "(iii++)++"); - expectFailNotAssignable(parser, ctx, "--(++iii)"); - expectFailSetValueNotSupported(parser, ctx, "(iii++)=(++iii)"); - - // OpDec - expectFailNotAssignable(parser, ctx, "(iii--)++"); - expectFailNotAssignable(parser, ctx, "--(--iii)"); - expectFailSetValueNotSupported(parser, ctx, "(iii--)=(--iii)"); - - // OperatorNot - expectFailNotAssignable(parser, ctx, "(!true)++"); - expectFailNotAssignable(parser, ctx, "--(!false)"); - expectFailSetValueNotSupported(parser, ctx, "(!true)=(!false)"); - - // OperatorPower - expectFailNotAssignable(parser, ctx, "(iii^2)++"); - expectFailNotAssignable(parser, ctx, "--(iii^2)"); - expectFailSetValueNotSupported(parser, ctx, "(iii^2)=(iii^3)"); - - // Assign - // iii=42 - e = parser.parseExpression("iii=iii++"); - assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(helper.iii).isEqualTo(42); - assertThat(return_iii).isEqualTo(42); - - // Identifier - e = parser.parseExpression("iii++"); - assertThat(helper.iii).isEqualTo(42); - return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(42); - assertThat(helper.iii).isEqualTo(43); - - e = parser.parseExpression("--iii"); - assertThat(helper.iii).isEqualTo(43); - return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(42); - assertThat(helper.iii).isEqualTo(42); - - e = parser.parseExpression("iii=99"); - assertThat(helper.iii).isEqualTo(42); - return_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_iii).isEqualTo(99); - assertThat(helper.iii).isEqualTo(99); - - // CompoundExpression - // foo.iii == 99 - e = parser.parseExpression("foo.iii++"); - assertThat(helper.foo.iii).isEqualTo(99); - int return_foo_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_foo_iii).isEqualTo(99); - assertThat(helper.foo.iii).isEqualTo(100); - - e = parser.parseExpression("--foo.iii"); - assertThat(helper.foo.iii).isEqualTo(100); - return_foo_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_foo_iii).isEqualTo(99); - assertThat(helper.foo.iii).isEqualTo(99); - - e = parser.parseExpression("foo.iii=999"); - assertThat(helper.foo.iii).isEqualTo(99); - return_foo_iii = e.getValue(ctx, Integer.TYPE); - assertThat(return_foo_iii).isEqualTo(999); - assertThat(helper.foo.iii).isEqualTo(999); - - // ConstructorReference - expectFailNotAssignable(parser, ctx, "(new String('abc'))++"); - expectFailNotAssignable(parser, ctx, "--(new String('abc'))"); - expectFailSetValueNotSupported(parser, ctx, "(new String('abc'))=(new String('abc'))"); - - // MethodReference - expectFailNotIncrementable(parser, ctx, "m()++"); - expectFailNotDecrementable(parser, ctx, "--m()"); - expectFailSetValueNotSupported(parser, ctx, "m()=m()"); - - // OperatorMatches - expectFailNotAssignable(parser, ctx, "('abc' matches '^a..')++"); - expectFailNotAssignable(parser, ctx, "--('abc' matches '^a..')"); - expectFailSetValueNotSupported(parser, ctx, "('abc' matches '^a..')=('abc' matches '^a..')"); - - // Selection - ctx.registerFunction("isEven", Spr9751.class.getDeclaredMethod("isEven", Integer.TYPE)); - - expectFailNotIncrementable(parser, ctx, "({1,2,3}.?[#isEven(#this)])++"); - expectFailNotDecrementable(parser, ctx, "--({1,2,3}.?[#isEven(#this)])"); - expectFailNotAssignable(parser, ctx, "({1,2,3}.?[#isEven(#this)])=({1,2,3}.?[#isEven(#this)])"); - - // slightly diff here because return value isn't a list, it is a single entity - expectFailNotAssignable(parser, ctx, "({1,2,3}.^[#isEven(#this)])++"); - expectFailNotAssignable(parser, ctx, "--({1,2,3}.^[#isEven(#this)])"); - expectFailNotAssignable(parser, ctx, "({1,2,3}.^[#isEven(#this)])=({1,2,3}.^[#isEven(#this)])"); - - expectFailNotAssignable(parser, ctx, "({1,2,3}.$[#isEven(#this)])++"); - expectFailNotAssignable(parser, ctx, "--({1,2,3}.$[#isEven(#this)])"); - expectFailNotAssignable(parser, ctx, "({1,2,3}.$[#isEven(#this)])=({1,2,3}.$[#isEven(#this)])"); - - // FunctionReference - expectFailNotAssignable(parser, ctx, "#isEven(3)++"); - expectFailNotAssignable(parser, ctx, "--#isEven(4)"); - expectFailSetValueNotSupported(parser, ctx, "#isEven(3)=#isEven(5)"); - - // VariableReference - ctx.setVariable("wibble", "hello world"); - expectFailNotIncrementable(parser, ctx, "#wibble++"); - expectFailNotDecrementable(parser, ctx, "--#wibble"); - e = parser.parseExpression("#wibble=#wibble+#wibble"); - String s = e.getValue(ctx, String.class); - assertThat(s).isEqualTo("hello worldhello world"); - assertThat(ctx.lookupVariable("wibble")).isEqualTo("hello worldhello world"); - - ctx.setVariable("wobble", 3); - e = parser.parseExpression("#wobble++"); - assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); - int r = e.getValue(ctx, Integer.TYPE); - assertThat(r).isEqualTo(3); - assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(4); - - e = parser.parseExpression("--#wobble"); - assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(4); - r = e.getValue(ctx, Integer.TYPE); - assertThat(r).isEqualTo(3); - assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); - - e = parser.parseExpression("#wobble=34"); - assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); - r = e.getValue(ctx, Integer.TYPE); - assertThat(r).isEqualTo(34); - assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(34); - - // Projection - expectFailNotIncrementable(parser, ctx, "({1,2,3}.![#isEven(#this)])++"); // projection would be {false,true,false} - expectFailNotDecrementable(parser, ctx, "--({1,2,3}.![#isEven(#this)])"); // projection would be {false,true,false} - expectFailNotAssignable(parser, ctx, "({1,2,3}.![#isEven(#this)])=({1,2,3}.![#isEven(#this)])"); - - // InlineList - expectFailNotAssignable(parser, ctx, "({1,2,3})++"); - expectFailNotAssignable(parser, ctx, "--({1,2,3})"); - expectFailSetValueNotSupported(parser, ctx, "({1,2,3})=({1,2,3})"); - - // InlineMap - expectFailNotAssignable(parser, ctx, "({'a':1,'b':2,'c':3})++"); - expectFailNotAssignable(parser, ctx, "--({'a':1,'b':2,'c':3})"); - expectFailSetValueNotSupported(parser, ctx, "({'a':1,'b':2,'c':3})=({'a':1,'b':2,'c':3})"); - - // BeanReference - ctx.setBeanResolver(new MyBeanResolver()); - expectFailNotAssignable(parser, ctx, "@foo++"); - expectFailNotAssignable(parser, ctx, "--@foo"); - expectFailSetValueNotSupported(parser, ctx, "@foo=@bar"); - - // PropertyOrFieldReference - helper.iii = 42; - e = parser.parseExpression("iii++"); - assertThat(helper.iii).isEqualTo(42); - r = e.getValue(ctx, Integer.TYPE); - assertThat(r).isEqualTo(42); - assertThat(helper.iii).isEqualTo(43); - - e = parser.parseExpression("--iii"); - assertThat(helper.iii).isEqualTo(43); - r = e.getValue(ctx, Integer.TYPE); - assertThat(r).isEqualTo(42); - assertThat(helper.iii).isEqualTo(42); - - e = parser.parseExpression("iii=100"); - assertThat(helper.iii).isEqualTo(42); - r = e.getValue(ctx, Integer.TYPE); - assertThat(r).isEqualTo(100); - assertThat(helper.iii).isEqualTo(100); - } - - private void expectFailNotAssignable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { - expectFail(parser, eContext, expressionString, SpelMessage.NOT_ASSIGNABLE); - } - - private void expectFailSetValueNotSupported(ExpressionParser parser, EvaluationContext eContext, String expressionString) { - expectFail(parser, eContext, expressionString, SpelMessage.SETVALUE_NOT_SUPPORTED); - } - - private void expectFailNotIncrementable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { - expectFail(parser, eContext, expressionString, SpelMessage.OPERAND_NOT_INCREMENTABLE); - } + } - private void expectFailNotDecrementable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { - expectFail(parser, eContext, expressionString, SpelMessage.OPERAND_NOT_DECREMENTABLE); - } + @Test + void incrementAndDecrementTogether() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // index1 is 2 at the start - the 'intArray[#root.index1++]' should not be evaluated twice! + // intArray[2] is 3 + e = parser.parseExpression("intArray[#root.index1++]++"); + e.getValue(ctx, Integer.class); + assertThat(helper.index1).isEqualTo(3); + assertThat(helper.intArray[2]).isEqualTo(4); + + // index1 is 3 intArray[3] is 4 + e = parser.parseExpression("intArray[#root.index1++]--"); + assertThat(e.getValue(ctx, Integer.class).intValue()).isEqualTo(4); + assertThat(helper.index1).isEqualTo(4); + assertThat(helper.intArray[3]).isEqualTo(3); + + // index1 is 4, intArray[3] is 3 + e = parser.parseExpression("intArray[--#root.index1]++"); + assertThat(e.getValue(ctx, Integer.class).intValue()).isEqualTo(3); + assertThat(helper.index1).isEqualTo(3); + assertThat(helper.intArray[3]).isEqualTo(4); + } - private void expectFail(ExpressionParser parser, EvaluationContext eContext, String expressionString, SpelMessage messageCode) { - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> { - Expression e = parser.parseExpression(expressionString); - SpelUtilities.printAbstractSyntaxTree(System.out, e); - e.getValue(eContext); - }).satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode)); - } + // Verify how all the nodes behave with assignment (++, --, =) + @Test + void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BooleanLiteral + expectFailNotAssignable(parser, ctx, "true++"); + expectFailNotAssignable(parser, ctx, "--false"); + expectFailSetValueNotSupported(parser, ctx, "true=false"); + + // IntLiteral + expectFailNotAssignable(parser, ctx, "12++"); + expectFailNotAssignable(parser, ctx, "--1222"); + expectFailSetValueNotSupported(parser, ctx, "12=16"); + + // LongLiteral + expectFailNotAssignable(parser, ctx, "1.0d++"); + expectFailNotAssignable(parser, ctx, "--3.4d"); + expectFailSetValueNotSupported(parser, ctx, "1.0d=3.2d"); + + // NullLiteral + expectFailNotAssignable(parser, ctx, "null++"); + expectFailNotAssignable(parser, ctx, "--null"); + expectFailSetValueNotSupported(parser, ctx, "null=null"); + expectFailSetValueNotSupported(parser, ctx, "null=123"); + + // OpAnd + expectFailNotAssignable(parser, ctx, "(true && false)++"); + expectFailNotAssignable(parser, ctx, "--(false AND true)"); + expectFailSetValueNotSupported(parser, ctx, "(true && false)=(false && true)"); + + // OpDivide + expectFailNotAssignable(parser, ctx, "(3/4)++"); + expectFailNotAssignable(parser, ctx, "--(2/5)"); + expectFailSetValueNotSupported(parser, ctx, "(1/2)=(3/4)"); + + // OpEq + expectFailNotAssignable(parser, ctx, "(3==4)++"); + expectFailNotAssignable(parser, ctx, "--(2==5)"); + expectFailSetValueNotSupported(parser, ctx, "(1==2)=(3==4)"); + + // OpGE + expectFailNotAssignable(parser, ctx, "(3>=4)++"); + expectFailNotAssignable(parser, ctx, "--(2>=5)"); + expectFailSetValueNotSupported(parser, ctx, "(1>=2)=(3>=4)"); + + // OpGT + expectFailNotAssignable(parser, ctx, "(3>4)++"); + expectFailNotAssignable(parser, ctx, "--(2>5)"); + expectFailSetValueNotSupported(parser, ctx, "(1>2)=(3>4)"); + + // OpLE + expectFailNotAssignable(parser, ctx, "(3<=4)++"); + expectFailNotAssignable(parser, ctx, "--(2<=5)"); + expectFailSetValueNotSupported(parser, ctx, "(1<=2)=(3<=4)"); + + // OpLT + expectFailNotAssignable(parser, ctx, "(3<4)++"); + expectFailNotAssignable(parser, ctx, "--(2<5)"); + expectFailSetValueNotSupported(parser, ctx, "(1<2)=(3<4)"); + + // OpMinus + expectFailNotAssignable(parser, ctx, "(3-4)++"); + expectFailNotAssignable(parser, ctx, "--(2-5)"); + expectFailSetValueNotSupported(parser, ctx, "(1-2)=(3-4)"); + + // OpModulus + expectFailNotAssignable(parser, ctx, "(3%4)++"); + expectFailNotAssignable(parser, ctx, "--(2%5)"); + expectFailSetValueNotSupported(parser, ctx, "(1%2)=(3%4)"); + + // OpMultiply + expectFailNotAssignable(parser, ctx, "(3*4)++"); + expectFailNotAssignable(parser, ctx, "--(2*5)"); + expectFailSetValueNotSupported(parser, ctx, "(1*2)=(3*4)"); + + // OpNE + expectFailNotAssignable(parser, ctx, "(3!=4)++"); + expectFailNotAssignable(parser, ctx, "--(2!=5)"); + expectFailSetValueNotSupported(parser, ctx, "(1!=2)=(3!=4)"); + + // OpOr + expectFailNotAssignable(parser, ctx, "(true || false)++"); + expectFailNotAssignable(parser, ctx, "--(false OR true)"); + expectFailSetValueNotSupported(parser, ctx, "(true || false)=(false OR true)"); + + // OpPlus + expectFailNotAssignable(parser, ctx, "(3+4)++"); + expectFailNotAssignable(parser, ctx, "--(2+5)"); + expectFailSetValueNotSupported(parser, ctx, "(1+2)=(3+4)"); + + // RealLiteral + expectFailNotAssignable(parser, ctx, "1.0d++"); + expectFailNotAssignable(parser, ctx, "--2.0d"); + expectFailSetValueNotSupported(parser, ctx, "(1.0d)=(3.0d)"); + expectFailNotAssignable(parser, ctx, "1.0f++"); + expectFailNotAssignable(parser, ctx, "--2.0f"); + expectFailSetValueNotSupported(parser, ctx, "(1.0f)=(3.0f)"); + + // StringLiteral + expectFailNotAssignable(parser, ctx, "'abc'++"); + expectFailNotAssignable(parser, ctx, "--'def'"); + expectFailSetValueNotSupported(parser, ctx, "'abc'='def'"); + + // Ternary + expectFailNotAssignable(parser, ctx, "(true?true:false)++"); + expectFailNotAssignable(parser, ctx, "--(true?true:false)"); + expectFailSetValueNotSupported(parser, ctx, "(true?true:false)=(true?true:false)"); + + // TypeReference + expectFailNotAssignable(parser, ctx, "T(String)++"); + expectFailNotAssignable(parser, ctx, "--T(Integer)"); + expectFailSetValueNotSupported(parser, ctx, "T(String)=T(Integer)"); + + // OperatorBetween + expectFailNotAssignable(parser, ctx, "(3 between {1,5})++"); + expectFailNotAssignable(parser, ctx, "--(3 between {1,5})"); + expectFailSetValueNotSupported(parser, ctx, "(3 between {1,5})=(3 between {1,5})"); + + // OperatorInstanceOf + expectFailNotAssignable(parser, ctx, "(type instanceof T(String))++"); + expectFailNotAssignable(parser, ctx, "--(type instanceof T(String))"); + expectFailSetValueNotSupported(parser, ctx, "(type instanceof T(String))=(type instanceof T(String))"); + + // Elvis + expectFailNotAssignable(parser, ctx, "(true?:false)++"); + expectFailNotAssignable(parser, ctx, "--(true?:false)"); + expectFailSetValueNotSupported(parser, ctx, "(true?:false)=(true?:false)"); + + // OpInc + expectFailNotAssignable(parser, ctx, "(iii++)++"); + expectFailNotAssignable(parser, ctx, "--(++iii)"); + expectFailSetValueNotSupported(parser, ctx, "(iii++)=(++iii)"); + + // OpDec + expectFailNotAssignable(parser, ctx, "(iii--)++"); + expectFailNotAssignable(parser, ctx, "--(--iii)"); + expectFailSetValueNotSupported(parser, ctx, "(iii--)=(--iii)"); + + // OperatorNot + expectFailNotAssignable(parser, ctx, "(!true)++"); + expectFailNotAssignable(parser, ctx, "--(!false)"); + expectFailSetValueNotSupported(parser, ctx, "(!true)=(!false)"); + + // OperatorPower + expectFailNotAssignable(parser, ctx, "(iii^2)++"); + expectFailNotAssignable(parser, ctx, "--(iii^2)"); + expectFailSetValueNotSupported(parser, ctx, "(iii^2)=(iii^3)"); + + // Assign + // iii=42 + e = parser.parseExpression("iii=iii++"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(helper.iii).isEqualTo(42); + assertThat(return_iii).isEqualTo(42); + + // Identifier + e = parser.parseExpression("iii++"); + assertThat(helper.iii).isEqualTo(42); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(43); + + e = parser.parseExpression("--iii"); + assertThat(helper.iii).isEqualTo(43); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(42); + + e = parser.parseExpression("iii=99"); + assertThat(helper.iii).isEqualTo(42); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(99); + assertThat(helper.iii).isEqualTo(99); + + // CompoundExpression + // foo.iii == 99 + e = parser.parseExpression("foo.iii++"); + assertThat(helper.foo.iii).isEqualTo(99); + int return_foo_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_foo_iii).isEqualTo(99); + assertThat(helper.foo.iii).isEqualTo(100); + + e = parser.parseExpression("--foo.iii"); + assertThat(helper.foo.iii).isEqualTo(100); + return_foo_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_foo_iii).isEqualTo(99); + assertThat(helper.foo.iii).isEqualTo(99); + + e = parser.parseExpression("foo.iii=999"); + assertThat(helper.foo.iii).isEqualTo(99); + return_foo_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_foo_iii).isEqualTo(999); + assertThat(helper.foo.iii).isEqualTo(999); + + // ConstructorReference + expectFailNotAssignable(parser, ctx, "(new String('abc'))++"); + expectFailNotAssignable(parser, ctx, "--(new String('abc'))"); + expectFailSetValueNotSupported(parser, ctx, "(new String('abc'))=(new String('abc'))"); + + // MethodReference + expectFailNotIncrementable(parser, ctx, "m()++"); + expectFailNotDecrementable(parser, ctx, "--m()"); + expectFailSetValueNotSupported(parser, ctx, "m()=m()"); + + // OperatorMatches + expectFailNotAssignable(parser, ctx, "('abc' matches '^a..')++"); + expectFailNotAssignable(parser, ctx, "--('abc' matches '^a..')"); + expectFailSetValueNotSupported(parser, ctx, "('abc' matches '^a..')=('abc' matches '^a..')"); + + // Selection + ctx.registerFunction("isEven", Spr9751.class.getDeclaredMethod("isEven", Integer.TYPE)); + + expectFailNotIncrementable(parser, ctx, "({1,2,3}.?[#isEven(#this)])++"); + expectFailNotDecrementable(parser, ctx, "--({1,2,3}.?[#isEven(#this)])"); + expectFailNotAssignable(parser, ctx, "({1,2,3}.?[#isEven(#this)])=({1,2,3}.?[#isEven(#this)])"); + + // slightly diff here because return value isn't a list, it is a single entity + expectFailNotAssignable(parser, ctx, "({1,2,3}.^[#isEven(#this)])++"); + expectFailNotAssignable(parser, ctx, "--({1,2,3}.^[#isEven(#this)])"); + expectFailNotAssignable(parser, ctx, "({1,2,3}.^[#isEven(#this)])=({1,2,3}.^[#isEven(#this)])"); + + expectFailNotAssignable(parser, ctx, "({1,2,3}.$[#isEven(#this)])++"); + expectFailNotAssignable(parser, ctx, "--({1,2,3}.$[#isEven(#this)])"); + expectFailNotAssignable(parser, ctx, "({1,2,3}.$[#isEven(#this)])=({1,2,3}.$[#isEven(#this)])"); + + // FunctionReference + expectFailNotAssignable(parser, ctx, "#isEven(3)++"); + expectFailNotAssignable(parser, ctx, "--#isEven(4)"); + expectFailSetValueNotSupported(parser, ctx, "#isEven(3)=#isEven(5)"); + + // VariableReference + ctx.setVariable("wibble", "hello world"); + expectFailNotIncrementable(parser, ctx, "#wibble++"); + expectFailNotDecrementable(parser, ctx, "--#wibble"); + e = parser.parseExpression("#wibble=#wibble+#wibble"); + String s = e.getValue(ctx, String.class); + assertThat(s).isEqualTo("hello worldhello world"); + assertThat(ctx.lookupVariable("wibble")).isEqualTo("hello worldhello world"); + + ctx.setVariable("wobble", 3); + e = parser.parseExpression("#wobble++"); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); + int r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(3); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(4); + + e = parser.parseExpression("--#wobble"); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(4); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(3); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); + + e = parser.parseExpression("#wobble=34"); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(34); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(34); + + // Projection + expectFailNotIncrementable(parser, ctx, "({1,2,3}.![#isEven(#this)])++"); // projection would be {false,true,false} + expectFailNotDecrementable(parser, ctx, "--({1,2,3}.![#isEven(#this)])"); // projection would be {false,true,false} + expectFailNotAssignable(parser, ctx, "({1,2,3}.![#isEven(#this)])=({1,2,3}.![#isEven(#this)])"); + + // InlineList + expectFailNotAssignable(parser, ctx, "({1,2,3})++"); + expectFailNotAssignable(parser, ctx, "--({1,2,3})"); + expectFailSetValueNotSupported(parser, ctx, "({1,2,3})=({1,2,3})"); + + // InlineMap + expectFailNotAssignable(parser, ctx, "({'a':1,'b':2,'c':3})++"); + expectFailNotAssignable(parser, ctx, "--({'a':1,'b':2,'c':3})"); + expectFailSetValueNotSupported(parser, ctx, "({'a':1,'b':2,'c':3})=({'a':1,'b':2,'c':3})"); + + // BeanReference + BeanResolver beanResolver = (context, beanName) -> { + if (beanName.equals("foo") || beanName.equals("bar")) { + return new Spr9751_2(); + } + throw new AccessException("unknown bean " + beanName); + }; + ctx.setBeanResolver(beanResolver); + expectFailNotAssignable(parser, ctx, "@foo++"); + expectFailNotAssignable(parser, ctx, "--@foo"); + expectFailSetValueNotSupported(parser, ctx, "@foo=@bar"); + + // PropertyOrFieldReference + helper.iii = 42; + e = parser.parseExpression("iii++"); + assertThat(helper.iii).isEqualTo(42); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(42); + assertThat(helper.iii).isEqualTo(43); + + e = parser.parseExpression("--iii"); + assertThat(helper.iii).isEqualTo(43); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(42); + assertThat(helper.iii).isEqualTo(42); + + e = parser.parseExpression("iii=100"); + assertThat(helper.iii).isEqualTo(42); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(100); + assertThat(helper.iii).isEqualTo(100); + } - static class CustomMethodResolver implements MethodResolver { + private void expectFailNotAssignable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.NOT_ASSIGNABLE); + } - @Override - public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, - List argumentTypes) throws AccessException { - return null; + private void expectFailSetValueNotSupported(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.SETVALUE_NOT_SUPPORTED); } - } + private void expectFailNotIncrementable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.OPERAND_NOT_INCREMENTABLE); + } - static class CustomMethodFilter implements MethodFilter { + private void expectFailNotDecrementable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.OPERAND_NOT_DECREMENTABLE); + } - @Override - public List filter(List methods) { - return null; + private void expectFail(ExpressionParser parser, EvaluationContext eContext, String expressionString, SpelMessage messageCode) { + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> { + Expression e = parser.parseExpression(expressionString); + SpelUtilities.printAbstractSyntaxTree(System.out, e); + e.getValue(eContext); + }).satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode)); } } @@ -1433,16 +1459,4 @@ static class Spr9751_2 { public int iii = 99; } - - static class MyBeanResolver implements BeanResolver { - - @Override - public Object resolve(EvaluationContext context, String beanName) throws AccessException { - if (beanName.equals("foo") || beanName.equals("bar")) { - return new Spr9751_2(); - } - throw new AccessException("not heard of " + beanName); - } - } - } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index dcc4511f4cd8..280b452007b0 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -41,16 +41,17 @@ /** * Test the examples specified in the documentation. * - * NOTE: any outgoing changes from this file upon synchronizing with the repo may indicate that + *

NOTE: any outgoing changes from this file upon synchronizing with the repo may indicate that * you need to update the documentation too ! * * @author Andy Clement */ @SuppressWarnings("rawtypes") -public class SpelDocumentationTests extends AbstractExpressionTests { +class SpelDocumentationTests extends AbstractExpressionTests { - static Inventor tesla ; - static Inventor pupin ; + static Inventor tesla; + + static Inventor pupin; static { GregorianCalendar c = new GregorianCalendar(); @@ -65,53 +66,24 @@ public class SpelDocumentationTests extends AbstractExpressionTests { pupin.setPlaceOfBirth(new PlaceOfBirth("Idvor")); } - static class IEEE { - private String name; - - - public Inventor[] Members = new Inventor[1]; - public List Members2 = new ArrayList(); - public Map officers = new HashMap<>(); - - public List> reverse = new ArrayList<>(); - - @SuppressWarnings("unchecked") - IEEE() { - officers.put("president",pupin); - List linv = new ArrayList(); - linv.add(tesla); - officers.put("advisors",linv); - Members2.add(tesla); - Members2.add(pupin); - - reverse.add(officers); - } - - public boolean isMember(String name) { - return true; - } - - public String getName() { return name; } - public void setName(String n) { this.name = n; } - } @Test - public void testMethodInvocation() { + void methodInvocation() { evaluate("'Hello World'.concat('!')","Hello World!",String.class); } @Test - public void testBeanPropertyAccess() { + void beanPropertyAccess() { evaluate("new String('Hello World'.bytes)","Hello World",String.class); } @Test - public void testArrayLengthAccess() { + void arrayLengthAccess() { evaluate("'Hello World'.bytes.length",11,Integer.class); } @Test - public void testRootObject() throws Exception { + void rootObject() throws Exception { GregorianCalendar c = new GregorianCalendar(); c.set(1856, 7, 9); @@ -129,7 +101,7 @@ public void testRootObject() throws Exception { } @Test - public void testEqualityCheck() throws Exception { + void equalityCheck() throws Exception { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); @@ -143,13 +115,13 @@ public void testEqualityCheck() throws Exception { // Section 7.4.1 @Test - public void testXMLBasedConfig() { + void xmlBasedConfig() { evaluate("(T(java.lang.Math).random() * 100.0 )>0",true,Boolean.class); } // Section 7.5 @Test - public void testLiterals() throws Exception { + void literals() throws Exception { ExpressionParser parser = new SpelExpressionParser(); String helloWorld = (String) parser.parseExpression("'Hello World'").getValue(); // evals to "Hello World" @@ -169,7 +141,7 @@ public void testLiterals() throws Exception { } @Test - public void testPropertyAccess() throws Exception { + void propertyAccess() throws Exception { EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context); // 1856 assertThat(year).isEqualTo(1856); @@ -179,12 +151,12 @@ public void testPropertyAccess() throws Exception { } @Test - public void testPropertyNavigation() throws Exception { + void propertyNavigation() throws Exception { ExpressionParser parser = new SpelExpressionParser(); // Inventions Array StandardEvaluationContext teslaContext = TestScenarioCreator.getTestEvaluationContext(); -// teslaContext.setRootObject(tesla); + // teslaContext.setRootObject(tesla); // evaluates to "Induction motor" String invention = parser.parseExpression("inventions[3]").getValue(teslaContext, String.class); @@ -206,9 +178,8 @@ public void testPropertyNavigation() throws Exception { assertThat(invention).isEqualTo("Wireless communication"); } - @Test - public void testDictionaryAccess() throws Exception { + void dictionaryAccess() throws Exception { StandardEvaluationContext societyContext = new StandardEvaluationContext(); societyContext.setRootObject(new IEEE()); // Officer's Dictionary @@ -233,7 +204,7 @@ public void testDictionaryAccess() throws Exception { // 7.5.3 @Test - public void testMethodInvocation2() throws Exception { + void methodInvocation2() throws Exception { // string literal, evaluates to "bc" String c = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class); assertThat(c).isEqualTo("bc"); @@ -248,7 +219,7 @@ public void testMethodInvocation2() throws Exception { // 7.5.4.1 @Test - public void testRelationalOperators() throws Exception { + void relationalOperators() throws Exception { boolean result = parser.parseExpression("2 == 2").getValue(Boolean.class); assertThat(result).isTrue(); // evaluates to false @@ -261,7 +232,7 @@ public void testRelationalOperators() throws Exception { } @Test - public void testOtherOperators() throws Exception { + void otherOperators() throws Exception { // evaluates to false boolean falseValue = parser.parseExpression("'xyz' instanceof T(int)").getValue(Boolean.class); assertThat(falseValue).isFalse(); @@ -278,7 +249,7 @@ public void testOtherOperators() throws Exception { // 7.5.4.2 @Test - public void testLogicalOperators() throws Exception { + void logicalOperators() throws Exception { StandardEvaluationContext societyContext = new StandardEvaluationContext(); societyContext.setRootObject(new IEEE()); @@ -319,7 +290,7 @@ public void testLogicalOperators() throws Exception { // 7.5.4.3 @Test - public void testNumericalOperators() throws Exception { + void numericalOperators() throws Exception { // Addition int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2 assertThat(two).isEqualTo(2); @@ -363,7 +334,7 @@ public void testNumericalOperators() throws Exception { // 7.5.5 @Test - public void testAssignment() throws Exception { + void assignment() throws Exception { Inventor inventor = new Inventor(); StandardEvaluationContext inventorContext = new StandardEvaluationContext(); inventorContext.setRootObject(inventor); @@ -381,7 +352,7 @@ public void testAssignment() throws Exception { // 7.5.6 @Test - public void testTypes() throws Exception { + void types() throws Exception { Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); assertThat(dateClass).isEqualTo(Date.class); boolean trueValue = parser.parseExpression("T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR").getValue(Boolean.class); @@ -391,7 +362,7 @@ public void testTypes() throws Exception { // 7.5.7 @Test - public void testConstructors() throws Exception { + void constructors() throws Exception { StandardEvaluationContext societyContext = new StandardEvaluationContext(); societyContext.setRootObject(new IEEE()); Inventor einstein = @@ -404,7 +375,7 @@ public void testConstructors() throws Exception { // 7.5.8 @Test - public void testVariables() throws Exception { + void variables() throws Exception { Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("newName", "Mike Tesla"); @@ -416,9 +387,9 @@ public void testVariables() throws Exception { assertThat(tesla.getFoo()).isEqualTo("Mike Tesla"); } - @SuppressWarnings("unchecked") @Test - public void testSpecialVariables() throws Exception { + @SuppressWarnings("unchecked") + void specialVariables() throws Exception { // create an array of integers List primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17); @@ -435,7 +406,7 @@ public void testSpecialVariables() throws Exception { // 7.5.9 @Test - public void testFunctions() throws Exception { + void functions() throws Exception { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.registerFunction("reverseString", StringUtils.class.getDeclaredMethod("reverseString", String.class)); @@ -447,7 +418,7 @@ public void testFunctions() throws Exception { // 7.5.10 @Test - public void testTernary() throws Exception { + void ternary() throws Exception { String falseString = parser.parseExpression("false ? 'trueExp' : 'falseExp'").getValue(String.class); assertThat(falseString).isEqualTo("falseExp"); @@ -468,9 +439,9 @@ public void testTernary() throws Exception { // 7.5.11 - @SuppressWarnings("unchecked") @Test - public void testSelection() throws Exception { + @SuppressWarnings("unchecked") + void selection() throws Exception { StandardEvaluationContext societyContext = new StandardEvaluationContext(); societyContext.setRootObject(new IEEE()); List list = (List) parser.parseExpression("Members2.?[nationality == 'Serbian']").getValue(societyContext); @@ -481,7 +452,7 @@ public void testSelection() throws Exception { // 7.5.12 @Test - public void testTemplating() throws Exception { + void templating() throws Exception { String randomPhrase = parser.parseExpression("random number is ${T(java.lang.Math).random()}", new TemplatedParserContext()).getValue(String.class); assertThat(randomPhrase.startsWith("random number")).isTrue(); @@ -505,14 +476,39 @@ public boolean isTemplate() { } } + static class IEEE { + private String name; + + public Inventor[] Members = new Inventor[1]; + public List Members2 = new ArrayList(); + public Map officers = new HashMap<>(); + + public List> reverse = new ArrayList<>(); + + @SuppressWarnings("unchecked") + IEEE() { + officers.put("president",pupin); + List linv = new ArrayList(); + linv.add(tesla); + officers.put("advisors",linv); + Members2.add(tesla); + Members2.add(pupin); + + reverse.add(officers); + } + + public boolean isMember(String name) { + return true; + } + + public String getName() { return name; } + public void setName(String n) { this.name = n; } + } + static class StringUtils { public static String reverseString(String input) { - StringBuilder backwards = new StringBuilder(); - for (int i = 0; i < input.length(); i++) { - backwards.append(input.charAt(input.length() - 1 - i)); - } - return backwards.toString(); + return new StringBuilder(input).reverse().toString(); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java index 4bb3f7da0cc3..ebeebcf7e0ce 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java @@ -30,7 +30,7 @@ *

  • The root context object is an Inventor instance {@link Inventor} * */ -public class TestScenarioCreator { +class TestScenarioCreator { public static StandardEvaluationContext getTestEvaluationContext() { StandardEvaluationContext testContext = new StandardEvaluationContext(); From a7d5fbfbea1b1c4948a8f8cc5541f90bd2a1d972 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 1 Mar 2022 13:27:37 +0100 Subject: [PATCH 013/131] Fix log messages for init/destroy method registration --- .../annotation/InitDestroyAnnotationBeanPostProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java index f76a03b8dc9d..97e65f155816 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -302,7 +302,7 @@ public void checkConfigMembers(RootBeanDefinition beanDefinition) { beanDefinition.registerExternallyManagedInitMethod(methodIdentifier); checkedInitMethods.add(element); if (logger.isTraceEnabled()) { - logger.trace("Registered init method on class [" + this.targetClass.getName() + "]: " + element); + logger.trace("Registered init method on class [" + this.targetClass.getName() + "]: " + methodIdentifier); } } } @@ -313,7 +313,7 @@ public void checkConfigMembers(RootBeanDefinition beanDefinition) { beanDefinition.registerExternallyManagedDestroyMethod(methodIdentifier); checkedDestroyMethods.add(element); if (logger.isTraceEnabled()) { - logger.trace("Registered destroy method on class [" + this.targetClass.getName() + "]: " + element); + logger.trace("Registered destroy method on class [" + this.targetClass.getName() + "]: " + methodIdentifier); } } } From f96872404d9ab495a2821955b649fec9a28f92ed Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 1 Mar 2022 15:02:57 +0100 Subject: [PATCH 014/131] Ensure private init/destroy method is invoked only once Closes gh-28083 --- .../AbstractAutowireCapableBeanFactory.java | 4 +- .../support/DisposableBeanAdapter.java | 5 +- .../factory/support/RootBeanDefinition.java | 63 ++++++++++++++++++- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 7a94a91c86a8..c1e764b22a5f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1844,7 +1844,7 @@ protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBea throws Throwable { boolean isInitializingBean = (bean instanceof InitializingBean); - if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { + if (isInitializingBean && (mbd == null || !mbd.hasAnyExternallyManagedInitMethod("afterPropertiesSet"))) { if (logger.isTraceEnabled()) { logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); } @@ -1868,7 +1868,7 @@ protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBea String initMethodName = mbd.getInitMethodName(); if (StringUtils.hasLength(initMethodName) && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && - !mbd.isExternallyManagedInitMethod(initMethodName)) { + !mbd.hasAnyExternallyManagedInitMethod(initMethodName)) { invokeCustomInitMethod(beanName, bean, mbd); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index b5fdef4dd813..4317ae901c9b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -52,6 +52,7 @@ * @author Juergen Hoeller * @author Costin Leau * @author Stephane Nicoll + * @author Sam Brannen * @since 2.0 * @see AbstractBeanFactory * @see org.springframework.beans.factory.DisposableBean @@ -109,12 +110,12 @@ public DisposableBeanAdapter(Object bean, String beanName, RootBeanDefinition be this.beanName = beanName; this.nonPublicAccessAllowed = beanDefinition.isNonPublicAccessAllowed(); this.invokeDisposableBean = (bean instanceof DisposableBean && - !beanDefinition.isExternallyManagedDestroyMethod(DESTROY_METHOD_NAME)); + !beanDefinition.hasAnyExternallyManagedDestroyMethod(DESTROY_METHOD_NAME)); String destroyMethodName = inferDestroyMethodIfNecessary(bean, beanDefinition); if (destroyMethodName != null && !(this.invokeDisposableBean && DESTROY_METHOD_NAME.equals(destroyMethodName)) && - !beanDefinition.isExternallyManagedDestroyMethod(destroyMethodName)) { + !beanDefinition.hasAnyExternallyManagedDestroyMethod(destroyMethodName)) { this.invokeAutoCloseable = (bean instanceof AutoCloseable && CLOSE_METHOD_NAME.equals(destroyMethodName)); if (!this.invokeAutoCloseable) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index f4fdafa3767e..13638768efa1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -49,6 +49,7 @@ * * @author Rod Johnson * @author Juergen Hoeller + * @author Sam Brannen * @see GenericBeanDefinition * @see ChildBeanDefinition */ @@ -479,6 +480,36 @@ public boolean isExternallyManagedInitMethod(String initMethod) { } } + /** + * Determine if the given method name indicates an externally managed + * initialization method, regardless of method visibility. + *

    In contrast to {@link #isExternallyManagedInitMethod(String)}, this + * method also returns {@code true} if there is a {@code private} external + * init method that has been + * {@linkplain #registerExternallyManagedInitMethod(String) registered} + * using a fully qualified method name instead of a simple method name. + * @since 5.3.17 + */ + boolean hasAnyExternallyManagedInitMethod(String initMethod) { + synchronized (this.postProcessingLock) { + if (isExternallyManagedInitMethod(initMethod)) { + return true; + } + if (this.externallyManagedInitMethods != null) { + for (String candidate : this.externallyManagedInitMethods) { + int indexOfDot = candidate.lastIndexOf("."); + if (indexOfDot >= 0) { + String methodName = candidate.substring(indexOfDot + 1); + if (methodName.equals(initMethod)) { + return true; + } + } + } + } + return false; + } + } + /** * Return all externally managed initialization methods (as an immutable Set). * @since 5.3.11 @@ -513,6 +544,36 @@ public boolean isExternallyManagedDestroyMethod(String destroyMethod) { } } + /** + * Determine if the given method name indicates an externally managed + * destruction method, regardless of method visibility. + *

    In contrast to {@link #isExternallyManagedDestroyMethod(String)}, this + * method also returns {@code true} if there is a {@code private} external + * destroy method that has been + * {@linkplain #registerExternallyManagedDestroyMethod(String) registered} + * using a fully qualified method name instead of a simple method name. + * @since 5.3.17 + */ + boolean hasAnyExternallyManagedDestroyMethod(String destroyMethod) { + synchronized (this.postProcessingLock) { + if (isExternallyManagedDestroyMethod(destroyMethod)) { + return true; + } + if (this.externallyManagedDestroyMethods != null) { + for (String candidate : this.externallyManagedDestroyMethods) { + int indexOfDot = candidate.lastIndexOf("."); + if (indexOfDot >= 0) { + String methodName = candidate.substring(indexOfDot + 1); + if (methodName.equals(destroyMethod)) { + return true; + } + } + } + } + return false; + } + } + /** * Return all externally managed destruction methods (as an immutable Set). * @since 5.3.11 From af14eea1ef76576acd07ba90cd0a656bd6b31969 Mon Sep 17 00:00:00 2001 From: Vikey Chen Date: Tue, 1 Mar 2022 00:49:43 +0800 Subject: [PATCH 015/131] Introduce tests for gh-28083 --- .../Spr3775InitDestroyLifecycleTests.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java index 0fb249595f43..8fb07dc306f9 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java @@ -149,6 +149,30 @@ public void testJsr250AnnotationsWithShadowedMethods() { bean.destroyMethods); } + @Test + public void testJsr250AnnotationsWithCustomPrivateInitDestroyMethods() { + Class beanClass = CustomAnnotatedPrivateInitDestroyBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); + CustomAnnotatedPrivateInitDestroyBean bean = + (CustomAnnotatedPrivateInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering("init-methods", Arrays.asList("privateCustomInit1","afterPropertiesSet"), bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering("destroy-methods", Arrays.asList("privateCustomDestroy1","destroy"), bean.destroyMethods); + } + + @Test + public void testJsr250AnnotationsWithCustomSameMethodNames() { + Class beanClass = CustomAnnotatedPrivateSameNameInitDestroyBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); + CustomAnnotatedPrivateSameNameInitDestroyBean bean = + (CustomAnnotatedPrivateSameNameInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering("init-methods", + Arrays.asList("privateCustomInit1","afterPropertiesSet","sameNameCustomInit1"), bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering("destroy-methods", + Arrays.asList("privateCustomDestroy1","destroy","sameNameCustomDestroy1"), bean.destroyMethods); + } + @Test public void testAllLifecycleMechanismsAtOnce() { final Class beanClass = AllInOneBean.class; @@ -205,6 +229,31 @@ public void customDestroy() throws Exception { } } + public static class CustomAnnotatedPrivateInitDestroyBean extends CustomInitializingDisposableBean{ + + @PostConstruct + private void customInit1() throws Exception { + this.initMethods.add("privateCustomInit1"); + } + + @PreDestroy + private void customDestroy1() throws Exception { + this.destroyMethods.add("privateCustomDestroy1"); + } + + } + + public static class CustomAnnotatedPrivateSameNameInitDestroyBean extends CustomAnnotatedPrivateInitDestroyBean { + + private void customInit1() throws Exception { + this.initMethods.add("sameNameCustomInit1"); + } + + private void customDestroy1() throws Exception { + this.destroyMethods.add("sameNameCustomDestroy1"); + } + + } public static class CustomInitializingDisposableBean extends CustomInitDestroyBean implements InitializingBean, DisposableBean { From a524857bd548dbb3245771a374e9ef609dfae7c0 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 1 Mar 2022 15:22:34 +0100 Subject: [PATCH 016/131] Fix init/destroy lifecycle method tests See gh-28083 --- .../Spr3775InitDestroyLifecycleTests.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java index 8fb07dc306f9..4de6652e65a3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -155,9 +155,9 @@ public void testJsr250AnnotationsWithCustomPrivateInitDestroyMethods() { DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); CustomAnnotatedPrivateInitDestroyBean bean = (CustomAnnotatedPrivateInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering("init-methods", Arrays.asList("privateCustomInit1","afterPropertiesSet"), bean.initMethods); + assertMethodOrdering(beanClass, "init-methods", Arrays.asList("@PostConstruct.privateCustomInit1", "afterPropertiesSet"), bean.initMethods); beanFactory.destroySingletons(); - assertMethodOrdering("destroy-methods", Arrays.asList("privateCustomDestroy1","destroy"), bean.destroyMethods); + assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("@PreDestroy.privateCustomDestroy1", "destroy"), bean.destroyMethods); } @Test @@ -166,11 +166,11 @@ public void testJsr250AnnotationsWithCustomSameMethodNames() { DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); CustomAnnotatedPrivateSameNameInitDestroyBean bean = (CustomAnnotatedPrivateSameNameInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering("init-methods", - Arrays.asList("privateCustomInit1","afterPropertiesSet","sameNameCustomInit1"), bean.initMethods); + assertMethodOrdering(beanClass, "init-methods", + Arrays.asList("@PostConstruct.privateCustomInit1", "@PostConstruct.sameNameCustomInit1", "afterPropertiesSet"), bean.initMethods); beanFactory.destroySingletons(); - assertMethodOrdering("destroy-methods", - Arrays.asList("privateCustomDestroy1","destroy","sameNameCustomDestroy1"), bean.destroyMethods); + assertMethodOrdering(beanClass, "destroy-methods", + Arrays.asList("@PreDestroy.sameNameCustomDestroy1", "@PreDestroy.privateCustomDestroy1", "destroy"), bean.destroyMethods); } @Test @@ -229,28 +229,32 @@ public void customDestroy() throws Exception { } } - public static class CustomAnnotatedPrivateInitDestroyBean extends CustomInitializingDisposableBean{ + public static class CustomAnnotatedPrivateInitDestroyBean extends CustomInitializingDisposableBean { @PostConstruct private void customInit1() throws Exception { - this.initMethods.add("privateCustomInit1"); + this.initMethods.add("@PostConstruct.privateCustomInit1"); } @PreDestroy private void customDestroy1() throws Exception { - this.destroyMethods.add("privateCustomDestroy1"); + this.destroyMethods.add("@PreDestroy.privateCustomDestroy1"); } } public static class CustomAnnotatedPrivateSameNameInitDestroyBean extends CustomAnnotatedPrivateInitDestroyBean { + @PostConstruct + @SuppressWarnings("unused") private void customInit1() throws Exception { - this.initMethods.add("sameNameCustomInit1"); + this.initMethods.add("@PostConstruct.sameNameCustomInit1"); } + @PreDestroy + @SuppressWarnings("unused") private void customDestroy1() throws Exception { - this.destroyMethods.add("sameNameCustomDestroy1"); + this.destroyMethods.add("@PreDestroy.sameNameCustomDestroy1"); } } From dcdea986f6316344705aa0b70d79a4c60ab96121 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 1 Mar 2022 15:43:25 +0100 Subject: [PATCH 017/131] Polish init/destroy lifecycle method tests See gh-28083 --- .../InitDestroyMethodLifecycleTests.java | 279 +++++++++++++++ .../Spr3775InitDestroyLifecycleTests.java | 325 ------------------ 2 files changed, 279 insertions(+), 325 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java delete mode 100644 spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java diff --git a/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java new file mode 100644 index 000000000000..034452bc0512 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java @@ -0,0 +1,279 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.context.annotation; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests which verify expected init and destroy bean lifecycle + * behavior as requested in + * SPR-3775. + * + *

    Specifically, combinations of the following are tested: + *

      + *
    • {@link InitializingBean} & {@link DisposableBean} interfaces
    • + *
    • Custom {@link RootBeanDefinition#getInitMethodName() init} & + * {@link RootBeanDefinition#getDestroyMethodName() destroy} methods
    • + *
    • JSR 250's {@link javax.annotation.PostConstruct @PostConstruct} & + * {@link javax.annotation.PreDestroy @PreDestroy} annotations
    • + *
    + * + * @author Sam Brannen + * @since 2.5 + */ +class InitDestroyMethodLifecycleTests { + + @Test + void initDestroyMethods() { + Class beanClass = InitDestroyBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "afterPropertiesSet", "destroy"); + InitDestroyBean bean = beanFactory.getBean(InitDestroyBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("afterPropertiesSet"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("destroy"); + } + + @Test + void initializingDisposableInterfaces() { + Class beanClass = CustomInitializingDisposableBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", "customDestroy"); + CustomInitializingDisposableBean bean = beanFactory.getBean(CustomInitializingDisposableBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("afterPropertiesSet", "customInit"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("destroy", "customDestroy"); + } + + @Test + void initializingDisposableInterfacesWithShadowedMethods() { + Class beanClass = InitializingDisposableWithShadowedMethodsBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "afterPropertiesSet", "destroy"); + InitializingDisposableWithShadowedMethodsBean bean = beanFactory.getBean(InitializingDisposableWithShadowedMethodsBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("InitializingBean.afterPropertiesSet"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("DisposableBean.destroy"); + } + + @Test + void jsr250Annotations() { + Class beanClass = CustomAnnotatedInitDestroyBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", "customDestroy"); + CustomAnnotatedInitDestroyBean bean = beanFactory.getBean(CustomAnnotatedInitDestroyBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("postConstruct", "afterPropertiesSet", "customInit"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("preDestroy", "destroy", "customDestroy"); + } + + @Test + void jsr250AnnotationsWithShadowedMethods() { + Class beanClass = CustomAnnotatedInitDestroyWithShadowedMethodsBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", "customDestroy"); + CustomAnnotatedInitDestroyWithShadowedMethodsBean bean = beanFactory.getBean(CustomAnnotatedInitDestroyWithShadowedMethodsBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("@PostConstruct.afterPropertiesSet", "customInit"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("@PreDestroy.destroy", "customDestroy"); + } + + @Test + void jsr250AnnotationsWithCustomPrivateInitDestroyMethods() { + Class beanClass = CustomAnnotatedPrivateInitDestroyBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); + CustomAnnotatedPrivateInitDestroyBean bean = beanFactory.getBean(CustomAnnotatedPrivateInitDestroyBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("@PostConstruct.privateCustomInit1", "afterPropertiesSet"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("@PreDestroy.privateCustomDestroy1", "destroy"); + } + + @Test + void jsr250AnnotationsWithCustomSameMethodNames() { + Class beanClass = CustomAnnotatedPrivateSameNameInitDestroyBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); + CustomAnnotatedPrivateSameNameInitDestroyBean bean = beanFactory.getBean(CustomAnnotatedPrivateSameNameInitDestroyBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("@PostConstruct.privateCustomInit1", "@PostConstruct.sameNameCustomInit1", "afterPropertiesSet"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("@PreDestroy.sameNameCustomDestroy1", "@PreDestroy.privateCustomDestroy1", "destroy"); + } + + @Test + void allLifecycleMechanismsAtOnce() { + Class beanClass = AllInOneBean.class; + DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "afterPropertiesSet", "destroy"); + AllInOneBean bean = beanFactory.getBean(AllInOneBean.class); + assertThat(bean.initMethods).as("init-methods").containsExactly("afterPropertiesSet"); + beanFactory.destroySingletons(); + assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("destroy"); + } + + + private static DefaultListableBeanFactory createBeanFactoryAndRegisterBean(Class beanClass, + String initMethodName, String destroyMethodName) { + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass); + beanDefinition.setInitMethodName(initMethodName); + beanDefinition.setDestroyMethodName(destroyMethodName); + beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + beanFactory.registerBeanDefinition("lifecycleTestBean", beanDefinition); + return beanFactory; + } + + + static class InitDestroyBean { + + final List initMethods = new ArrayList<>(); + final List destroyMethods = new ArrayList<>(); + + + public void afterPropertiesSet() throws Exception { + this.initMethods.add("afterPropertiesSet"); + } + + public void destroy() throws Exception { + this.destroyMethods.add("destroy"); + } + } + + static class InitializingDisposableWithShadowedMethodsBean extends InitDestroyBean implements + InitializingBean, DisposableBean { + + @Override + public void afterPropertiesSet() throws Exception { + this.initMethods.add("InitializingBean.afterPropertiesSet"); + } + + @Override + public void destroy() throws Exception { + this.destroyMethods.add("DisposableBean.destroy"); + } + } + + + static class CustomInitDestroyBean { + + final List initMethods = new ArrayList<>(); + final List destroyMethods = new ArrayList<>(); + + public void customInit() throws Exception { + this.initMethods.add("customInit"); + } + + public void customDestroy() throws Exception { + this.destroyMethods.add("customDestroy"); + } + } + + static class CustomAnnotatedPrivateInitDestroyBean extends CustomInitializingDisposableBean { + + @PostConstruct + private void customInit1() throws Exception { + this.initMethods.add("@PostConstruct.privateCustomInit1"); + } + + @PreDestroy + private void customDestroy1() throws Exception { + this.destroyMethods.add("@PreDestroy.privateCustomDestroy1"); + } + } + + static class CustomAnnotatedPrivateSameNameInitDestroyBean extends CustomAnnotatedPrivateInitDestroyBean { + + @PostConstruct + @SuppressWarnings("unused") + private void customInit1() throws Exception { + this.initMethods.add("@PostConstruct.sameNameCustomInit1"); + } + + @PreDestroy + @SuppressWarnings("unused") + private void customDestroy1() throws Exception { + this.destroyMethods.add("@PreDestroy.sameNameCustomDestroy1"); + } + } + + static class CustomInitializingDisposableBean extends CustomInitDestroyBean + implements InitializingBean, DisposableBean { + + @Override + public void afterPropertiesSet() throws Exception { + this.initMethods.add("afterPropertiesSet"); + } + + @Override + public void destroy() throws Exception { + this.destroyMethods.add("destroy"); + } + } + + static class CustomAnnotatedInitDestroyBean extends CustomInitializingDisposableBean { + + @PostConstruct + public void postConstruct() throws Exception { + this.initMethods.add("postConstruct"); + } + + @PreDestroy + public void preDestroy() throws Exception { + this.destroyMethods.add("preDestroy"); + } + } + + static class CustomAnnotatedInitDestroyWithShadowedMethodsBean extends CustomInitializingDisposableBean { + + @PostConstruct + @Override + public void afterPropertiesSet() throws Exception { + this.initMethods.add("@PostConstruct.afterPropertiesSet"); + } + + @PreDestroy + @Override + public void destroy() throws Exception { + this.destroyMethods.add("@PreDestroy.destroy"); + } + } + + static class AllInOneBean implements InitializingBean, DisposableBean { + + final List initMethods = new ArrayList<>(); + final List destroyMethods = new ArrayList<>(); + + @PostConstruct + @Override + public void afterPropertiesSet() throws Exception { + this.initMethods.add("afterPropertiesSet"); + } + + @PreDestroy + @Override + public void destroy() throws Exception { + this.destroyMethods.add("destroy"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java deleted file mode 100644 index 4de6652e65a3..000000000000 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright 2002-2022 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 - * - * https://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.context.annotation; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.util.ObjectUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - *

    - * JUnit-3.8-based unit test which verifies expected init and - * destroy bean lifecycle behavior as requested in SPR-3775. - *

    - *

    - * Specifically, combinations of the following are tested: - *

    - *
      - *
    • {@link InitializingBean} & {@link DisposableBean} interfaces
    • - *
    • Custom {@link RootBeanDefinition#getInitMethodName() init} & - * {@link RootBeanDefinition#getDestroyMethodName() destroy} methods
    • - *
    • JSR 250's {@link javax.annotation.PostConstruct @PostConstruct} & - * {@link javax.annotation.PreDestroy @PreDestroy} annotations
    • - *
    - * - * @author Sam Brannen - * @since 2.5 - */ -public class Spr3775InitDestroyLifecycleTests { - - private static final Log logger = LogFactory.getLog(Spr3775InitDestroyLifecycleTests.class); - - /** LIFECYCLE_TEST_BEAN. */ - private static final String LIFECYCLE_TEST_BEAN = "lifecycleTestBean"; - - - private void debugMethods(Class clazz, String category, List methodNames) { - if (logger.isDebugEnabled()) { - logger.debug(clazz.getSimpleName() + ": " + category + ": " + methodNames); - } - } - - private void assertMethodOrdering(Class clazz, String category, List expectedMethods, - List actualMethods) { - debugMethods(clazz, category, actualMethods); - assertThat(ObjectUtils.nullSafeEquals(expectedMethods, actualMethods)).as("Verifying " + category + ": expected<" + expectedMethods + "> but got<" + actualMethods + ">.").isTrue(); - } - - private DefaultListableBeanFactory createBeanFactoryAndRegisterBean(final Class beanClass, - final String initMethodName, final String destroyMethodName) { - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass); - beanDefinition.setInitMethodName(initMethodName); - beanDefinition.setDestroyMethodName(destroyMethodName); - beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); - beanFactory.registerBeanDefinition(LIFECYCLE_TEST_BEAN, beanDefinition); - return beanFactory; - } - - @Test - public void testInitDestroyMethods() { - final Class beanClass = InitDestroyBean.class; - final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, - "afterPropertiesSet", "destroy"); - final InitDestroyBean bean = (InitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", Arrays.asList("afterPropertiesSet"), bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("destroy"), bean.destroyMethods); - } - - @Test - public void testInitializingDisposableInterfaces() { - final Class beanClass = CustomInitializingDisposableBean.class; - final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", - "customDestroy"); - final CustomInitializingDisposableBean bean = (CustomInitializingDisposableBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", Arrays.asList("afterPropertiesSet", "customInit"), - bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("destroy", "customDestroy"), - bean.destroyMethods); - } - - @Test - public void testInitializingDisposableInterfacesWithShadowedMethods() { - final Class beanClass = InitializingDisposableWithShadowedMethodsBean.class; - final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, - "afterPropertiesSet", "destroy"); - final InitializingDisposableWithShadowedMethodsBean bean = (InitializingDisposableWithShadowedMethodsBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", Arrays.asList("InitializingBean.afterPropertiesSet"), - bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("DisposableBean.destroy"), bean.destroyMethods); - } - - @Test - public void testJsr250Annotations() { - final Class beanClass = CustomAnnotatedInitDestroyBean.class; - final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", - "customDestroy"); - final CustomAnnotatedInitDestroyBean bean = (CustomAnnotatedInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", Arrays.asList("postConstruct", "afterPropertiesSet", - "customInit"), bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("preDestroy", "destroy", "customDestroy"), - bean.destroyMethods); - } - - @Test - public void testJsr250AnnotationsWithShadowedMethods() { - final Class beanClass = CustomAnnotatedInitDestroyWithShadowedMethodsBean.class; - final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", - "customDestroy"); - final CustomAnnotatedInitDestroyWithShadowedMethodsBean bean = (CustomAnnotatedInitDestroyWithShadowedMethodsBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", - Arrays.asList("@PostConstruct.afterPropertiesSet", "customInit"), bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("@PreDestroy.destroy", "customDestroy"), - bean.destroyMethods); - } - - @Test - public void testJsr250AnnotationsWithCustomPrivateInitDestroyMethods() { - Class beanClass = CustomAnnotatedPrivateInitDestroyBean.class; - DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); - CustomAnnotatedPrivateInitDestroyBean bean = - (CustomAnnotatedPrivateInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", Arrays.asList("@PostConstruct.privateCustomInit1", "afterPropertiesSet"), bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("@PreDestroy.privateCustomDestroy1", "destroy"), bean.destroyMethods); - } - - @Test - public void testJsr250AnnotationsWithCustomSameMethodNames() { - Class beanClass = CustomAnnotatedPrivateSameNameInitDestroyBean.class; - DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit1", "customDestroy1"); - CustomAnnotatedPrivateSameNameInitDestroyBean bean = - (CustomAnnotatedPrivateSameNameInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", - Arrays.asList("@PostConstruct.privateCustomInit1", "@PostConstruct.sameNameCustomInit1", "afterPropertiesSet"), bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", - Arrays.asList("@PreDestroy.sameNameCustomDestroy1", "@PreDestroy.privateCustomDestroy1", "destroy"), bean.destroyMethods); - } - - @Test - public void testAllLifecycleMechanismsAtOnce() { - final Class beanClass = AllInOneBean.class; - final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, - "afterPropertiesSet", "destroy"); - final AllInOneBean bean = (AllInOneBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); - assertMethodOrdering(beanClass, "init-methods", Arrays.asList("afterPropertiesSet"), bean.initMethods); - beanFactory.destroySingletons(); - assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("destroy"), bean.destroyMethods); - } - - - public static class InitDestroyBean { - - final List initMethods = new ArrayList<>(); - final List destroyMethods = new ArrayList<>(); - - - public void afterPropertiesSet() throws Exception { - this.initMethods.add("afterPropertiesSet"); - } - - public void destroy() throws Exception { - this.destroyMethods.add("destroy"); - } - } - - public static class InitializingDisposableWithShadowedMethodsBean extends InitDestroyBean implements - InitializingBean, DisposableBean { - - @Override - public void afterPropertiesSet() throws Exception { - this.initMethods.add("InitializingBean.afterPropertiesSet"); - } - - @Override - public void destroy() throws Exception { - this.destroyMethods.add("DisposableBean.destroy"); - } - } - - - public static class CustomInitDestroyBean { - - final List initMethods = new ArrayList<>(); - final List destroyMethods = new ArrayList<>(); - - public void customInit() throws Exception { - this.initMethods.add("customInit"); - } - - public void customDestroy() throws Exception { - this.destroyMethods.add("customDestroy"); - } - } - - public static class CustomAnnotatedPrivateInitDestroyBean extends CustomInitializingDisposableBean { - - @PostConstruct - private void customInit1() throws Exception { - this.initMethods.add("@PostConstruct.privateCustomInit1"); - } - - @PreDestroy - private void customDestroy1() throws Exception { - this.destroyMethods.add("@PreDestroy.privateCustomDestroy1"); - } - - } - - public static class CustomAnnotatedPrivateSameNameInitDestroyBean extends CustomAnnotatedPrivateInitDestroyBean { - - @PostConstruct - @SuppressWarnings("unused") - private void customInit1() throws Exception { - this.initMethods.add("@PostConstruct.sameNameCustomInit1"); - } - - @PreDestroy - @SuppressWarnings("unused") - private void customDestroy1() throws Exception { - this.destroyMethods.add("@PreDestroy.sameNameCustomDestroy1"); - } - - } - - public static class CustomInitializingDisposableBean extends CustomInitDestroyBean - implements InitializingBean, DisposableBean { - - @Override - public void afterPropertiesSet() throws Exception { - this.initMethods.add("afterPropertiesSet"); - } - - @Override - public void destroy() throws Exception { - this.destroyMethods.add("destroy"); - } - } - - - public static class CustomAnnotatedInitDestroyBean extends CustomInitializingDisposableBean { - - @PostConstruct - public void postConstruct() throws Exception { - this.initMethods.add("postConstruct"); - } - - @PreDestroy - public void preDestroy() throws Exception { - this.destroyMethods.add("preDestroy"); - } - } - - - public static class CustomAnnotatedInitDestroyWithShadowedMethodsBean extends CustomInitializingDisposableBean { - - @PostConstruct - @Override - public void afterPropertiesSet() throws Exception { - this.initMethods.add("@PostConstruct.afterPropertiesSet"); - } - - @PreDestroy - @Override - public void destroy() throws Exception { - this.destroyMethods.add("@PreDestroy.destroy"); - } - } - - - public static class AllInOneBean implements InitializingBean, DisposableBean { - - final List initMethods = new ArrayList<>(); - final List destroyMethods = new ArrayList<>(); - - @Override - @PostConstruct - public void afterPropertiesSet() throws Exception { - this.initMethods.add("afterPropertiesSet"); - } - - @Override - @PreDestroy - public void destroy() throws Exception { - this.destroyMethods.add("destroy"); - } - } - -} From d67034f99b5dad82e2daecc1e31ce99b1c551593 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 1 Mar 2022 16:18:46 +0100 Subject: [PATCH 018/131] Document semantics for externally managed init/destroy methods This commit introduces Javadoc to explain the difference between init/destroy method names when such methods are private, namely that a private method is registered via its qualified method name; whereas, a non-private method is registered via its simple name. See gh-28083 --- .../factory/support/RootBeanDefinition.java | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index 13638768efa1..f93c08f3d548 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -437,7 +437,7 @@ public void registerExternallyManagedConfigMember(Member configMember) { } /** - * Check whether the given method or field is an externally managed configuration member. + * Determine if the given method or field is an externally managed configuration member. */ public boolean isExternallyManagedConfigMember(Member configMember) { synchronized (this.postProcessingLock) { @@ -447,7 +447,7 @@ public boolean isExternallyManagedConfigMember(Member configMember) { } /** - * Return all externally managed configuration methods and fields (as an immutable Set). + * Get all externally managed configuration methods and fields (as an immutable Set). * @since 5.3.11 */ public Set getExternallyManagedConfigMembers() { @@ -459,7 +459,15 @@ public Set getExternallyManagedConfigMembers() { } /** - * Register an externally managed configuration initialization method. + * Register an externally managed configuration initialization method — + * for example, a method annotated with JSR-250's + * {@link javax.annotation.PostConstruct} annotation. + *

    The supplied {@code initMethod} may be the + * {@linkplain Method#getName() simple method name} for non-private methods or the + * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) + * qualified method name} for {@code private} methods. A qualified name is + * necessary for {@code private} methods in order to disambiguate between + * multiple private methods with the same name within a class hierarchy. */ public void registerExternallyManagedInitMethod(String initMethod) { synchronized (this.postProcessingLock) { @@ -471,7 +479,10 @@ public void registerExternallyManagedInitMethod(String initMethod) { } /** - * Check whether the given method name indicates an externally managed initialization method. + * Determine if the given method name indicates an externally managed + * initialization method. + *

    See {@link #registerExternallyManagedInitMethod} for details + * regarding the format for the supplied {@code initMethod}. */ public boolean isExternallyManagedInitMethod(String initMethod) { synchronized (this.postProcessingLock) { @@ -484,10 +495,10 @@ public boolean isExternallyManagedInitMethod(String initMethod) { * Determine if the given method name indicates an externally managed * initialization method, regardless of method visibility. *

    In contrast to {@link #isExternallyManagedInitMethod(String)}, this - * method also returns {@code true} if there is a {@code private} external - * init method that has been + * method also returns {@code true} if there is a {@code private} externally + * managed initialization method that has been * {@linkplain #registerExternallyManagedInitMethod(String) registered} - * using a fully qualified method name instead of a simple method name. + * using a qualified method name instead of a simple method name. * @since 5.3.17 */ boolean hasAnyExternallyManagedInitMethod(String initMethod) { @@ -512,6 +523,8 @@ boolean hasAnyExternallyManagedInitMethod(String initMethod) { /** * Return all externally managed initialization methods (as an immutable Set). + *

    See {@link #registerExternallyManagedInitMethod} for details + * regarding the format for the initialization methods in the returned set. * @since 5.3.11 */ public Set getExternallyManagedInitMethods() { @@ -523,7 +536,15 @@ public Set getExternallyManagedInitMethods() { } /** - * Register an externally managed configuration destruction method. + * Register an externally managed configuration destruction method — + * for example, a method annotated with JSR-250's + * {@link javax.annotation.PreDestroy} annotation. + *

    The supplied {@code destroyMethod} may be the + * {@linkplain Method#getName() simple method name} for non-private methods or the + * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) + * qualified method name} for {@code private} methods. A qualified name is + * necessary for {@code private} methods in order to disambiguate between + * multiple private methods with the same name within a class hierarchy. */ public void registerExternallyManagedDestroyMethod(String destroyMethod) { synchronized (this.postProcessingLock) { @@ -535,7 +556,10 @@ public void registerExternallyManagedDestroyMethod(String destroyMethod) { } /** - * Check whether the given method name indicates an externally managed destruction method. + * Determine if the given method name indicates an externally managed + * destruction method. + *

    See {@link #registerExternallyManagedDestroyMethod} for details + * regarding the format for the supplied {@code destroyMethod}. */ public boolean isExternallyManagedDestroyMethod(String destroyMethod) { synchronized (this.postProcessingLock) { @@ -548,10 +572,10 @@ public boolean isExternallyManagedDestroyMethod(String destroyMethod) { * Determine if the given method name indicates an externally managed * destruction method, regardless of method visibility. *

    In contrast to {@link #isExternallyManagedDestroyMethod(String)}, this - * method also returns {@code true} if there is a {@code private} external - * destroy method that has been + * method also returns {@code true} if there is a {@code private} externally + * managed destruction method that has been * {@linkplain #registerExternallyManagedDestroyMethod(String) registered} - * using a fully qualified method name instead of a simple method name. + * using a qualified method name instead of a simple method name. * @since 5.3.17 */ boolean hasAnyExternallyManagedDestroyMethod(String destroyMethod) { @@ -575,7 +599,9 @@ boolean hasAnyExternallyManagedDestroyMethod(String destroyMethod) { } /** - * Return all externally managed destruction methods (as an immutable Set). + * Get all externally managed destruction methods (as an immutable Set). + *

    See {@link #registerExternallyManagedDestroyMethod} for details + * regarding the format for the destruction methods in the returned set. * @since 5.3.11 */ public Set getExternallyManagedDestroyMethods() { From 67b91b239091afe169045cf0dafa800aaa5884aa Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 1 Mar 2022 19:10:33 +0100 Subject: [PATCH 019/131] Polish RollbackRuleTests See gh-28098 --- .../interceptor/RollbackRuleTests.java | 68 +++++++++++-------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleTests.java index 8445de81bb51..47f73dee2743 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleTests.java @@ -37,67 +37,81 @@ class RollbackRuleTests { @Test - void foundImmediatelyWithString() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class.getName()); - assertThat(rr.getDepth(new Exception())).isEqualTo(0); + void constructorArgumentMustBeThrowableClassWithNonThrowableType() { + assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute(Object.class)); } @Test - void foundImmediatelyWithClass() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); - assertThat(rr.getDepth(new Exception())).isEqualTo(0); + void constructorArgumentMustBeThrowableClassWithNullThrowableType() { + assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((Class) null)); + } + + @Test + void constructorArgumentMustBeStringWithNull() { + assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((String) null)); } @Test void notFound() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(java.io.IOException.class.getName()); + RollbackRuleAttribute rr = new RollbackRuleAttribute(IOException.class); assertThat(rr.getDepth(new MyRuntimeException(""))).isEqualTo(-1); } @Test - void ancestry() { + void foundImmediatelyWithString() { RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class.getName()); - // Exception -> Runtime -> NestedRuntime -> MyRuntimeException - assertThat(rr.getDepth(new MyRuntimeException(""))).isEqualTo(3); + assertThat(rr.getDepth(new Exception())).isEqualTo(0); } @Test - void alwaysTrueForThrowable() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class.getName()); - assertThat(rr.getDepth(new MyRuntimeException(""))).isGreaterThan(0); - assertThat(rr.getDepth(new IOException())).isGreaterThan(0); - assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); - assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); + void foundImmediatelyWithClass() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); + assertThat(rr.getDepth(new Exception())).isEqualTo(0); } @Test - void ctorArgMustBeAThrowableClassWithNonThrowableType() { - assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute(Object.class)); + void foundInSuperclassHierarchy() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); + // Exception -> RuntimeException -> NestedRuntimeException -> MyRuntimeException + assertThat(rr.getDepth(new MyRuntimeException(""))).isEqualTo(3); } @Test - void ctorArgMustBeAThrowableClassWithNullThrowableType() { - assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((Class) null)); + void alwaysFoundForThrowable() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class); + assertThat(rr.getDepth(new MyRuntimeException(""))).isGreaterThan(0); + assertThat(rr.getDepth(new IOException())).isGreaterThan(0); + assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); + assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); } @Test - void ctorArgExceptionStringNameVersionWithNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((String) null)); + void foundNestedExceptionInEnclosingException() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(EnclosingException.class); + assertThat(rr.getDepth(new EnclosingException.NestedException())).isEqualTo(0); } @Test - void foundEnclosedExceptionWithEnclosingException() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(EnclosingException.class); - assertThat(rr.getDepth(new EnclosingException.EnclosedException())).isEqualTo(0); + void foundWhenNameOfExceptionThrownStartsWithTheNameOfTheRegisteredExceptionType() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(MyException.class); + assertThat(rr.getDepth(new MyException2())).isEqualTo(0); } + @SuppressWarnings("serial") static class EnclosingException extends RuntimeException { @SuppressWarnings("serial") - static class EnclosedException extends RuntimeException { - + static class NestedException extends RuntimeException { } } + static class MyException extends RuntimeException { + } + + // Name intentionally starts with MyException (including package) but does + // NOT extend MyException. + static class MyException2 extends RuntimeException { + } + } From 25aa295c2c78e5d4047bf2f4fa1229c2ad0862d8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 2 Mar 2022 17:25:37 +0100 Subject: [PATCH 020/131] Rename test class to adhere to conventions --- .../{RollbackRuleTests.java => RollbackRuleAttributeTests.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spring-tx/src/test/java/org/springframework/transaction/interceptor/{RollbackRuleTests.java => RollbackRuleAttributeTests.java} (99%) diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java similarity index 99% rename from spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleTests.java rename to spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java index 47f73dee2743..488ca914518a 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java @@ -34,7 +34,7 @@ * @author Sam Brannen * @since 09.04.2003 */ -class RollbackRuleTests { +class RollbackRuleAttributeTests { @Test void constructorArgumentMustBeThrowableClassWithNonThrowableType() { From 340f41af6d52b8a780ffc813727333ad10247382 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 2 Mar 2022 17:28:45 +0100 Subject: [PATCH 021/131] Suppress warnings in Gradle build --- .../transaction/interceptor/RollbackRuleAttributeTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java index 488ca914518a..fd05ff675531 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java @@ -106,11 +106,13 @@ static class NestedException extends RuntimeException { } } + @SuppressWarnings("serial") static class MyException extends RuntimeException { } // Name intentionally starts with MyException (including package) but does // NOT extend MyException. + @SuppressWarnings("serial") static class MyException2 extends RuntimeException { } From b3e5f86277e73c91990a85d23779509225085d63 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 3 Mar 2022 16:20:13 +0100 Subject: [PATCH 022/131] Polish rollback rule support --- .../interceptor/RollbackRuleAttribute.java | 78 +++++----- .../interceptor/MyRuntimeException.java | 8 +- .../RollbackRuleAttributeTests.java | 139 ++++++++++++------ .../RuleBasedTransactionAttributeTests.java | 32 ++-- .../TransactionAttributeEditorTests.java | 74 ++++------ 5 files changed, 188 insertions(+), 143 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java index 07a59cc1131b..4c3d4ec53c23 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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,13 +22,13 @@ import org.springframework.util.Assert; /** - * Rule determining whether or not a given exception (and any subclasses) - * should cause a rollback. + * Rule determining whether or not a given exception should cause a rollback. * *

    Multiple such rules can be applied to determine whether a transaction * should commit or rollback after an exception has been thrown. * * @author Rod Johnson + * @author Sam Brannen * @since 09.04.2003 * @see NoRollbackRuleAttribute */ @@ -36,7 +36,7 @@ public class RollbackRuleAttribute implements Serializable{ /** - * The {@link RollbackRuleAttribute rollback rule} for + * The {@linkplain RollbackRuleAttribute rollback rule} for * {@link RuntimeException RuntimeExceptions}. */ public static final RollbackRuleAttribute ROLLBACK_ON_RUNTIME_EXCEPTIONS = @@ -48,30 +48,31 @@ public class RollbackRuleAttribute implements Serializable{ * This way does multiple string comparisons, but how often do we decide * whether to roll back a transaction following an exception? */ - private final String exceptionName; + private final String exceptionPattern; /** - * Create a new instance of the {@code RollbackRuleAttribute} class. + * Create a new instance of the {@code RollbackRuleAttribute} class + * for the given {@code exceptionType}. *

    This is the preferred way to construct a rollback rule that matches - * the supplied {@link Exception} class, its subclasses, and its nested classes. - * @param clazz throwable class; must be {@link Throwable} or a subclass + * the supplied exception type, its subclasses, and its nested classes. + * @param exceptionType exception type; must be {@link Throwable} or a subclass * of {@code Throwable} - * @throws IllegalArgumentException if the supplied {@code clazz} is + * @throws IllegalArgumentException if the supplied {@code exceptionType} is * not a {@code Throwable} type or is {@code null} */ - public RollbackRuleAttribute(Class clazz) { - Assert.notNull(clazz, "'clazz' cannot be null"); - if (!Throwable.class.isAssignableFrom(clazz)) { + public RollbackRuleAttribute(Class exceptionType) { + Assert.notNull(exceptionType, "'exceptionType' cannot be null"); + if (!Throwable.class.isAssignableFrom(exceptionType)) { throw new IllegalArgumentException( - "Cannot construct rollback rule from [" + clazz.getName() + "]: it's not a Throwable"); + "Cannot construct rollback rule from [" + exceptionType.getName() + "]: it's not a Throwable"); } - this.exceptionName = clazz.getName(); + this.exceptionPattern = exceptionType.getName(); } /** * Create a new instance of the {@code RollbackRuleAttribute} class - * for the given {@code exceptionName}. + * for the given {@code exceptionPattern}. *

    This can be a substring, with no wildcard support at present. A value * of "ServletException" would match * {@code javax.servlet.ServletException} and subclasses, for example. @@ -79,40 +80,49 @@ public RollbackRuleAttribute(Class clazz) { * whether to include package information (which is not mandatory). For * example, "Exception" will match nearly anything, and will probably hide * other rules. "java.lang.Exception" would be correct if "Exception" was - * meant to define a rule for all checked exceptions. With more unusual + * meant to define a rule for all checked exceptions. With more unique * exception names such as "BaseBusinessException" there's no need to use a * fully package-qualified name. - * @param exceptionName the exception name pattern; can also be a fully + * @param exceptionPattern the exception name pattern; can also be a fully * package-qualified class name - * @throws IllegalArgumentException if the supplied - * {@code exceptionName} is {@code null} or empty + * @throws IllegalArgumentException if the supplied {@code exceptionPattern} + * is {@code null} or empty */ - public RollbackRuleAttribute(String exceptionName) { - Assert.hasText(exceptionName, "'exceptionName' cannot be null or empty"); - this.exceptionName = exceptionName; + public RollbackRuleAttribute(String exceptionPattern) { + Assert.hasText(exceptionPattern, "'exceptionPattern' cannot be null or empty"); + this.exceptionPattern = exceptionPattern; } /** - * Return the pattern for the exception name. + * Get the configured exception name pattern that this rule uses for matching. + * @see #getDepth(Throwable) */ public String getExceptionName() { - return this.exceptionName; + return this.exceptionPattern; } /** - * Return the depth of the superclass matching. - *

    {@code 0} means {@code ex} matches exactly. Returns - * {@code -1} if there is no match. Otherwise, returns depth with the - * lowest depth winning. + * Return the depth of the superclass matching, with the following semantics. + *

      + *
    • {@code -1} means this rule does not match the supplied {@code exception}.
    • + *
    • {@code 0} means this rule matches the supplied {@code exception} exactly.
    • + *
    • Any other positive value means this rule matches the supplied {@code exception} + * within the superclass hierarchy, where the value is the number of levels in the + * class hierarchy between the supplied {@code exception} and the exception against + * which this rule matches directly.
    • + *
    + *

    When comparing roll back rules that match against a given exception, a rule + * with a lower matching depth wins. For example, a direct match ({@code depth == 0}) + * wins over a match in the superclass hierarchy ({@code depth > 0}). */ - public int getDepth(Throwable ex) { - return getDepth(ex.getClass(), 0); + public int getDepth(Throwable exception) { + return getDepth(exception.getClass(), 0); } private int getDepth(Class exceptionClass, int depth) { - if (exceptionClass.getName().contains(this.exceptionName)) { + if (exceptionClass.getName().contains(this.exceptionPattern)) { // Found it! return depth; } @@ -133,17 +143,17 @@ public boolean equals(@Nullable Object other) { return false; } RollbackRuleAttribute rhs = (RollbackRuleAttribute) other; - return this.exceptionName.equals(rhs.exceptionName); + return this.exceptionPattern.equals(rhs.exceptionPattern); } @Override public int hashCode() { - return this.exceptionName.hashCode(); + return this.exceptionPattern.hashCode(); } @Override public String toString() { - return "RollbackRuleAttribute with pattern [" + this.exceptionName + "]"; + return "RollbackRuleAttribute with pattern [" + this.exceptionPattern + "]"; } } diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/MyRuntimeException.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/MyRuntimeException.java index 80affe6bc24f..5b512e1eb8e8 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/MyRuntimeException.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/MyRuntimeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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. @@ -25,7 +25,13 @@ */ @SuppressWarnings("serial") class MyRuntimeException extends NestedRuntimeException { + + public MyRuntimeException() { + super(""); + } + public MyRuntimeException(String msg) { super(msg); } + } diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java index fd05ff675531..f210659af3b6 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java @@ -18,6 +18,7 @@ import java.io.IOException; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.FatalBeanException; @@ -36,65 +37,105 @@ */ class RollbackRuleAttributeTests { - @Test - void constructorArgumentMustBeThrowableClassWithNonThrowableType() { - assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute(Object.class)); - } + @Nested + class ExceptionPatternTests { - @Test - void constructorArgumentMustBeThrowableClassWithNullThrowableType() { - assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((Class) null)); - } + @Test + void constructorPreconditions() { + assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((String) null)); + } - @Test - void constructorArgumentMustBeStringWithNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((String) null)); - } + @Test + void notFound() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(IOException.class.getName()); + assertThat(rr.getDepth(new MyRuntimeException())).isEqualTo(-1); + } - @Test - void notFound() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(IOException.class); - assertThat(rr.getDepth(new MyRuntimeException(""))).isEqualTo(-1); - } + @Test + void alwaysFoundForThrowable() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class.getName()); + assertThat(rr.getDepth(new MyRuntimeException())).isGreaterThan(0); + assertThat(rr.getDepth(new IOException())).isGreaterThan(0); + assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); + assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); + } - @Test - void foundImmediatelyWithString() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class.getName()); - assertThat(rr.getDepth(new Exception())).isEqualTo(0); - } + @Test + void foundImmediatelyWhenDirectMatch() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class.getName()); + assertThat(rr.getDepth(new Exception())).isEqualTo(0); + } - @Test - void foundImmediatelyWithClass() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); - assertThat(rr.getDepth(new Exception())).isEqualTo(0); - } + @Test + void foundImmediatelyWhenExceptionThrownIsNestedTypeOfRegisteredException() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(EnclosingException.class.getName()); + assertThat(rr.getDepth(new EnclosingException.NestedException())).isEqualTo(0); + } - @Test - void foundInSuperclassHierarchy() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); - // Exception -> RuntimeException -> NestedRuntimeException -> MyRuntimeException - assertThat(rr.getDepth(new MyRuntimeException(""))).isEqualTo(3); - } + @Test + void foundImmediatelyWhenNameOfExceptionThrownStartsWithNameOfRegisteredException() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(MyException.class.getName()); + assertThat(rr.getDepth(new MyException2())).isEqualTo(0); + } - @Test - void alwaysFoundForThrowable() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class); - assertThat(rr.getDepth(new MyRuntimeException(""))).isGreaterThan(0); - assertThat(rr.getDepth(new IOException())).isGreaterThan(0); - assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); - assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); - } + @Test + void foundInSuperclassHierarchy() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class.getName()); + // Exception -> RuntimeException -> NestedRuntimeException -> MyRuntimeException + assertThat(rr.getDepth(new MyRuntimeException())).isEqualTo(3); + } - @Test - void foundNestedExceptionInEnclosingException() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(EnclosingException.class); - assertThat(rr.getDepth(new EnclosingException.NestedException())).isEqualTo(0); } - @Test - void foundWhenNameOfExceptionThrownStartsWithTheNameOfTheRegisteredExceptionType() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(MyException.class); - assertThat(rr.getDepth(new MyException2())).isEqualTo(0); + @Nested + class ExceptionTypeTests { + + @Test + void constructorPreconditions() { + assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute(Object.class)); + assertThatIllegalArgumentException().isThrownBy(() -> new RollbackRuleAttribute((Class) null)); + } + + @Test + void notFound() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(IOException.class); + assertThat(rr.getDepth(new MyRuntimeException())).isEqualTo(-1); + } + + @Test + void alwaysFoundForThrowable() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class); + assertThat(rr.getDepth(new MyRuntimeException())).isGreaterThan(0); + assertThat(rr.getDepth(new IOException())).isGreaterThan(0); + assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); + assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); + } + + @Test + void foundImmediatelyWhenDirectMatch() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); + assertThat(rr.getDepth(new Exception())).isEqualTo(0); + } + + @Test + void foundImmediatelyWhenExceptionThrownIsNestedTypeOfRegisteredException() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(EnclosingException.class); + assertThat(rr.getDepth(new EnclosingException.NestedException())).isEqualTo(0); + } + + @Test + void foundImmediatelyWhenNameOfExceptionThrownStartsWithNameOfRegisteredException() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(MyException.class); + assertThat(rr.getDepth(new MyException2())).isEqualTo(0); + } + + @Test + void foundInSuperclassHierarchy() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); + // Exception -> RuntimeException -> NestedRuntimeException -> MyRuntimeException + assertThat(rr.getDepth(new MyRuntimeException())).isEqualTo(3); + } + } diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttributeTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttributeTests.java index 8aa9815e7c94..3f4cffe2b824 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttributeTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttributeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -35,13 +35,13 @@ * @author Chris Beams * @since 09.04.2003 */ -public class RuleBasedTransactionAttributeTests { +class RuleBasedTransactionAttributeTests { @Test - public void testDefaultRule() { + void defaultRule() { RuleBasedTransactionAttribute rta = new RuleBasedTransactionAttribute(); assertThat(rta.rollbackOn(new RuntimeException())).isTrue(); - assertThat(rta.rollbackOn(new MyRuntimeException(""))).isTrue(); + assertThat(rta.rollbackOn(new MyRuntimeException())).isTrue(); assertThat(rta.rollbackOn(new Exception())).isFalse(); assertThat(rta.rollbackOn(new IOException())).isFalse(); } @@ -50,20 +50,20 @@ public void testDefaultRule() { * Test one checked exception that should roll back. */ @Test - public void testRuleForRollbackOnChecked() { + void ruleForRollbackOnChecked() { List list = new ArrayList<>(); list.add(new RollbackRuleAttribute(IOException.class.getName())); RuleBasedTransactionAttribute rta = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED, list); assertThat(rta.rollbackOn(new RuntimeException())).isTrue(); - assertThat(rta.rollbackOn(new MyRuntimeException(""))).isTrue(); + assertThat(rta.rollbackOn(new MyRuntimeException())).isTrue(); assertThat(rta.rollbackOn(new Exception())).isFalse(); // Check that default behaviour is overridden assertThat(rta.rollbackOn(new IOException())).isTrue(); } @Test - public void testRuleForCommitOnUnchecked() { + void ruleForCommitOnUnchecked() { List list = new ArrayList<>(); list.add(new NoRollbackRuleAttribute(MyRuntimeException.class.getName())); list.add(new RollbackRuleAttribute(IOException.class.getName())); @@ -71,14 +71,14 @@ public void testRuleForCommitOnUnchecked() { assertThat(rta.rollbackOn(new RuntimeException())).isTrue(); // Check default behaviour is overridden - assertThat(rta.rollbackOn(new MyRuntimeException(""))).isFalse(); + assertThat(rta.rollbackOn(new MyRuntimeException())).isFalse(); assertThat(rta.rollbackOn(new Exception())).isFalse(); // Check that default behaviour is overridden assertThat(rta.rollbackOn(new IOException())).isTrue(); } @Test - public void testRuleForSelectiveRollbackOnCheckedWithString() { + void ruleForSelectiveRollbackOnCheckedWithString() { List l = new ArrayList<>(); l.add(new RollbackRuleAttribute(java.rmi.RemoteException.class.getName())); RuleBasedTransactionAttribute rta = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED, l); @@ -86,7 +86,7 @@ public void testRuleForSelectiveRollbackOnCheckedWithString() { } @Test - public void testRuleForSelectiveRollbackOnCheckedWithClass() { + void ruleForSelectiveRollbackOnCheckedWithClass() { List l = Collections.singletonList(new RollbackRuleAttribute(RemoteException.class)); RuleBasedTransactionAttribute rta = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED, l); doTestRuleForSelectiveRollbackOnChecked(rta); @@ -105,7 +105,7 @@ private void doTestRuleForSelectiveRollbackOnChecked(RuleBasedTransactionAttribu * when Exception prompts a rollback. */ @Test - public void testRuleForCommitOnSubclassOfChecked() { + void ruleForCommitOnSubclassOfChecked() { List list = new ArrayList<>(); // Note that it's important to ensure that we have this as // a FQN: otherwise it will match everything! @@ -120,20 +120,20 @@ public void testRuleForCommitOnSubclassOfChecked() { } @Test - public void testRollbackNever() { + void rollbackNever() { List list = new ArrayList<>(); list.add(new NoRollbackRuleAttribute("Throwable")); RuleBasedTransactionAttribute rta = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED, list); assertThat(rta.rollbackOn(new Throwable())).isFalse(); assertThat(rta.rollbackOn(new RuntimeException())).isFalse(); - assertThat(rta.rollbackOn(new MyRuntimeException(""))).isFalse(); + assertThat(rta.rollbackOn(new MyRuntimeException())).isFalse(); assertThat(rta.rollbackOn(new Exception())).isFalse(); assertThat(rta.rollbackOn(new IOException())).isFalse(); } @Test - public void testToStringMatchesEditor() { + void toStringMatchesEditor() { List list = new ArrayList<>(); list.add(new NoRollbackRuleAttribute("Throwable")); RuleBasedTransactionAttribute rta = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED, list); @@ -144,7 +144,7 @@ public void testToStringMatchesEditor() { assertThat(rta.rollbackOn(new Throwable())).isFalse(); assertThat(rta.rollbackOn(new RuntimeException())).isFalse(); - assertThat(rta.rollbackOn(new MyRuntimeException(""))).isFalse(); + assertThat(rta.rollbackOn(new MyRuntimeException())).isFalse(); assertThat(rta.rollbackOn(new Exception())).isFalse(); assertThat(rta.rollbackOn(new IOException())).isFalse(); } @@ -153,7 +153,7 @@ public void testToStringMatchesEditor() { * See this forum post. */ @Test - public void testConflictingRulesToDetermineExactContract() { + void conflictingRulesToDetermineExactContract() { List list = new ArrayList<>(); list.add(new NoRollbackRuleAttribute(MyBusinessWarningException.class)); list.add(new RollbackRuleAttribute(MyBusinessException.class)); diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionAttributeEditorTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionAttributeEditorTests.java index 4614f2648a3c..3bfd82bb679b 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionAttributeEditorTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionAttributeEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,7 +16,6 @@ package org.springframework.transaction.interceptor; - import java.io.IOException; import org.junit.jupiter.api.Test; @@ -27,72 +26,65 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Tests to check conversion from String to TransactionAttribute. + * Tests to check conversion from String to TransactionAttribute using + * a {@link TransactionAttributeEditor}. * * @author Rod Johnson * @author Juergen Hoeller * @author Chris Beams * @since 26.04.2003 */ -public class TransactionAttributeEditorTests { +class TransactionAttributeEditorTests { + + private final TransactionAttributeEditor pe = new TransactionAttributeEditor(); + @Test - public void testNull() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void nullText() { pe.setAsText(null); - TransactionAttribute ta = (TransactionAttribute) pe.getValue(); - assertThat(ta == null).isTrue(); + assertThat(pe.getValue()).isNull(); } @Test - public void testEmptyString() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void emptyString() { pe.setAsText(""); - TransactionAttribute ta = (TransactionAttribute) pe.getValue(); - assertThat(ta == null).isTrue(); + assertThat(pe.getValue()).isNull(); } @Test - public void testValidPropagationCodeOnly() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void validPropagationCodeOnly() { pe.setAsText("PROPAGATION_REQUIRED"); TransactionAttribute ta = (TransactionAttribute) pe.getValue(); - assertThat(ta != null).isTrue(); - assertThat(ta.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED).isTrue(); - assertThat(ta.getIsolationLevel() == TransactionDefinition.ISOLATION_DEFAULT).isTrue(); - boolean condition = !ta.isReadOnly(); - assertThat(condition).isTrue(); + assertThat(ta).isNotNull(); + assertThat(ta.getPropagationBehavior()).isEqualTo(TransactionDefinition.PROPAGATION_REQUIRED); + assertThat(ta.getIsolationLevel()).isEqualTo(TransactionDefinition.ISOLATION_DEFAULT); + assertThat(ta.isReadOnly()).isFalse(); } @Test - public void testInvalidPropagationCodeOnly() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void invalidPropagationCodeOnly() { // should have failed with bogus propagation code - assertThatIllegalArgumentException().isThrownBy(() -> - pe.setAsText("XXPROPAGATION_REQUIRED")); + assertThatIllegalArgumentException().isThrownBy(() -> pe.setAsText("XXPROPAGATION_REQUIRED")); } @Test - public void testValidPropagationCodeAndIsolationCode() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void validPropagationCodeAndIsolationCode() { pe.setAsText("PROPAGATION_REQUIRED, ISOLATION_READ_UNCOMMITTED"); TransactionAttribute ta = (TransactionAttribute) pe.getValue(); - assertThat(ta != null).isTrue(); - assertThat(ta.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED).isTrue(); - assertThat(ta.getIsolationLevel() == TransactionDefinition.ISOLATION_READ_UNCOMMITTED).isTrue(); + assertThat(ta).isNotNull(); + assertThat(ta.getPropagationBehavior()).isEqualTo(TransactionDefinition.PROPAGATION_REQUIRED); + assertThat(ta.getIsolationLevel()).isEqualTo(TransactionDefinition.ISOLATION_READ_UNCOMMITTED); } @Test - public void testValidPropagationAndIsolationCodesAndInvalidRollbackRule() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void validPropagationAndIsolationCodesAndInvalidRollbackRule() { // should fail with bogus rollback rule - assertThatIllegalArgumentException().isThrownBy(() -> - pe.setAsText("PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED,XXX")); + assertThatIllegalArgumentException() + .isThrownBy(() -> pe.setAsText("PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED,XXX")); } @Test - public void testValidPropagationCodeAndIsolationCodeAndRollbackRules1() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void validPropagationCodeAndIsolationCodeAndRollbackRules1() { pe.setAsText("PROPAGATION_MANDATORY,ISOLATION_REPEATABLE_READ,timeout_10,-IOException,+MyRuntimeException"); TransactionAttribute ta = (TransactionAttribute) pe.getValue(); assertThat(ta).isNotNull(); @@ -104,13 +96,11 @@ public void testValidPropagationCodeAndIsolationCodeAndRollbackRules1() { assertThat(ta.rollbackOn(new Exception())).isFalse(); // Check for our bizarre customized rollback rules assertThat(ta.rollbackOn(new IOException())).isTrue(); - boolean condition = !ta.rollbackOn(new MyRuntimeException("")); - assertThat(condition).isTrue(); + assertThat(ta.rollbackOn(new MyRuntimeException())).isFalse(); } @Test - public void testValidPropagationCodeAndIsolationCodeAndRollbackRules2() { - TransactionAttributeEditor pe = new TransactionAttributeEditor(); + void validPropagationCodeAndIsolationCodeAndRollbackRules2() { pe.setAsText("+IOException,readOnly,ISOLATION_READ_COMMITTED,-MyRuntimeException,PROPAGATION_SUPPORTS"); TransactionAttribute ta = (TransactionAttribute) pe.getValue(); assertThat(ta).isNotNull(); @@ -122,18 +112,17 @@ public void testValidPropagationCodeAndIsolationCodeAndRollbackRules2() { assertThat(ta.rollbackOn(new Exception())).isFalse(); // Check for our bizarre customized rollback rules assertThat(ta.rollbackOn(new IOException())).isFalse(); - assertThat(ta.rollbackOn(new MyRuntimeException(""))).isTrue(); + assertThat(ta.rollbackOn(new MyRuntimeException())).isTrue(); } @Test - public void testDefaultTransactionAttributeToString() { + void defaultTransactionAttributeToString() { DefaultTransactionAttribute source = new DefaultTransactionAttribute(); source.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); source.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); source.setTimeout(10); source.setReadOnly(true); - TransactionAttributeEditor pe = new TransactionAttributeEditor(); pe.setAsText(source.toString()); TransactionAttribute ta = (TransactionAttribute) pe.getValue(); assertThat(source).isEqualTo(ta); @@ -151,7 +140,7 @@ public void testDefaultTransactionAttributeToString() { } @Test - public void testRuleBasedTransactionAttributeToString() { + void ruleBasedTransactionAttributeToString() { RuleBasedTransactionAttribute source = new RuleBasedTransactionAttribute(); source.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); source.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); @@ -160,7 +149,6 @@ public void testRuleBasedTransactionAttributeToString() { source.getRollbackRules().add(new RollbackRuleAttribute("IllegalArgumentException")); source.getRollbackRules().add(new NoRollbackRuleAttribute("IllegalStateException")); - TransactionAttributeEditor pe = new TransactionAttributeEditor(); pe.setAsText(source.toString()); TransactionAttribute ta = (TransactionAttribute) pe.getValue(); assertThat(source).isEqualTo(ta); From fa3130d71631f6de36a34d0a5cdf1a37bece3102 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 4 Mar 2022 16:39:11 +0100 Subject: [PATCH 023/131] Document that TX rollback rules may result in unintentional matches Closes gh-28125 --- .../transaction/annotation/Transactional.java | 105 +++++++++++----- .../interceptor/NoRollbackRuleAttribute.java | 25 ++-- .../interceptor/RollbackRuleAttribute.java | 47 ++++--- src/docs/asciidoc/data-access.adoc | 119 +++++++++++++----- 4 files changed, 209 insertions(+), 87 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java index 42537572223f..935933af4228 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -37,17 +37,57 @@ * Transaction Management * section of the reference manual. * - *

    This annotation type is generally directly comparable to Spring's + *

    This annotation is generally directly comparable to Spring's * {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute} * class, and in fact {@link AnnotationTransactionAttributeSource} will directly - * convert the data to the latter class, so that Spring's transaction support code - * does not have to know about annotations. If no custom rollback rules apply, - * the transaction will roll back on {@link RuntimeException} and {@link Error} - * but not on checked exceptions. + * convert this annotation's attributes to properties in {@code RuleBasedTransactionAttribute}, + * so that Spring's transaction support code does not have to know about annotations. * - *

    For specific information about the semantics of this annotation's attributes, - * consult the {@link org.springframework.transaction.TransactionDefinition} and - * {@link org.springframework.transaction.interceptor.TransactionAttribute} javadocs. + *

    Attribute Semantics

    + * + *

    If no custom rollback rules are configured in this annotation, the transaction + * will roll back on {@link RuntimeException} and {@link Error} but not on checked + * exceptions. + * + *

    Rollback rules determine if a transaction should be rolled back when a given + * exception is thrown, and the rules are based on patterns. A pattern can be a + * fully qualified class name or a substring of a fully qualified class name for + * an exception type (which must be a subclass of {@code Throwable}), with no + * wildcard support at present. For example, a value of + * {@code "javax.servlet.ServletException"} or {@code "ServletException"} will + * match {@code javax.servlet.ServletException} and its subclasses. + * + *

    Rollback rules may be configured via {@link #rollbackFor}/{@link #noRollbackFor} + * and {@link #rollbackForClassName}/{@link #noRollbackForClassName}, which allow + * patterns to be specified as {@link Class} references or {@linkplain String + * strings}, respectively. When an exception type is specified as a class reference + * its fully qualified name will be used as the pattern. Consequently, + * {@code @Transactional(rollbackFor = example.CustomException.class)} is equivalent + * to {@code @Transactional(rollbackForClassName = "example.CustomException")}. + * + *

    WARNING: You must carefully consider how specific the pattern + * is and whether to include package information (which isn't mandatory). For example, + * {@code "Exception"} will match nearly anything and will probably hide other + * rules. {@code "java.lang.Exception"} would be correct if {@code "Exception"} + * were meant to define a rule for all checked exceptions. With more unique + * exception names such as {@code "BaseBusinessException"} there is likely no + * need to use the fully qualified class name for the exception pattern. Furthermore, + * rollback rules may result in unintentional matches for similarly named exceptions + * and nested classes. This is due to the fact that a thrown exception is considered + * to be a match for a given rollback rule if the name of thrown exception contains + * the exception pattern configured for the rollback rule. For example, given a + * rule configured to match on {@code com.example.CustomException}, that rule + * would match against an exception named + * {@code com.example.CustomExceptionV2} (an exception in the same package as + * {@code CustomException} but with an additional suffix) or an exception named + * {@code com.example.CustomException$AnotherException} + * (an exception declared as a nested class in {@code CustomException}). + * + *

    For specific information about the semantics of other attributes in this + * annotation, consult the {@link org.springframework.transaction.TransactionDefinition} + * and {@link org.springframework.transaction.interceptor.TransactionAttribute} javadocs. + * + *

    Transaction Management

    * *

    This annotation commonly works with thread-bound transactions managed by a * {@link org.springframework.transaction.PlatformTransactionManager}, exposing a @@ -167,37 +207,33 @@ boolean readOnly() default false; /** - * Defines zero (0) or more exception {@link Class classes}, which must be + * Defines zero (0) or more exception {@linkplain Class classes}, which must be * subclasses of {@link Throwable}, indicating which exception types must cause * a transaction rollback. - *

    By default, a transaction will be rolling back on {@link RuntimeException} + *

    By default, a transaction will be rolled back on {@link RuntimeException} * and {@link Error} but not on checked exceptions (business exceptions). See * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)} * for a detailed explanation. *

    This is the preferred way to construct a rollback rule (in contrast to - * {@link #rollbackForClassName}), matching the exception class and its subclasses. - *

    Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}. + * {@link #rollbackForClassName}), matching the exception type, its subclasses, + * and its nested classes. See the {@linkplain Transactional class-level javadocs} + * for further details on rollback rule semantics and warnings regarding possible + * unintentional matches. * @see #rollbackForClassName + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class) * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable) */ Class[] rollbackFor() default {}; /** - * Defines zero (0) or more exception names (for exceptions which must be a + * Defines zero (0) or more exception name patterns (for exceptions which must be a * subclass of {@link Throwable}), indicating which exception types must cause * a transaction rollback. - *

    This can be a substring of a fully qualified class name, with no wildcard - * support at present. For example, a value of {@code "ServletException"} would - * match {@code javax.servlet.ServletException} and its subclasses. - *

    NB: Consider carefully how specific the pattern is and whether - * to include package information (which isn't mandatory). For example, - * {@code "Exception"} will match nearly anything and will probably hide other - * rules. {@code "java.lang.Exception"} would be correct if {@code "Exception"} - * were meant to define a rule for all checked exceptions. With more unusual - * {@link Exception} names such as {@code "BaseBusinessException"} there is no - * need to use a FQN. - *

    Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(String exceptionName)}. + *

    See the {@linkplain Transactional class-level javadocs} for further details + * on rollback rule semantics, patterns, and warnings regarding possible + * unintentional matches. * @see #rollbackFor + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(String) * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable) */ String[] rollbackForClassName() default {}; @@ -206,23 +242,26 @@ * Defines zero (0) or more exception {@link Class Classes}, which must be * subclasses of {@link Throwable}, indicating which exception types must * not cause a transaction rollback. - *

    This is the preferred way to construct a rollback rule (in contrast - * to {@link #noRollbackForClassName}), matching the exception class and - * its subclasses. - *

    Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(Class clazz)}. + *

    This is the preferred way to construct a rollback rule (in contrast to + * {@link #noRollbackForClassName}), matching the exception type, its subclasses, + * and its nested classes. See the {@linkplain Transactional class-level javadocs} + * for further details on rollback rule semantics and warnings regarding possible + * unintentional matches. * @see #noRollbackForClassName + * @see org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(Class) * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable) */ Class[] noRollbackFor() default {}; /** - * Defines zero (0) or more exception names (for exceptions which must be a + * Defines zero (0) or more exception name patterns (for exceptions which must be a * subclass of {@link Throwable}) indicating which exception types must not * cause a transaction rollback. - *

    See the description of {@link #rollbackForClassName} for further - * information on how the specified names are treated. - *

    Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(String exceptionName)}. + *

    See the {@linkplain Transactional class-level javadocs} for further details + * on rollback rule semantics, patterns, and warnings regarding possible + * unintentional matches. * @see #noRollbackFor + * @see org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(String) * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable) */ String[] noRollbackForClassName() default {}; diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/NoRollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/NoRollbackRuleAttribute.java index a92e14b9ffb4..2282274a94c8 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/NoRollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/NoRollbackRuleAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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. @@ -21,6 +21,7 @@ * to the {@code RollbackRuleAttribute} superclass. * * @author Rod Johnson + * @author Sam Brannen * @since 09.04.2003 */ @SuppressWarnings("serial") @@ -28,22 +29,28 @@ public class NoRollbackRuleAttribute extends RollbackRuleAttribute { /** * Create a new instance of the {@code NoRollbackRuleAttribute} class - * for the supplied {@link Throwable} class. - * @param clazz the {@code Throwable} class + * for the given {@code exceptionType}. + * @param exceptionType exception type; must be {@link Throwable} or a subclass + * of {@code Throwable} + * @throws IllegalArgumentException if the supplied {@code exceptionType} is + * not a {@code Throwable} type or is {@code null} * @see RollbackRuleAttribute#RollbackRuleAttribute(Class) */ - public NoRollbackRuleAttribute(Class clazz) { - super(clazz); + public NoRollbackRuleAttribute(Class exceptionType) { + super(exceptionType); } /** * Create a new instance of the {@code NoRollbackRuleAttribute} class - * for the supplied {@code exceptionName}. - * @param exceptionName the exception name pattern + * for the supplied {@code exceptionPattern}. + * @param exceptionPattern the exception name pattern; can also be a fully + * package-qualified class name + * @throws IllegalArgumentException if the supplied {@code exceptionPattern} + * is {@code null} or empty * @see RollbackRuleAttribute#RollbackRuleAttribute(String) */ - public NoRollbackRuleAttribute(String exceptionName) { - super(exceptionName); + public NoRollbackRuleAttribute(String exceptionPattern) { + super(exceptionPattern); } @Override diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java index 4c3d4ec53c23..a643c4c9b168 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java @@ -27,6 +27,22 @@ *

    Multiple such rules can be applied to determine whether a transaction * should commit or rollback after an exception has been thrown. * + *

    Each rule is based on an exception pattern which can be a fully qualified + * class name or a substring of a fully qualified class name for an exception + * type (which must be a subclass of {@code Throwable}), with no wildcard support + * at present. For example, a value of {@code "javax.servlet.ServletException"} + * or {@code "ServletException"} would match {@code javax.servlet.ServletException} + * and its subclasses. + * + *

    An exception pattern can be specified as a {@link Class} reference or a + * {@link String} in {@link #RollbackRuleAttribute(Class)} and + * {@link #RollbackRuleAttribute(String)}, respectively. When an exception type + * is specified as a class reference its fully qualified name will be used as the + * pattern. See the javadocs for + * {@link org.springframework.transaction.annotation.Transactional @Transactional} + * for further details on rollback rule semantics, patterns, and warnings regarding + * possible unintentional matches. + * * @author Rod Johnson * @author Sam Brannen * @since 09.04.2003 @@ -56,6 +72,10 @@ public class RollbackRuleAttribute implements Serializable{ * for the given {@code exceptionType}. *

    This is the preferred way to construct a rollback rule that matches * the supplied exception type, its subclasses, and its nested classes. + *

    See the javadocs for + * {@link org.springframework.transaction.annotation.Transactional @Transactional} + * for further details on rollback rule semantics, patterns, and warnings regarding + * possible unintentional matches. * @param exceptionType exception type; must be {@link Throwable} or a subclass * of {@code Throwable} * @throws IllegalArgumentException if the supplied {@code exceptionType} is @@ -73,16 +93,10 @@ public RollbackRuleAttribute(Class exceptionType) { /** * Create a new instance of the {@code RollbackRuleAttribute} class * for the given {@code exceptionPattern}. - *

    This can be a substring, with no wildcard support at present. A value - * of "ServletException" would match - * {@code javax.servlet.ServletException} and subclasses, for example. - *

    NB: Consider carefully how specific the pattern is, and - * whether to include package information (which is not mandatory). For - * example, "Exception" will match nearly anything, and will probably hide - * other rules. "java.lang.Exception" would be correct if "Exception" was - * meant to define a rule for all checked exceptions. With more unique - * exception names such as "BaseBusinessException" there's no need to use a - * fully package-qualified name. + *

    See the javadocs for + * {@link org.springframework.transaction.annotation.Transactional @Transactional} + * for further details on rollback rule semantics, patterns, and warnings regarding + * possible unintentional matches. * @param exceptionPattern the exception name pattern; can also be a fully * package-qualified class name * @throws IllegalArgumentException if the supplied {@code exceptionPattern} @@ -106,7 +120,7 @@ public String getExceptionName() { * Return the depth of the superclass matching, with the following semantics. *