Merge branch '5.3.x' into main

This commit is contained in:
Rossen Stoyanchev 2021-10-13 14:58:07 +01:00
commit 28a497f3b3
7 changed files with 84 additions and 28 deletions

View File

@ -65,7 +65,7 @@ configure(allprojects) { project ->
} }
dependency "io.reactivex.rxjava3:rxjava:3.1.1" 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 "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
dependency "com.fasterxml:aalto-xml:1.3.0" dependency "com.fasterxml:aalto-xml:1.3.0"

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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<String> locationValues = new ArrayList<>(4); private final List<String> locationValues = new ArrayList<>(4);
private final List<Resource> locations = new ArrayList<>(4); private final List<Resource> locationResources = new ArrayList<>(4);
private final List<Resource> locationsToUse = new ArrayList<>(4);
private final List<ResourceResolver> resourceResolvers = new ArrayList<>(4); private final List<ResourceResolver> resourceResolvers = new ArrayList<>(4);
@ -147,9 +149,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
* for serving static resources. * for serving static resources.
*/ */
public void setLocations(@Nullable List<Resource> locations) { public void setLocations(@Nullable List<Resource> locations) {
this.locations.clear(); this.locationResources.clear();
if (locations != null) { if (locations != null) {
this.locations.addAll(locations); this.locationResources.addAll(locations);
} }
} }
@ -159,11 +161,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
* <p>Note that if {@link #setLocationValues(List) locationValues} are provided, * <p>Note that if {@link #setLocationValues(List) locationValues} are provided,
* instead of loaded Resource-based locations, this method will return * instead of loaded Resource-based locations, this method will return
* empty until after initialization via {@link #afterPropertiesSet()}. * empty until after initialization via {@link #afterPropertiesSet()}.
* <p><strong>Note:</strong> 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 #setLocationValues
* @see #setLocations * @see #setLocations
*/ */
public List<Resource> getLocations() { public List<Resource> 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 { public void afterPropertiesSet() throws Exception {
resolveResourceLocations(); 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 " + logger.warn("Locations list is empty. No resources will be served unless a " +
"custom ResourceResolver is configured as an alternative to PathResourceResolver."); "custom ResourceResolver is configured as an alternative to PathResourceResolver.");
} }
@ -316,21 +325,22 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
} }
private void resolveResourceLocations() { private void resolveResourceLocations() {
if (CollectionUtils.isEmpty(this.locationValues)) { List<Resource> result = new ArrayList<>(this.locationResources);
return;
} if (!this.locationValues.isEmpty()) {
else if (!CollectionUtils.isEmpty(this.locations)) { Assert.notNull(this.resourceLoader,
throw new IllegalArgumentException("Please set either Resource-based \"locations\" or " + "ResourceLoader is required when \"locationValues\" are configured.");
"String-based \"locationValues\", but not both."); 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, result = result.stream().filter(Resource::exists).collect(Collectors.toList());
"ResourceLoader is required when \"locationValues\" are configured.");
for (String location : this.locationValues) { this.locationsToUse.clear();
Resource resource = this.resourceLoader.getResource(location); this.locationsToUse.addAll(result);
this.locations.add(resource);
}
} }
/** /**
@ -339,7 +349,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
* match the {@link #setLocations locations} configured on this class. * match the {@link #setLocations locations} configured on this class.
*/ */
protected void initAllowedLocations() { protected void initAllowedLocations() {
if (CollectionUtils.isEmpty(this.locations)) { if (CollectionUtils.isEmpty(getLocations())) {
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info("Locations list is empty. No resources will be served unless a " + logger.info("Locations list is empty. No resources will be served unless a " +
"custom ResourceResolver is configured as an alternative to PathResourceResolver."); "custom ResourceResolver is configured as an alternative to PathResourceResolver.");
@ -618,8 +628,8 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
if (!this.locationValues.isEmpty()) { if (!this.locationValues.isEmpty()) {
return this.locationValues.stream().collect(Collectors.joining("\", \"", "[\"", "\"]")); return this.locationValues.stream().collect(Collectors.joining("\", \"", "[\"", "\"]"));
} }
else if (!this.locations.isEmpty()) { if (!getLocations().isEmpty()) {
return "[" + this.locations.toString() return "[" + getLocations().toString()
.replaceAll("class path resource", "Classpath") .replaceAll("class path resource", "Classpath")
.replaceAll("ServletContext resource", "ServletContext") + "]"; .replaceAll("ServletContext resource", "ServletContext") + "]";
} }

View File

@ -52,7 +52,7 @@ import org.springframework.web.server.ServerWebExchange;
*/ */
public abstract class AbstractMessageWriterResultHandler extends HandlerResultHandlerSupport { 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<HttpMessageWriter<?>> messageWriters; private final List<HttpMessageWriter<?>> messageWriters;

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 reactor.core.publisher.Mono;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
@ -108,6 +109,7 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
@Override @Override
@SuppressWarnings("ConstantConditions")
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
Mono<?> returnValueMono; Mono<?> returnValueMono;
@ -118,7 +120,9 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
if (adapter != null) { if (adapter != null) {
Assert.isTrue(!adapter.isMultiValue(), "Only a single ResponseEntity supported"); Assert.isTrue(!adapter.isMultiValue(), "Only a single ResponseEntity supported");
returnValueMono = Mono.from(adapter.toPublisher(result.getReturnValue())); 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 { else {
returnValueMono = Mono.justOrEmpty(result.getReturnValue()); returnValueMono = Mono.justOrEmpty(result.getReturnValue());

View File

@ -253,6 +253,23 @@ public class ResourceWebHandlerTests {
assertResponseBody(exchange, "h1 { color:red; }"); assertResponseBody(exchange, "h1 { color:red; }");
} }
@Test // gh-27538
public void filterNonExistingLocations() throws Exception {
List<Resource> 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<Resource> 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 @Test // SPR-14577
public void getMediaTypeWithFavorPathExtensionOff() throws Exception { public void getMediaTypeWithFavorPathExtensionOff() throws Exception {
List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass())); List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass()));

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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.ApplicationContext
import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus 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.GetMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.HttpServerErrorException import org.springframework.web.client.HttpServerErrorException
import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer
class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() {
@ -63,6 +65,15 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() {
assertThat(entity.body).isEqualTo("foo") assertThat(entity.body).isEqualTo("foo")
} }
@ParameterizedHttpServerTest // gh-27292
fun `Suspending ResponseEntity handler method`(httpServer: HttpServer) {
startServer(httpServer)
val entity = performGet<String>("/suspend-response-entity", HttpHeaders.EMPTY, String::class.java)
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).isEqualTo("{\"value\":\"foo\"}")
}
@ParameterizedHttpServerTest @ParameterizedHttpServerTest
fun `Handler method returning Flow`(httpServer: HttpServer) { fun `Handler method returning Flow`(httpServer: HttpServer) {
startServer(httpServer) startServer(httpServer)
@ -119,6 +130,12 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() {
"foo" "foo"
} }
@GetMapping("/suspend-response-entity")
suspend fun suspendingResponseEntityEndpoint(): ResponseEntity<FooContainer<String>> {
delay(1)
return ResponseEntity.ok(FooContainer("foo"))
}
@GetMapping("/flow") @GetMapping("/flow")
fun flowEndpoint()= flow { fun flowEndpoint()= flow {
emit("foo") emit("foo")
@ -151,4 +168,8 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() {
} }
} }
class FooContainer<T>(val value: T)
} }

View File

@ -188,6 +188,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
* locations provided via {@link #setLocations(List) setLocations}. * locations provided via {@link #setLocations(List) setLocations}.
* <p>Note that the returned list is fully initialized only after * <p>Note that the returned list is fully initialized only after
* initialization via {@link #afterPropertiesSet()}. * initialization via {@link #afterPropertiesSet()}.
* <p><strong>Note:</strong> 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 #setLocationValues
* @see #setLocations * @see #setLocations
*/ */
@ -466,6 +469,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
result.addAll(this.locationResources); result.addAll(this.locationResources);
result = result.stream().filter(Resource::exists).collect(Collectors.toList()); result = result.stream().filter(Resource::exists).collect(Collectors.toList());
this.locationsToUse.clear();
this.locationsToUse.addAll(result); this.locationsToUse.addAll(result);
} }