Browse Source

Allow serving static files from RouterFunctions

This commit adds the ability to serve Resources (static files) through a
RouterFunction. Two methods have been added to RouterFunctions: one that
exposes a given directory given a path pattern, and a generic method
that requires a lookup function.

Issue: SPR-14913
pull/1252/merge
Arjen Poutsma 9 years ago
parent
commit
136b33bc4a
  1. 31
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultServerResponseBuilder.java
  2. 29
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/HandlerFilterFunction.java
  3. 159
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/PathResourceLookupFunction.java
  4. 136
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/ResourceHandlerFunction.java
  5. 65
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RouterFunctions.java
  6. 22
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/ServerResponse.java
  7. 8
      spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultServerResponseBuilderTests.java
  8. 78
      spring-web-reactive/src/test/java/org/springframework/web/reactive/function/PathResourceLookupFunctionTests.java
  9. 151
      spring-web-reactive/src/test/java/org/springframework/web/reactive/function/ResourceHandlerFunctionTests.java
  10. 2
      spring-web-reactive/src/test/resources/org/springframework/web/reactive/function/child/response.txt
  11. 2
      spring-web-reactive/src/test/resources/org/springframework/web/reactive/function/response.txt
  12. 1
      spring-web/src/main/java/org/springframework/http/codec/BodyInserter.java
  13. 10
      spring-web/src/main/java/org/springframework/http/codec/BodyInserters.java
  14. 25
      spring-web/src/main/java/org/springframework/web/util/UriUtils.java

31
spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultServerResponseBuilder.java

