Compare commits

...

71 Commits

Author SHA1 Message Date
Spring Buildmaster 91509805b7 Release v5.3.3 2021-01-12 06:26:25 +00:00
Rossen Stoyanchev fceceb480f Validate Resource type in ResourceHttpRequestHandler 2021-01-11 20:54:27 +00:00
Rossen Stoyanchev e5ab67bd71 ResourceHandlerRegistration exposes List<Resource> locations 2021-01-11 18:40:01 +00:00
Sébastien Deleuze 2d53570f4c Prevent kotlinx.serialization usage on interfaces
Remove support for open polymorphic serialization in
kotlinx.serialization web converters and codecs in order
to prevent serialization handling suitable for Jackson
or other general purpose Java JSON libraries.

This will probably need further refinements for collections
for example, and could ultimately be fixed when
kotlinx.serialization will provide a dedicated function to
evaluate upfront if a type can be serialized or not.

Closes gh-26298
2021-01-11 19:07:59 +01:00
Juergen Hoeller be5eb7037f Explicit notes on non-null enforcement and deep cause support in 5.3
Closes gh-26296
See gh-26317
2021-01-11 19:04:12 +01:00
Sam Brannen fff2291f2a Document context lifecycle & logging semantics for the TestContext framework
This commit documents ApplicationContext initialization and shutdown
logging semantics for console output triggered in conjunction with the
Spring TestContext Framework.

Closes gh-25385
2021-01-11 17:48:33 +01:00
Stephane Nicoll 917f3ad101 Upgrade to Reactor 2020.0.3
Closes gh-26367
2021-01-11 16:41:03 +01:00
Sam Brannen 2fd6e6e87c Polish Javadoc for base ExceptionHandlerMethodResolvers 2021-01-11 12:58:19 +01:00
Sam Brannen 570bdbd253 Avoid unnecessary sorting in base ExceptionHandlerMethodResolvers 2021-01-11 12:58:19 +01:00
Juergen Hoeller b587a16d46 Expose all exception causes as provided handler method arguments
Closes gh-26317
2021-01-11 11:10:07 +01:00
Rossen Stoyanchev dd8ea89bea Upgrade to Reactor 2020.0.3 snapshots
See gh-26367
2021-01-11 09:30:47 +00:00
Juergen Hoeller b0dea4d3f6 Upgrade to RxJava 2.2.20, OpenPDF 1.3.24, HtmlUnit 2.46 2021-01-11 09:45:14 +01:00
Juergen Hoeller 4ac27e4dab Suppress ClassCastException for payload type mismatch as well
Closes gh-26349
2021-01-11 09:44:38 +01:00
Nelson Osacky b0abdee012 Add Revved up by Gradle Enterprise badge to Readme
Similar to: https://github.com/spring-projects/spring-boot/pull/24640

Closes gh-26348
2021-01-11 09:40:00 +01:00
Brian Clozel e4dc863ad0 Fix headers keySet in WebFlux adapters
Prior to this commit, WebFlux native headers adapters would delegate the
`httpHeaders.keySet` to underlying implementations that do not honor the
`remove*` methods.

This commit fixes the `Set` implementation backing the
`httpHeaders.keySet` and ensures that headers can be safely removed from
the set.

Fixes gh-26361
2021-01-11 09:18:10 +01:00
Rossen Stoyanchev 689b5566bf Cache "no match" result from ExceptionHandler methods
Closes gh-26339
2021-01-08 19:25:11 +00:00
Arjen Poutsma 234b4719c6 Use default bounded elastic scheduler
Instead of using a new bounded elastic scheduler per
DefaultPartHttpMessageReader instance, which creates daemon threads that
are not shut down, we now use the shared bounded elastic scheduler.

Closes gh-26347
2021-01-08 14:32:49 +01:00
Arjen Poutsma ce1ae2f1b2 Only write non-default charset in FormHttpMessageConverter
This commit only writes the 'charset' parameter in the written headers
if it is non-default (not UTF-8), since RFC7578 states that the only
allowed parameter is 'boundary'.

See gh-25885
Closes gh-26290
2021-01-08 14:03:58 +01:00
Arjen Poutsma 69ce7d33b9 Polishing 2021-01-08 12:02:13 +01:00
Rossen Stoyanchev e36d4162c2 Filter empty buffers PayloadMethodArgumentResolver
An empty buffer for RSocket is an empty message and while some codecs
such as StringDecoder may be able turn that into something such as an
empty String, for other formats such as JSON it is invalid input.

Closes gh-26344
2021-01-07 17:26:00 +00:00
Arjen Poutsma d387d9ae1e Allow quartz expression in cron expression lists
This commit introduces support for lists of quartz cron fields, such
as "1L, LW" or "TUE#1, TUE#3, TUE#5".

Closes gh-26289
2021-01-07 14:27:07 +01:00
Rossen Stoyanchev 138f6bfd84 Update Javadoc of FilePart#filename
See gh-26299
2021-01-07 11:26:00 +00:00
Rossen Stoyanchev a1cf6bbc37 Avoid wrapping of AssertionError in ExchangeResult
Closes gh-26303
2021-01-06 21:56:14 +00:00
Rossen Stoyanchev fb040479eb Update Javadoc of MultipartFile#getOriginalFilename
Closes gh-26299
2021-01-06 21:56:14 +00:00
Rossen Stoyanchev d50375da8e Add more static imports to MockMvc snippets
Closes gh-26311
2021-01-06 21:56:14 +00:00
Rossen Stoyanchev fb2e53276c Assert WebFlux present for WebTestClient
Closes gh-26308
2021-01-06 21:56:14 +00:00
Rossen Stoyanchev c040cd7b05 Parsed RequestPath is recalculated on Forward
Closes gh-26318
2021-01-06 21:56:14 +00:00
Brian Clozel dcc8dcdff8 Set content length on ServletHttpResponse
Prior to this commit, the `ServletServerHttpResponse` would copy headers
from the `HttpHeaders` map and calls methods related to headers exposed
as properties (content-type, character encoding).

Unlike its reactive variant, this would not set the content length.
Depending on the Servlet container implementation, this could cause
duplicate Content-Length response headers in the actual HTTP response.

This commit aligns both implementations and ensures that the
`setContentLengthLong` method is called if necessary so that the Servlet
container can ensure a single header for that.

Fixes gh-26330
2021-01-06 20:38:45 +01:00
Paul Warren efa360cff7 Avoid writing content-length twice in resource handling
Prior to this commit, the `ResourceHttpRequestHandler` would write the
resource size to the "Content-Length" response header.
This is already done by the underlying `ResourceHttpMessageConverter`
and `ResourceRegionHttpMessageConverter`.

This commit avoid this duplicate operation and delegates instead to the
converters.

See gh-26330
Closes gh-26340
2021-01-06 20:38:45 +01:00
Juergen Hoeller 5804db6479 Upgrade to Mockito 3.7, Checkstyle 8.39, RxJava 3.0.9, Joda-Time 2.10.9 2021-01-05 19:33:01 +01:00
Rossen Stoyanchev faa749934b Minor refactoring
- Remove unused clone() code left-over from previous way of cloning.
- Lazy instantiation of ServerCodecConfigurer in
  HttpWebHandlerAdapter since in most cases the configurer instance
  is set externally.

Closes gh-26263
2021-01-05 17:20:26 +00:00
Rossen Stoyanchev e1385090e4 Cached codec instances in DefaultCodecs
Closes gh-26263
2021-01-05 17:20:26 +00:00
Sébastien Deleuze 3113917c55 Update ReactiveAdapterRegistry to do classpath checks during init
To improve GraalVM native image footprint and compatibility.

Closes gh-26295
2021-01-05 09:17:34 +01:00
Rossen Stoyanchev 994a35d691 Mutated ServerHttpRequest returns native request correctly
Closes gh-26304
2021-01-04 21:40:25 +00:00
Juergen Hoeller 8095ba4cd2 Upgrade to Hibernate Validator 6.2 (and Hibernate ORM 5.4.27)
Closes gh-26255
2021-01-04 19:10:46 +01:00
Mattison chao bd2edf26df Fix implementation of isOpen() in ReactorNettyWebSocketSession
See gh-26332 for details.

Closes gh-26341
2021-01-04 17:13:39 +01:00
Arjen Poutsma 4f6d26b25c Parse list with range delta in CronExpression
Closes gh-26313
2021-01-04 16:02:28 +01:00
Sébastien Deleuze 6d85a8a0cc Fix JdbcOperationsExtensions
This commit reverts some changes done in 2d5f9723fa
in order to fix JdbcOperations.queryForObject and
JdbcOperations.queryForList.

It is not possible to provide a vararg variant in Kotlin
for now due to a signature clash, but that will probably
make sense in Spring Framework 6 timeframe along to array
based variant removal for both Java and Kotlin.

Closes gh-26312
2021-01-04 14:57:31 +01:00
Sam Brannen 44b29b6ecf Remove obsolete commandName attribute in spring-form.tld
Since support for the commandName attribute was removed from the
implementation of FormTag in Spring Framework 5.0, the presence of the
commandName attribute in the spring-form.tld file is no longer valid
and can lead one to assume that the commandName attribute is still
supported -- for example when using code completion in a JSP editor.

This commit therefore removes the obsolete commandName attribute in
spring-form.tld.

Closes gh-26337
2021-01-03 17:46:20 +01:00
Sam Brannen 91a6254187 Test status quo for repeatable @PropertySource on composed annotation
This commit introduces tests that verify support for using
@PropertySource as a repeatable annotation without the
@PropertySources container, both locally on an @Configuration class
and on a custom composed annotation.

See gh-26329
2021-01-02 16:21:53 +01:00
Sam Brannen c8ad762b40 Polishing 2021-01-02 16:21:53 +01:00
Sam Brannen 5c1176786c Test status quo for list support in CronExpression
See gh-26289
2020-12-30 14:38:58 +01:00
diguage eab61f692f Merge source and substitution configuration in reference docs
Closes gh-25545
2020-12-28 20:49:36 +01:00
Sam Brannen ee41ebc1ab Polish MockMvcReuseTests 2020-12-28 16:03:16 +01:00
Sam Brannen 885f6dbab9 Ensure ParallelApplicationEventsIntegrationTests passes on CI server
See gh-25616
2020-12-23 10:16:13 +01:00
Juergen Hoeller b0f404722e Upgrade to ASM master (including early support for Java 17 bytecode)
Closes gh-26307
2020-12-21 19:33:33 +01:00
Juergen Hoeller cd7b0b11f3 Upgrade to Hibernate ORM 5.4.26, Netty 4.1.56, XMLUnit 2.8.2 2020-12-21 19:10:17 +01:00
Sam Brannen 1565f4b83e Introduce ApplicationEvents to assert events published during tests
This commit introduces a new feature in the Spring TestContext
Framework (TCF) that provides support for recording application events
published in the ApplicationContext so that assertions can be performed
against those events within tests. All events published during the
execution of a single test are made available via the ApplicationEvents
API which allows one to process the events as a java.util.Stream.

The following example demonstrates usage of this new feature.

@SpringJUnitConfig(/* ... */)
@RecordApplicationEvents
class OrderServiceTests {

    @Autowired
    OrderService orderService;

    @Autowired
    ApplicationEvents events;

    @Test
    void submitOrder() {
        // Invoke method in OrderService that publishes an event
        orderService.submitOrder(new Order(/* ... */));
        // Verify that an OrderSubmitted event was published
        int numEvents = events.stream(OrderSubmitted.class).count();
        assertThat(numEvents).isEqualTo(1);
    }
}

To enable the feature, a test class must be annotated or meta-annotated
with @RecordApplicationEvents. Behind the scenes, a new
ApplicationEventsTestExecutionListener manages the registration of
ApplicationEvents for the current thread at various points within the
test execution lifecycle and makes the current instance of
ApplicationEvents available to tests via an @Autowired field in the
test class. The latter is made possible by a custom ObjectFactory that
is registered as a "resolvable dependency". Thanks to the magic of
ObjectFactoryDelegatingInvocationHandler in the spring-beans module,
the ApplicationEvents instance injected into test classes is
effectively a scoped-proxy that always accesses the ApplicationEvents
managed for the current test.

The current ApplicationEvents instance is stored in a ThreadLocal
variable which is made available in the ApplicationEventsHolder.
Although this class is public, it is only intended for use within the
TCF or in the implementation of third-party extensions.

ApplicationEventsApplicationListener is responsible for listening to
all application events and recording them in the current
ApplicationEvents instance. A single
ApplicationEventsApplicationListener is registered with the test's
ApplicationContext by the ApplicationEventsTestExecutionListener.

The SpringExtension has also been updated to support parameters of type
ApplicationEvents via the JUnit Jupiter ParameterResolver extension
API. This allows JUnit Jupiter based tests to receive access to the
current ApplicationEvents via test and lifecycle method parameters as
an alternative to @Autowired fields in the test class.

Closes gh-25616
2020-12-20 23:46:19 +01:00
Sam Brannen 65a395ef0e Fix syntax in Kotlin example 2020-12-19 15:29:40 +01:00
Juergen Hoeller 947255e377 Always propagate checked exceptions from Kotlin code behind CGLIB proxies
Closes gh-23844
2020-12-18 11:55:20 +01:00
Rossen Stoyanchev 499be70a71 Update async dispatch check in OncePerRequestFilter
We no longer need to rely on an indirect check since Servlet 3.0 is expected
so we can just check the DispatcherType of the request.

Closes gh-26282
2020-12-17 17:33:39 +00:00
Rossen Stoyanchev 0cf5005a3d Apply abortOnCancel in JettyClientHttpConnector
This new option allows a cancel signal to abort the request, which is
how we expect a connection to be aborted in a reactive chain that
involves the WebClient.

Closes gh-26287
2020-12-17 17:18:38 +00:00
Rossen Stoyanchev f07fc76cf3 Limit scheme/host check in fromUriString to HTTP URLs
Closes gh-26258
2020-12-17 14:49:57 +00:00
Juergen Hoeller d3e1c54354 Upgrade to Hibernate Validator 6.1.7, Jetty Reactive HttpClient 1.1.5, Joda-Time 2.10.8, XMLUnit 2.8.1 2020-12-17 10:46:13 +01:00
Juergen Hoeller a109b4c31a Translate PostgreSQL code 21000 (cardinality_violation)
Closes gh-26276
2020-12-16 22:27:57 +01:00
Juergen Hoeller 00b56c026a Consistent handling of NullBean instances in resolveNamedBean
Closes gh-26271
2020-12-16 22:27:41 +01:00
Juergen Hoeller fbd2ffdd23 Consistent declarations and assertions in MockMultipartFile
See gh-26261
2020-12-16 22:27:33 +01:00
Sam Brannen b3a47c76f8 Introduce TestContextAnnotationUtils.hasAnnotation() 2020-12-15 22:48:33 +01:00
Sam Brannen 1292947f78 Introduce computeAttribute() in AttributeAccessor
This commit introduces computeAttribute() as an interface default method
in the AttributeAccessor API. This serves as a convenience analogous to
the computeIfAbsent() method in java.util.Map.

Closes gh-26281
2020-12-15 22:47:02 +01:00
Sam Brannen ef6a582c78 Clean up warnings in Gradle build 2020-12-15 22:47:02 +01:00
Rossen Stoyanchev a11d1c8510 Enrich WebSocketHandler context
Closes gh-26210
2020-12-15 21:34:16 +00:00
Brian Clozel 83c19cd60e Fix NPE when calling NettyHeadersAdapter.add()
Prior to this commit, the `NettyHeadersAdapter` would directly delegate
the `add()` and `set()` calls to the adapted
`io.netty.handler.codec.http.HttpHeaders`. This implementation rejects
`null` values with exceptions.

This commit aligns the behavior here with other implementations, by not
rejecting null values but simply ignoring them.

