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
This commit is contained in:
@@ -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;
|
||||
*/
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@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 {
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
|
||||
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;
|
||||
|
||||
@@ -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> {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
* @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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Resource> 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 {
|
||||
* <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 {
|
||||
* <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 {
|
||||
* 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -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> {
|
||||
*/
|
||||
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> {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
Hello World
|
||||
This is a sample response text file.
|
||||
@@ -0,0 +1,2 @@
|
||||
Hello World
|
||||
This is a sample response text file.
|
||||
@@ -30,6 +30,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
* @see BodyInserters
|
||||
*/
|
||||
public interface BodyInserter<T, M extends ReactiveHttpOutputMessage> {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
* @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 {
|
||||
bos.write(ch);
|
||||
}
|
||||
}
|
||||
return (changed ? new String(bos.toByteArray(), encoding) : source);
|
||||
return (changed ? new String(bos.toByteArray(), charset) : source);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user