@ -25,6 +25,7 @@ import java.util.LinkedHashMap; @@ -25,6 +25,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -55,12 +56,12 @@ import org.springframework.web.server.ServerWebExchange; @@ -55,12 +56,12 @@ import org.springframework.web.server.ServerWebExchange;
*/
class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
private final int statusCode;
private final HttpStatus statusCode;
private final HttpHeaders headers = new HttpHeaders();
public DefaultServerResponseBuilder(int statusCode) {
public DefaultServerResponseBuilder(HttpStatus statusCode) {
this.statusCode = statusCode;
}
@ -87,6 +88,12 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -87,6 +88,12 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
return this;
}
@Override
public ServerResponse.BodyBuilder allow(Set<HttpMethod> allowedMethods) {
this.headers.setAllow(allowedMethods);
return this;
}
@Override
public ServerResponse.BodyBuilder contentLength(long contentLength) {
this.headers.setContentLength(contentLength);
@ -144,9 +151,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -144,9 +151,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
@Override
public ServerResponse<Void> build() {
return body(BodyInserter.of(
(response, context) -> response.setComplete(),
() -> null));
return body(BodyInserters.empty());
}
@Override
@ -194,20 +199,20 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -194,20 +199,20 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
}
private static abstract class AbstractServerResponse<T> implements ServerResponse<T> {
static abstract class AbstractServerResponse<T> implements ServerResponse<T> {
private final int statusCode;
private final HttpStatus statusCode;
private final HttpHeaders headers;
protected AbstractServerResponse(int statusCode, HttpHeaders headers) {
protected AbstractServerResponse(HttpStatus statusCode, HttpHeaders headers) {
this.statusCode = statusCode;
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
}
@Override
public final HttpStatus statusCode() {
return HttpStatus.valueOf(this.statusCode);
return this.statusCode;
}
@Override
@ -216,7 +221,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -216,7 +221,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
}
protected void writeStatusAndHeaders(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.valueOf(this.statusCode));
response.setStatusCode(this.statusCode);
HttpHeaders responseHeaders = response.getHeaders();
if (!this.headers.isEmpty()) {
@ -233,7 +238,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -233,7 +238,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
private final BodyInserter<T, ? super ServerHttpResponse> inserter;
public BodyInserterServerResponse(int statusCode, HttpHeaders headers,
public BodyInserterServerResponse(HttpStatus statusCode, HttpHeaders headers,
BodyInserter<T, ? super ServerHttpResponse> inserter) {
super(statusCode, headers);
@ -267,7 +272,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -267,7 +272,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
private final Rendering rendering;
public RenderingServerResponse(int statusCode, HttpHeaders headers, String name, Map<String, Object> model) {
public RenderingServerResponse(HttpStatus statusCode, HttpHeaders headers, String name,
Map<String, Object> model) {
super(statusCode, headers);
this.name = name;
this.model = model;

29
spring-web-reactive/src/main/java/org/springframework/web/reactive/function/HandlerFilterFunction.java

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.web.reactive.function;
import java.util.function.Function;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.support.ServerRequestWrapper;
@ -70,4 +72,31 @@ public interface HandlerFilterFunction<T, R> { @@ -70,4 +72,31 @@ public interface HandlerFilterFunction<T, R> {
return request -> this.filter(request, handler);
}
/**
* Adapt the given request processor function to a filter function that only operates on the
* {@code ClientRequest}.
* @param requestProcessor the request processor
* @return the filter adaptation of the request processor
*/
static HandlerFilterFunction<?, ?> ofRequestProcessor(Function<ServerRequest,
ServerRequest> requestProcessor) {
Assert.notNull(requestProcessor, "'requestProcessor' must not be null");
return (request, next) -> next.handle(requestProcessor.apply(request));
}
/**
* Adapt the given response processor function to a filter function that only operates on the
* {@code ClientResponse}.
* @param responseProcessor the response processor
* @return the filter adaptation of the request processor
*/
static <T, R> HandlerFilterFunction<T, R> ofResponseProcessor(Function<ServerResponse<T>,
ServerResponse<R>> responseProcessor) {
Assert.notNull(responseProcessor, "'responseProcessor' must not be null");
return (request, next) -> responseProcessor.apply(next.handle(request));
}
}

159
spring-web-reactive/src/main/java/org/springframework/web/reactive/function/PathResourceLookupFunction.java

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
/*
* Copyright 2002-2016 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
*
* http://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.web.reactive.function;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriUtils;
/**
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
*
* @author Arjen Poutsma
* @since 5.0
*/
class PathResourceLookupFunction implements Function<ServerRequest, Optional<Resource>> {
private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
private final String pattern;
private final Resource location;
public PathResourceLookupFunction(String pattern, Resource location) {
this.pattern = pattern;
this.location = location;
}
@Override
public Optional<Resource> apply(ServerRequest request) {
String path = processPath(request.path());
if (path.contains("%")) {
path = UriUtils.decode(path, StandardCharsets.UTF_8);
}
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
return Optional.empty();
}
if (!PATH_MATCHER.match(this.pattern, path)) {
return Optional.empty();
}
else {
path = PATH_MATCHER.extractPathWithinPattern(this.pattern, path);
}
try {
Resource resource = this.location.createRelative(path);
if (resource.exists() && resource.isReadable() && isResourceUnderLocation(resource)) {
return Optional.of(resource);
}
else {
return Optional.empty();
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static String processPath(String path) {
boolean slash = false;
for (int i = 0; i < path.length(); i++) {
if (path.charAt(i) == '/') {
slash = true;
}
else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
if (i == 0 || (i == 1 && slash)) {
return path;
}
path = slash ? "/" + path.substring(i) : path.substring(i);
return path;
}
}
return (slash ? "/" : "");
}
private static boolean isInvalidPath(String path) {
if (path.contains("WEB-INF") || path.contains("META-INF")) {
return true;
}
if (path.contains(":/")) {
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}
if (path.contains("..")) {
path = StringUtils.cleanPath(path);
if (path.contains("../")) {
return true;
}
}
return false;
}
private boolean isResourceUnderLocation(Resource resource) throws
IOException {
if (resource.getClass() != this.location.getClass()) {
return false;
}
String resourcePath;
String locationPath;
if (resource instanceof UrlResource) {
resourcePath = resource.getURL().toExternalForm();
locationPath = StringUtils.cleanPath(this.location.getURL().toString());
}
else if (resource instanceof ClassPathResource) {
resourcePath = ((ClassPathResource) resource).getPath();
locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath());
}
else {
resourcePath = resource.getURL().getPath();
locationPath = StringUtils.cleanPath(this.location.getURL().getPath());
}
if (locationPath.equals(resourcePath)) {
return true;
}
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath :
locationPath + "/");
if (!resourcePath.startsWith(locationPath)) {
return false;
}
if (resourcePath.contains("%")) {
if (UriUtils.decode(resourcePath, "UTF-8").contains("../")) {
return false;
}
}
return true;
}
}

136
spring-web-reactive/src/main/java/org/springframework/web/reactive/function/ResourceHandlerFunction.java

@ -0,0 +1,136 @@ @@ -0,0 +1,136 @@
/*
* Copyright 2002-2016 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
*
* http://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.web.reactive.function;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.EnumSet;
import java.util.Set;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.BodyInserters;
/**
* @author Arjen Poutsma
* @since 5.0
*/
class ResourceHandlerFunction implements HandlerFunction<Resource> {
private static final Set<HttpMethod> SUPPORTED_METHODS =
EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
private final Resource resource;
public ResourceHandlerFunction(Resource resource) {
this.resource = resource;
}
@Override
public ServerResponse<Resource> handle(ServerRequest request) {
switch (request.method()) {
case GET:
return ServerResponse.ok()
.body(BodyInserters.fromResource(this.resource));
case HEAD:
Resource headResource = new HeadMethodResource(this.resource);
return ServerResponse.ok()
.body(BodyInserters.fromResource(headResource));
case OPTIONS:
return ServerResponse.ok()
.allow(SUPPORTED_METHODS)
.body(BodyInserters.empty());
default:
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED)
.allow(SUPPORTED_METHODS)
.body(BodyInserters.empty());
}
}
private static class HeadMethodResource implements Resource {
private static final byte[] EMPTY = new byte[0];
private final Resource delegate;
public HeadMethodResource(Resource delegate) {
this.delegate = delegate;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(EMPTY);
}
// delegation
@Override
public boolean exists() {
return this.delegate.exists();
}
@Override
public URL getURL() throws IOException {
return this.delegate.getURL();
}
@Override
public URI getURI() throws IOException {
return this.delegate.getURI();
}
@Override
public File getFile() throws IOException {
return this.delegate.getFile();
}
@Override
public long contentLength() throws IOException {
return this.delegate.contentLength();
}
@Override
public long lastModified() throws IOException {
return this.delegate.lastModified();
}
@Override
public Resource createRelative(String relativePath) throws IOException {
return this.delegate.createRelative(relativePath);
}
@Override
public String getFilename() {
return this.delegate.getFilename();
}
@Override
public String getDescription() {
return this.delegate.getDescription();
}
}
}

65
spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RouterFunctions.java

@ -18,9 +18,11 @@ package org.springframework.web.reactive.function; @@ -18,9 +18,11 @@ package org.springframework.web.reactive.function;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerMapping;
@ -67,7 +69,7 @@ public abstract class RouterFunctions { @@ -67,7 +69,7 @@ public abstract class RouterFunctions {
* @param predicate the predicate to test
* @param handlerFunction the handler function to route to
* @param <T> the type of the handler function
* @return a routing function that routes to {@code handlerFunction} if
* @return a router function that routes to {@code handlerFunction} if
* {@code predicate} evaluates to {@code true}
* @see RequestPredicates
*/
@ -79,11 +81,11 @@ public abstract class RouterFunctions { @@ -79,11 +81,11 @@ public abstract class RouterFunctions {
}
/**
* Route to the given routing function if the given request predicate applies.
* Route to the given router function if the given request predicate applies.
* @param predicate the predicate to test
* @param routerFunction the routing function to route to
* @param routerFunction the router function to route to
* @param <T> the type of the handler function
* @return a routing function that routes to {@code routerFunction} if
* @return a router function that routes to {@code routerFunction} if
* {@code predicate} evaluates to {@code true}
* @see RequestPredicates
*/
@ -103,7 +105,40 @@ public abstract class RouterFunctions { @@ -103,7 +105,40 @@ public abstract class RouterFunctions {
}
/**
* Convert the given {@linkplain RouterFunction routing function} into a {@link HttpHandler}.
* Route requests that match the given pattern to resources relative to the given root location.
* For instance
* <pre class="code">
* Resource location = new FileSystemResource("public-resources/");
* RoutingFunction&lt;Resource&gt; resources = RouterFunctions.resources("/resources/**", location);
* </pre>
* @param pattern the pattern to match
* @param location the location directory relative to which resources should be resolved
* @return a router function that routes to resources
*/
public static RouterFunction<Resource> resources(String pattern, Resource location) {
Assert.hasLength(pattern, "'pattern' must not be empty");
Assert.notNull(location, "'location' must not be null");
return resources(new PathResourceLookupFunction(pattern, location));
}
/**
* Route to resources using the provided lookup function. If the lookup function provides a
* {@link Resource} for the given request, it will be it will be exposed using a
* {@link HandlerFunction} that handles GET, HEAD, and OPTIONS requests.
* @param lookupFunction the function to provide a {@link Resource} given the {@link ServerRequest}
* @return a router function that routes to resources
*/
public static RouterFunction<Resource> resources(Function<ServerRequest, Optional<Resource>> lookupFunction) {
Assert.notNull(lookupFunction, "'lookupFunction' must not be null");
// TODO: make lookupFunction return Mono<Resource> once SPR-14870 is resolved
return request -> lookupFunction.apply(request).map(ResourceHandlerFunction::new);
}
/**
* Convert the given {@linkplain RouterFunction router function} into a {@link HttpHandler}.
* This conversion uses {@linkplain HandlerStrategies#builder() default strategies}.
* <p>The returned {@code HttpHandler} can be adapted to run in
* <ul>
@ -116,15 +151,15 @@ public abstract class RouterFunctions { @@ -116,15 +151,15 @@ public abstract class RouterFunctions {
* <li>Undertow using the
* {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.</li>
* </ul>
* @param routerFunction the routing function to convert
* @return an http handler that handles HTTP request using the given routing function
* @param routerFunction the router function to convert
* @return an http handler that handles HTTP request using the given router function
*/
public static HttpHandler toHttpHandler(RouterFunction<?> routerFunction) {
return toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
}
/**
* Convert the given {@linkplain RouterFunction routing function} into a {@link HttpHandler},
* Convert the given {@linkplain RouterFunction router function} into a {@link HttpHandler},
* using the given strategies.
* <p>The returned {@code HttpHandler} can be adapted to run in
* <ul>
@ -137,9 +172,9 @@ public abstract class RouterFunctions { @@ -137,9 +172,9 @@ public abstract class RouterFunctions {
* <li>Undertow using the
* {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.</li>
* </ul>
* @param routerFunction the routing function to convert
* @param routerFunction the router function to convert
* @param strategies the strategies to use
* @return an http handler that handles HTTP request using the given routing function
* @return an http handler that handles HTTP request using the given router function
*/
public static HttpHandler toHttpHandler(RouterFunction<?> routerFunction, HandlerStrategies strategies) {
Assert.notNull(routerFunction, "RouterFunction must not be null");
@ -159,8 +194,8 @@ public abstract class RouterFunctions { @@ -159,8 +194,8 @@ public abstract class RouterFunctions {
* This conversion uses {@linkplain HandlerStrategies#builder() default strategies}.
* <p>The returned {@code HandlerMapping} can be run in a
* {@link org.springframework.web.reactive.DispatcherHandler}.
* @param routerFunction the routing function to convert
* @return an handler mapping that maps HTTP request to a handler using the given routing function
* @param routerFunction the router function to convert
* @return an handler mapping that maps HTTP request to a handler using the given router function
* @see org.springframework.web.reactive.function.support.HandlerFunctionAdapter
* @see org.springframework.web.reactive.function.support.ServerResponseResultHandler
*/
@ -169,13 +204,13 @@ public abstract class RouterFunctions { @@ -169,13 +204,13 @@ public abstract class RouterFunctions {
}
/**
* Convert the given {@linkplain RouterFunction routing function} into a {@link HandlerMapping},
* Convert the given {@linkplain RouterFunction router function} into a {@link HandlerMapping},
* using the given strategies.
* <p>The returned {@code HandlerMapping} can be run in a
* {@link org.springframework.web.reactive.DispatcherHandler}.
* @param routerFunction the routing function to convert
* @param routerFunction the router function to convert
* @param strategies the strategies to use
* @return an handler mapping that maps HTTP request to a handler using the given routing function
* @return an handler mapping that maps HTTP request to a handler using the given router function
* @see org.springframework.web.reactive.function.support.HandlerFunctionAdapter
* @see org.springframework.web.reactive.function.support.ServerResponseResultHandler
*/

22
spring-web-reactive/src/main/java/org/springframework/web/reactive/function/ServerResponse.java

@ -82,7 +82,7 @@ public interface ServerResponse<T> { @@ -82,7 +82,7 @@ public interface ServerResponse<T> {
*/
static BodyBuilder from(ServerResponse<?> other) {
Assert.notNull(other, "'other' must not be null");
DefaultServerResponseBuilder builder = new DefaultServerResponseBuilder(other.statusCode().value());
DefaultServerResponseBuilder builder = new DefaultServerResponseBuilder(other.statusCode());
return builder.headers(other.headers());
}
@ -94,16 +94,6 @@ public interface ServerResponse<T> { @@ -94,16 +94,6 @@ public interface ServerResponse<T> {
*/
static BodyBuilder status(HttpStatus status) {
Assert.notNull(status, "HttpStatus must not be null");
return new DefaultServerResponseBuilder(status.value());
}
/**
* Create a builder with the given status.
*
* @param status the response status
* @return the created builder
*/
static BodyBuilder status(int status) {
return new DefaultServerResponseBuilder(status);
}
@ -211,6 +201,16 @@ public interface ServerResponse<T> { @@ -211,6 +201,16 @@ public interface ServerResponse<T> {
*/
B allow(HttpMethod... allowedMethods);
/**
* Set the set of allowed {@link HttpMethod HTTP methods}, as specified
* by the {@code Allow} header.
*
* @param allowedMethods the allowed methods
* @return this builder
* @see HttpHeaders#setAllow(Set)
*/
B allow(Set<HttpMethod> allowedMethods);
/**
* Set the entity tag of the body, as specified by the {@code ETag} header.
*

8
spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultServerResponseBuilderTests.java

@ -77,12 +77,6 @@ public class DefaultServerResponseBuilderTests { @@ -77,12 +77,6 @@ public class DefaultServerResponseBuilderTests {
assertEquals(HttpStatus.CREATED, result.statusCode());
}
@Test
public void statusInt() throws Exception {
ServerResponse<Void> result = ServerResponse.status(201).build();
assertEquals(HttpStatus.CREATED, result.statusCode());
}
@Test
public void ok() throws Exception {
ServerResponse<Void> result = ServerResponse.ok().build();
@ -186,7 +180,7 @@ public class DefaultServerResponseBuilderTests { @@ -186,7 +180,7 @@ public class DefaultServerResponseBuilderTests {
@Test
public void build() throws Exception {
ServerResponse<Void> result = ServerResponse.status(201).header("MyKey", "MyValue").build();
ServerResponse<Void> result = ServerResponse.status(HttpStatus.CREATED).header("MyKey", "MyValue").build();
ServerWebExchange exchange = mock(ServerWebExchange.class);
MockServerHttpResponse response = new MockServerHttpResponse();

78
spring-web-reactive/src/test/java/org/springframework/web/reactive/function/PathResourceLookupFunctionTests.java

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
/*
* Copyright 2002-2016 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
*
* http://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.web.reactive.function;
import java.net.URI;
import java.util.Optional;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author Arjen Poutsma
*/
public class PathResourceLookupFunctionTests {
@Test
public void normal() throws Exception {
ClassPathResource location = new ClassPathResource("org/springframework/web/reactive/function/");
PathResourceLookupFunction function = new PathResourceLookupFunction("/resources/**", location);
MockServerRequest<Void> request = MockServerRequest.builder()
.uri(new URI("http://localhost/resources/response.txt"))
.build();
Optional<Resource> result = function.apply(request);
assertTrue(result.isPresent());
ClassPathResource expected = new ClassPathResource("response.txt", getClass());
assertEquals(expected.getFile(), result.get().getFile());
}
@Test
public void subPath() throws Exception {
ClassPathResource location = new ClassPathResource("org/springframework/web/reactive/function/");
PathResourceLookupFunction function = new PathResourceLookupFunction("/resources/**", location);
MockServerRequest<Void> request = MockServerRequest.builder()
.uri(new URI("http://localhost/resources/child/response.txt"))
.build();
Optional<Resource> result = function.apply(request);
assertTrue(result.isPresent());
ClassPathResource expected = new ClassPathResource("org/springframework/web/reactive/function/child/response.txt");
assertEquals(expected.getFile(), result.get().getFile());
}
@Test
public void notFound() throws Exception {
ClassPathResource location = new ClassPathResource("org/springframework/web/reactive/function/");
PathResourceLookupFunction function = new PathResourceLookupFunction("/resources/**", location);
MockServerRequest<Void> request = MockServerRequest.builder()
.uri(new URI("http://localhost/resources/foo"))
.build();
Optional<Resource> result = function.apply(request);
assertFalse(result.isPresent());
}
}

151
spring-web-reactive/src/test/java/org/springframework/web/reactive/function/ResourceHandlerFunctionTests.java

@ -0,0 +1,151 @@ @@ -0,0 +1,151 @@
/*
* Copyright 2002-2016 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
*
* http://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.web.reactive.function;
import java.io.IOException;
import java.nio.file.Files;
import java.util.EnumSet;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* @author Arjen Poutsma
*/
public class ResourceHandlerFunctionTests {
private Resource resource;
private ResourceHandlerFunction handlerFunction;
@Before
public void createResource() {
this.resource = new ClassPathResource("response.txt", getClass());
this.handlerFunction = new ResourceHandlerFunction(this.resource);
}
@Test
public void get() throws IOException {
MockServerHttpRequest mockRequest =
new MockServerHttpRequest(HttpMethod.GET, "http://localhost");
MockServerHttpResponse mockResponse = new MockServerHttpResponse();
ServerWebExchange exchange = new DefaultServerWebExchange(mockRequest, mockResponse,
new MockWebSessionManager());
ServerRequest request = new DefaultServerRequest(exchange, HandlerStrategies.withDefaults());
ServerResponse<Resource> response = this.handlerFunction.handle(request);
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(this.resource, response.body());
Mono<Void> result = response.writeTo(exchange, HandlerStrategies.withDefaults());
StepVerifier.create(result)
.expectComplete()
.verify();
StepVerifier.create(result).expectComplete().verify();
byte[] expectedBytes = Files.readAllBytes(this.resource.getFile().toPath());
StepVerifier.create(mockResponse.getBody())
.consumeNextWith(dataBuffer -> {
byte[] resultBytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(resultBytes);
assertArrayEquals(expectedBytes, resultBytes);
})
.expectComplete()
.verify();
assertEquals(MediaType.TEXT_PLAIN, mockResponse.getHeaders().getContentType());
assertEquals(49, mockResponse.getHeaders().getContentLength());
}
@Test
public void head() throws IOException {
MockServerHttpRequest mockRequest =
new MockServerHttpRequest(HttpMethod.HEAD, "http://localhost");
MockServerHttpResponse mockResponse = new MockServerHttpResponse();
ServerWebExchange exchange = new DefaultServerWebExchange(mockRequest, mockResponse,
new MockWebSessionManager());
ServerRequest request = new DefaultServerRequest(exchange, HandlerStrategies.withDefaults());
ServerResponse<Resource> response = this.handlerFunction.handle(request);
assertEquals(HttpStatus.OK, response.statusCode());
Mono<Void> result = response.writeTo(exchange, HandlerStrategies.withDefaults());
StepVerifier.create(result)
.expectComplete()
.verify();
StepVerifier.create(result).expectComplete().verify();
StepVerifier.create(mockResponse.getBody())
.expectComplete()
.verify();
assertEquals(MediaType.TEXT_PLAIN, mockResponse.getHeaders().getContentType());
assertEquals(49, mockResponse.getHeaders().getContentLength());
}
@Test
public void options() {
MockServerHttpRequest mockRequest =
new MockServerHttpRequest(HttpMethod.OPTIONS, "http://localhost");
MockServerHttpResponse mockResponse = new MockServerHttpResponse();
ServerWebExchange exchange = new DefaultServerWebExchange(mockRequest, mockResponse,
new MockWebSessionManager());
ServerRequest request = new DefaultServerRequest(exchange, HandlerStrategies.withDefaults());
ServerResponse<Resource> response = this.handlerFunction.handle(request);
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS),
response.headers().getAllow());
assertNull(response.body());
Mono<Void> result = response.writeTo(exchange, HandlerStrategies.withDefaults());
StepVerifier.create(result)
.expectComplete()
.verify();
assertEquals(HttpStatus.OK, mockResponse.getStatusCode());
assertEquals(EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS),
mockResponse.getHeaders().getAllow());
assertNull(mockResponse.getBody());
}
}

2
spring-web-reactive/src/test/resources/org/springframework/web/reactive/function/child/response.txt

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
Hello World
This is a sample response text file.

2
spring-web-reactive/src/test/resources/org/springframework/web/reactive/function/response.txt

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
Hello World
This is a sample response text file.

1
spring-web/src/main/java/org/springframework/http/codec/BodyInserter.java

@ -30,6 +30,7 @@ import org.springframework.util.Assert; @@ -30,6 +30,7 @@ import org.springframework.util.Assert;
*
* @author Arjen Poutsma
* @since 5.0
* @see BodyInserters
*/
public interface BodyInserter<T, M extends ReactiveHttpOutputMessage> {

10
spring-web/src/main/java/org/springframework/http/codec/BodyInserters.java

@ -48,6 +48,16 @@ public abstract class BodyInserters { @@ -48,6 +48,16 @@ public abstract class BodyInserters {
private static final ResolvableType SERVER_SIDE_EVENT_TYPE =
ResolvableType.forClass(ServerSentEvent.class);
/**
* Return an empty {@code BodyInserter} that writes nothing.
* @return an empty {@code BodyInserter}
*/
public static <T> BodyInserter<T, ReactiveHttpOutputMessage> empty() {
return BodyInserter.of(
(response, context) -> response.setComplete(),
() -> null);
}
/**
* Return a {@code BodyInserter} that writes the given single object.
* @param body the body of the response

25
spring-web/src/main/java/org/springframework/web/util/UriUtils.java

@ -18,6 +18,7 @@ package org.springframework.web.util; @@ -18,6 +18,7 @@ package org.springframework.web.util;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import org.springframework.util.Assert;
@ -183,8 +184,26 @@ public abstract class UriUtils { @@ -183,8 +184,26 @@ public abstract class UriUtils {
* @see java.net.URLDecoder#decode(String, String)
*/
public static String decode(String source, String encoding) throws UnsupportedEncodingException {
Assert.notNull(source, "Source must not be null");
Assert.hasLength(encoding, "Encoding must not be empty");
return decode(source, Charset.forName(encoding));
}
/**
* Decodes the given encoded source String into an URI. Based on the following rules:
* <ul>
* <li>Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, and
* {@code "0"} through {@code "9"} stay the same.</li>
* <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.</li>
* <li>A sequence "{@code %<i>xy</i>}" is interpreted as a hexadecimal representation of the character.</li>
* </ul>
* @param source the source string
* @param charset the character set
* @return the decoded URI
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
* @see java.net.URLDecoder#decode(String, String)
*/
public static String decode(String source, Charset charset) {
Assert.notNull(source, "'source' must not be null");
Assert.notNull(charset, "'charset' must not be null");
int length = source.length();
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
boolean changed = false;
@ -211,7 +230,7 @@ public abstract class UriUtils { @@ -211,7 +230,7 @@ public abstract class UriUtils {
bos.write(ch);
}
}
return (changed ? new String(bos.toByteArray(), encoding) : source);
return (changed ? new String(bos.toByteArray(), charset) : source);
}
/**

Loading…
Cancel
Save