Fixes gh-26274
2020-12-15 13:23:53 +01:00
Rossen Stoyanchev bcfbde9848 Parse parts in MockMultipartHttpServletRequestBuilder
Closes gh-26261
2020-12-14 21:13:29 +00:00
izeye 17e6cf1cc1 Replace AtomicReference<Boolean> with AtomicBoolean in AbstractServerHttpResponse.writeWith() 2020-12-12 10:40:05 +01:00
Juergen Hoeller ec33b4241a Upgrade to Netty 4.1.55, Tomcat 9.0.41, Caffeine 2.8.8 2020-12-11 11:53:53 +01:00
Brian Clozel 657641ebaa Remove JDK14 CI variant from build pipeline 2020-12-11 11:08:33 +01:00
Brian Clozel 4597e9b547 Upgrade CI container images to Ubuntu Focal 2020-12-11 09:46:00 +01:00
Brian Clozel 721dacca5a Upgrade JDK8, JDK11 and JDK15 versions in CI build 2020-12-11 09:45:21 +01:00
alexscari 9d124fffcb Fix typo in Javadoc for AbstractJdbcCall
Closes gh-26254
2020-12-10 17:32:58 +01:00
Juergen Hoeller 2a47751fcd Defensively handle loadClass null result in BeanUtils.findEditorByConvention
Closes gh-26252
2020-12-10 16:24:32 +01:00
Spring Buildmaster 06e352822a Next development version (v5.3.3-SNAPSHOT) 2020-12-09 06:11:23 +00:00
146 changed files with 4079 additions and 1390 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# <img src="src/docs/spring-framework.png" width="80" height="80"> Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build")
# <img src="src/docs/spring-framework.png" width="80" height="80"> Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring)
This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring".
+17 -17
View File
@@ -26,8 +26,8 @@ configure(allprojects) { project ->
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.0"
mavenBom "io.netty:netty-bom:4.1.54.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.2"
mavenBom "io.netty:netty-bom:4.1.56.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.3"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8"
mavenBom "io.rsocket:rsocket-bom:1.1.0"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.35.v20201120"
@@ -63,8 +63,8 @@ configure(allprojects) { project ->
dependency "io.reactivex:rxjava:1.3.8"
dependency "io.reactivex:rxjava-reactive-streams:1.2.1"
dependency "io.reactivex.rxjava2:rxjava:2.2.19"
dependency "io.reactivex.rxjava3:rxjava:3.0.8"
dependency "io.reactivex.rxjava2:rxjava:2.2.20"
dependency "io.reactivex.rxjava3:rxjava:3.0.9"
dependency "io.projectreactor.tools:blockhound:1.0.4.RELEASE"
dependency "com.caucho:hessian:4.0.63"
@@ -95,8 +95,8 @@ configure(allprojects) { project ->
}
dependency "com.h2database:h2:1.4.200"
dependency "com.github.ben-manes.caffeine:caffeine:2.8.6"
dependency "com.github.librepdf:openpdf:1.3.23"
dependency "com.github.ben-manes.caffeine:caffeine:2.8.8"
dependency "com.github.librepdf:openpdf:1.3.24"
dependency "com.rometools:rome:1.15.0"
dependency "commons-io:commons-io:2.5"
dependency "io.vavr:vavr:0.10.3"
@@ -123,19 +123,19 @@ configure(allprojects) { project ->
dependency "net.sf.ehcache:ehcache:2.10.6"
dependency "org.ehcache:jcache:1.0.1"
dependency "org.ehcache:ehcache:3.4.0"
dependency "org.hibernate:hibernate-core:5.4.25.Final"
dependency "org.hibernate:hibernate-validator:6.1.6.Final"
dependency "org.hibernate:hibernate-core:5.4.27.Final"
dependency "org.hibernate:hibernate-validator:6.2.0.Final"
dependency "org.webjars:webjars-locator-core:0.46"
dependency "org.webjars:underscorejs:1.8.3"
dependencySet(group: 'org.apache.tomcat', version: '9.0.40') {
dependencySet(group: 'org.apache.tomcat', version: '9.0.41') {
entry 'tomcat-util'
entry('tomcat-websocket') {
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
}
}
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.40') {
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.41') {
entry 'tomcat-embed-core'
entry 'tomcat-embed-websocket'
}
@@ -162,7 +162,7 @@ configure(allprojects) { project ->
}
dependency 'org.apache.httpcomponents.client5:httpclient5:5.0.3'
dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.3'
dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.4"
dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.5"
dependency "org.jruby:jruby:9.2.13.0"
dependency "org.python:jython-standalone:2.7.1"
@@ -191,13 +191,13 @@ configure(allprojects) { project ->
dependency "org.hamcrest:hamcrest:2.1"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.18.1"
dependencySet(group: 'org.xmlunit', version: '2.6.2') {
dependencySet(group: 'org.xmlunit', version: '2.8.2') {
entry 'xmlunit-assertj'
entry('xmlunit-matchers') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
}
dependencySet(group: 'org.mockito', version: '3.6.28') {
dependencySet(group: 'org.mockito', version: '3.7.0') {
entry('mockito-core') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
@@ -205,10 +205,10 @@ configure(allprojects) { project ->
}
dependency "io.mockk:mockk:1.10.2"
dependency("net.sourceforge.htmlunit:htmlunit:2.45.0") {
dependency("net.sourceforge.htmlunit:htmlunit:2.46.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:htmlunit-driver:2.45.0") {
dependency("org.seleniumhq.selenium:htmlunit-driver:2.46.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:selenium-java:3.141.59") {
@@ -236,7 +236,7 @@ configure(allprojects) { project ->
dependency "com.ibm.websphere:uow:6.0.2.17"
dependency "com.jamonapi:jamon:2.82"
dependency "joda-time:joda-time:2.10.6"
dependency "joda-time:joda-time:2.10.9"
dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.7"
dependency "org.javamoney:moneta:1.3"
@@ -339,7 +339,7 @@ configure([rootProject] + javaProjects) { project ->
}
checkstyle {
toolVersion = "8.38"
toolVersion = "8.39"
configDirectory.set(rootProject.file("src/checkstyle"))
}
+3 -6
View File
@@ -3,16 +3,13 @@ set -e
case "$1" in
java8)
echo "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u265-b01/OpenJDK8U-jdk_x64_linux_hotspot_8u265b01.tar.gz"
echo "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u275-b01/OpenJDK8U-jdk_x64_linux_hotspot_8u275b01.tar.gz"
;;
java11)
echo "https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.8%2B10/OpenJDK11U-jdk_x64_linux_hotspot_11.0.8_10.tar.gz"
;;
java14)
echo "https://github.com/AdoptOpenJDK/openjdk14-binaries/releases/download/jdk-14.0.2%2B12/OpenJDK14U-jdk_x64_linux_hotspot_14.0.2_12.tar.gz"
echo "https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.9.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.9.1_1.tar.gz"
;;
java15)
echo "https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15%2B36/OpenJDK15U-jdk_x64_linux_hotspot_15_36.tar.gz"
echo "https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15.0.1%2B9/OpenJDK15U-jdk_x64_linux_hotspot_15.0.1_9.tar.gz"
;;
*)
echo $"Unknown java version"
+4 -1
View File
@@ -5,8 +5,11 @@ set -ex
# UTILS
###########################################################
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install --no-install-recommends -y ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig
apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig
ln -fs /usr/share/zoneinfo/UTC /etc/localtime
dpkg-reconfigure --frontend noninteractive tzdata
rm -rf /var/lib/apt/lists/*
curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.3/concourse-java.sh > /opt/concourse-java.sh
@@ -1,4 +1,4 @@
FROM ubuntu:bionic-20200713
FROM ubuntu:focal-20201106
ADD setup.sh /setup.sh
ADD get-jdk-url.sh /get-jdk-url.sh
@@ -1,4 +1,4 @@
FROM ubuntu:bionic-20200713
FROM ubuntu:focal-20201106
ADD setup.sh /setup.sh
ADD get-jdk-url.sh /get-jdk-url.sh
@@ -1,8 +0,0 @@
FROM ubuntu:bionic-20200713
ADD setup.sh /setup.sh
ADD get-jdk-url.sh /get-jdk-url.sh
RUN ./setup.sh java14
ENV JAVA_HOME /opt/openjdk
ENV PATH $JAVA_HOME/bin:$PATH
@@ -1,4 +1,4 @@
FROM ubuntu:bionic-20200713
FROM ubuntu:focal-20201106
ADD setup.sh /setup.sh
ADD get-jdk-url.sh /get-jdk-url.sh
+1 -47
View File
@@ -87,12 +87,6 @@ resources:
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-jdk11-ci-image
- name: spring-framework-jdk14-ci-image
type: docker-image
icon: docker
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-jdk14-ci-image
- name: spring-framework-jdk15-ci-image
type: docker-image
icon: docker
@@ -123,14 +117,6 @@ resources:
access_token: ((github-ci-status-token))
branch: ((branch))
context: jdk11-build
- name: repo-status-jdk14-build
type: github-status-resource
icon: eye-check-outline
source:
repository: ((github-repo-name))
access_token: ((github-ci-status-token))
branch: ((branch))
context: jdk14-build
- name: repo-status-jdk15-build
type: github-status-resource
icon: eye-check-outline
@@ -176,10 +162,6 @@ jobs:
params:
build: ci-images-git-repo/ci/images
dockerfile: ci-images-git-repo/ci/images/spring-framework-jdk11-ci-image/Dockerfile
- put: spring-framework-jdk14-ci-image
params:
build: ci-images-git-repo/ci/images
dockerfile: ci-images-git-repo/ci/images/spring-framework-jdk14-ci-image/Dockerfile
- put: spring-framework-jdk15-ci-image
params:
build: ci-images-git-repo/ci/images
@@ -268,34 +250,6 @@ jobs:
<<: *slack-fail-params
- put: repo-status-jdk11-build
params: { state: "success", commit: "git-repo" }
- name: jdk14-build
serial: true
public: true
plan:
- get: spring-framework-jdk14-ci-image
- get: git-repo
- get: every-morning
trigger: true
- put: repo-status-jdk14-build
params: { state: "pending", commit: "git-repo" }
- do:
- task: check-project
privileged: true
timeout: ((task-timeout))
image: spring-framework-jdk14-ci-image
file: git-repo/ci/tasks/check-project.yml
params:
BRANCH: ((branch))
<<: *gradle-enterprise-task-params
on_failure:
do:
- put: repo-status-jdk14-build
params: { state: "failure", commit: "git-repo" }
- put: slack-alert
params:
<<: *slack-fail-params
- put: repo-status-jdk14-build
params: { state: "success", commit: "git-repo" }
- name: jdk15-build
serial: true
public: true
@@ -480,7 +434,7 @@ jobs:
groups:
- name: "builds"
jobs: ["build", "jdk11-build", "jdk14-build", "jdk15-build"]
jobs: ["build", "jdk11-build", "jdk15-build"]
- name: "releases"
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone","promote-rc", "promote-release", "sync-to-maven-central"]
- name: "ci-images"
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.2-SNAPSHOT
version=5.3.3
org.gradle.jvmargs=-Xmx1536M
org.gradle.caching=true
org.gradle.parallel=true
@@ -48,6 +48,7 @@ import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.cglib.proxy.NoOp;
import org.springframework.core.KotlinDetector;
import org.springframework.core.SmartClassLoader;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -752,10 +753,17 @@ class CglibAopProxy implements AopProxy, Serializable {
throw ex;
}
catch (Exception ex) {
if (ReflectionUtils.declaresException(getMethod(), ex.getClass())) {
if (ReflectionUtils.declaresException(getMethod(), ex.getClass()) ||
KotlinDetector.isKotlinType(getMethod().getDeclaringClass())) {
// Propagate original exception if declared on the target method
// (with callers expecting it). Always propagate it for Kotlin code
// since checked exceptions do not have to be explicitly declared there.
throw ex;
}
else {
// Checked exception thrown in the interceptor but not declared on the
// target method signature -> apply an UndeclaredThrowableException,
// aligned with standard JDK dynamic proxy behavior.
throw new UndeclaredThrowableException(ex);
}
}
@@ -543,6 +543,7 @@ public abstract class BeanUtils {
if (targetType == null || targetType.isArray() || unknownEditorTypes.contains(targetType)) {
return null;
}
ClassLoader cl = targetType.getClassLoader();
if (cl == null) {
try {
@@ -559,28 +560,34 @@ public abstract class BeanUtils {
return null;
}
}
String targetTypeName = targetType.getName();
String editorName = targetTypeName + "Editor";
try {
Class<?> editorClass = cl.loadClass(editorName);
if (!PropertyEditor.class.isAssignableFrom(editorClass)) {
if (logger.isInfoEnabled()) {
logger.info("Editor class [" + editorName +
"] does not implement [java.beans.PropertyEditor] interface");
if (editorClass != null) {
if (!PropertyEditor.class.isAssignableFrom(editorClass)) {
if (logger.isInfoEnabled()) {
logger.info("Editor class [" + editorName +
"] does not implement [java.beans.PropertyEditor] interface");
}
unknownEditorTypes.add(targetType);
return null;
}
unknownEditorTypes.add(targetType);
return null;
return (PropertyEditor) instantiateClass(editorClass);
}
return (PropertyEditor) instantiateClass(editorClass);
// Misbehaving ClassLoader returned null instead of ClassNotFoundException
// - fall back to unknown editor type registration below
}
catch (ClassNotFoundException ex) {
if (logger.isTraceEnabled()) {
logger.trace("No property editor [" + editorName + "] found for type " +
targetTypeName + " according to 'Editor' suffix convention");
}
unknownEditorTypes.add(targetType);
return null;
// Ignore - fall back to unknown editor type registration below
}
if (logger.isTraceEnabled()) {
logger.trace("No property editor [" + editorName + "] found for type " +
targetTypeName + " according to 'Editor' suffix convention");
}
unknownEditorTypes.add(targetType);
return null;
}
/**
@@ -250,7 +250,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
throws BeansException {
String beanName = transformedBeanName(name);
Object bean;
Object beanInstance;
// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
@@ -264,7 +264,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
}
}
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
@@ -342,7 +342,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
@@ -355,7 +355,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
else {
@@ -377,7 +377,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new ScopeNotActiveException(beanName, scopeName, ex);
@@ -395,14 +395,19 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
}
}
return adaptBeanInstance(name, beanInstance, requiredType);
}
@SuppressWarnings("unchecked")
<T> T adaptBeanInstance(String name, Object bean, @Nullable Class<?> requiredType) {
// Check if required type matches the type of the actual bean instance.
if (requiredType != null && !requiredType.isInstance(bean)) {
try {
T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
Object convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
if (convertedBean == null) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return convertedBean;
return (T) convertedBean;
}
catch (TypeMismatchException ex) {
if (logger.isTraceEnabled()) {
@@ -1231,8 +1231,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
if (candidateNames.length == 1) {
String beanName = candidateNames[0];
return new NamedBeanHolder<>(beanName, (T) getBean(beanName, requiredType.toClass(), args));
return resolveNamedBean(candidateNames[0], requiredType, args);
}
else if (candidateNames.length > 1) {
Map<String, Object> candidates = CollectionUtils.newLinkedHashMap(candidateNames.length);
@@ -1251,8 +1250,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
if (candidateName != null) {
Object beanInstance = candidates.get(candidateName);
if (beanInstance == null || beanInstance instanceof Class) {
beanInstance = getBean(candidateName, requiredType.toClass(), args);
if (beanInstance == null) {
return null;
}
if (beanInstance instanceof Class) {
return resolveNamedBean(candidateName, requiredType, args);
}
return new NamedBeanHolder<>(candidateName, (T) beanInstance);
}
@@ -1264,6 +1266,17 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
return null;
}
@Nullable
private <T> NamedBeanHolder<T> resolveNamedBean(
String beanName, ResolvableType requiredType, @Nullable Object[] args) throws BeansException {
Object bean = getBean(beanName, null, args);
if (bean instanceof NullBean) {
return null;
}
return new NamedBeanHolder<T>(beanName, adaptBeanInstance(beanName, bean, requiredType.toClass()));
}
@Override
@Nullable
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 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.
@@ -16,6 +16,8 @@
package org.springframework.context;
import java.util.function.Consumer;
import org.springframework.core.ResolvableType;
import org.springframework.core.ResolvableTypeProvider;
import org.springframework.util.Assert;
@@ -23,11 +25,12 @@ import org.springframework.util.Assert;
/**
* An {@link ApplicationEvent} that carries an arbitrary payload.
*
* <p>Mainly intended for internal use within the framework.
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 4.2
* @param <T> the payload type of the event
* @see ApplicationEventPublisher#publishEvent(Object)
* @see ApplicationListener#forPayload(Consumer)
*/
@SuppressWarnings("serial")
public class PayloadApplicationEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -24,6 +24,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.ErrorHandler;
@@ -55,6 +56,9 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM
@Nullable
private ErrorHandler errorHandler;
@Nullable
private volatile Log lazyLogger;
/**
* Create a new SimpleApplicationEventMulticaster.
@@ -173,12 +177,18 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
if (msg == null || matchesClassCastMessage(msg, event.getClass()) ||
(event instanceof PayloadApplicationEvent &&
matchesClassCastMessage(msg, ((PayloadApplicationEvent) event).getPayload().getClass()))) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
// -> let's suppress the exception and just log a debug message.
Log logger = LogFactory.getLog(getClass());
if (logger.isTraceEnabled()) {
logger.trace("Non-matching event type for listener: " + listener, ex);
// -> let's suppress the exception.
Log loggerToUse = this.lazyLogger;
if (loggerToUse == null) {
loggerToUse = LogFactory.getLog(getClass());
this.lazyLogger = loggerToUse;
}
if (loggerToUse.isTraceEnabled()) {
loggerToUse.trace("Non-matching event type for listener: " + listener, ex);
}
}
else {
@@ -19,14 +19,13 @@ package org.springframework.scheduling.support;
import java.time.DateTimeException;
import java.time.temporal.Temporal;
import java.time.temporal.ValueRange;
import java.util.BitSet;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Efficient {@link BitSet}-based extension of {@link CronField}.
* Efficient bitwise-operator extension of {@link CronField}.
* Created using the {@code parse*} methods.
*
* @author Arjen Poutsma
@@ -130,8 +129,8 @@ final class BitsCronField extends CronField {
result.setBits(range);
}
else {
String rangeStr = value.substring(0, slashPos);
String deltaStr = value.substring(slashPos + 1);
String rangeStr = field.substring(0, slashPos);
String deltaStr = field.substring(slashPos + 1);
ValueRange range = parseRange(rangeStr, type);
if (rangeStr.indexOf('-') == -1) {
range = ValueRange.of(range.getMinimum(), type.range().getMaximum());
@@ -0,0 +1,97 @@
/*
* 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.
* 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.scheduling.support;
import java.time.temporal.Temporal;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Extension of {@link CronField} that wraps an array of cron fields.
*
* @author Arjen Poutsma
* @since 5.3.3
*/
final class CompositeCronField extends CronField {
private final CronField[] fields;
private final String value;
private CompositeCronField(Type type, CronField[] fields, String value) {
super(type);
this.fields = fields;
this.value = value;
}
/**
* Composes the given fields into a {@link CronField}.
*/
public static CronField compose(CronField[] fields, Type type, String value) {
Assert.notEmpty(fields, "Fields must not be empty");
Assert.hasLength(value, "Value must not be empty");
if (fields.length == 1) {
return fields[0];
}
else {
return new CompositeCronField(type, fields, value);
}
}
@Nullable
@Override
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
T result = null;
for (CronField field : this.fields) {
T candidate = field.nextOrSame(temporal);
if (result == null ||
candidate != null && candidate.compareTo(result) < 0) {
result = candidate;
}
}
return result;
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CompositeCronField)) {
return false;
}
CompositeCronField other = (CompositeCronField) o;
return type() == other.type() &&
this.value.equals(other.value);
}
@Override
public String toString() {
return type() + " '" + this.value + "'";
}
}
@@ -20,8 +20,10 @@ import java.time.DateTimeException;
import java.time.temporal.ChronoField;
import java.time.temporal.Temporal;
import java.time.temporal.ValueRange;
import java.util.function.BiFunction;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -77,11 +79,18 @@ abstract class CronField {
* Parse the given value into a days of months {@code CronField}, the fourth entry of a cron expression.
*/
public static CronField parseDaysOfMonth(String value) {
if (value.contains("L") || value.contains("W")) {
return QuartzCronField.parseDaysOfMonth(value);
if (!QuartzCronField.isQuartzDaysOfMonthField(value)) {
return BitsCronField.parseDaysOfMonth(value);
}
else {
return BitsCronField.parseDaysOfMonth(value);
return parseList(value, Type.DAY_OF_MONTH, (field, type) -> {
if (QuartzCronField.isQuartzDaysOfMonthField(field)) {
return QuartzCronField.parseDaysOfMonth(field);
}
else {
return BitsCronField.parseDaysOfMonth(field);
}
});
}
}
@@ -98,15 +107,32 @@ abstract class CronField {
*/
public static CronField parseDaysOfWeek(String value) {
value = replaceOrdinals(value, DAYS);
if (value.contains("L") || value.contains("#")) {
return QuartzCronField.parseDaysOfWeek(value);
if (!QuartzCronField.isQuartzDaysOfWeekField(value)) {
return BitsCronField.parseDaysOfWeek(value);
}
else {
return BitsCronField.parseDaysOfWeek(value);
return parseList(value, Type.DAY_OF_WEEK, (field, type) -> {
if (QuartzCronField.isQuartzDaysOfWeekField(field)) {
return QuartzCronField.parseDaysOfWeek(field);
}
else {
return BitsCronField.parseDaysOfWeek(field);
}
});
}
}
private static CronField parseList(String value, Type type, BiFunction<String, Type, CronField> parseFieldFunction) {
Assert.hasLength(value, "Value must not be empty");
String[] fields = StringUtils.delimitedListToStringArray(value, ",");
CronField[] cronFields = new CronField[fields.length];
for (int i = 0; i < fields.length; i++) {
cronFields[i] = parseFieldFunction.apply(fields[i], type);
}
return CompositeCronField.compose(cronFields, type, value);
}
private static String replaceOrdinals(String value, String[] list) {
value = value.toUpperCase();
for (int i = 0; i < list.length; i++) {
@@ -78,6 +78,12 @@ final class QuartzCronField extends CronField {
this.rollForwardType = rollForwardType;
}
/**
* Returns whether the given value is a Quartz day-of-month field.
*/
public static boolean isQuartzDaysOfMonthField(String value) {
return value.contains("L") || value.contains("W");
}
/**
* Parse the given value into a days of months {@code QuartzCronField}, the fourth entry of a cron expression.
@@ -125,6 +131,13 @@ final class QuartzCronField extends CronField {
throw new IllegalArgumentException("No 'L' or 'W' found in '" + value + "'");
}
/**
* Returns whether the given value is a Quartz day-of-week field.
*/
public static boolean isQuartzDaysOfWeekField(String value) {
return value.contains("L") || value.contains("#");
}
/**
* Parse the given value into a days of week {@code QuartzCronField}, the sixth entry of a cron expression.
* Expects a "L" or "#" in the given value.
@@ -271,9 +271,15 @@ class AnnotationConfigApplicationContextTests {
assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanA.class), "a")).isTrue();
assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanB.class), "b")).isTrue();
assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanC.class), "c")).isTrue();
assertThat(context.getBeansOfType(BeanA.class)).isEmpty();
assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()).isSameAs(context.getBean(BeanB.class));
assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()).isSameAs(context.getBean(BeanC.class));
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() ->
context.getBeanFactory().resolveNamedBean(BeanA.class));
assertThat(context.getBeanFactory().resolveNamedBean(BeanB.class).getBeanInstance()).isSameAs(context.getBean(BeanB.class));
assertThat(context.getBeanFactory().resolveNamedBean(BeanC.class).getBeanInstance()).isSameAs(context.getBean(BeanC.class));
}
@Test
@@ -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.
@@ -18,8 +18,10 @@ package org.springframework.context.annotation;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.Iterator;
import java.util.Properties;
@@ -31,6 +33,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
@@ -47,13 +50,13 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*
* @author Chris Beams
* @author Phillip Webb
* @author Sam Brannen
* @since 3.1
*/
public class PropertySourceAnnotationTests {
class PropertySourceAnnotationTests {
@Test
public void withExplicitName() {
void withExplicitName() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithExplicitName.class);
ctx.refresh();
@@ -73,29 +76,25 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withImplicitName() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithImplicitName.class);
ctx.refresh();
void withImplicitName() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithImplicitName.class);
assertThat(ctx.getEnvironment().getPropertySources().contains("class path resource [org/springframework/context/annotation/p1.properties]")).as("property source p1 was not added").isTrue();
assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean");
}
@Test
public void withTestProfileBeans() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithTestProfileBeans.class);
ctx.refresh();
void withTestProfileBeans() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithTestProfileBeans.class);
assertThat(ctx.containsBean("testBean")).isTrue();
assertThat(ctx.containsBean("testProfileBean")).isTrue();
}
/**
* Tests the LIFO behavior of @PropertySource annotaitons.
* The last one registered should 'win'.
* Tests the LIFO behavior of @PropertySource annotations.
* <p>The last one registered should 'win'.
*/
@Test
public void orderingIsLifo() {
void orderingIsLifo() {
{
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithImplicitName.class, P2Config.class);
@@ -114,7 +113,7 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withCustomFactory() {
void withCustomFactory() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithImplicitName.class, WithCustomFactory.class);
ctx.refresh();
@@ -122,7 +121,7 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withCustomFactoryAsMeta() {
void withCustomFactoryAsMeta() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithImplicitName.class, WithCustomFactoryAsMeta.class);
ctx.refresh();
@@ -130,59 +129,43 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withUnresolvablePlaceholder() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithUnresolvablePlaceholder.class);
try {
ctx.refresh();
}
catch (BeanDefinitionStoreException ex) {
assertThat(ex.getCause() instanceof IllegalArgumentException).isTrue();
}
void withUnresolvablePlaceholder() {
assertThatExceptionOfType(BeanDefinitionStoreException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class))
.withCauseInstanceOf(IllegalArgumentException.class);
}
@Test
public void withUnresolvablePlaceholderAndDefault() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithUnresolvablePlaceholderAndDefault.class);
ctx.refresh();
void withUnresolvablePlaceholderAndDefault() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholderAndDefault.class);
assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean");
}
@Test
public void withResolvablePlaceholder() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithResolvablePlaceholder.class);
void withResolvablePlaceholder() {
System.setProperty("path.to.properties", "org/springframework/context/annotation");
ctx.refresh();
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithResolvablePlaceholder.class);
assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean");
System.clearProperty("path.to.properties");
}
@Test
public void withResolvablePlaceholderAndFactoryBean() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithResolvablePlaceholderAndFactoryBean.class);
void withResolvablePlaceholderAndFactoryBean() {
System.setProperty("path.to.properties", "org/springframework/context/annotation");
ctx.refresh();
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithResolvablePlaceholderAndFactoryBean.class);
assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean");
System.clearProperty("path.to.properties");
}
@Test
public void withEmptyResourceLocations() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigWithEmptyResourceLocations.class);
try {
ctx.refresh();
}
catch (BeanDefinitionStoreException ex) {
assertThat(ex.getCause() instanceof IllegalArgumentException).isTrue();
}
void withEmptyResourceLocations() {
assertThatExceptionOfType(BeanDefinitionStoreException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithEmptyResourceLocations.class))
.withCauseInstanceOf(IllegalArgumentException.class);
}
@Test
public void withNameAndMultipleResourceLocations() {
void withNameAndMultipleResourceLocations() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNameAndMultipleResourceLocations.class);
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
@@ -191,7 +174,7 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withMultipleResourceLocations() {
void withMultipleResourceLocations() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithMultipleResourceLocations.class);
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
@@ -200,7 +183,7 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withPropertySources() {
void withRepeatedPropertySourcesInContainerAnnotation() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithPropertySources.class);
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
@@ -209,7 +192,43 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withNamedPropertySources() {
void withRepeatedPropertySources() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithRepeatedPropertySourceAnnotations.class)) {
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
// p2 should 'win' as it was registered last
assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean");
}
}
@Test
void withRepeatedPropertySourcesOnComposedAnnotation() {
Class<?> configClass = ConfigWithRepeatedPropertySourceAnnotationsOnComposedAnnotation.class;
String key = "custom.config.package";
System.clearProperty(key);
try (ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(configClass)) {
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
// p2 should 'win' as it was registered last
assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean");
}
System.setProperty(key, "org/springframework/context/annotation");
try (ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(configClass)) {
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p3")).isTrue();
// p3 should 'win' as it was registered last
assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p3TestBean");
}
finally {
System.clearProperty(key);
}
}
@Test
void withNamedPropertySources() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNamedPropertySources.class);
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
@@ -218,21 +237,21 @@ public class PropertySourceAnnotationTests {
}
@Test
public void withMissingPropertySource() {
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() ->
new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class))
void withMissingPropertySource() {
assertThatExceptionOfType(BeanDefinitionStoreException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class))
.withCauseInstanceOf(FileNotFoundException.class);
}
@Test
public void withIgnoredPropertySource() {
void withIgnoredPropertySource() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithIgnoredPropertySource.class);
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
}
@Test
public void withSameSourceImportedInDifferentOrder() {
void withSameSourceImportedInDifferentOrder() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithSameSourceImportedInDifferentOrder.class);
assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue();
assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue();
@@ -240,7 +259,7 @@ public class PropertySourceAnnotationTests {
}
@Test
public void orderingWithAndWithoutNameAndMultipleResourceLocations() {
void orderingWithAndWithoutNameAndMultipleResourceLocations() {
// SPR-10820: p2 should 'win' as it was registered last
AnnotationConfigApplicationContext ctxWithName = new AnnotationConfigApplicationContext(ConfigWithNameAndMultipleResourceLocations.class);
AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(ConfigWithMultipleResourceLocations.class);
@@ -249,14 +268,14 @@ public class PropertySourceAnnotationTests {
}
@Test
public void orderingWithAndWithoutNameAndFourResourceLocations() {
void orderingWithAndWithoutNameAndFourResourceLocations() {
// SPR-12198: p4 should 'win' as it was registered last
AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(ConfigWithFourResourceLocations.class);
assertThat(ctxWithoutName.getEnvironment().getProperty("testbean.name")).isEqualTo("p4TestBean");
}
@Test
public void orderingDoesntReplaceExisting() throws Exception {
void orderingDoesntReplaceExisting() throws Exception {
// SPR-12198: mySource should 'win' as it was registered manually
AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext();
MapPropertySource mySource = new MapPropertySource("mine", Collections.singletonMap("testbean.name", "myTestBean"));
@@ -267,51 +286,51 @@ public class PropertySourceAnnotationTests {
}
@Configuration
@PropertySource(value="classpath:${unresolvable}/p1.properties")
@PropertySource("classpath:${unresolvable}/p1.properties")
static class ConfigWithUnresolvablePlaceholder {
}
@Configuration
@PropertySource(value="classpath:${unresolvable:org/springframework/context/annotation}/p1.properties")
@PropertySource("classpath:${unresolvable:org/springframework/context/annotation}/p1.properties")
static class ConfigWithUnresolvablePlaceholderAndDefault {
@Inject Environment env;
@Bean
public TestBean testBean() {
TestBean testBean() {
return new TestBean(env.getProperty("testbean.name"));
}
}
@Configuration
@PropertySource(value="classpath:${path.to.properties}/p1.properties")
@PropertySource("classpath:${path.to.properties}/p1.properties")
static class ConfigWithResolvablePlaceholder {
@Inject Environment env;
@Bean
public TestBean testBean() {
TestBean testBean() {
return new TestBean(env.getProperty("testbean.name"));
}
}
@Configuration
@PropertySource(value="classpath:${path.to.properties}/p1.properties")
@PropertySource("classpath:${path.to.properties}/p1.properties")
static class ConfigWithResolvablePlaceholderAndFactoryBean {
@Inject Environment env;
@SuppressWarnings("rawtypes")
@Bean
public FactoryBean testBean() {
FactoryBean<TestBean> testBean() {
final String name = env.getProperty("testbean.name");
return new FactoryBean() {
return new FactoryBean<TestBean>() {
@Override
public Object getObject() {
public TestBean getObject() {
return new TestBean(name);
}
@Override
@@ -334,7 +353,7 @@ public class PropertySourceAnnotationTests {
@Inject Environment env;
@Bean
public TestBean testBean() {
TestBean testBean() {
return new TestBean(env.getProperty("testbean.name"));
}
}
@@ -347,7 +366,7 @@ public class PropertySourceAnnotationTests {
@Inject Environment env;
@Bean
public TestBean testBean() {
TestBean testBean() {
return new TestBean(env.getProperty("testbean.name"));
}
}
@@ -361,7 +380,7 @@ public class PropertySourceAnnotationTests {
@Inject Environment env;
@Bean @Profile("test")
public TestBean testBean() {
TestBean testBean() {
return new TestBean(env.getProperty("testbean.name"));
}
}
@@ -380,21 +399,21 @@ public class PropertySourceAnnotationTests {
@Configuration
@MyPropertySource(value = "classpath:org/springframework/context/annotation/p2.properties")
@MyPropertySource("classpath:org/springframework/context/annotation/p2.properties")
static class WithCustomFactoryAsMeta {
}
@Retention(RetentionPolicy.RUNTIME)
@PropertySource(value = {}, factory = MyCustomFactory.class)
public @interface MyPropertySource {
@interface MyPropertySource {
@AliasFor(annotation = PropertySource.class)
String value();
}
public static class MyCustomFactory implements PropertySourceFactory {
static class MyCustomFactory implements PropertySourceFactory {
@Override
public org.springframework.core.env.PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
@@ -422,11 +441,10 @@ public class PropertySourceAnnotationTests {
@Configuration
@PropertySource(
value = {
"classpath:org/springframework/context/annotation/p1.properties",
"classpath:org/springframework/context/annotation/p2.properties"
})
@PropertySource({
"classpath:org/springframework/context/annotation/p1.properties",
"classpath:org/springframework/context/annotation/p2.properties"
})
static class ConfigWithMultipleResourceLocations {
}
@@ -434,12 +452,33 @@ public class PropertySourceAnnotationTests {
@Configuration
@PropertySources({
@PropertySource("classpath:org/springframework/context/annotation/p1.properties"),
@PropertySource("classpath:${base.package}/p2.properties"),
@PropertySource("classpath:${base.package}/p2.properties")
})
static class ConfigWithPropertySources {
}
@Configuration
@PropertySource("classpath:org/springframework/context/annotation/p1.properties")
@PropertySource(value = "classpath:${base.package}/p2.properties", ignoreResourceNotFound = true)
static class ConfigWithRepeatedPropertySourceAnnotations {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@PropertySource("classpath:org/springframework/context/annotation/p1.properties")
@PropertySource(value = "classpath:${base.package}/p2.properties", ignoreResourceNotFound = true)
@PropertySource(value = "classpath:${custom.config.package:bogus/config}/p3.properties", ignoreResourceNotFound = true)
@interface ComposedConfiguration {
}
@ComposedConfiguration
static class ConfigWithRepeatedPropertySourceAnnotationsOnComposedAnnotation {
}
@Configuration
@PropertySources({
@PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/p1.properties"),
@@ -471,7 +510,7 @@ public class PropertySourceAnnotationTests {
@Configuration
@PropertySource(value = {})
@PropertySource({})
static class ConfigWithEmptyResourceLocations {
}
@@ -482,7 +521,7 @@ public class PropertySourceAnnotationTests {
@PropertySource("classpath:org/springframework/context/annotation/p2.properties")
})
@Configuration
public static class ConfigWithSameSourceImportedInDifferentOrder {
static class ConfigWithSameSourceImportedInDifferentOrder {
}
@@ -492,18 +531,17 @@ public class PropertySourceAnnotationTests {
@PropertySource("classpath:org/springframework/context/annotation/p2.properties"),
@PropertySource("classpath:org/springframework/context/annotation/p1.properties")
})
public static class ConfigImportedWithSameSourceImportedInDifferentOrder {
static class ConfigImportedWithSameSourceImportedInDifferentOrder {
}
@Configuration
@PropertySource(
value = {
"classpath:org/springframework/context/annotation/p1.properties",
"classpath:org/springframework/context/annotation/p2.properties",
"classpath:org/springframework/context/annotation/p3.properties",
"classpath:org/springframework/context/annotation/p4.properties"
})
@PropertySource({
"classpath:org/springframework/context/annotation/p1.properties",
"classpath:org/springframework/context/annotation/p2.properties",
"classpath:org/springframework/context/annotation/p3.properties",
"classpath:org/springframework/context/annotation/p4.properties"
})
static class ConfigWithFourResourceLocations {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -49,9 +49,11 @@ public class PayloadApplicationEventTests {
public void testProgrammaticEventListener() {
List<Auditable> events = new ArrayList<>();
ApplicationListener<AuditablePayloadEvent<String>> listener = events::add;
ApplicationListener<AuditablePayloadEvent<Integer>> mismatch = (event -> event.getPayload().intValue());
ConfigurableApplicationContext ac = new GenericApplicationContext();
ac.addApplicationListener(listener);
ac.addApplicationListener(mismatch);
ac.refresh();
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
@@ -63,9 +65,11 @@ public class PayloadApplicationEventTests {
public void testProgrammaticPayloadListener() {
List<String> events = new ArrayList<>();
ApplicationListener<PayloadApplicationEvent<String>> listener = ApplicationListener.forPayload(events::add);
ApplicationListener<PayloadApplicationEvent<Integer>> mismatch = ApplicationListener.forPayload(payload -> payload.intValue());
ConfigurableApplicationContext ac = new GenericApplicationContext();
ac.addApplicationListener(listener);
ac.addApplicationListener(mismatch);
ac.refresh();
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
@@ -25,18 +25,47 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Unit tests for {@link BitsCronField}.
*
* @author Arjen Poutsma
* @author Sam Brannen
*/
public class BitsCronFieldTests {
class BitsCronFieldTests {
@Test
void parse() {
assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59));
assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(clear(0)).has(set(1, 2)).has(clearRange(3,4)).has(set(5)).has(clearRange(6,8)).has(set(9)).has(clearRange(10,59));
assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)).has(setRange(8, 12)).has(clearRange(13,59));
assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23));
assertThat(BitsCronField.parseDaysOfWeek("0")).has(clearRange(0, 6)).has(set(7, 7));
assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)).has(clear(58)).has(set(59));
assertThat(BitsCronField.parseMinutes("30")).has(set(30)).has(clearRange(1, 29)).has(clearRange(31, 59));
assertThat(BitsCronField.parseHours("23")).has(set(23)).has(clearRange(0, 23));
assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23));
assertThat(BitsCronField.parseDaysOfMonth("1")).has(set(1)).has(clearRange(2, 31));
assertThat(BitsCronField.parseMonth("1")).has(set(1)).has(clearRange(2, 12));
assertThat(BitsCronField.parseDaysOfWeek("0")).has(set(7, 7)).has(clearRange(0, 6));
}
@Test
void parseLists() {
assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)).has(clearRange(31, 59));
assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)).has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59));
assertThat(BitsCronField.parseHours("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 23));
assertThat(BitsCronField.parseDaysOfMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 31));
assertThat(BitsCronField.parseMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 12));
assertThat(BitsCronField.parseDaysOfWeek("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 7));
assertThat(BitsCronField.parseMinutes("5,10-30/2"))
.has(clearRange(0, 5))
.has(set(5))
.has(clearRange(6,10))
.has(set(10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30))
.has(clear(11, 13, 15, 17, 19, 21, 23, 25, 27, 29))
.has(clearRange(31, 60));
}
@Test
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.DayOfWeek.MONDAY;
import static java.time.DayOfWeek.SUNDAY;
import static java.time.DayOfWeek.THURSDAY;
import static java.time.DayOfWeek.TUESDAY;
import static java.time.DayOfWeek.WEDNESDAY;
import static java.time.temporal.TemporalAdjusters.next;
@@ -116,6 +117,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withMinute(10);
LocalDateTime expected = last.plusMinutes(1).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -149,6 +151,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.of(year, 10, 30, 11, 1);
LocalDateTime expected = last.withHour(12).withMinute(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -173,6 +176,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withDayOfMonth(1);
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -207,6 +211,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withMonth(9).withDayOfMonth(30);
LocalDateTime expected = LocalDateTime.of(last.getYear(), 10, 1, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -222,6 +227,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withMonth(8).withDayOfMonth(30);
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -237,6 +243,7 @@ class CronExpressionTests {
ZonedDateTime last = ZonedDateTime.now(ZoneId.of("CET")).withMonth(10).withDayOfMonth(30);
ZonedDateTime expected = last.withDayOfMonth(31).withHour(0).withMinute(0).withSecond(0).withNano(0);
ZonedDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -251,6 +258,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30);
LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -265,6 +273,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withYear(2010).withMonth(12).withDayOfMonth(31);
LocalDateTime expected = LocalDateTime.of(2011, 1, 1, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -297,6 +306,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().with(next(MONDAY));
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY);
}
@@ -308,6 +318,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().with(next(WEDNESDAY));
LocalDateTime expected = last.plusDays(6).withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY);
}
@@ -319,6 +330,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withMinute(4).withSecond(54);
LocalDateTime expected = last.plusMinutes(1).withSecond(55).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -333,6 +345,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withHour(9).withSecond(54);
LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(55).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -347,6 +360,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withHour(9).withMinute(4);
LocalDateTime expected = last.plusHours(1).plusMinutes(1).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -362,6 +376,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withDayOfMonth(2).withSecond(54);
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(55).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -376,6 +391,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(2);
LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 3, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -398,6 +414,7 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.now().withYear(2007).withMonth(2).withDayOfMonth(10);
LocalDateTime expected = LocalDateTime.of(2008, 2, 29, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -413,12 +430,14 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.of(LocalDate.of(2009, 9, 26), LocalTime.now());
LocalDateTime expected = last.plusDays(2).withHour(7).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
// Next day is a week day so add one
last = actual;
expected = expected.plusDays(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -433,12 +452,14 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.of(LocalDate.of(2010, 12, 30), LocalTime.now());
LocalDateTime expected = last.plusMonths(1).withHour(23).withMinute(30).withSecond(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
// Next trigger is 3 months later
last = actual;
expected = expected.plusMonths(3);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -485,11 +506,13 @@ class CronExpressionTests {
LocalDateTime expected = LocalDateTime.of(last.getYear() + 1, 1, 1, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = expected.plusYears(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -513,11 +536,13 @@ class CronExpressionTests {
LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = expected.plusMonths(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -534,11 +559,13 @@ class CronExpressionTests {
LocalDateTime expected = last.with(next(SUNDAY)).withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = expected.plusWeeks(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -555,11 +582,13 @@ class CronExpressionTests {
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = expected.plusDays(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -583,11 +612,13 @@ class CronExpressionTests {
LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(0).withNano(0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = expected.plusHours(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -603,16 +634,19 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 31, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 2, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 3, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -628,16 +662,19 @@ class CronExpressionTests {
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 28, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 2, 26, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 3, 28, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
@@ -1042,4 +1079,62 @@ class CronExpressionTests {
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY);
}
@Test
void dayOfMonthListWithQuartz() {
CronExpression expression = CronExpression.parse("0 0 0 1W,15,LW * ?");
LocalDateTime last = LocalDateTime.of(2019, 12, 30, 0, 0);
LocalDateTime expected = LocalDateTime.of(2019, 12, 31, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 1, 1, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 1, 15, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2020, 1, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
}
@Test
void dayOfWeekListWithQuartz() {
CronExpression expression = CronExpression.parse("0 0 0 ? * THU#1,THU#3,THU#5");
LocalDateTime last = LocalDateTime.of(2019, 12, 31, 0, 0);
LocalDateTime expected = LocalDateTime.of(2020, 1, 2, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(THURSDAY);
last = actual;
expected = LocalDateTime.of(2020, 1, 16, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(THURSDAY);
last = actual;
expected = LocalDateTime.of(2020, 1, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(THURSDAY);
}
}
@@ -25,6 +25,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Unit tests for {@link QuartzCronField}.
*
* @author Arjen Poutsma
*/
class QuartzCronFieldTests {
@@ -95,6 +97,7 @@ class QuartzCronFieldTests {
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("1#L"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("L#1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("8#1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("2#1,2#3,2#5"));
}
}
@@ -1 +1,2 @@
testbean.name=p3TestBean
from.p3=p3Value
@@ -129,9 +129,9 @@ public abstract class AnnotationVisitor {
}
/**
* Visits an array value of the annotation. Note that arrays of primitive types (such as byte,
* Visits an array value of the annotation. Note that arrays of primitive values (such as byte,
* boolean, short, char, int, long, float or double) can be passed as value to {@link #visit
* visit}. This is what {@link ClassReader} does.
* visit}. This is what {@link ClassReader} does for non empty arrays of primitive values.
*
* @param name the value name.
* @return a visitor to visit the actual array value elements, or {@literal null} if this visitor
@@ -100,8 +100,10 @@ public class ClassReader {
@Deprecated
// DontCheck(MemberName): can't be renamed (for backward binary compatibility).
public final byte[] b;
/** The offset in bytes of the ClassFile's access_flags field. */
public final int header;
/**
* A byte array containing the JVMS ClassFile structure to be parsed. <i>The content of this array
* must not be modified. This field is intended for {@link Attribute} sub classes, and is normally
@@ -112,6 +114,7 @@ public class ClassReader {
* ClassFile element offsets within this byte array.
*/
final byte[] classFileBuffer;
/**
* The offset in bytes, in {@link #classFileBuffer}, of each cp_info entry of the ClassFile's
* constant_pool array, <i>plus one</i>. In other words, the offset of constant pool entry i is
@@ -119,16 +122,19 @@ public class ClassReader {
* 1].
*/
private final int[] cpInfoOffsets;
/**
* The String objects corresponding to the CONSTANT_Utf8 constant pool items. This cache avoids
* multiple parsing of a given CONSTANT_Utf8 constant pool item.
*/
private final String[] constantUtf8Values;
/**
* The ConstantDynamic objects corresponding to the CONSTANT_Dynamic constant pool items. This
* cache avoids multiple parsing of a given CONSTANT_Dynamic constant pool item.
*/
private final ConstantDynamic[] constantDynamicValues;
/**
* The start offsets in {@link #classFileBuffer} of each element of the bootstrap_methods array
* (in the BootstrapMethods attribute).
@@ -137,6 +143,7 @@ public class ClassReader {
* 4.7.23</a>
*/
private final int[] bootstrapMethodOffsets;
/**
* A conservative estimate of the maximum length of the strings contained in the constant pool of
* the class.
@@ -184,7 +191,7 @@ public class ClassReader {
this.b = classFileBuffer;
// Check the class' major_version. This field is after the magic and minor_version fields, which
// use 4 and 2 bytes respectively.
if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V16) {
if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V17) {
throw new IllegalArgumentException(
"Unsupported class file major version " + readShort(classFileOffset + 6));
}
@@ -499,6 +506,9 @@ public class ClassReader {
} else if (Constants.SYNTHETIC.equals(attributeName)) {
accessFlags |= Opcodes.ACC_SYNTHETIC;
} else if (Constants.SOURCE_DEBUG_EXTENSION.equals(attributeName)) {
if (attributeLength > classFileBuffer.length - currentAttributeOffset) {
throw new IllegalArgumentException();
}
sourceDebugExtension =
readUtf(currentAttributeOffset, attributeLength, new char[attributeLength]);
} else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) {
@@ -1509,6 +1519,9 @@ public class ClassReader {
final int maxLocals = readUnsignedShort(currentOffset + 2);
final int codeLength = readInt(currentOffset + 4);
currentOffset += 8;
if (codeLength > classFileBuffer.length - currentOffset) {
throw new IllegalArgumentException();
}
// Read the bytecode 'code' array to create a label for each referenced instruction.
final int bytecodeStartOffset = currentOffset;
@@ -2965,7 +2978,7 @@ public class ClassReader {
// Parse the array_value array.
while (numElementValuePairs-- > 0) {
currentOffset =
readElementValue(annotationVisitor, currentOffset, /* elementName = */ null, charBuffer);
readElementValue(annotationVisitor, currentOffset, /* elementName= */ null, charBuffer);
}
}
if (annotationVisitor != null) {
@@ -132,7 +132,7 @@ public interface Opcodes {
* <pre>
* public class StuffVisitor {
* &#64;Deprecated public void visitOldStuff(int arg, ...) {
* visitNewStuf(arg | SOURCE_DEPRECATED, ...);
* visitNewStuff(arg | SOURCE_DEPRECATED, ...);
* }
* public void visitNewStuff(int argAndSource...) {
* if ((argAndSource & SOURCE_DEPRECATED) == 0) {
@@ -154,7 +154,7 @@ public interface Opcodes {
* <p>and there are two cases:
*
* <ul>
* <li>call visitOldSuff: in the call to super.visitOldStuff, the source is set to
* <li>call visitOldStuff: in the call to super.visitOldStuff, the source is set to
* SOURCE_DEPRECATED and visitNewStuff is called. Here 'do stuff' is run because the source
* was previously set to SOURCE_DEPRECATED, and execution eventually returns to
* UserStuffVisitor.visitOldStuff, where 'do user stuff' is run.
@@ -281,6 +281,7 @@ public interface Opcodes {
int V14 = 0 << 16 | 58;
int V15 = 0 << 16 | 59;
int V16 = 0 << 16 | 60;
int V17 = 0 << 16 | 61;
/**
* Version flag indicating that the class is using 'preview' features.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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,20 +16,24 @@
package org.springframework.core;
import java.util.function.Function;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Interface defining a generic contract for attaching and accessing metadata
* to/from arbitrary objects.
*
* @author Rob Harrop
* @author Sam Brannen
* @since 2.0
*/
public interface AttributeAccessor {
/**
* Set the attribute defined by {@code name} to the supplied {@code value}.
* If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}.
* <p>If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}.
* <p>In general, users should take care to prevent overlaps with other
* metadata attributes by using fully-qualified names, perhaps using
* class or package names as prefix.
@@ -40,16 +44,48 @@ public interface AttributeAccessor {
/**
* Get the value of the attribute identified by {@code name}.
* Return {@code null} if the attribute doesn't exist.
* <p>Return {@code null} if the attribute doesn't exist.
* @param name the unique attribute key
* @return the current value of the attribute, if any
*/
@Nullable
Object getAttribute(String name);
/**
* Compute a new value for the attribute identified by {@code name} if
* necessary and {@linkplain #setAttribute set} the new value in this
* {@code AttributeAccessor}.
* <p>If a value for the attribute identified by {@code name} already exists
* in this {@code AttributeAccessor}, the existing value will be returned
* without applying the supplied compute function.
* <p>The default implementation of this method is not thread safe but can
* overridden by concrete implementations of this interface.
* @param <T> the type of the attribute value
* @param name the unique attribute key
* @param computeFunction a function that computes a new value for the attribute
* name; the function must not return a {@code null} value
* @return the existing value or newly computed value for the named attribute
* @see #getAttribute(String)
* @see #setAttribute(String, Object)
* @since 5.3.3
*/
@SuppressWarnings("unchecked")
default <T> T computeAttribute(String name, Function<String, T> computeFunction) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(computeFunction, "Compute function must not be null");
Object value = getAttribute(name);
if (value == null) {
value = computeFunction.apply(name);
Assert.state(value != null,
() -> String.format("Compute function must not return null for attribute named '%s'", name));
setAttribute(name, value);
}
return (T) value;
}
/**
* Remove the attribute identified by {@code name} and return its value.
* Return {@code null} if no attribute under {@code name} is found.
* <p>Return {@code null} if no attribute under {@code name} is found.
* @param name the unique attribute key
* @return the last value of the attribute, if any
*/
@@ -58,7 +94,7 @@ public interface AttributeAccessor {
/**
* Return {@code true} if the attribute identified by {@code name} exists.
* Otherwise return {@code false}.
* <p>Otherwise return {@code false}.
* @param name the unique attribute key
*/
boolean hasAttribute(String name);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -19,6 +19,7 @@ package org.springframework.core;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -32,6 +33,7 @@ import org.springframework.util.StringUtils;
*
* @author Rob Harrop
* @author Juergen Hoeller
* @author Sam Brannen
* @since 2.0
*/
@SuppressWarnings("serial")
@@ -59,6 +61,17 @@ public abstract class AttributeAccessorSupport implements AttributeAccessor, Ser
return this.attributes.get(name);
}
@Override
@SuppressWarnings("unchecked")
public <T> T computeAttribute(String name, Function<String, T> computeFunction) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(computeFunction, "Compute function must not be null");
Object value = this.attributes.computeIfAbsent(name, computeFunction);
Assert.state(value != null,
() -> String.format("Compute function must not return null for attribute named '%s'", name));
return (T) value;
}
@Override
@Nullable
public Object removeAttribute(String name) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -60,7 +60,28 @@ public class ReactiveAdapterRegistry {
@Nullable
private static volatile ReactiveAdapterRegistry sharedInstance;
private final boolean reactorPresent;
private static final boolean reactorPresent;
private static final boolean rxjava1Present;
private static final boolean rxjava2Present;
private static final boolean rxjava3Present;
private static final boolean flowPublisherPresent;
private static final boolean kotlinCoroutinesPresent;
static {
ClassLoader classLoader = ReactiveAdapterRegistry.class.getClassLoader();
reactorPresent = ClassUtils.isPresent("reactor.core.publisher.Flux", classLoader);
rxjava1Present = ClassUtils.isPresent("rx.Observable", classLoader) &&
ClassUtils.isPresent("rx.RxReactiveStreams", classLoader);
rxjava2Present = ClassUtils.isPresent("io.reactivex.Flowable", classLoader);
rxjava3Present = ClassUtils.isPresent("io.reactivex.rxjava3.core.Flowable", classLoader);
flowPublisherPresent = ClassUtils.isPresent("java.util.concurrent.Flow.Publisher", classLoader);
kotlinCoroutinesPresent = ClassUtils.isPresent("kotlinx.coroutines.reactor.MonoKt", classLoader);
}
private final List<ReactiveAdapter> adapters = new ArrayList<>();
@@ -70,41 +91,34 @@ public class ReactiveAdapterRegistry {
* @see #getSharedInstance()
*/
public ReactiveAdapterRegistry() {
ClassLoader classLoader = ReactiveAdapterRegistry.class.getClassLoader();
// Reactor
boolean reactorRegistered = false;
if (ClassUtils.isPresent("reactor.core.publisher.Flux", classLoader)) {
if (reactorPresent) {
new ReactorRegistrar().registerAdapters(this);
reactorRegistered = true;
}
this.reactorPresent = reactorRegistered;
// RxJava1 (deprecated)
if (ClassUtils.isPresent("rx.Observable", classLoader) &&
ClassUtils.isPresent("rx.RxReactiveStreams", classLoader)) {
if (rxjava1Present) {
new RxJava1Registrar().registerAdapters(this);
}
// RxJava2
if (ClassUtils.isPresent("io.reactivex.Flowable", classLoader)) {
if (rxjava2Present) {
new RxJava2Registrar().registerAdapters(this);
}
// RxJava3
if (ClassUtils.isPresent("io.reactivex.rxjava3.core.Flowable", classLoader)) {
if (rxjava3Present) {
new RxJava3Registrar().registerAdapters(this);
}
// Java 9+ Flow.Publisher
if (ClassUtils.isPresent("java.util.concurrent.Flow.Publisher", classLoader)) {
if (flowPublisherPresent) {
new ReactorJdkFlowAdapterRegistrar().registerAdapter(this);
}
// If not present, do nothing for the time being...
// We can fall back on "reactive-streams-flow-bridge" (once released)
// Coroutines
if (this.reactorPresent && ClassUtils.isPresent("kotlinx.coroutines.reactor.MonoKt", classLoader)) {
// Kotlin Coroutines
if (reactorPresent && kotlinCoroutinesPresent) {
new CoroutinesRegistrar().registerAdapters(this);
}
}
@@ -125,7 +139,7 @@ public class ReactiveAdapterRegistry {
public void registerReactiveType(ReactiveTypeDescriptor descriptor,
Function<Object, Publisher<?>> toAdapter, Function<Publisher<?>, Object> fromAdapter) {
if (this.reactorPresent) {
if (reactorPresent) {
this.adapters.add(new ReactorAdapter(descriptor, toAdapter, fromAdapter));
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,39 +17,61 @@
package org.springframework.core;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link AttributeAccessorSupport}.
*
* @author Rob Harrop
* @author Sam Brannen
* @since 2.0
*/
class AttributeAccessorSupportTests {
private static final String NAME = "foo";
private static final String NAME = "name";
private static final String VALUE = "bar";
private static final String VALUE = "value";
private final AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport();
private AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport();
@Test
void setAndGet() throws Exception {
void setAndGet() {
this.attributeAccessor.setAttribute(NAME, VALUE);
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo(VALUE);
}
@Test
void setAndHas() throws Exception {
void setAndHas() {
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
this.attributeAccessor.setAttribute(NAME, VALUE);
assertThat(this.attributeAccessor.hasAttribute(NAME)).isTrue();
}
@Test
void remove() throws Exception {
void computeAttribute() {
AtomicInteger atomicInteger = new AtomicInteger();
Function<String, String> computeFunction = name -> "computed-" + atomicInteger.incrementAndGet();
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
this.attributeAccessor.computeAttribute(NAME, computeFunction);
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1");
this.attributeAccessor.computeAttribute(NAME, computeFunction);
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1");
this.attributeAccessor.removeAttribute(NAME);
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
this.attributeAccessor.computeAttribute(NAME, computeFunction);
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-2");
}
@Test
void remove() {
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
this.attributeAccessor.setAttribute(NAME, VALUE);
assertThat(this.attributeAccessor.removeAttribute(NAME)).isEqualTo(VALUE);
@@ -57,13 +79,13 @@ class AttributeAccessorSupportTests {
}
@Test
void attributeNames() throws Exception {
void attributeNames() {
this.attributeAccessor.setAttribute(NAME, VALUE);
this.attributeAccessor.setAttribute("abc", "123");
String[] attributeNames = this.attributeAccessor.attributeNames();
Arrays.sort(attributeNames);
assertThat(Arrays.binarySearch(attributeNames, NAME) > -1).isTrue();
assertThat(Arrays.binarySearch(attributeNames, "abc") > -1).isTrue();
assertThat(Arrays.binarySearch(attributeNames, "abc")).isEqualTo(0);
assertThat(Arrays.binarySearch(attributeNames, NAME)).isEqualTo(1);
}
@SuppressWarnings("serial")
@@ -433,7 +433,7 @@ public abstract class AbstractJdbcCall {
/**
* Match the provided in parameter values with registered parameters and
* parameters defined via meta-data processing.
* @param parameterSource the parameter vakues provided as a {@link SqlParameterSource}
* @param parameterSource the parameter values provided as a {@link SqlParameterSource}
* @return a Map with parameter names and values
*/
protected Map<String, Object> matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -54,8 +54,10 @@ inline fun <reified T> JdbcOperations.queryForObject(sql: String, args: Array<ou
* @author Mario Arias
* @since 5.0
*/
@Suppress("DEPRECATION")
// TODO Replace by the vararg variant in Spring Framework 6
inline fun <reified T> JdbcOperations.queryForObject(sql: String, args: Array<out Any>): T? =
queryForObject(sql, T::class.java, args) as T
queryForObject(sql, args, T::class.java) as T
/**
* Extension for [JdbcOperations.queryForList] providing a `queryForList<Foo>("...")` variant.
@@ -86,8 +88,11 @@ inline fun <reified T> JdbcOperations.queryForList(sql: String, args: Array<out
* @author Mario Arias
* @since 5.0
*/
@Suppress("DEPRECATION")
// TODO Replace by the vararg variant in Spring Framework 6
inline fun <reified T> JdbcOperations.queryForList(sql: String, args: Array<out Any>): List<T> =
queryForList(sql, T::class.java, args)
queryForList(sql, args, T::class.java)
/**
* Extension for [JdbcOperations.query] providing a ResultSetExtractor-like function
@@ -247,7 +247,7 @@
<value>03000,42000,42601,42602,42622,42804,42P01</value>
</property>
<property name="duplicateKeyCodes">
<value>23505</value>
<value>21000,23505</value>
</property>
<property name="dataIntegrityViolationCodes">
<value>23000,23502,23503,23514</value>
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -67,11 +67,12 @@ class JdbcOperationsExtensionsTests {
}
@Test
@Suppress("DEPRECATION")
fun `queryForObject with reified type parameters and args`() {
val args = arrayOf(3)
every { template.queryForObject(sql, any<Class<Int>>(), args) } returns 2
val args = arrayOf(3, 4)
every { template.queryForObject(sql, args, any<Class<Int>>()) } returns 2
assertThat(template.queryForObject<Int>(sql, args)).isEqualTo(2)
verify { template.queryForObject(sql, any<Class<Int>>(), args) }
verify { template.queryForObject(sql, args, any<Class<Int>>()) }
}
@Test
@@ -93,12 +94,13 @@ class JdbcOperationsExtensionsTests {
}
@Test
@Suppress("DEPRECATION")
fun `queryForList with reified type parameters and args`() {
val list = listOf(1, 2, 3)
val args = arrayOf(3)
every { template.queryForList(sql, any<Class<Int>>(), args) } returns list
val args = arrayOf(3, 4)
every { template.queryForList(sql, args, any<Class<Int>>()) } returns list
template.queryForList<Int>(sql, args)
verify { template.queryForList(sql, any<Class<Int>>(), args) }
verify { template.queryForList(sql, args, any<Class<Int>>()) }
}
@Test
@@ -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.
@@ -38,6 +38,7 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
@@ -232,6 +233,7 @@ public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResol
if (decoder.canDecode(elementType, mimeType)) {
if (adapter != null && adapter.isMultiValue()) {
Flux<?> flux = content
.filter(this::nonEmptyDataBuffer)
.map(buffer -> decoder.decode(buffer, elementType, mimeType, hints))
.onErrorResume(ex -> Flux.error(handleReadError(parameter, message, ex)));
if (isContentRequired) {
@@ -245,6 +247,7 @@ public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResol
else {
// Single-value (with or without reactive type wrapper)
Mono<?> mono = content.next()
.filter(this::nonEmptyDataBuffer)
.map(buffer -> decoder.decode(buffer, elementType, mimeType, hints))
.onErrorResume(ex -> Mono.error(handleReadError(parameter, message, ex)));
if (isContentRequired) {
@@ -262,6 +265,14 @@ public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResol
message, parameter, "Cannot decode to [" + targetType + "]" + message));
}
private boolean nonEmptyDataBuffer(DataBuffer buffer) {
if (buffer.readableByteCount() > 0) {
return true;
}
DataBufferUtils.release(buffer);
return false;
}
private Throwable handleReadError(MethodParameter parameter, Message<?> message, Throwable ex) {
return ex instanceof DecodingException ?
new MethodArgumentResolutionException(message, parameter, "Failed to read HTTP message", ex) : ex;
@@ -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.
@@ -34,10 +34,24 @@ import org.springframework.util.ConcurrentReferenceHashMap;
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.0
*/
public abstract class AbstractExceptionHandlerMethodResolver {
private static final Method NO_MATCHING_EXCEPTION_HANDLER_METHOD;
static {
try {
NO_MATCHING_EXCEPTION_HANDLER_METHOD =
AbstractExceptionHandlerMethodResolver.class.getDeclaredMethod("noMatchingExceptionHandler");
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException("Expected method not found: " + ex);
}
}
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
@@ -52,9 +66,9 @@ public abstract class AbstractExceptionHandlerMethodResolver {
}
/**
* Extract the exceptions this method handles.This implementation looks for
* Extract the exceptions this method handles. This implementation looks for
* sub-classes of Throwable in the method signature.
* The method is static to ensure safe use from sub-class constructors.
* <p>The method is static to ensure safe use from sub-class constructors.
*/
@SuppressWarnings("unchecked")
protected static List<Class<? extends Throwable>> getExceptionsFromMethodSignature(Method method) {
@@ -80,7 +94,7 @@ public abstract class AbstractExceptionHandlerMethodResolver {
/**
* Find a {@link Method} to handle the given exception.
* Use {@link ExceptionDepthComparator} if more than one match is found.
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
* @param exception the exception
* @return a Method to handle the exception, or {@code null} if none found
*/
@@ -99,6 +113,7 @@ public abstract class AbstractExceptionHandlerMethodResolver {
/**
* Find a {@link Method} to handle the given exception type. This can be
* useful if an {@link Exception} instance is not available (e.g. for tools).
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
* @param exceptionType the exception type
* @return a Method to handle the exception, or {@code null} if none found
* @since 4.3.1
@@ -110,11 +125,12 @@ public abstract class AbstractExceptionHandlerMethodResolver {
method = getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, method);
}
return method;
return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);
}
/**
* Return the {@link Method} mapped to the given exception type, or {@code null} if none.
* Return the {@link Method} mapped to the given exception type, or
* {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none.
*/
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
@@ -125,12 +141,21 @@ public abstract class AbstractExceptionHandlerMethodResolver {
}
}
if (!matches.isEmpty()) {
matches.sort(new ExceptionDepthComparator(exceptionType));
if (matches.size() > 1) {
matches.sort(new ExceptionDepthComparator(exceptionType));
}
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
}
}
/**
* For the {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} constant.
*/
@SuppressWarnings("unused")
private void noMatchingExceptionHandler() {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -19,7 +19,6 @@ package org.springframework.messaging.rsocket;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.core.RSocketServer;
@@ -43,6 +42,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.rsocket.annotation.ConnectMapping;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.stereotype.Controller;
@@ -164,6 +164,12 @@ public class RSocketClientToServerIntegrationTests {
.verify(Duration.ofSeconds(5));
}
@Test // gh-26344
public void echoChannelWithEmptyInput() {
Flux<String> result = requester.route("echo-channel-empty").data(Flux.empty()).retrieveFlux(String.class);
StepVerifier.create(result).verifyComplete();
}
@Test
public void metadataPush() {
Flux.just("bar", "baz")
@@ -254,6 +260,11 @@ public class RSocketClientToServerIntegrationTests {
return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async");
}
@MessageMapping("echo-channel-empty")
Flux<String> echoChannelEmpty(@Payload(required = false) Flux<String> payloads) {
return payloads.map(payload -> payload + " echoed");
}
@MessageMapping("thrown-exception")
Mono<String> handleAndThrow(String payload) {
throw new IllegalArgumentException("Invalid input error");
@@ -338,29 +349,29 @@ public class RSocketClientToServerIntegrationTests {
}
@Override
public Mono<Void> fireAndForget(Payload payload) {
public Mono<Void> fireAndForget(io.rsocket.Payload payload) {
return this.delegate.fireAndForget(payload)
.doOnSuccess(aVoid -> this.fireAndForgetCount.incrementAndGet());
}
@Override
public Mono<Void> metadataPush(Payload payload) {
public Mono<Void> metadataPush(io.rsocket.Payload payload) {
return this.delegate.metadataPush(payload)
.doOnSuccess(aVoid -> this.metadataPushCount.incrementAndGet());
}
@Override
public Mono<Payload> requestResponse(Payload payload) {
public Mono<io.rsocket.Payload> requestResponse(io.rsocket.Payload payload) {
return this.delegate.requestResponse(payload);
}
@Override
public Flux<Payload> requestStream(Payload payload) {
public Flux<io.rsocket.Payload> requestStream(io.rsocket.Payload payload) {
return this.delegate.requestStream(payload);
}
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
public Flux<io.rsocket.Payload> requestChannel(Publisher<io.rsocket.Payload> payloads) {
return this.delegate.requestChannel(payloads);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
@@ -42,10 +43,10 @@ public class MockMultipartFile implements MultipartFile {
private final String name;
private String originalFilename;
private final String originalFilename;
@Nullable
private String contentType;
private final String contentType;
private final byte[] content;
@@ -79,7 +80,7 @@ public class MockMultipartFile implements MultipartFile {
public MockMultipartFile(
String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content) {
Assert.hasLength(name, "Name must not be null");
Assert.hasLength(name, "Name must not be empty");
this.name = name;
this.originalFilename = (originalFilename != null ? originalFilename : "");
this.contentType = contentType;
@@ -108,6 +109,7 @@ public class MockMultipartFile implements MultipartFile {
}
@Override
@NonNull
public String getOriginalFilename() {
return this.originalFilename;
}
@@ -179,7 +179,7 @@ abstract class BootstrapUtils {
}
private static Class<?> resolveDefaultTestContextBootstrapper(Class<?> testClass) throws Exception {
boolean webApp = (TestContextAnnotationUtils.findMergedAnnotation(testClass, webAppConfigurationClass) != null);
boolean webApp = TestContextAnnotationUtils.hasAnnotation(testClass, webAppConfigurationClass);
String bootstrapperClassName = (webApp ? DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME :
DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME);
return ClassUtils.forName(bootstrapperClassName, BootstrapUtils.class.getClassLoader());
@@ -78,6 +78,22 @@ public abstract class TestContextAnnotationUtils {
private static volatile EnclosingConfiguration defaultEnclosingConfigurationMode;
/**
* Determine if an annotation of the specified {@code annotationType} is
* present or meta-present on the supplied {@link Class} according to the
* search algorithm used in {@link #findMergedAnnotation(Class, Class)}.
* <p>If this method returns {@code true}, then {@code findMergedAnnotation(...)}
* will return a non-null value.
* @param clazz the class to look for annotations on
* @param annotationType the type of annotation to look for
* @return {@code true} if a matching annotation is present
* @since 5.3.3
* @see #findMergedAnnotation(Class, Class)
*/
public static boolean hasAnnotation(Class<?> clazz, Class<? extends Annotation> annotationType) {
return (findMergedAnnotation(clazz, annotationType) != null);
}
/**
* Find the first annotation of the specified {@code annotationType} within
* the annotation hierarchy <em>above</em> the supplied class, merge that
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -48,6 +48,8 @@ package org.springframework.test.context;
* ServletTestExecutionListener}</li>
* <li>{@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener
* DirtiesContextBeforeModesTestExecutionListener}</li>
* <li>{@link org.springframework.test.context.event.ApplicationEventsTestExecutionListener
* ApplicationEventsTestExecutionListener}</li>
* <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener
* DependencyInjectionTestExecutionListener}</li>
* <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener
@@ -67,6 +67,7 @@ public @interface TestExecutionListeners {
* {@link #value}, but it may be used instead of {@link #value}.
* @see org.springframework.test.context.web.ServletTestExecutionListener
* @see org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener
* @see org.springframework.test.context.event.ApplicationEventsTestExecutionListener
* @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener
* @see org.springframework.test.context.support.DirtiesContextTestExecutionListener
* @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
@@ -0,0 +1,82 @@
/*
* Copyright 2002-2020 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.test.context.event;
import java.util.stream.Stream;
import org.springframework.context.ApplicationEvent;
/**
* {@code ApplicationEvents} encapsulates all {@linkplain ApplicationEvent
* application events} that were fired during the execution of a single test method.
*
* <p>To use {@code ApplicationEvents} in your tests, do the following.
* <ul>
* <li>Ensure that your test class is annotated or meta-annotated with
* {@link RecordApplicationEvents @RecordApplicationEvents}.</li>
* <li>Ensure that the {@link ApplicationEventsTestExecutionListener} is
* registered. Note, however, that it is registered by default and only needs
* to be manually registered if you have custom configuration via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}
* that does not include the default listeners.</li>
* <li>Annotate a field of type {@code ApplicationEvents} with
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired} and
* use that instance of {@code ApplicationEvents} in your test and lifecycle methods.</li>
* <li>With JUnit Jupiter, you may optionally declare a parameter of type
* {@code ApplicationEvents} in a test or lifecycle method as an alternative to
* an {@code @Autowired} field in the test class.</li>
* </ul>
*
* @author Sam Brannen
* @author Oliver Drotbohm
* @since 5.3.3
* @see RecordApplicationEvents
* @see ApplicationEventsTestExecutionListener
* @see org.springframework.context.ApplicationEvent
*/
public interface ApplicationEvents {
/**
* Stream all application events that were fired during test execution.
* @return a stream of all application events
* @see #stream(Class)
* @see #clear()
*/
Stream<ApplicationEvent> stream();
/**
* Stream all application events or event payloads of the given type that
* were fired during test execution.
* @param <T> the event type
* @param type the type of events or payloads to stream; never {@code null}
* @return a stream of all application events or event payloads of the
* specified type
* @see #stream()
* @see #clear()
*/
<T> Stream<T> stream(Class<T> type);
/**
* Clear all application events recorded by this {@code ApplicationEvents} instance.
* <p>Subsequent calls to {@link #stream()} or {@link #stream(Class)} will
* only include events recorded since this method was invoked.
* @see #stream()
* @see #stream(Class)
*/
void clear();
}
@@ -0,0 +1,41 @@
/*
* Copyright 2002-2020 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.test.context.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
/**
* {@link ApplicationListener} that listens to all events and adds them to the
* current {@link ApplicationEvents} instance if registered for the current thread.
*
* @author Sam Brannen
* @author Oliver Drotbohm
* @since 5.3.3
*/
class ApplicationEventsApplicationListener implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event) {
DefaultApplicationEvents applicationEvents =
(DefaultApplicationEvents) ApplicationEventsHolder.getApplicationEvents();
if (applicationEvents != null) {
applicationEvents.addEvent(event);
}
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2002-2020 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.test.context.event;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Holder class to expose the application events published during the execution
* of a test in the form of a thread-bound {@link ApplicationEvents} object.
*
* <p>{@code ApplicationEvents} are registered in this holder and managed by
* the {@link ApplicationEventsTestExecutionListener}.
*
* <p>Although this class is {@code public}, it is only intended for use within
* the <em>Spring TestContext Framework</em> or in the implementation of
* third-party extensions. Test authors should therefore allow the current
* instance of {@code ApplicationEvents} to be
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired}
* into a field in the test class or injected via a parameter in test and
* lifecycle methods when using JUnit Jupiter and the {@link
* org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension}.
*
* @author Sam Brannen
* @author Oliver Drotbohm
* @since 5.3.3
* @see ApplicationEvents
* @see RecordApplicationEvents
* @see ApplicationEventsTestExecutionListener
*/
public abstract class ApplicationEventsHolder {
private static final ThreadLocal<DefaultApplicationEvents> applicationEvents = new ThreadLocal<>();
private ApplicationEventsHolder() {
// no-op to prevent instantiation of this holder class
}
/**
* Get the {@link ApplicationEvents} for the current thread.
* @return the current {@code ApplicationEvents}, or {@code null} if not registered
*/
@Nullable
public static ApplicationEvents getApplicationEvents() {
return applicationEvents.get();
}
/**
* Get the {@link ApplicationEvents} for the current thread.
* @return the current {@code ApplicationEvents}
* @throws IllegalStateException if an instance of {@code ApplicationEvents}
* has not been registered for the current thread
*/
@Nullable
public static ApplicationEvents getRequiredApplicationEvents() {
ApplicationEvents events = applicationEvents.get();
Assert.state(events != null, "Failed to retrieve ApplicationEvents for the current thread. " +
"Ensure that your test class is annotated with @RecordApplicationEvents " +
"and that the ApplicationEventsTestExecutionListener is registered.");
return events;
}
/**
* Register a new {@link DefaultApplicationEvents} instance to be used for the
* current thread, if necessary.
* <p>If {@link #registerApplicationEvents()} has already been called for the
* current thread, this method does not do anything.
*/
static void registerApplicationEventsIfNecessary() {
if (getApplicationEvents() == null) {
registerApplicationEvents();
}
}
/**
* Register a new {@link DefaultApplicationEvents} instance to be used for the
* current thread.
*/
static void registerApplicationEvents() {
applicationEvents.set(new DefaultApplicationEvents());
}
/**
* Remove the registration of the {@link ApplicationEvents} for the current thread.
*/
static void unregisterApplicationEvents() {
applicationEvents.remove();
}
}
@@ -0,0 +1,138 @@
/*
* Copyright 2002-2020 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.test.context.event;
import java.io.Serializable;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.Conventions;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.util.Assert;
/**
* {@code TestExecutionListener} which provides support for {@link ApplicationEvents}.
*
* <p>This listener manages the registration of {@code ApplicationEvents} for the
* current thread at various points within the test execution lifecycle and makes
* the current instance of {@code ApplicationEvents} available to tests via an
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired}
* field in the test class.
*
* <p>If the test class is not annotated or meta-annotated with
* {@link RecordApplicationEvents @RecordApplicationEvents}, this listener
* effectively does nothing.
*
* @author Sam Brannen
* @since 5.3.3
* @see ApplicationEvents
* @see ApplicationEventsHolder
*/
public class ApplicationEventsTestExecutionListener extends AbstractTestExecutionListener {
/**
* Attribute name for a {@link TestContext} attribute which indicates
* whether the test class for the given test context is annotated with
* {@link RecordApplicationEvents @RecordApplicationEvents}.
* <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
*/
private static final String RECORD_APPLICATION_EVENTS = Conventions.getQualifiedAttributeName(
ApplicationEventsTestExecutionListener.class, "recordApplicationEvents");
private static final Object applicationEventsMonitor = new Object();
/**
* Returns {@code 1800}.
*/
@Override
public final int getOrder() {
return 1800;
}
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
if (recordApplicationEvents(testContext)) {
registerListenerAndResolvableDependencyIfNecessary(testContext.getApplicationContext());
ApplicationEventsHolder.registerApplicationEvents();
}
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
if (recordApplicationEvents(testContext)) {
// Register a new ApplicationEvents instance for the current thread
// in case the test instance is shared -- for example, in TestNG or
// JUnit Jupiter with @TestInstance(PER_CLASS) semantics.
ApplicationEventsHolder.registerApplicationEventsIfNecessary();
}
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
if (recordApplicationEvents(testContext)) {
ApplicationEventsHolder.unregisterApplicationEvents();
}
}
private boolean recordApplicationEvents(TestContext testContext) {
return testContext.computeAttribute(RECORD_APPLICATION_EVENTS, name ->
TestContextAnnotationUtils.hasAnnotation(testContext.getTestClass(), RecordApplicationEvents.class));
}
private void registerListenerAndResolvableDependencyIfNecessary(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext,
"The ApplicationContext for the test must be an AbstractApplicationContext");
AbstractApplicationContext aac = (AbstractApplicationContext) applicationContext;
// Synchronize to avoid race condition in parallel test execution
synchronized(applicationEventsMonitor) {
boolean notAlreadyRegistered = aac.getApplicationListeners().stream()
.map(Object::getClass)
.noneMatch(ApplicationEventsApplicationListener.class::equals);
if (notAlreadyRegistered) {
// Register a new ApplicationEventsApplicationListener.
aac.addApplicationListener(new ApplicationEventsApplicationListener());
// Register ApplicationEvents as a resolvable dependency for @Autowired support in test classes.
ConfigurableListableBeanFactory beanFactory = aac.getBeanFactory();
beanFactory.registerResolvableDependency(ApplicationEvents.class, new ApplicationEventsObjectFactory());
}
}
}
/**
* Factory that exposes the current {@link ApplicationEvents} object on demand.
*/
@SuppressWarnings("serial")
private static class ApplicationEventsObjectFactory implements ObjectFactory<ApplicationEvents>, Serializable {
@Override
public ApplicationEvents getObject() {
return ApplicationEventsHolder.getRequiredApplicationEvents();
}
@Override
public String toString() {
return "Current ApplicationEvents";
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2002-2020 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.test.context.event;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.PayloadApplicationEvent;
/**
* Default implementation of {@link ApplicationEvents}.
*
* @author Oliver Drotbohm
* @author Sam Brannen
* @since 5.3.3
*/
class DefaultApplicationEvents implements ApplicationEvents {
private final List<ApplicationEvent> events = new ArrayList<>();
void addEvent(ApplicationEvent event) {
this.events.add(event);
}
@Override
public Stream<ApplicationEvent> stream() {
return this.events.stream();
}
@Override
public <T> Stream<T> stream(Class<T> type) {
return this.events.stream()
.map(this::unwrapPayloadEvent)
.filter(type::isInstance)
.map(type::cast);
}
@Override
public void clear() {
this.events.clear();
}
private Object unwrapPayloadEvent(Object source) {
return (PayloadApplicationEvent.class.isInstance(source) ?
((PayloadApplicationEvent<?>) source).getPayload() : source);
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2002-2020 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.test.context.event;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@code @RecordApplicationEvents} is a class-level annotation that is used to
* instruct the <em>Spring TestContext Framework</em> to record all
* {@linkplain org.springframework.context.ApplicationEvent application events}
* that are published in the {@link org.springframework.context.ApplicationContext
* ApplicationContext} during the execution of a single test.
*
* <p>The recorded events can be accessed via the {@link ApplicationEvents} API
* within your tests.
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em>.
*
* @author Sam Brannen
* @since 5.3.3
* @see ApplicationEvents
* @see ApplicationEventsTestExecutionListener
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RecordApplicationEvents {
}
@@ -52,6 +52,7 @@ import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.lang.Nullable;
import org.springframework.test.context.TestConstructor;
import org.springframework.test.context.TestContextManager;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.support.PropertyProvider;
import org.springframework.test.context.support.TestConstructorUtils;
import org.springframework.util.Assert;
@@ -218,6 +219,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
* invoked with a fallback {@link PropertyProvider} that delegates its lookup
* to {@link ExtensionContext#getConfigurationParameter(String)}.</li>
* <li>The parameter is of type {@link ApplicationContext} or a sub-type thereof.</li>
* <li>The parameter is of type {@link ApplicationEvents} or a sub-type thereof.</li>
* <li>{@link ParameterResolutionDelegate#isAutowirable} returns {@code true}.</li>
* </ol>
* <p><strong>WARNING</strong>: If a test class {@code Constructor} is annotated
@@ -238,9 +240,19 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
extensionContext.getConfigurationParameter(propertyName).orElse(null);
return (TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) ||
ApplicationContext.class.isAssignableFrom(parameter.getType()) ||
supportsApplicationEvents(parameterContext) ||
ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex()));
}
private boolean supportsApplicationEvents(ParameterContext parameterContext) {
if (ApplicationEvents.class.isAssignableFrom(parameterContext.getParameter().getType())) {
Assert.isTrue(parameterContext.getDeclaringExecutable() instanceof Method,
"ApplicationEvents can only be injected into test and lifecycle methods");
return true;
}
return false;
}
/**
* Resolve a value for the {@link Parameter} in the supplied {@link ParameterContext} by
* retrieving the corresponding dependency from the test's {@link ApplicationContext}.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,6 +27,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextManager;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
@@ -54,6 +55,7 @@ import org.springframework.test.context.web.ServletTestExecutionListener;
* <ul>
* <li>{@link org.springframework.test.context.web.ServletTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener}
* <li>{@link org.springframework.test.context.event.ApplicationEventsTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener}
* <li>{@link org.springframework.test.context.event.EventPublishingTestExecutionListener}
@@ -82,6 +84,7 @@ import org.springframework.test.context.web.ServletTestExecutionListener;
* @see TestExecutionListeners
* @see ServletTestExecutionListener
* @see DirtiesContextBeforeModesTestExecutionListener
* @see ApplicationEventsTestExecutionListener
* @see DependencyInjectionTestExecutionListener
* @see DirtiesContextTestExecutionListener
* @see EventPublishingTestExecutionListener
@@ -90,8 +93,8 @@ import org.springframework.test.context.web.ServletTestExecutionListener;
*/
@RunWith(SpringRunner.class)
@TestExecutionListeners({ ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class,
EventPublishingTestExecutionListener.class })
ApplicationEventsTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, EventPublishingTestExecutionListener.class })
public abstract class AbstractJUnit4SpringContextTests implements ApplicationContextAware {
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,6 +27,7 @@ import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
@@ -62,6 +63,7 @@ import org.springframework.util.Assert;
* <ul>
* <li>{@link org.springframework.test.context.web.ServletTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener}
* <li>{@link org.springframework.test.context.event.ApplicationEventsTestExecutionListener}</li>
* <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener}
* <li>{@link org.springframework.test.context.transaction.TransactionalTestExecutionListener}
@@ -100,8 +102,8 @@ import org.springframework.util.Assert;
* @see org.springframework.test.jdbc.JdbcTestUtils
* @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
*/
@TestExecutionListeners(listeners = { ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
@TestExecutionListeners(listeners = { ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class,
ApplicationEventsTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class, EventPublishingTestExecutionListener.class }, inheritListeners = false)
@Transactional
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -19,6 +19,7 @@ package org.springframework.test.context.support;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
@@ -200,6 +201,17 @@ public class DefaultTestContext implements TestContext {
return this.attributes.get(name);
}
@Override
@SuppressWarnings("unchecked")
public <T> T computeAttribute(String name, Function<String, T> computeFunction) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(computeFunction, "Compute function must not be null");
Object value = this.attributes.computeIfAbsent(name, computeFunction);
Assert.state(value != null,
() -> String.format("Compute function must not return null for attribute named '%s'", name));
return (T) value;
}
@Override
@Nullable
public Object removeAttribute(String name) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -36,6 +36,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextManager;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
@@ -64,6 +65,7 @@ import org.springframework.test.context.web.ServletTestExecutionListener;
* <ul>
* <li>{@link org.springframework.test.context.web.ServletTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener}
* <li>{@link org.springframework.test.context.event.ApplicationEventsTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener}
* <li>{@link org.springframework.test.context.event.EventPublishingTestExecutionListener}
@@ -78,6 +80,7 @@ import org.springframework.test.context.web.ServletTestExecutionListener;
* @see TestExecutionListeners
* @see ServletTestExecutionListener
* @see DirtiesContextBeforeModesTestExecutionListener
* @see ApplicationEventsTestExecutionListener
* @see DependencyInjectionTestExecutionListener
* @see DirtiesContextTestExecutionListener
* @see EventPublishingTestExecutionListener
@@ -85,8 +88,8 @@ import org.springframework.test.context.web.ServletTestExecutionListener;
* @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests
*/
@TestExecutionListeners({ ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class,
EventPublishingTestExecutionListener.class })
ApplicationEventsTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, EventPublishingTestExecutionListener.class })
public abstract class AbstractTestNGSpringContextTests implements IHookable, ApplicationContextAware {
/** Logger available to subclasses. */
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -26,6 +26,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
@@ -61,6 +62,7 @@ import org.springframework.util.Assert;
* <ul>
* <li>{@link org.springframework.test.context.web.ServletTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener}
* <li>{@link org.springframework.test.context.event.ApplicationEventsTestExecutionListener}</li>
* <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener}
* <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener}
* <li>{@link org.springframework.test.context.transaction.TransactionalTestExecutionListener}
@@ -84,8 +86,8 @@ import org.springframework.util.Assert;
* @see org.springframework.test.jdbc.JdbcTestUtils
* @see org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
*/
@TestExecutionListeners(listeners = { ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
@TestExecutionListeners(listeners = { ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class,
ApplicationEventsTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class, EventPublishingTestExecutionListener.class }, inheritListeners = false)
@Transactional
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -57,6 +57,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
private static final boolean httpComponentsClientPresent;
private static final boolean webFluxPresent;
static {
ClassLoader loader = DefaultWebTestClientBuilder.class.getClassLoader();
reactorClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader);
@@ -64,6 +66,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
httpComponentsClientPresent =
ClassUtils.isPresent("org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient", loader) &&
ClassUtils.isPresent("org.apache.hc.core5.reactive.ReactiveDataConsumer", loader);
webFluxPresent = ClassUtils.isPresent(
"org.springframework.web.reactive.function.client.ExchangeFunction", loader);
}
@@ -119,6 +123,10 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
Assert.isTrue(httpHandlerBuilder == null || connector == null,
"Expected WebHttpHandlerBuilder or ClientHttpConnector but not both.");
// Helpful message especially for MockMvcWebTestClient users
Assert.state(webFluxPresent,
"To use WebTestClient, please add spring-webflux to the test classpath.");
this.connector = connector;
this.httpHandlerBuilder = (httpHandlerBuilder != null ? httpHandlerBuilder.clone() : null);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -24,6 +24,8 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
@@ -54,6 +56,8 @@ import org.springframework.util.MultiValueMap;
*/
public class ExchangeResult {
private static Log logger = LogFactory.getLog(ExchangeResult.class);
private static final List<MediaType> PRINTABLE_MEDIA_TYPES = Arrays.asList(
MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML,
MediaType.parseMediaType("text/*"), MediaType.APPLICATION_FORM_URLENCODED);
@@ -223,7 +227,10 @@ public class ExchangeResult {
assertion.run();
}
catch (AssertionError ex) {
throw new AssertionError(ex.getMessage() + "\n" + this, ex);
if (logger.isErrorEnabled()) {
logger.error("Request details for assertion failure:\n" + this);
}
throw ex;
}
}
@@ -35,7 +35,6 @@ import org.springframework.web.context.request.async.CallableProcessingIntercept
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.ModelAndView;
@@ -68,10 +67,6 @@ final class TestDispatcherServlet extends DispatcherServlet {
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (!request.getParts().isEmpty()) {
request = new StandardMultipartHttpServletRequest(request);
}
registerAsyncResultInterceptors(request);
super.service(request, response);
@@ -17,7 +17,10 @@
package org.springframework.test.web.servlet.request;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -25,17 +28,17 @@ import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.http.Part;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.mock.web.MockPart;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
/**
* Default builder for {@link MockMultipartHttpServletRequest}.
@@ -141,26 +144,47 @@ public class MockMultipartHttpServletRequestBuilder extends MockHttpServletReque
@Override
protected final MockHttpServletRequest createServletRequest(ServletContext servletContext) {
MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(servletContext);
this.files.forEach(file -> request.addPart(toMockPart(file)));
this.parts.values().stream().flatMap(Collection::stream).forEach(request::addPart);
this.files.forEach(request::addFile);
this.parts.values().stream().flatMap(Collection::stream).forEach(part -> {
request.addPart(part);
try {
MultipartFile file = asMultipartFile(part);
if (file != null) {
request.addFile(file);
return;
}
String value = toParameterValue(part);
if (value != null) {
request.addParameter(part.getName(), toParameterValue(part));
}
}
catch (IOException ex) {
throw new IllegalStateException("Failed to read content for part " + part.getName(), ex);
}
});
return request;
}
private MockPart toMockPart(MockMultipartFile file) {
byte[] bytes = null;
if (!file.isEmpty()) {
try {
bytes = file.getBytes();
}
catch (IOException ex) {
throw new IllegalStateException("Unexpected IOException", ex);
}
@Nullable
private MultipartFile asMultipartFile(Part part) throws IOException {
String name = part.getName();
String filename = part.getSubmittedFileName();
if (filename != null) {
return new MockMultipartFile(name, filename, part.getContentType(), part.getInputStream());
}
MockPart part = new MockPart(file.getName(), file.getOriginalFilename(), bytes);
if (file.getContentType() != null) {
part.getHeaders().set(HttpHeaders.CONTENT_TYPE, file.getContentType());
return null;
}
@Nullable
private String toParameterValue(Part part) throws IOException {
String rawType = part.getContentType();
MediaType mediaType = (rawType != null ? MediaType.parseMediaType(rawType) : MediaType.TEXT_PLAIN);
if (!mediaType.isCompatibleWith(MediaType.TEXT_PLAIN)) {
return null;
}
return part;
Charset charset = (mediaType.getCharset() != null ? mediaType.getCharset() : StandardCharsets.UTF_8);
InputStreamReader reader = new InputStreamReader(part.getInputStream(), charset);
return FileCopyUtils.copyToString(reader);
}
}
@@ -3,6 +3,7 @@
org.springframework.test.context.TestExecutionListener = \
org.springframework.test.context.web.ServletTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\
org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
@@ -56,10 +57,15 @@ class TestExecutionListenersTests {
@Test
void defaultListeners() {
List<Class<?>> expected = asList(ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class, EventPublishingTestExecutionListener.class);
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class
);
assertRegisteredListeners(DefaultListenersTestCase.class, expected);
}
@@ -68,10 +74,16 @@ class TestExecutionListenersTests {
*/
@Test
void defaultListenersMergedWithCustomListenerPrepended() {
List<Class<?>> expected = asList(QuuxTestExecutionListener.class, ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class, EventPublishingTestExecutionListener.class);
List<Class<?>> expected = asList(QuuxTestExecutionListener.class,//
ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class
);
assertRegisteredListeners(MergedDefaultListenersWithCustomListenerPrependedTestCase.class, expected);
}
@@ -80,11 +92,16 @@ class TestExecutionListenersTests {
*/
@Test
void defaultListenersMergedWithCustomListenerAppended() {
List<Class<?>> expected = asList(ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class, EventPublishingTestExecutionListener.class,
BazTestExecutionListener.class);
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class,//
BazTestExecutionListener.class
);
assertRegisteredListeners(MergedDefaultListenersWithCustomListenerAppendedTestCase.class, expected);
}
@@ -93,11 +110,16 @@ class TestExecutionListenersTests {
*/
@Test
void defaultListenersMergedWithCustomListenerInserted() {
List<Class<?>> expected = asList(ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
BarTestExecutionListener.class, DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class, SqlScriptsTestExecutionListener.class,
EventPublishingTestExecutionListener.class);
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
DirtiesContextBeforeModesTestExecutionListener.class,//
ApplicationEventsTestExecutionListener.class,//
DependencyInjectionTestExecutionListener.class,//
BarTestExecutionListener.class,//
DirtiesContextTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class
);
assertRegisteredListeners(MergedDefaultListenersWithCustomListenerInsertedTestCase.class, expected);
}
@@ -0,0 +1,91 @@
/*
* Copyright 2002-2020 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.test.context.junit.jupiter.event;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.test.context.event.ApplicationEventsHolder;
/**
* Default implementation of {@link PublishedEvents}.
*
* <p>Copied from the Moduliths project.
*
* @author Oliver Drotbohm
* @author Sam Brannen
* @since 5.3.3
*/
class DefaultPublishedEvents implements PublishedEvents {
@Override
public <T> TypedPublishedEvents<T> ofType(Class<T> type) {
return SimpleTypedPublishedEvents.of(ApplicationEventsHolder.getRequiredApplicationEvents().stream(type));
}
private static class SimpleTypedPublishedEvents<T> implements TypedPublishedEvents<T> {
private final List<T> events;
private SimpleTypedPublishedEvents(List<T> events) {
this.events = events;
}
static <T> SimpleTypedPublishedEvents<T> of(Stream<T> stream) {
return new SimpleTypedPublishedEvents<>(stream.collect(Collectors.toList()));
}
@Override
public <S extends T> TypedPublishedEvents<S> ofSubType(Class<S> subType) {
return SimpleTypedPublishedEvents.of(getFilteredEvents(subType::isInstance)//
.map(subType::cast));
}
@Override
public TypedPublishedEvents<T> matching(Predicate<? super T> predicate) {
return SimpleTypedPublishedEvents.of(getFilteredEvents(predicate));
}
@Override
public <S> TypedPublishedEvents<T> matchingMapped(Function<T, S> mapper, Predicate<? super S> predicate) {
return SimpleTypedPublishedEvents.of(this.events.stream().flatMap(it -> {
S mapped = mapper.apply(it);
return predicate.test(mapped) ? Stream.of(it) : Stream.empty();
}));
}
private Stream<T> getFilteredEvents(Predicate<? super T> predicate) {
return this.events.stream().filter(predicate);
}
@Override
public Iterator<T> iterator() {
return this.events.iterator();
}
@Override
public String toString() {
return this.events.toString();
}
}
}
@@ -0,0 +1,275 @@
/*
* Copyright 2002-2020 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.test.context.junit.jupiter.event;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD;
/**
* Integration tests for {@link ApplicationEvents} in conjunction with JUnit Jupiter.
*
* @author Sam Brannen
* @since 5.3.3
*/
@SpringJUnitConfig
@RecordApplicationEvents
class JUnitJupiterApplicationEventsIntegrationTests {
@Autowired
ApplicationContext context;
@Autowired
ApplicationEvents applicationEvents;
@Nested
@TestInstance(PER_METHOD)
class TestInstancePerMethodTests {
@BeforeEach
void beforeEach() {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
context.publishEvent(new CustomEvent("beforeEach"));
assertCustomEvents(applicationEvents, "beforeEach");
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
}
@Test
void test1(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
@Test
void test2(@Autowired ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
String testName = testInfo.getTestMethod().get().getName();
assertEventTypes(events, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent");
context.publishEvent(new CustomEvent(testName));
context.publishEvent("payload1");
context.publishEvent("payload2");
assertCustomEvents(events, "beforeEach", testName);
assertPayloads(events.stream(String.class), "payload1", "payload2");
}
@AfterEach
void afterEach(@Autowired ApplicationEvents events, TestInfo testInfo) {
String testName = testInfo.getTestMethod().get().getName();
assertEventTypes(events, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent", "PayloadApplicationEvent", "PayloadApplicationEvent",
"AfterTestExecutionEvent");
context.publishEvent(new CustomEvent("afterEach"));
assertCustomEvents(events, "beforeEach", testName, "afterEach");
assertEventTypes(events, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent", "PayloadApplicationEvent", "PayloadApplicationEvent",
"AfterTestExecutionEvent", "CustomEvent");
}
}
@Nested
@TestInstance(PER_METHOD)
class TestInstancePerMethodWithClearedEventsTests {
@BeforeEach
void beforeEach() {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
context.publishEvent(new CustomEvent("beforeEach"));
assertCustomEvents(applicationEvents, "beforeEach");
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
applicationEvents.clear();
assertThat(applicationEvents.stream()).isEmpty();
}
@Test
void test1(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
@Test
void test2(@Autowired ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
String testName = testInfo.getTestMethod().get().getName();
assertEventTypes(events, "BeforeTestExecutionEvent");
context.publishEvent(new CustomEvent(testName));
assertCustomEvents(events, testName);
assertEventTypes(events, "BeforeTestExecutionEvent", "CustomEvent");
}
@AfterEach
void afterEach(@Autowired ApplicationEvents events, TestInfo testInfo) {
events.clear();
context.publishEvent(new CustomEvent("afterEach"));
assertCustomEvents(events, "afterEach");
assertEventTypes(events, "CustomEvent");
}
}
@Nested
@TestInstance(PER_CLASS)
class TestInstancePerClassTests {
private boolean testAlreadyExecuted = false;
@BeforeEach
void beforeEach(TestInfo testInfo) {
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
"BeforeTestMethodEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent");
}
context.publishEvent(new CustomEvent("beforeEach"));
assertCustomEvents(applicationEvents, "beforeEach");
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
"BeforeTestMethodEvent", "CustomEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent");
}
}
@Test
void test1(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
@Test
void test2(@Autowired ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
String testName = testInfo.getTestMethod().get().getName();
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
"BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent");
}
context.publishEvent(new CustomEvent(testName));
assertCustomEvents(events, "beforeEach", testName);
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
"BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent", "CustomEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
"CustomEvent");
}
}
@AfterEach
void afterEach(@Autowired ApplicationEvents events, TestInfo testInfo) {
String testName = testInfo.getTestMethod().get().getName();
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
"BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent", "CustomEvent",
"AfterTestExecutionEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
"CustomEvent", "AfterTestExecutionEvent");
}
context.publishEvent(new CustomEvent("afterEach"));
assertCustomEvents(events, "beforeEach", testName, "afterEach");
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
"BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent", "CustomEvent",
"AfterTestExecutionEvent", "CustomEvent");
testAlreadyExecuted = true;
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
"CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
}
}
}
private static void assertEventTypes(ApplicationEvents applicationEvents, String... types) {
assertThat(applicationEvents.stream().map(event -> event.getClass().getSimpleName()))
.containsExactly(types);
}
private static void assertPayloads(Stream<String> events, String... values) {
assertThat(events).extracting(Object::toString).containsExactly(values);
}
private static void assertCustomEvents(ApplicationEvents events, String... messages) {
assertThat(events.stream(CustomEvent.class)).extracting(CustomEvent::getMessage).containsExactly(messages);
}
@Configuration
static class Config {
}
@SuppressWarnings("serial")
static class CustomEvent extends ApplicationEvent {
private final String message;
CustomEvent(String message) {
super(message);
this.message = message;
}
String getMessage() {
return message;
}
}
}
@@ -0,0 +1,174 @@
/*
* Copyright 2002-2020 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.test.context.junit.jupiter.event;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
/**
* Integration tests that verify parallel execution support for {@link ApplicationEvents}
* in conjunction with JUnit Jupiter.
*
* @author Sam Brannen
* @since 5.3.3
*/
class ParallelApplicationEventsIntegrationTests {
private static final Set<String> payloads = ConcurrentHashMap.newKeySet();
@ParameterizedTest
@ValueSource(classes = {TestInstancePerMethodTestCase.class, TestInstancePerClassTestCase.class})
void executeTestsInParallel(Class<?> testClass) {
EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(testClass))//
.configurationParameter("junit.jupiter.execution.parallel.enabled", "true")//
.configurationParameter("junit.jupiter.execution.parallel.config.dynamic.factor", "10")//
.execute()//
.testEvents()//
.assertStatistics(stats -> stats.started(10).succeeded(10).failed(0));
Set<String> testNames = payloads.stream()//
.map(payload -> payload.substring(0, payload.indexOf("-")))//
.collect(Collectors.toSet());
Set<String> threadNames = payloads.stream()//
.map(payload -> payload.substring(payload.indexOf("-")))//
.collect(Collectors.toSet());
assertThat(payloads).hasSize(10);
assertThat(testNames).hasSize(10);
// There are probably 10 different thread names on a developer's machine,
// but we really just want to assert that at least two different threads
// were used, since the CI server seems to have fewer threads available.
assertThat(threadNames).hasSizeGreaterThanOrEqualTo(2);
}
@AfterEach
void resetPayloads() {
payloads.clear();
}
@SpringJUnitConfig
@RecordApplicationEvents
@Execution(ExecutionMode.CONCURRENT)
@TestInstance(Lifecycle.PER_METHOD)
static class TestInstancePerMethodTestCase {
@Autowired
ApplicationContext context;
@Autowired
ApplicationEvents events;
@Test
void test1(TestInfo testInfo) {
assertTestExpectations(this.events, testInfo);
}
@Test
void test2(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
@Test
void test3(TestInfo testInfo) {
assertTestExpectations(this.events, testInfo);
}
@Test
void test4(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
@Test
void test5(TestInfo testInfo) {
assertTestExpectations(this.events, testInfo);
}
@Test
void test6(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
@Test
void test7(TestInfo testInfo) {
assertTestExpectations(this.events, testInfo);
}
@Test
void test8(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
@Test
void test9(TestInfo testInfo) {
assertTestExpectations(this.events, testInfo);
}
@Test
void test10(ApplicationEvents events, TestInfo testInfo) {
assertTestExpectations(events, testInfo);
}
private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
String testName = testInfo.getTestMethod().get().getName();
String threadName = Thread.currentThread().getName();
String localPayload = testName + "-" + threadName;
context.publishEvent(localPayload);
assertPayloads(events.stream(String.class), localPayload);
}
private static void assertPayloads(Stream<String> events, String... values) {
assertThat(events.peek(payloads::add)).extracting(Object::toString).containsExactly(values);
}
@Configuration
static class Config {
}
}
@TestInstance(Lifecycle.PER_CLASS)
static class TestInstancePerClassTestCase extends TestInstancePerMethodTestCase {
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2002-2020 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.test.context.junit.jupiter.event;
import java.util.Arrays;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* All Spring application events fired during the test execution.
*
* <p>Copied from the Moduliths project.
*
* @author Oliver Drotbohm
* @since 5.3.3
*/
public interface PublishedEvents {
/**
* Creates a new {@link PublishedEvents} instance for the given events.
*
* @param events must not be {@literal null}
* @return will never be {@literal null}
*/
public static PublishedEvents of(Object... events) {
return of(Arrays.asList(events));
}
/**
* Returns all application events of the given type that were fired during the test execution.
*
* @param <T> the event type
* @param type must not be {@literal null}
*/
<T> TypedPublishedEvents<T> ofType(Class<T> type);
/**
* All application events of a given type that were fired during a test execution.
*
* @param <T> the event type
*/
interface TypedPublishedEvents<T> extends Iterable<T> {
/**
* Further constrain the event type for downstream assertions.
*
* @param subType the sub type
* @return will never be {@literal null}
*/
<S extends T> TypedPublishedEvents<S> ofSubType(Class<S> subType);
/**
* Returns all {@link TypedPublishedEvents} that match the given predicate.
*
* @param predicate must not be {@literal null}
* @return will never be {@literal null}
*/
TypedPublishedEvents<T> matching(Predicate<? super T> predicate);
/**
* Returns all {@link TypedPublishedEvents} that match the given predicate
* after applying the given mapping step.
*
* @param <S> the intermediate type to apply the {@link Predicate} on
* @param mapper the mapping step to extract a part of the original event
* subject to test for the {@link Predicate}
* @param predicate the {@link Predicate} to apply on the value extracted
* @return will never be {@literal null}
*/
<S> TypedPublishedEvents<T> matchingMapped(Function<T, S> mapper, Predicate<? super S> predicate);
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2002-2020 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.test.context.junit.jupiter.event;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
/**
* @author Sam Brannen
* @since 5.3.3
*/
public class PublishedEventsExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return PublishedEvents.class.isAssignableFrom(parameterContext.getParameter().getType());
}
@Override
public PublishedEvents resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return new DefaultPublishedEvents();
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2002-2020 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.test.context.junit.jupiter.event;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.BeforeTestExecutionEvent;
import org.springframework.test.context.event.BeforeTestMethodEvent;
import org.springframework.test.context.event.PrepareTestInstanceEvent;
import org.springframework.test.context.event.RecordApplicationEvents;
import org.springframework.test.context.event.TestContextEvent;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the example {@link PublishedEvents} extension to the
* {@link ApplicationEvents} feature.
*
* @author Sam Brannen
* @since 5.3.3
*/
@SpringJUnitConfig
@RecordApplicationEvents
@ExtendWith(PublishedEventsExtension.class)
class PublishedEventsIntegrationTests {
@Test
void test(PublishedEvents publishedEvents) {
assertThat(publishedEvents).isNotNull();
assertThat(publishedEvents.ofType(TestContextEvent.class)).hasSize(3);
assertThat(publishedEvents.ofType(PrepareTestInstanceEvent.class)).hasSize(1);
assertThat(publishedEvents.ofType(BeforeTestMethodEvent.class)).hasSize(1);
assertThat(publishedEvents.ofType(BeforeTestExecutionEvent.class)).hasSize(1);
assertThat(publishedEvents.ofType(TestContextEvent.class).ofSubType(BeforeTestExecutionEvent.class)).hasSize(1);
}
@Configuration
static class Config {
}
}
@@ -0,0 +1,123 @@
/*
* Copyright 2002-2020 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.test.context.junit4.event;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link ApplicationEvents} in conjunction with JUnit 4.
*
* @author Sam Brannen
* @since 5.3.3
*/
@RunWith(SpringRunner.class)
@RecordApplicationEvents
public class JUnit4ApplicationEventsIntegrationTests {
@Rule
public final TestName testName = new TestName();
@Autowired
ApplicationContext context;
@Autowired
ApplicationEvents applicationEvents;
@Before
public void beforeEach() {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
context.publishEvent(new CustomEvent("beforeEach"));
assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
.containsExactly("beforeEach");
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
}
@Test
public void test1() {
assertTestExpectations("test1");
}
@Test
public void test2() {
assertTestExpectations("test2");
}
private void assertTestExpectations(String testName) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent");
context.publishEvent(new CustomEvent(testName));
assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
.containsExactly("beforeEach", testName);
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent");
}
@After
public void afterEach() {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent");
context.publishEvent(new CustomEvent("afterEach"));
assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
.containsExactly("beforeEach", this.testName.getMethodName(), "afterEach");
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
}
private static void assertEventTypes(ApplicationEvents applicationEvents, String... types) {
assertThat(applicationEvents.stream().map(event -> event.getClass().getSimpleName()))
.containsExactly(types);
}
@Configuration
static class Config {
}
@SuppressWarnings("serial")
static class CustomEvent extends ApplicationEvent {
private final String message;
CustomEvent(String message) {
super(message);
this.message = message;
}
String getMessage() {
return message;
}
}
}
@@ -0,0 +1,160 @@
/*
* Copyright 2002-2020 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.test.context.testng.event;
import java.lang.reflect.Method;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link ApplicationEvents} in conjunction with TestNG.
*
* @author Sam Brannen
* @since 5.3.3
*/
@RecordApplicationEvents
class TestNGApplicationEventsIntegrationTests extends AbstractTestNGSpringContextTests {
@Autowired
ApplicationContext context;
@Autowired
ApplicationEvents applicationEvents;
private boolean testAlreadyExecuted = false;
@BeforeMethod
void beforeEach() {
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent");
}
context.publishEvent(new CustomEvent("beforeEach"));
assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
.containsExactly("beforeEach");
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent");
}
}
@Test
void test1() {
assertTestExpectations("test1");
}
@Test
void test2() {
assertTestExpectations("test2");
}
private void assertTestExpectations(String testName) {
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent");
}
context.publishEvent(new CustomEvent(testName));
assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
.containsExactly("beforeEach", testName);
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
"CustomEvent");
}
}
@AfterMethod
void afterEach(Method testMethod) {
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent");
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
"CustomEvent", "AfterTestExecutionEvent");
}
context.publishEvent(new CustomEvent("afterEach"));
assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
.containsExactly("beforeEach", testMethod.getName(), "afterEach");
if (!testAlreadyExecuted) {
assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
"BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
testAlreadyExecuted = true;
}
else {
assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
"CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
}
}
private static void assertEventTypes(ApplicationEvents applicationEvents, String... types) {
assertThat(applicationEvents.stream().map(event -> event.getClass().getSimpleName()))
.containsExactly(types);
}
@Configuration
static class Config {
}
@SuppressWarnings("serial")
static class CustomEvent extends ApplicationEvent {
private final String message;
CustomEvent(String message) {
super(message);
this.message = message;
}
String getMessage() {
return message;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -100,7 +100,7 @@ class HeaderAssertionTests {
// Wrong pattern
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertions.valueMatches("Content-Type", ".*ISO-8859-1.*"))
.satisfies(ex -> assertThat(ex.getCause()).hasMessage("Response header " +
.satisfies(ex -> assertThat(ex).hasMessage("Response header " +
"'Content-Type'=[application/json;charset=UTF-8] does not match " +
"[.*ISO-8859-1.*]"));
}
@@ -117,13 +117,13 @@ class HeaderAssertionTests {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertions.valuesMatch("foo", ".*", "val.*5"))
.satisfies(ex -> assertThat(ex.getCause()).hasMessage(
.satisfies(ex -> assertThat(ex).hasMessage(
"Response header 'foo' has fewer or more values [value1, value2, value3] " +
"than number of patterns to match with [.*, val.*5]"));
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertions.valuesMatch("foo", ".*", "val.*5", ".*"))
.satisfies(ex -> assertThat(ex.getCause()).hasMessage(
.satisfies(ex -> assertThat(ex).hasMessage(
"Response header 'foo'[1]='value2' does not match 'val.*5'"));
}
@@ -158,7 +158,7 @@ class HeaderAssertionTests {
// Header should not exist
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertions.exists("Framework"))
.satisfies(ex -> assertThat(ex.getCause()).hasMessage("Response header 'Framework' does not exist"));
.satisfies(ex -> assertThat(ex).hasMessage("Response header 'Framework' does not exist"));
}
@Test
@@ -173,7 +173,7 @@ class HeaderAssertionTests {
// Existing header
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertions.doesNotExist("Content-Type"))
.satisfies(ex -> assertThat(ex.getCause()).hasMessage("Response header " +
.satisfies(ex -> assertThat(ex).hasMessage("Response header " +
"'Content-Type' exists with value=[application/json;charset=UTF-8]"));
}
@@ -189,7 +189,6 @@ class HeaderAssertionTests {
// MediaTypes not compatible
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertions.contentTypeCompatibleWith(MediaType.TEXT_XML))
.havingCause()
.withMessage("Response header 'Content-Type'=[application/xml] is not compatible with [text/xml]");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,17 +16,14 @@
package org.springframework.test.web.servlet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -47,29 +44,25 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
* @author Rob Winch
* @since 4.2
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
public class MockMvcReuseTests {
@SpringJUnitWebConfig
@TestInstance(Lifecycle.PER_CLASS)
class MockMvcReuseTests {
private static final String HELLO = "hello";
private static final String ENIGMA = "enigma";
private static final String FOO = "foo";
private static final String BAR = "bar";
@Autowired
private WebApplicationContext wac;
private MockMvc mvc;
private final MockMvc mvc;
@BeforeEach
public void setUp() {
this.mvc = webAppContextSetup(this.wac).build();
MockMvcReuseTests(WebApplicationContext wac) {
this.mvc = webAppContextSetup(wac).build();
}
@Test
public void sessionAttributesAreClearedBetweenInvocations() throws Exception {
void sessionAttributesAreClearedBetweenInvocations() throws Exception {
this.mvc.perform(get("/"))
.andExpect(content().string(HELLO))
@@ -85,7 +78,7 @@ public class MockMvcReuseTests {
}
@Test
public void requestParametersAreClearedBetweenInvocations() throws Exception {
void requestParametersAreClearedBetweenInvocations() throws Exception {
this.mvc.perform(get("/"))
.andExpect(content().string(HELLO));
@@ -102,7 +95,7 @@ public class MockMvcReuseTests {
static class Config {
@Bean
public MyController myController() {
MyController myController() {
return new MyController();
}
}
@@ -110,13 +103,13 @@ public class MockMvcReuseTests {
@RestController
static class MyController {
@RequestMapping("/")
public String hello() {
@GetMapping("/")
String hello() {
return HELLO;
}
@RequestMapping(path = "/", params = ENIGMA)
public String enigma() {
@GetMapping(path = "/", params = ENIGMA)
String enigma() {
return ENIGMA;
}
}
@@ -16,19 +16,19 @@
package org.springframework.test.web.servlet.request;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.Part;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.mock.web.MockPart;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -38,17 +38,32 @@ import static org.assertj.core.api.Assertions.assertThat;
public class MockMultipartHttpServletRequestBuilderTests {
@Test // gh-26166
void addFilesAndParts() throws Exception {
MockHttpServletRequest mockRequest = new MockMultipartHttpServletRequestBuilder("/upload")
.file(new MockMultipartFile("file", "test.txt", "text/plain", "Test".getBytes(StandardCharsets.UTF_8)))
.part(new MockPart("data", "{\"node\":\"node\"}".getBytes(StandardCharsets.UTF_8)))
.buildRequest(new MockServletContext());
void addFileAndParts() throws Exception {
MockMultipartHttpServletRequest mockRequest =
(MockMultipartHttpServletRequest) new MockMultipartHttpServletRequestBuilder("/upload")
.file(new MockMultipartFile("file", "test.txt", "text/plain", "Test".getBytes(UTF_8)))
.part(new MockPart("name", "value".getBytes(UTF_8)))
.buildRequest(new MockServletContext());
StandardMultipartHttpServletRequest parsedRequest = new StandardMultipartHttpServletRequest(mockRequest);
assertThat(mockRequest.getFileMap()).containsOnlyKeys("file");
assertThat(mockRequest.getParameterMap()).containsOnlyKeys("name");
assertThat(mockRequest.getParts()).extracting(Part::getName).containsExactly("name");
}
assertThat(parsedRequest.getParameterMap()).containsOnlyKeys("data");
assertThat(parsedRequest.getFileMap()).containsOnlyKeys("file");
assertThat(parsedRequest.getParts()).extracting(Part::getName).containsExactly("file", "data");
@Test // gh-26261
void addFileWithoutFilename() throws Exception {
MockPart jsonPart = new MockPart("data", "{\"node\":\"node\"}".getBytes(UTF_8));
jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON);
MockMultipartHttpServletRequest mockRequest =
(MockMultipartHttpServletRequest) new MockMultipartHttpServletRequestBuilder("/upload")
.file(new MockMultipartFile("file", "Test".getBytes(UTF_8)))
.part(jsonPart)
.buildRequest(new MockServletContext());
assertThat(mockRequest.getFileMap()).containsOnlyKeys("file");
assertThat(mockRequest.getParameterMap()).isEmpty();
assertThat(mockRequest.getParts()).extracting(Part::getName).containsExactly("data");
}
@Test
@@ -123,7 +123,7 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
Request request = this.httpClient.newRequest(uri).method(method.toString());
return requestCallback.apply(new JettyClientHttpRequest(request, this.bufferFactory))
.then(Mono.fromDirect(ReactiveRequest.newBuilder(request).build()
.then(Mono.fromDirect(ReactiveRequest.newBuilder(request).abortOnCancel(true).build()
.response((reactiveResponse, chunkPublisher) -> {
Flux<DataBuffer> content = Flux.from(chunkPublisher).map(this::toDataBuffer);
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -154,7 +154,7 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public Set<String> keySet() {
return this.headers.getFieldNamesCollection();
return new HeaderNames();
}
@Override
@@ -227,4 +227,52 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
}
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(headers.getFieldNamesCollection().iterator());
}
@Override
public int size() {
return headers.getFieldNamesCollection().size();
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<String> iterator;
@Nullable
private String currentName;
private HeaderNamesIterator(Iterator<String> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public String next() {
this.currentName = this.iterator.next();
return this.currentName;
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
if (!headers.containsKey(this.currentName)) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
headers.remove(this.currentName);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -56,7 +56,9 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public void add(String key, @Nullable String value) {
this.headers.add(key, value);
if (value != null) {
this.headers.add(key, value);
}
}
@Override
@@ -71,7 +73,9 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public void set(String key, @Nullable String value) {
this.headers.set(key, value);
if (value != null) {
this.headers.set(key, value);
}
}
@Override
@@ -153,7 +157,7 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public Set<String> keySet() {
return this.headers.names();
return new HeaderNames();
}
@Override
@@ -226,4 +230,52 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
}
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(headers.names().iterator());
}
@Override
public int size() {
return headers.names().size();
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<String> iterator;
@Nullable
private String currentName;
private HeaderNamesIterator(Iterator<String> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public String next() {
this.currentName = this.iterator.next();
return this.currentName;
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
if (!headers.contains(this.currentName)) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
headers.remove(this.currentName);
}
}
}
@@ -21,6 +21,7 @@ import java.util.Map;
import kotlinx.serialization.KSerializer;
import kotlinx.serialization.SerializersKt;
import kotlinx.serialization.descriptors.PolymorphicKind;
import kotlinx.serialization.json.Json;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -39,7 +40,9 @@ import org.springframework.util.MimeType;
* Decode a byte stream into JSON and convert to Object's with
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
*
* <p>This decoder can be used to bind {@code @Serializable} Kotlin classes.
* <p>This decoder can be used to bind {@code @Serializable} Kotlin classes,
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
* is not supported.
* It supports {@code application/json} and {@code application/*+json} with
* various character sets, {@code UTF-8} being the default.
*
@@ -132,6 +135,9 @@ public class KotlinSerializationJsonDecoder extends AbstractDecoder<Object> {
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
serializer = SerializersKt.serializer(type);
if (serializer.getDescriptor().getKind().equals(PolymorphicKind.OPEN.INSTANCE)) {
throw new UnsupportedOperationException("Open polymorphic serialization is not supported yet");
}
serializerCache.put(type, serializer);
}
return serializer;
@@ -22,6 +22,7 @@ import java.util.Map;
import kotlinx.serialization.KSerializer;
import kotlinx.serialization.SerializersKt;
import kotlinx.serialization.descriptors.PolymorphicKind;
import kotlinx.serialization.json.Json;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -42,7 +43,9 @@ import org.springframework.util.MimeType;
* Encode from an {@code Object} stream to a byte stream of JSON objects using
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
*
* <p>This encoder can be used to bind {@code @Serializable} Kotlin classes.
* <p>This encoder can be used to bind {@code @Serializable} Kotlin classes,
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
* is not supported.
* It supports {@code application/json} and {@code application/*+json} with
* various character sets, {@code UTF-8} being the default.
*
@@ -120,6 +123,9 @@ public class KotlinSerializationJsonEncoder extends AbstractEncoder<Object> {
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
serializer = SerializersKt.serializer(type);
if (serializer.getDescriptor().getKind().equals(PolymorphicKind.OPEN.INSTANCE)) {
throw new UnsupportedOperationException("Open polymorphic serialization is not supported yet");
}
serializerCache.put(type, serializer);
}
return serializer;
@@ -74,8 +74,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
private boolean streaming;
private Scheduler blockingOperationScheduler = Schedulers.newBoundedElastic(Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE,
Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, IDENTIFIER, 60, true);
private Scheduler blockingOperationScheduler = Schedulers.boundedElastic();
private Mono<Path> fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache();
@@ -153,8 +152,9 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
/**
* Sets the Reactor {@link Scheduler} to be used for creating files and
* directories, and writing to files. By default, a bounded scheduler is
* created with default properties.
* directories, and writing to files. By default,
* {@link Schedulers#boundedElastic()} is used, but this property allows for
* changing it to an externally managed scheduler.
* <p>Note that this property is ignored when
* {@linkplain #setStreaming(boolean) streaming} is enabled, or when
* {@link #setMaxInMemorySize(int) maxInMemorySize} is set to -1.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -33,6 +33,14 @@ public interface FilePart extends Part {
/**
* Return the original filename in the client's filesystem.
* <p><strong>Note:</strong> Please keep in mind this filename is supplied
* by the client and should not be used blindly. In addition to not using
* the directory portion, the file name could also contain characters such
* as ".." and others that can be used maliciously.
* @return the original filename, or the empty String if no file has been chosen
* in the multipart form, or {@code null} if not defined or not available
* @see <a href="https://tools.ietf.org/html/rfc7578#section-4.2">RFC 7578, Section 4.2</a>
* @see <a href="https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload">Unrestricted File Upload</a>
*/
String filename();
@@ -118,7 +118,7 @@ public class MultipartWriterSupport extends LoggingCodecSupport {
Charset charset = getCharset();
if (!charset.equals(StandardCharsets.UTF_8) &&
!charset.equals(StandardCharsets.US_ASCII) ) {
params.put("charset", getCharset().name());
params.put("charset", charset.name());
}
mediaType = (mediaType != null ? mediaType : MediaType.MULTIPART_FORM_DATA);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -64,6 +64,7 @@ import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Default implementation of {@link CodecConfigurer.DefaultCodecs} that serves
@@ -147,7 +148,38 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
private boolean registerDefaults = true;
// The default reader and writer instances to use
private final List<HttpMessageReader<?>> typedReaders = new ArrayList<>();
private final List<HttpMessageReader<?>> objectReaders = new ArrayList<>();
private final List<HttpMessageWriter<?>> typedWriters = new ArrayList<>();
private final List<HttpMessageWriter<?>> objectWriters = new ArrayList<>();
BaseDefaultCodecs() {
initReaders();
initWriters();
}
/**
* Reset and initialize typed readers and object readers.
* @since 5.3.3
*/
protected void initReaders() {
initTypedReaders();
initObjectReaders();
}
/**
* Reset and initialize typed writers and object writers.
* @since 5.3.3
*/
protected void initWriters() {
initTypedWriters();
initObjectWriters();
}
/**
@@ -167,61 +199,78 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
this.maxInMemorySize = other.maxInMemorySize;
this.enableLoggingRequestDetails = other.enableLoggingRequestDetails;
this.registerDefaults = other.registerDefaults;
this.typedReaders.addAll(other.typedReaders);
this.objectReaders.addAll(other.objectReaders);
this.typedWriters.addAll(other.typedWriters);
this.objectWriters.addAll(other.objectWriters);
}
@Override
public void jackson2JsonDecoder(Decoder<?> decoder) {
this.jackson2JsonDecoder = decoder;
initObjectReaders();
}
@Override
public void jackson2JsonEncoder(Encoder<?> encoder) {
this.jackson2JsonEncoder = encoder;
}
@Override
public void protobufDecoder(Decoder<?> decoder) {
this.protobufDecoder = decoder;
initObjectWriters();
}
@Override
public void jackson2SmileDecoder(Decoder<?> decoder) {
this.jackson2SmileDecoder = decoder;
initObjectReaders();
}
@Override
public void jackson2SmileEncoder(Encoder<?> encoder) {
this.jackson2SmileEncoder = encoder;
initObjectWriters();
}
@Override
public void protobufDecoder(Decoder<?> decoder) {
this.protobufDecoder = decoder;
initTypedReaders();
}
@Override
public void protobufEncoder(Encoder<?> encoder) {
this.protobufEncoder = encoder;
initTypedWriters();
}
@Override
public void jaxb2Decoder(Decoder<?> decoder) {
this.jaxb2Decoder = decoder;
initObjectReaders();
}
@Override
public void jaxb2Encoder(Encoder<?> encoder) {
this.jaxb2Encoder = encoder;
initObjectWriters();
}
@Override
public void kotlinSerializationJsonDecoder(Decoder<?> decoder) {
this.kotlinSerializationJsonDecoder = decoder;
initObjectReaders();
}
@Override
public void kotlinSerializationJsonEncoder(Encoder<?> encoder) {
this.kotlinSerializationJsonEncoder = encoder;
initObjectWriters();
}
@Override
public void maxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
if (!ObjectUtils.nullSafeEquals(this.maxInMemorySize, byteCount)) {
this.maxInMemorySize = byteCount;
initReaders();
}
}
@Override
@@ -232,7 +281,11 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
@Override
public void enableLoggingRequestDetails(boolean enable) {
this.enableLoggingRequestDetails = enable;
if (!ObjectUtils.nullSafeEquals(this.enableLoggingRequestDetails, enable)) {
this.enableLoggingRequestDetails = enable;
initReaders();
initWriters();
}
}
@Override
@@ -245,7 +298,11 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
* Delegate method used from {@link BaseCodecConfigurer#registerDefaults}.
*/
void registerDefaults(boolean registerDefaults) {
this.registerDefaults = registerDefaults;
if (this.registerDefaults != registerDefaults) {
this.registerDefaults = registerDefaults;
initReaders();
initWriters();
}
}
@@ -253,28 +310,34 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
* Return readers that support specific types.
*/
final List<HttpMessageReader<?>> getTypedReaders() {
return this.typedReaders;
}
/**
* Reset and initialize typed readers.
* @since 5.3.3
*/
protected void initTypedReaders() {
this.typedReaders.clear();
if (!this.registerDefaults) {
return Collections.emptyList();
return;
}
List<HttpMessageReader<?>> readers = new ArrayList<>();
addCodec(readers, new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(new DataBufferDecoder()));
addCodec(this.typedReaders, new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
addCodec(this.typedReaders, new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
addCodec(this.typedReaders, new DecoderHttpMessageReader<>(new DataBufferDecoder()));
if (nettyByteBufPresent) {
addCodec(readers, new DecoderHttpMessageReader<>(new NettyByteBufDecoder()));
addCodec(this.typedReaders, new DecoderHttpMessageReader<>(new NettyByteBufDecoder()));
}
addCodec(readers, new ResourceHttpMessageReader(new ResourceDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
addCodec(this.typedReaders, new ResourceHttpMessageReader(new ResourceDecoder()));
addCodec(this.typedReaders, new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
if (protobufPresent) {
addCodec(readers, new DecoderHttpMessageReader<>(this.protobufDecoder != null ?
addCodec(this.typedReaders, new DecoderHttpMessageReader<>(this.protobufDecoder != null ?
(ProtobufDecoder) this.protobufDecoder : new ProtobufDecoder()));
}
addCodec(readers, new FormHttpMessageReader());
addCodec(this.typedReaders, new FormHttpMessageReader());
// client vs server..
extendTypedReaders(readers);
return readers;
extendTypedReaders(this.typedReaders);
}
/**
@@ -293,7 +356,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
*/
@SuppressWarnings("rawtypes")
private void initCodec(@Nullable Object codec) {
if (codec instanceof DecoderHttpMessageReader) {
codec = ((DecoderHttpMessageReader) codec).getDecoder();
}
@@ -386,29 +448,35 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
* Return Object readers (JSON, XML, SSE).
*/
final List<HttpMessageReader<?>> getObjectReaders() {
return this.objectReaders;
}
/**
* Reset and initialize object readers.
* @since 5.3.3
*/
protected void initObjectReaders() {
this.objectReaders.clear();
if (!this.registerDefaults) {
return Collections.emptyList();
return;
}
List<HttpMessageReader<?>> readers = new ArrayList<>();
if (kotlinSerializationJsonPresent) {
addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
}
if (jackson2Present) {
addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
}
if (jackson2SmilePresent) {
addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
}
if (jaxb2Present && !shouldIgnoreXml) {
addCodec(readers, new DecoderHttpMessageReader<>(this.jaxb2Decoder != null ?
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jaxb2Decoder != null ?
(Jaxb2XmlDecoder) this.jaxb2Decoder : new Jaxb2XmlDecoder()));
}
// client vs server..
extendObjectReaders(readers);
return readers;
extendObjectReaders(this.objectReaders);
}
/**
@@ -434,12 +502,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
*/
@SuppressWarnings({"rawtypes" })
final List<HttpMessageWriter<?>> getTypedWriters() {
return this.typedWriters;
}
/**
* Reset and initialize typed writers.
* @since 5.3.3
*/
protected void initTypedWriters() {
this.typedWriters.clear();
if (!this.registerDefaults) {
return Collections.emptyList();
return;
}
List<HttpMessageWriter<?>> writers = getBaseTypedWriters();
extendTypedWriters(writers);
return writers;
this.typedWriters.addAll(getBaseTypedWriters());
extendTypedWriters(this.typedWriters);
}
/**
@@ -476,12 +552,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
* Return Object writers (JSON, XML, SSE).
*/
final List<HttpMessageWriter<?>> getObjectWriters() {
return this.objectWriters;
}
/**
* Reset and initialize object writers.
* @since 5.3.3
*/
protected void initObjectWriters() {
this.objectWriters.clear();
if (!this.registerDefaults) {
return Collections.emptyList();
return;
}
List<HttpMessageWriter<?>> writers = getBaseObjectWriters();
extendObjectWriters(writers);
return writers;
this.objectWriters.addAll(getBaseObjectWriters());
extendObjectWriters(this.objectWriters);
}
/**
@@ -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.
@@ -68,6 +68,7 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
*/
void setPartWritersSupplier(Supplier<List<HttpMessageWriter<?>>> supplier) {
this.partWritersSupplier = supplier;
initTypedWriters();
}
@@ -82,15 +83,7 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
@Override
public void serverSentEventDecoder(Decoder<?> decoder) {
this.sseDecoder = decoder;
}
@Override
public ClientDefaultCodecsImpl clone() {
ClientDefaultCodecsImpl codecs = new ClientDefaultCodecsImpl();
codecs.multipartCodecs = this.multipartCodecs;
codecs.sseDecoder = this.sseDecoder;
codecs.partWritersSupplier = this.partWritersSupplier;
return codecs;
initObjectReaders();
}
@Override
@@ -125,7 +118,7 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
/**
* Default implementation of {@link ClientCodecConfigurer.MultipartCodecs}.
*/
private static class DefaultMultipartCodecs implements ClientCodecConfigurer.MultipartCodecs {
private class DefaultMultipartCodecs implements ClientCodecConfigurer.MultipartCodecs {
private final List<HttpMessageWriter<?>> writers = new ArrayList<>();
@@ -141,12 +134,14 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
@Override
public ClientCodecConfigurer.MultipartCodecs encoder(Encoder<?> encoder) {
writer(new EncoderHttpMessageWriter<>(encoder));
initTypedWriters();
return this;
}
@Override
public ClientCodecConfigurer.MultipartCodecs writer(HttpMessageWriter<?> writer) {
this.writers.add(writer);
initTypedWriters();
return this;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -55,11 +55,13 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
@Override
public void multipartReader(HttpMessageReader<?> reader) {
this.multipartReader = reader;
initTypedReaders();
}
@Override
public void serverSentEventEncoder(Encoder<?> encoder) {
this.sseEncoder = encoder;
initObjectWriters();
}
@@ -475,7 +475,10 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
byte[] boundary = generateMultipartBoundary();
if (!isFilenameCharsetSet()) {
parameters.put("charset", this.charset.name());
if (!this.charset.equals(StandardCharsets.UTF_8) &&
!this.charset.equals(StandardCharsets.US_ASCII)) {
parameters.put("charset", this.charset.name());
}
}
parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -25,6 +25,7 @@ import java.util.Map;
import kotlinx.serialization.KSerializer;
import kotlinx.serialization.SerializationException;
import kotlinx.serialization.SerializersKt;
import kotlinx.serialization.descriptors.PolymorphicKind;
import kotlinx.serialization.json.Json;
import org.springframework.core.GenericTypeResolver;
@@ -43,7 +44,9 @@ import org.springframework.util.StreamUtils;
* that can read and write JSON using
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
*
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes.
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes,
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
* is not supported.
* It supports {@code application/json} and {@code application/*+json} with
* various character sets, {@code UTF-8} being the default.
*
@@ -182,6 +185,9 @@ public class KotlinSerializationJsonHttpMessageConverter extends AbstractGeneric
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
serializer = SerializersKt.serializer(type);
if (serializer.getDescriptor().getKind().equals(PolymorphicKind.OPEN.INSTANCE)) {
throw new UnsupportedOperationException("Open polymorphic serialization is not supported yet");
}
serializerCache.put(type, serializer);
}
return serializer;
@@ -125,6 +125,10 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
this.headers.getContentType().getCharset() != null) {
this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
}
long contentLength = getHeaders().getContentLength();
if (contentLength != -1) {
this.servletResponse.setContentLengthLong(contentLength);
}
this.headersWritten = true;
}
}
@@ -18,6 +18,7 @@ package org.springframework.http.server.reactive;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
@@ -213,7 +214,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
return ((Mono<? extends DataBuffer>) body)
.flatMap(buffer -> {
touchDataBuffer(buffer);
AtomicReference<Boolean> subscribed = new AtomicReference<>(false);
AtomicBoolean subscribed = new AtomicBoolean();
return doCommit(
() -> {
try {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -236,7 +236,7 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
@SuppressWarnings("unchecked")
@Override
public <T> T getNativeRequest() {
return (T) this.originalRequest;
return ServerHttpRequestDecorator.getNativeRequest(this.originalRequest);
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -154,7 +154,7 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public Set<String> keySet() {
return this.headers.getFieldNamesCollection();
return new HeaderNames();
}
@Override
@@ -227,4 +227,52 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
}
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(headers.getFieldNamesCollection().iterator());
}
@Override
public int size() {
return headers.getFieldNamesCollection().size();
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<String> iterator;
@Nullable
private String currentName;
private HeaderNamesIterator(Iterator<String> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public String next() {
this.currentName = this.iterator.next();
return this.currentName;
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
if (!headers.containsKey(this.currentName)) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
headers.remove(this.currentName);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -56,7 +56,9 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public void add(String key, @Nullable String value) {
this.headers.add(key, value);
if (value != null) {
this.headers.add(key, value);
}
}
@Override
@@ -71,7 +73,9 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public void set(String key, @Nullable String value) {
this.headers.set(key, value);
if (value != null) {
this.headers.set(key, value);
}
}
@Override
@@ -153,7 +157,7 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
@Override
public Set<String> keySet() {
return this.headers.names();
return new HeaderNames();
}
@Override
@@ -226,4 +230,51 @@ class NettyHeadersAdapter implements MultiValueMap<String, String> {
}
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(headers.names().iterator());
}
@Override
public int size() {
return headers.names().size();
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<String> iterator;
@Nullable
private String currentName;
private HeaderNamesIterator(Iterator<String> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public String next() {
this.currentName = this.iterator.next();
return this.currentName;
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
if (!headers.contains(this.currentName)) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
headers.remove(this.currentName);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -120,6 +120,28 @@ public class ServerHttpRequestDecorator implements ServerHttpRequest {
}
/**
* Return the native request of the underlying server API, if possible,
* also unwrapping {@link ServerHttpRequestDecorator} if necessary.
* @param request the request to check
* @param <T> the expected native request type
* @throws IllegalArgumentException if the native request can't be obtained
* @since 5.3.3
*/
public static <T> T getNativeRequest(ServerHttpRequest request) {
if (request instanceof AbstractServerHttpRequest) {
return ((AbstractServerHttpRequest) request).getNativeRequest();
}
else if (request instanceof ServerHttpRequestDecorator) {
return getNativeRequest(((ServerHttpRequestDecorator) request).getDelegate());
}
else {
throw new IllegalArgumentException(
"Can't find native request in " + request.getClass().getName());
}
}
@Override
public String toString() {
return getClass().getSimpleName() + " [delegate=" + getDelegate() + "]";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -110,6 +110,28 @@ public class ServerHttpResponseDecorator implements ServerHttpResponse {
}
/**
* Return the native response of the underlying server API, if possible,
* also unwrapping {@link ServerHttpResponseDecorator} if necessary.
* @param response the response to check
* @param <T> the expected native response type
* @throws IllegalArgumentException if the native response can't be obtained
* @since 5.3.3
*/
public static <T> T getNativeResponse(ServerHttpResponse response) {
if (response instanceof AbstractServerHttpResponse) {
return ((AbstractServerHttpResponse) response).getNativeResponse();
}
else if (response instanceof ServerHttpResponseDecorator) {
return getNativeResponse(((ServerHttpResponseDecorator) response).getDelegate());
}
else {
throw new IllegalArgumentException(
"Can't find native response in " + response.getClass().getName());
}
}
@Override
public String toString() {
return getClass().getSimpleName() + " [delegate=" + getDelegate() + "]";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -20,7 +20,6 @@ import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -167,12 +166,7 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
@Override
public Set<String> keySet() {
Set<String> result = new HashSet<>(8);
Enumeration<String> names = this.headers.names();
while (names.hasMoreElements()) {
result.add(names.nextElement());
}
return result;
return new HeaderNames();
}
@Override
@@ -247,4 +241,59 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
}
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(headers.names());
}
@Override
public int size() {
Enumeration<String> names = headers.names();
int size = 0;
while (names.hasMoreElements()) {
names.nextElement();
size++;
}
return size;
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Enumeration<String> enumeration;
@Nullable
private String currentName;
private HeaderNamesIterator(Enumeration<String> enumeration) {
this.enumeration = enumeration;
}
@Override
public boolean hasNext() {
return this.enumeration.hasMoreElements();
}
@Override
public String next() {
this.currentName = this.enumeration.nextElement();
return this.currentName;
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
int index = headers.findHeader(this.currentName, 0);
if (index == -1) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
headers.removeHeader(index);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -149,9 +149,7 @@ class UndertowHeadersAdapter implements MultiValueMap<String, String> {
@Override
public Set<String> keySet() {
return this.headers.getHeaderNames().stream()
.map(HttpString::toString)
.collect(Collectors.toSet());
return new HeaderNames();
}
@Override
@@ -225,4 +223,52 @@ class UndertowHeadersAdapter implements MultiValueMap<String, String> {
}
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(headers.getHeaderNames().iterator());
}
@Override
public int size() {
return headers.getHeaderNames().size();
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<HttpString> iterator;
@Nullable
private String currentName;
private HeaderNamesIterator(Iterator<HttpString> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public String next() {
this.currentName = this.iterator.next().toString();
return this.currentName;
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
if (!headers.contains(this.currentName)) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
headers.remove(this.currentName);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -33,6 +33,9 @@ import java.lang.annotation.Target;
* <li>An exception argument: declared as a general Exception or as a more
* specific exception. This also serves as a mapping hint if the annotation
* itself does not narrow the exception types through its {@link #value()}.
* You may refer to a top-level exception being propagated or to a nested
* cause within a wrapper exception. As of 5.3, any cause level is being
* exposed, whereas previously only an immediate cause was considered.
* <li>Request and/or response objects (typically from the Servlet API).
* You may choose any specific request/response type, e.g.
* {@link javax.servlet.ServletRequest} / {@link javax.servlet.http.HttpServletRequest}.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -145,7 +145,7 @@ public abstract class OncePerRequestFilter extends GenericFilterBean {
* @see WebAsyncManager#hasConcurrentResult()
*/
protected boolean isAsyncDispatch(HttpServletRequest request) {
return WebAsyncUtils.getAsyncManager(request).hasConcurrentResult();
return request.getDispatcherType().equals(DispatcherType.ASYNC);
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -24,6 +24,7 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.server.RequestPath;
import org.springframework.web.util.ServletRequestPathUtils;
/**
@@ -48,12 +49,13 @@ public class ServletRequestPathFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
RequestPath previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
ServletRequestPathUtils.parseAndCache((HttpServletRequest) request);
try {
chain.doFilter(request, response);
}
finally {
ServletRequestPathUtils.clearParsedRequestPath(request);
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}

Some files were not shown because too many files have changed in this diff Show More