diff --git a/build.gradle b/build.gradle index 135c166736e..ac36b1b71eb 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ configure(allprojects) { project -> } dependency "io.reactivex.rxjava3:rxjava:3.1.1" - dependency "io.smallrye.reactive:mutiny:1.0.0" + dependency "io.smallrye.reactive:mutiny:1.1.1" dependency "io.projectreactor.tools:blockhound:1.0.6.RELEASE" dependency "com.fasterxml:aalto-xml:1.3.0" diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index be4edf688ce..2361d153289 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -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. @@ -96,7 +96,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { private final List locationValues = new ArrayList<>(4); - private final List locations = new ArrayList<>(4); + private final List locationResources = new ArrayList<>(4); + + private final List locationsToUse = new ArrayList<>(4); private final List resourceResolvers = new ArrayList<>(4); @@ -147,9 +149,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { * for serving static resources. */ public void setLocations(@Nullable List locations) { - this.locations.clear(); + this.locationResources.clear(); if (locations != null) { - this.locations.addAll(locations); + this.locationResources.addAll(locations); } } @@ -159,11 +161,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { *

Note that if {@link #setLocationValues(List) locationValues} are provided, * instead of loaded Resource-based locations, this method will return * empty until after initialization via {@link #afterPropertiesSet()}. + *

Note: As of 5.3.11 the list of locations is filtered + * to exclude those that don't actually exist and therefore the list returned + * from this method may be a subset of all given locations. * @see #setLocationValues * @see #setLocations */ public List getLocations() { - return this.locations; + if (this.locationsToUse.isEmpty()) { + // Possibly not yet initialized, return only what we have so far + return this.locationResources; + } + return this.locationsToUse; } /** @@ -295,7 +304,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { public void afterPropertiesSet() throws Exception { resolveResourceLocations(); - if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) { + if (logger.isWarnEnabled() && CollectionUtils.isEmpty(getLocations())) { logger.warn("Locations list is empty. No resources will be served unless a " + "custom ResourceResolver is configured as an alternative to PathResourceResolver."); } @@ -316,21 +325,22 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { } private void resolveResourceLocations() { - if (CollectionUtils.isEmpty(this.locationValues)) { - return; - } - else if (!CollectionUtils.isEmpty(this.locations)) { - throw new IllegalArgumentException("Please set either Resource-based \"locations\" or " + - "String-based \"locationValues\", but not both."); + List result = new ArrayList<>(this.locationResources); + + if (!this.locationValues.isEmpty()) { + Assert.notNull(this.resourceLoader, + "ResourceLoader is required when \"locationValues\" are configured."); + Assert.isTrue(CollectionUtils.isEmpty(this.locationResources), "Please set " + + "either Resource-based \"locations\" or String-based \"locationValues\", but not both."); + for (String location : this.locationValues) { + result.add(this.resourceLoader.getResource(location)); + } } - Assert.notNull(this.resourceLoader, - "ResourceLoader is required when \"locationValues\" are configured."); + result = result.stream().filter(Resource::exists).collect(Collectors.toList()); - for (String location : this.locationValues) { - Resource resource = this.resourceLoader.getResource(location); - this.locations.add(resource); - } + this.locationsToUse.clear(); + this.locationsToUse.addAll(result); } /** @@ -339,7 +349,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { * match the {@link #setLocations locations} configured on this class. */ protected void initAllowedLocations() { - if (CollectionUtils.isEmpty(this.locations)) { + if (CollectionUtils.isEmpty(getLocations())) { if (logger.isInfoEnabled()) { logger.info("Locations list is empty. No resources will be served unless a " + "custom ResourceResolver is configured as an alternative to PathResourceResolver."); @@ -618,8 +628,8 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { if (!this.locationValues.isEmpty()) { return this.locationValues.stream().collect(Collectors.joining("\", \"", "[\"", "\"]")); } - else if (!this.locations.isEmpty()) { - return "[" + this.locations.toString() + if (!getLocations().isEmpty()) { + return "[" + getLocations().toString() .replaceAll("class path resource", "Classpath") .replaceAll("ServletContext resource", "ServletContext") + "]"; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index cbdd2f851fc..45e1e17974c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -52,7 +52,7 @@ import org.springframework.web.server.ServerWebExchange; */ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHandlerSupport { - private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; + protected static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; private final List> messageWriters; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index 3ad66a9bfce..26b59a4bd4b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -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. @@ -23,6 +23,7 @@ import java.util.Set; import reactor.core.publisher.Mono; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; @@ -108,6 +109,7 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand @Override + @SuppressWarnings("ConstantConditions") public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Mono returnValueMono; @@ -118,7 +120,9 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand if (adapter != null) { Assert.isTrue(!adapter.isMultiValue(), "Only a single ResponseEntity supported"); returnValueMono = Mono.from(adapter.toPublisher(result.getReturnValue())); - bodyParameter = actualParameter.nested().nested(); + boolean isContinuation = (KotlinDetector.isSuspendingFunction(actualParameter.getMethod()) && + !COROUTINES_FLOW_CLASS_NAME.equals(actualParameter.getParameterType().getName())); + bodyParameter = (isContinuation ? actualParameter.nested() : actualParameter.nested().nested()); } else { returnValueMono = Mono.justOrEmpty(result.getReturnValue()); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index 691afde68ee..b668172197d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -253,6 +253,23 @@ public class ResourceWebHandlerTests { assertResponseBody(exchange, "h1 { color:red; }"); } + @Test // gh-27538 + public void filterNonExistingLocations() throws Exception { + List inputLocations = Arrays.asList( + new ClassPathResource("test/", getClass()), + new ClassPathResource("testalternatepath/", getClass()), + new ClassPathResource("nosuchpath/", getClass())); + + ResourceWebHandler handler = new ResourceWebHandler(); + handler.setLocations(inputLocations); + handler.afterPropertiesSet(); + + List actual = handler.getLocations(); + assertThat(actual).hasSize(2); + assertThat(actual.get(0).getURL().toString()).endsWith("test/"); + assertThat(actual.get(1).getURL().toString()).endsWith("testalternatepath/"); + } + @Test // SPR-14577 public void getMediaTypeWithFavorPathExtensionOff() throws Exception { List paths = Collections.singletonList(new ClassPathResource("test/", getClass())); diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt index af2502c09f8..7a43168c7b3 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt @@ -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. @@ -22,18 +22,20 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import org.assertj.core.api.Assertions.* +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.springframework.context.ApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus -import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.client.HttpServerErrorException import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { @@ -63,6 +65,15 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { assertThat(entity.body).isEqualTo("foo") } + @ParameterizedHttpServerTest // gh-27292 + fun `Suspending ResponseEntity handler method`(httpServer: HttpServer) { + startServer(httpServer) + + val entity = performGet("/suspend-response-entity", HttpHeaders.EMPTY, String::class.java) + assertThat(entity.statusCode).isEqualTo(HttpStatus.OK) + assertThat(entity.body).isEqualTo("{\"value\":\"foo\"}") + } + @ParameterizedHttpServerTest fun `Handler method returning Flow`(httpServer: HttpServer) { startServer(httpServer) @@ -119,6 +130,12 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { "foo" } + @GetMapping("/suspend-response-entity") + suspend fun suspendingResponseEntityEndpoint(): ResponseEntity> { + delay(1) + return ResponseEntity.ok(FooContainer("foo")) + } + @GetMapping("/flow") fun flowEndpoint()= flow { emit("foo") @@ -151,4 +168,8 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { } } + + + class FooContainer(val value: T) + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index f0d420d79be..21503bcd3d6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -188,6 +188,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator * locations provided via {@link #setLocations(List) setLocations}. *

Note that the returned list is fully initialized only after * initialization via {@link #afterPropertiesSet()}. + *

Note: As of 5.3.11 the list of locations is filtered + * to exclude those that don't actually exist and therefore the list returned + * from this method may be a subset of all given locations. * @see #setLocationValues * @see #setLocations */ @@ -466,6 +469,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator result.addAll(this.locationResources); result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + this.locationsToUse.clear(); this.locationsToUse.addAll(result); }