Introduce Servlet.fn

This commit introduces Servlet.fn, a servlet-based version of
WebFlux.fn, i.e. HandlerFunctions and RouterFunctions.

This commit contains the web framework only, further commits provide
tests and DispatcherServlet integration.

See gh-21490
This commit is contained in:
Arjen Poutsma
2019-03-06 15:56:33 +01:00
parent 7d498ba681
commit 5d2fd75cf9
20 changed files with 6153 additions and 0 deletions

View File

@@ -0,0 +1,469 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.servlet.ModelAndView;
/**
* Default {@link EntityResponse.Builder} implementation.
*
* @author Arjen Poutsma
* @since 5.2
*/
class DefaultEntityResponseBuilder<T> implements EntityResponse.Builder<T> {
private final T entity;
private final BuilderFunction<T> builderFunction;
private int status = HttpStatus.OK.value();
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, Cookie> cookies = new LinkedMultiValueMap<>();
private DefaultEntityResponseBuilder(T entity, BuilderFunction<T> builderFunction) {
this.entity = entity;
this.builderFunction = builderFunction;
}
@Override
public EntityResponse.Builder<T> status(HttpStatus status) {
Assert.notNull(status, "HttpStatus must not be null");
this.status = status.value();
return this;
}
@Override
public EntityResponse.Builder<T> status(int status) {
this.status = status;
return this;
}
@Override
public EntityResponse.Builder<T> cookie(Cookie cookie) {
Assert.notNull(cookie, "Cookie must not be null");
this.cookies.add(cookie.getName(), cookie);
return this;
}
@Override
public EntityResponse.Builder<T> cookies(
Consumer<MultiValueMap<String, Cookie>> cookiesConsumer) {
cookiesConsumer.accept(this.cookies);
return this;
}
@Override
public EntityResponse.Builder<T> header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public EntityResponse.Builder<T> headers(HttpHeaders headers) {
this.headers.putAll(headers);
return this;
}
@Override
public EntityResponse.Builder<T> allow(HttpMethod... allowedMethods) {
this.headers.setAllow(new LinkedHashSet<>(Arrays.asList(allowedMethods)));
return this;
}
@Override
public EntityResponse.Builder<T> allow(Set<HttpMethod> allowedMethods) {
this.headers.setAllow(allowedMethods);
return this;
}
@Override
public EntityResponse.Builder<T> contentLength(long contentLength) {
this.headers.setContentLength(contentLength);
return this;
}
@Override
public EntityResponse.Builder<T> contentType(MediaType contentType) {
this.headers.setContentType(contentType);
return this;
}
@Override
public EntityResponse.Builder<T> eTag(String etag) {
if (!etag.startsWith("\"") && !etag.startsWith("W/\"")) {
etag = "\"" + etag;
}
if (!etag.endsWith("\"")) {
etag = etag + "\"";
}
this.headers.setETag(etag);
return this;
}
@Override
public EntityResponse.Builder<T> lastModified(ZonedDateTime lastModified) {
this.headers.setLastModified(lastModified);
return this;
}
@Override
public EntityResponse.Builder<T> lastModified(Instant lastModified) {
this.headers.setLastModified(lastModified);
return this;
}
@Override
public EntityResponse.Builder<T> location(URI location) {
this.headers.setLocation(location);
return this;
}
@Override
public EntityResponse.Builder<T> cacheControl(CacheControl cacheControl) {
this.headers.setCacheControl(cacheControl);
return this;
}
@Override
public EntityResponse.Builder<T> varyBy(String... requestHeaders) {
this.headers.setVary(Arrays.asList(requestHeaders));
return this;
}
@Override
public EntityResponse<T> build() {
return this.builderFunction.build(this.status, this.headers, this.cookies, this.entity);
}
/**
* Return a new {@link EntityResponse.Builder} from the given object.
*/
public static <T> EntityResponse.Builder<T> fromObject(T t) {
return new DefaultEntityResponseBuilder<>(t, DefaultEntityResponse::new);
}
/**
* Return a new {@link EntityResponse.Builder} from the given completion stage.
*/
public static <T> EntityResponse.Builder<CompletionStage<T>> fromCompletionStage(
CompletionStage<T> completionStage) {
return new DefaultEntityResponseBuilder<>(completionStage,
CompletionStageEntityResponse::new);
}
/**
* Return a new {@link EntityResponse.Builder} from the given Reactive Streams publisher.
*/
public static <T> EntityResponse.Builder<Publisher<T>> fromPublisher(Publisher<T> publisher) {
return new DefaultEntityResponseBuilder<>(publisher, PublisherEntityResponse::new);
}
@SuppressWarnings("unchecked")
private static <T> HttpMessageConverter<T> cast(HttpMessageConverter<?> messageConverter) {
return (HttpMessageConverter<T>) messageConverter;
}
/**
* Defines contract for building {@link EntityResponse} instances.
*/
private interface BuilderFunction<T> {
EntityResponse<T> build(int statusCode, HttpHeaders headers,
MultiValueMap<String, Cookie> cookies, T entity);
}
/**
* Default {@link EntityResponse} implementation for synchronous bodies.
*/
private static class DefaultEntityResponse<T>
extends DefaultServerResponseBuilder.AbstractServerResponse
implements EntityResponse<T> {
private final T entity;
public DefaultEntityResponse(int statusCode, HttpHeaders headers,
MultiValueMap<String, Cookie> cookies, T entity) {
super(statusCode, headers, cookies);
this.entity = entity;
}
@Override
public T entity() {
return this.entity;
}
@Override
protected ModelAndView writeToInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse, Context context)
throws ServletException, IOException {
writeEntityWithMessageConverters(this.entity, servletRequest, servletResponse, context);
return null;
}
protected void writeEntityWithMessageConverters(Object entity,
HttpServletRequest request, HttpServletResponse response,
ServerResponse.Context context)
throws ServletException, IOException {
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
MediaType contentType = getContentType(response);
Class<?> entityType = entity.getClass();
HttpMessageConverter<Object> messageConverter = context.messageConverters().stream()
.filter(converter -> converter.canWrite(entityType, contentType))
.findFirst()
.map(DefaultEntityResponseBuilder::cast)
.orElseThrow(() -> new HttpMediaTypeNotAcceptableException(
producibleMediaTypes(context.messageConverters(), entityType)));
messageConverter.write(entity, contentType, serverResponse);
}
@Nullable
private MediaType getContentType(HttpServletResponse response) {
try {
return MediaType.parseMediaType(response.getContentType());
}
catch (InvalidMediaTypeException ex) {
return null;
}
}
protected final void tryWriteEntityWithMessageConverters(Object entity,
HttpServletRequest request, HttpServletResponse response,
ServerResponse.Context context) {
try {
writeEntityWithMessageConverters(entity, request, response, context);
}
catch (IOException | ServletException ex) {
handleError(ex, request, response, context);
}
}
private static List<MediaType> producibleMediaTypes(
List<HttpMessageConverter<?>> messageConverters,
Class<?> entityClass) {
return messageConverters.stream()
.filter(messageConverter -> messageConverter.canWrite(entityClass, null))
.flatMap(messageConverter -> messageConverter.getSupportedMediaTypes().stream())
.collect(Collectors.toList());
}
}
/**
* {@link EntityResponse} implementation for asynchronous {@link CompletionStage} bodies.
*/
private static class CompletionStageEntityResponse<T>
extends DefaultEntityResponse<CompletionStage<T>> {
public CompletionStageEntityResponse(int statusCode,
HttpHeaders headers,
MultiValueMap<String, Cookie> cookies, CompletionStage<T> entity) {
super(statusCode, headers, cookies, entity);
}
@Override
protected ModelAndView writeToInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse, Context context) {
AsyncContext asyncContext = servletRequest.startAsync(servletRequest, servletResponse);
entity().whenComplete((entity, throwable) -> {
try {
if (entity != null) {
tryWriteEntityWithMessageConverters(entity,
(HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(),
context);
}
else if (throwable != null) {
handleError(throwable, servletRequest, servletResponse, context);
}
}
finally {
asyncContext.complete();
}
});
return null;
}
}
private static class PublisherEntityResponse<T> extends DefaultEntityResponse<Publisher<T>> {
public PublisherEntityResponse(int statusCode, HttpHeaders headers,
MultiValueMap<String, Cookie> cookies, Publisher<T> entity) {
super(statusCode, headers, cookies, entity);
}
@Override
protected ModelAndView writeToInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse, Context context) {
AsyncContext asyncContext = servletRequest.startAsync(servletRequest,
new NoContentLengthResponseWrapper(servletResponse));
entity().subscribe(new ProducingSubscriber(asyncContext, context));
return null;
}
@SuppressWarnings("SubscriberImplementation")
private class ProducingSubscriber implements Subscriber<T> {
private final AsyncContext asyncContext;
private final Context context;
@Nullable
private Subscription subscription;
public ProducingSubscriber(AsyncContext asyncContext,
Context context) {
this.asyncContext = asyncContext;
this.context = context;
}
@Override
public void onSubscribe(Subscription s) {
Objects.requireNonNull(s);
if (this.subscription == null) {
this.subscription = s;
this.subscription.request(Long.MAX_VALUE);
}
else {
s.cancel();
}
}
@Override
public void onNext(T element) {
Objects.requireNonNull(element);
HttpServletRequest servletRequest =
(HttpServletRequest) this.asyncContext.getRequest();
HttpServletResponse servletResponse =
(HttpServletResponse) this.asyncContext.getResponse();
tryWriteEntityWithMessageConverters(element,
servletRequest,
servletResponse,
this.context);
}
@Override
public void onError(Throwable t) {
Objects.requireNonNull(t);
handleError(t,
(HttpServletRequest) this.asyncContext.getRequest(),
(HttpServletResponse) this.asyncContext.getResponse(),
this.context);
}
@Override
public void onComplete() {
this.asyncContext.complete();
}
}
private static class NoContentLengthResponseWrapper extends HttpServletResponseWrapper {
public NoContentLengthResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void addIntHeader(String name, int value) {
if (!HttpHeaders.CONTENT_LENGTH.equals(name)) {
super.addIntHeader(name, value);
}
}
@Override
public void addHeader(String name, String value) {
if (!HttpHeaders.CONTENT_LENGTH.equals(name)) {
super.addHeader(name, value);
}
}
@Override
public void setContentLength(int len) {
}
@Override
public void setContentLengthLong(long len) {
}
}
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.Conventions;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.servlet.ModelAndView;
/**
* Default {@link RenderingResponse.Builder} implementation.
*
* @author Arjen Poutsma
* @since 5.1
*/
final class DefaultRenderingResponseBuilder implements RenderingResponse.Builder {
private final String name;
private int status = HttpStatus.OK.value();
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, Cookie> cookies = new LinkedMultiValueMap<>();
private final Map<String, Object> model = new LinkedHashMap<>();
public DefaultRenderingResponseBuilder(RenderingResponse other) {
Assert.notNull(other, "RenderingResponse must not be null");
this.name = other.name();
this.status = (other instanceof DefaultRenderingResponse ?
((DefaultRenderingResponse) other).statusCode : other.statusCode().value());
this.headers.putAll(other.headers());
this.model.putAll(other.model());
}
public DefaultRenderingResponseBuilder(String name) {
Assert.notNull(name, "Name must not be null");
this.name = name;
}
@Override
public RenderingResponse.Builder status(HttpStatus status) {
Assert.notNull(status, "HttpStatus must not be null");
this.status = status.value();
return this;
}
@Override
public RenderingResponse.Builder status(int status) {
this.status = status;
return this;
}
@Override
public RenderingResponse.Builder cookie(Cookie cookie) {
Assert.notNull(cookie, "Cookie must not be null");
this.cookies.add(cookie.getName(), cookie);
return this;
}
@Override
public RenderingResponse.Builder cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer) {
cookiesConsumer.accept(this.cookies);
return this;
}
@Override
public RenderingResponse.Builder modelAttribute(Object attribute) {
Assert.notNull(attribute, "Attribute must not be null");
if (attribute instanceof Collection && ((Collection<?>) attribute).isEmpty()) {
return this;
}
return modelAttribute(Conventions.getVariableName(attribute), attribute);
}
@Override
public RenderingResponse.Builder modelAttribute(String name, @Nullable Object value) {
Assert.notNull(name, "Name must not be null");
this.model.put(name, value);
return this;
}
@Override
public RenderingResponse.Builder modelAttributes(Object... attributes) {
modelAttributes(Arrays.asList(attributes));
return this;
}
@Override
public RenderingResponse.Builder modelAttributes(Collection<?> attributes) {
attributes.forEach(this::modelAttribute);
return this;
}
@Override
public RenderingResponse.Builder modelAttributes(Map<String, ?> attributes) {
this.model.putAll(attributes);
return this;
}
@Override
public RenderingResponse.Builder header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public RenderingResponse.Builder headers(HttpHeaders headers) {
this.headers.putAll(headers);
return this;
}
@Override
public RenderingResponse build() {
return new DefaultRenderingResponse(this.status, this.headers, this.cookies, this.name, this.model);
}
private static final class DefaultRenderingResponse extends DefaultServerResponseBuilder.AbstractServerResponse
implements RenderingResponse {
private final String name;
private final Map<String, Object> model;
public DefaultRenderingResponse(int statusCode, HttpHeaders headers,
MultiValueMap<String, Cookie> cookies, String name, Map<String, Object> model) {
super(statusCode, headers, cookies);
this.name = name;
this.model = Collections.unmodifiableMap(new LinkedHashMap<>(model));
}
@Override
public String name() {
return this.name;
}
@Override
public Map<String, Object> model() {
return this.model;
}
@Override
protected ModelAndView writeToInternal(HttpServletRequest request,
HttpServletResponse response, Context context) {
HttpStatus status = HttpStatus.resolve(this.statusCode);
ModelAndView mav;
if (status != null) {
mav = new ModelAndView(this.name, status);
}
else {
mav = new ModelAndView(this.name);
}
mav.addAllObjects(this.model);
return mav;
}
}
}

View File

@@ -0,0 +1,402 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriBuilder;
/**
* {@code ServerRequest} implementation based on a {@link HttpServletRequest}.
*
* @author Arjen Poutsma
* @since 5.2
*/
class DefaultServerRequest implements ServerRequest {
private final ServletServerHttpRequest serverHttpRequest;
private final Headers headers;
private final List<HttpMessageConverter<?>> messageConverters;
private final List<MediaType> allSupportedMediaTypes;
private final MultiValueMap<String, String> params;
private final Map<String, Object> attributes;
public DefaultServerRequest(HttpServletRequest servletRequest,
List<HttpMessageConverter<?>> messageConverters) {
this.serverHttpRequest = new ServletServerHttpRequest(servletRequest);
this.messageConverters = Collections.unmodifiableList(new ArrayList<>(messageConverters));
this.allSupportedMediaTypes = allSupportedMediaTypes(messageConverters);
this.headers = new DefaultRequestHeaders(this.serverHttpRequest.getHeaders());
this.params = CollectionUtils.toMultiValueMap(new ServletParametersMap(servletRequest));
this.attributes = new ServletAttributesMap(servletRequest);
}
private static List<MediaType> allSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
return messageConverters.stream()
.flatMap(converter -> converter.getSupportedMediaTypes().stream())
.sorted(MediaType.SPECIFICITY_COMPARATOR)
.collect(Collectors.toList());
}
@Override
public String methodName() {
return servletRequest().getMethod();
}
@Override
public URI uri() {
return this.serverHttpRequest.getURI();
}
@Override
public UriBuilder uriBuilder() {
return ServletUriComponentsBuilder.fromRequest(servletRequest());
}
@Override
public Headers headers() {
return this.headers;
}
@Override
public MultiValueMap<String, Cookie> cookies() {
Cookie[] cookies = servletRequest().getCookies();
if (cookies == null) {
cookies = new Cookie[0];
}
MultiValueMap<String, Cookie> result = new LinkedMultiValueMap<>(cookies.length);
for (Cookie cookie : cookies) {
result.add(cookie.getName(), cookie);
}
return result;
}
@Override
public HttpServletRequest servletRequest() {
return this.serverHttpRequest.getServletRequest();
}
@Override
public Optional<InetSocketAddress> remoteAddress() {
return Optional.of(this.serverHttpRequest.getRemoteAddress());
}
@Override
public List<HttpMessageConverter<?>> messageConverters() {
return this.messageConverters;
}
@Override
public <T> T body(Class<T> bodyType) throws IOException, ServletException {
return bodyInternal(bodyType, bodyType);
}
@Override
public <T> T body(ParameterizedTypeReference<T> bodyType) throws IOException, ServletException {
Type type = bodyType.getType();
Class<?> contextClass = null;
if (type instanceof Class) {
contextClass = (Class<?>) type;
}
return bodyInternal(type, contextClass);
}
@SuppressWarnings("unchecked")
private <T> T bodyInternal(Type type, @Nullable Class<?> contextClass)
throws ServletException, IOException {
MediaType contentType =
this.headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter<?>) {
GenericHttpMessageConverter<T> genericMessageConverter =
(GenericHttpMessageConverter<T>) messageConverter;
if (genericMessageConverter.canRead(type, contextClass, contentType)) {
return genericMessageConverter.read(type, contextClass, this.serverHttpRequest);
}
}
else {
if (messageConverter.canRead(contextClass, contentType)) {
HttpMessageConverter<T> theConverter =
(HttpMessageConverter<T>) messageConverter;
Class<? extends T> clazz = (Class<? extends T>) contextClass;
return theConverter.read(clazz, this.serverHttpRequest);
}
}
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}
@Override
public Optional<Object> attribute(String name) {
return Optional.ofNullable(servletRequest().getAttribute(name));
}
@Override
public Map<String, Object> attributes() {
return this.attributes;
}
@Override
public MultiValueMap<String, String> params() {
return this.params;
}
@Override
public Map<String, String> pathVariables() {
@SuppressWarnings("unchecked")
Map<String, String> pathVariables = (Map<String, String>) servletRequest()
.getAttribute(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (pathVariables != null) {
return pathVariables;
} else {
return Collections.emptyMap();
}
}
@Override
public HttpSession session() {
return servletRequest().getSession(true);
}
@Override
public Optional<Principal> principal() {
return Optional.ofNullable(this.serverHttpRequest.getPrincipal());
}
static class DefaultRequestHeaders implements Headers {
private final HttpHeaders delegate;
public DefaultRequestHeaders(HttpHeaders delegate) {
this.delegate = delegate;
}
@Override
public List<MediaType> accept() {
return this.delegate.getAccept();
}
@Override
public List<Charset> acceptCharset() {
return this.delegate.getAcceptCharset();
}
@Override
public List<Locale.LanguageRange> acceptLanguage() {
return this.delegate.getAcceptLanguage();
}
@Override
public OptionalLong contentLength() {
long value = this.delegate.getContentLength();
return (value != -1 ? OptionalLong.of(value) : OptionalLong.empty());
}
@Override
public Optional<MediaType> contentType() {
return Optional.ofNullable(this.delegate.getContentType());
}
@Override
public InetSocketAddress host() {
return this.delegate.getHost();
}
@Override
public List<HttpRange> range() {
return this.delegate.getRange();
}
@Override
public List<String> header(String headerName) {
List<String> headerValues = this.delegate.get(headerName);
return (headerValues != null ? headerValues : Collections.emptyList());
}
@Override
public HttpHeaders asHttpHeaders() {
return HttpHeaders.readOnlyHttpHeaders(this.delegate);
}
@Override
public String toString() {
return this.delegate.toString();
}
}
private static class ServletParametersMap extends AbstractMap<String, List<String>> {
private final HttpServletRequest servletRequest;
private ServletParametersMap(HttpServletRequest servletRequest) {
this.servletRequest = servletRequest;
}
@NotNull
@Override
public Set<Entry<String, List<String>>> entrySet() {
return this.servletRequest.getParameterMap().entrySet().stream()
.map(entry -> {
List<String> value = Arrays.asList(entry.getValue());
return new SimpleImmutableEntry<>(entry.getKey(), value);
})
.collect(Collectors.toSet());
}
@Override
public int size() {
return this.servletRequest.getParameterMap().size();
}
@Override
public List<String> get(Object key) {
String name = (String) key;
String[] parameterValues = this.servletRequest.getParameterValues(name);
if (!ObjectUtils.isEmpty(parameterValues)) {
return Arrays.asList(parameterValues);
}
else {
return Collections.emptyList();
}
}
@Override
public List<String> put(String key, List<String> value) {
throw new UnsupportedOperationException();
}
@Override
public List<String> remove(Object key) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
}
private static class ServletAttributesMap extends AbstractMap<String, Object> {
private final HttpServletRequest servletRequest;
private ServletAttributesMap(HttpServletRequest servletRequest) {
this.servletRequest = servletRequest;
}
@Override
public boolean containsKey(Object key) {
String name = (String) key;
return this.servletRequest.getAttribute(name) != null;
}
@Override
public void clear() {
Enumeration<String> attributeNames = this.servletRequest.getAttributeNames();
while (attributeNames.hasMoreElements()) {
String name = attributeNames.nextElement();
this.servletRequest.removeAttribute(name);
}
}
@NotNull
@Override
public Set<Entry<String, Object>> entrySet() {
return Collections.list(this.servletRequest.getAttributeNames()).stream()
.map(name -> {
Object value = this.servletRequest.getAttribute(name);
return new SimpleImmutableEntry<>(name, value);
})
.collect(Collectors.toSet());
}
@Override
public Object get(Object key) {
String name = (String) key;
return this.servletRequest.getAttribute(name);
}
@Override
public Object put(String key, Object value) {
Object oldValue = this.servletRequest.getAttribute(key);
this.servletRequest.setAttribute(key, value);
return oldValue;
}
@Override
public Object remove(Object key) {
String name = (String) key;
Object value = this.servletRequest.getAttribute(name);
this.servletRequest.removeAttribute(name);
return value;
}
}
}

View File

@@ -0,0 +1,397 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Default {@link ServerRequest.Builder} implementation.
* @author Arjen Poutsma
* @since 5.2
*/
class DefaultServerRequestBuilder implements ServerRequest.Builder {
private final List<HttpMessageConverter<?>> messageConverters;
private HttpServletRequest servletRequest;
private String methodName;
private URI uri;
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, Cookie> cookies = new LinkedMultiValueMap<>();
private final Map<String, Object> attributes = new LinkedHashMap<>();
private byte[] body = new byte[0];
public DefaultServerRequestBuilder(ServerRequest other) {
Assert.notNull(other, "ServerRequest must not be null");
this.messageConverters = other.messageConverters();
this.servletRequest = other.servletRequest();
this.methodName = other.methodName();
this.uri = other.uri();
headers(headers -> headers.addAll(other.headers().asHttpHeaders()));
cookies(cookies -> cookies.addAll(other.cookies()));
attributes(attributes -> attributes.putAll(other.attributes()));
}
@Override
public ServerRequest.Builder method(HttpMethod method) {
Assert.notNull(method, "HttpMethod must not be null");
this.methodName = method.name();
return this;
}
@Override
public ServerRequest.Builder uri(URI uri) {
Assert.notNull(uri, "URI must not be null");
this.uri = uri;
return this;
}
@Override
public ServerRequest.Builder header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public ServerRequest.Builder headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(this.headers);
return this;
}
@Override
public ServerRequest.Builder cookie(String name, String... values) {
for (String value : values) {
this.cookies.add(name, new Cookie(name, value));
}
return this;
}
@Override
public ServerRequest.Builder cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer) {
cookiesConsumer.accept(this.cookies);
return this;
}
@Override
public ServerRequest.Builder body(byte[] body) {
this.body = body;
return this;
}
@Override
public ServerRequest.Builder body(String body) {
return body(body.getBytes(StandardCharsets.UTF_8));
}
@Override
public ServerRequest.Builder attribute(String name, Object value) {
Assert.notNull(name, "'name' must not be null");
this.attributes.put(name, value);
return this;
}
@Override
public ServerRequest.Builder attributes(Consumer<Map<String, Object>> attributesConsumer) {
attributesConsumer.accept(this.attributes);
return this;
}
@Override
public ServerRequest build() {
return new BuiltServerRequest(this.servletRequest,
this.methodName, this.uri, this.headers, this.cookies, this.attributes, this.body,
this.messageConverters);
}
private static class BuiltServerRequest implements ServerRequest {
private final String methodName;
private final URI uri;
private final HttpHeaders headers;
private final HttpServletRequest servletRequest;
private MultiValueMap<String, Cookie> cookies;
private final Map<String, Object> attributes;
private final byte[] body;
private final List<HttpMessageConverter<?>> messageConverters;
public BuiltServerRequest(HttpServletRequest servletRequest, String methodName, URI uri,
HttpHeaders headers, MultiValueMap<String, Cookie> cookies,
Map<String, Object> attributes, byte[] body,
List<HttpMessageConverter<?>> messageConverters) {
this.servletRequest = servletRequest;
this.methodName = methodName;
this.uri = uri;
this.headers = headers;
this.cookies = cookies;
this.attributes = attributes;
this.body = body;
this.messageConverters = messageConverters;
}
@Override
public String methodName() {
return this.methodName;
}
@Override
public URI uri() {
return this.uri;
}
@Override
public UriBuilder uriBuilder() {
return UriComponentsBuilder.fromUri(this.uri);
}
@Override
public Headers headers() {
return new DefaultServerRequest.DefaultRequestHeaders(this.headers);
}
@Override
public MultiValueMap<String, Cookie> cookies() {
return this.cookies;
}
@Override
public Optional<InetSocketAddress> remoteAddress() {
return Optional.empty();
}
@Override
public List<HttpMessageConverter<?>> messageConverters() {
return this.messageConverters;
}
@Override
public <T> T body(Class<T> bodyType) throws IOException, ServletException {
return bodyInternal(bodyType, bodyType);
}
@Override
public <T> T body(ParameterizedTypeReference<T> bodyType) throws IOException, ServletException {
Type type = bodyType.getType();
Class<?> contextClass = null;
if (type instanceof Class) {
contextClass = (Class<?>) type;
}
return bodyInternal(type, contextClass);
}
@SuppressWarnings("unchecked")
private <T> T bodyInternal(Type type, @Nullable Class<?> contextClass)
throws ServletException, IOException {
HttpInputMessage inputMessage = new BuiltInputMessage();
MediaType contentType = headers().contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter<?>) {
GenericHttpMessageConverter<T> genericMessageConverter =
(GenericHttpMessageConverter<T>) messageConverter;
if (genericMessageConverter.canRead(type, contextClass, contentType)) {
return genericMessageConverter.read(type, contextClass, inputMessage);
}
}
else {
if (messageConverter.canRead(contextClass, contentType)) {
HttpMessageConverter<T> theConverter =
(HttpMessageConverter<T>) messageConverter;
Class<? extends T> clazz = (Class<? extends T>) contextClass;
return theConverter.read(clazz, inputMessage);
}
}
}
throw new HttpMediaTypeNotSupportedException(contentType, Collections.emptyList());
}
@Override
public Map<String, Object> attributes() {
return this.attributes;
}
@Override
public MultiValueMap<String, String> params() {
return new LinkedMultiValueMap<>();
}
@Override
public Map<String, String> pathVariables() {
@SuppressWarnings("unchecked")
Map<String, String> pathVariables = (Map<String, String>) attributes()
.get(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (pathVariables != null) {
return pathVariables;
} else {
return Collections.emptyMap();
}
}
@Override
public HttpSession session() {
return this.servletRequest.getSession();
}
@Override
public Optional<Principal> principal() {
return Optional.ofNullable(this.servletRequest.getUserPrincipal());
}
@Override
public HttpServletRequest servletRequest() {
return this.servletRequest;
}
private class BuiltInputMessage implements HttpInputMessage {
@Override
public InputStream getBody() throws IOException {
return new BodyInputStream(body);
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
}
private static class BodyInputStream extends ServletInputStream {
private final InputStream delegate;
public BodyInputStream(byte[] body) {
this.delegate = new ByteArrayInputStream(body);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return this.delegate.read();
}
@Override
public int read(@NotNull byte[] b, int off, int len) throws IOException {
return this.delegate.read(b, off, len);
}
@Override
public int read(@NotNull byte[] b) throws IOException {
return this.delegate.read(b);
}
@Override
public long skip(long n) throws IOException {
return this.delegate.skip(n);
}
@Override
public int available() throws IOException {
return this.delegate.available();
}
@Override
public void close() throws IOException {
this.delegate.close();
}
@Override
public synchronized void mark(int readlimit) {
this.delegate.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
this.delegate.reset();
}
@Override
public boolean markSupported() {
return this.delegate.markSupported();
}
}
}

View File

@@ -0,0 +1,417 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.reactivestreams.Publisher;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.ModelAndView;
/**
* Default {@link ServerResponse.BodyBuilder} implementation.
* @author Arjen Poutsma
* @since 5.2
*/
class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
private final int statusCode;
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, Cookie> cookies = new LinkedMultiValueMap<>();
public DefaultServerResponseBuilder(ServerResponse other) {
Assert.notNull(other, "ServerResponse must not be null");
this.statusCode = (other instanceof AbstractServerResponse ?
((AbstractServerResponse) other).statusCode : other.statusCode().value());
this.headers.addAll(other.headers());
}
public DefaultServerResponseBuilder(HttpStatus status) {
Assert.notNull(status, "HttpStatus must not be null");
this.statusCode = status.value();
}
public DefaultServerResponseBuilder(int statusCode) {
this.statusCode = statusCode;
}
@Override
public ServerResponse.BodyBuilder header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public ServerResponse.BodyBuilder headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(this.headers);
return this;
}
@Override
public ServerResponse.BodyBuilder cookie(Cookie cookie) {
Assert.notNull(cookie, "Cookie must not be null");
this.cookies.add(cookie.getName(), cookie);
return this;
}
@Override
public ServerResponse.BodyBuilder cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer) {
cookiesConsumer.accept(this.cookies);
return this;
}
@Override
public ServerResponse.BodyBuilder allow(HttpMethod... allowedMethods) {
this.headers.setAllow(new LinkedHashSet<>(Arrays.asList(allowedMethods)));
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);
return this;
}
@Override
public ServerResponse.BodyBuilder contentType(MediaType contentType) {
this.headers.setContentType(contentType);
return this;
}
@Override
public ServerResponse.BodyBuilder eTag(String etag) {
if (!etag.startsWith("\"") && !etag.startsWith("W/\"")) {
etag = "\"" + etag;
}
if (!etag.endsWith("\"")) {
etag = etag + "\"";
}
this.headers.setETag(etag);
return this;
}
@Override
public ServerResponse.BodyBuilder lastModified(ZonedDateTime lastModified) {
this.headers.setLastModified(lastModified);
return this;
}
@Override
public ServerResponse.BodyBuilder lastModified(Instant lastModified) {
this.headers.setLastModified(lastModified);
return this;
}
@Override
public ServerResponse.BodyBuilder location(URI location) {
this.headers.setLocation(location);
return this;
}
@Override
public ServerResponse.BodyBuilder cacheControl(CacheControl cacheControl) {
this.headers.setCacheControl(cacheControl);
return this;
}
@Override
public ServerResponse.BodyBuilder varyBy(String... requestHeaders) {
this.headers.setVary(Arrays.asList(requestHeaders));
return this;
}
@Override
public ServerResponse build() {
return build((request, response) -> null);
}
@Override
public ServerResponse build(
BiFunction<HttpServletRequest, HttpServletResponse, ModelAndView> writeFunction) {
return new WriterFunctionResponse(this.statusCode, this.headers, this.cookies, writeFunction);
}
@Override
public ServerResponse body(Object body) {
return DefaultEntityResponseBuilder.fromObject(body)
.headers(this.headers)
.status(this.statusCode)
.build();
}
@Override
public ServerResponse asyncBody(CompletionStage<?> asyncBody) {
return DefaultEntityResponseBuilder.fromCompletionStage(asyncBody)
.headers(this.headers)
.status(this.statusCode)
.build();
}
@Override
public ServerResponse asyncBody(Publisher<?> futureBody) {
return DefaultEntityResponseBuilder.fromPublisher(futureBody)
.headers(this.headers)
.status(this.statusCode)
.build();
}
@Override
public ServerResponse render(String name, Object... modelAttributes) {
return new DefaultRenderingResponseBuilder(name)
.headers(this.headers)
.status(this.statusCode)
.modelAttributes(modelAttributes)
.build();
}
@Override
public ServerResponse render(String name, Map<String, ?> model) {
return new DefaultRenderingResponseBuilder(name)
.headers(this.headers)
.status(this.statusCode)
.modelAttributes(model)
.build();
}
/**
* Abstract base class for {@link ServerResponse} implementations.
*/
abstract static class AbstractServerResponse implements ServerResponse {
private static final Set<HttpMethod> SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
final int statusCode;
private final HttpHeaders headers;
private final MultiValueMap<String, Cookie> cookies;
private final List<ErrorHandler<?>> errorHandlers = new ArrayList<>();
protected AbstractServerResponse(
int statusCode, HttpHeaders headers, MultiValueMap<String, Cookie> cookies) {
this.statusCode = statusCode;
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
this.cookies =
CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
}
protected <T extends ServerResponse> void addErrorHandler(Predicate<Throwable> predicate,
BiFunction<Throwable, ServerRequest, T> errorHandler) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(errorHandler, "ErrorHandler must not be null");
this.errorHandlers.add(new ErrorHandler<>(predicate, errorHandler));
}
@Override
public final HttpStatus statusCode() {
return HttpStatus.valueOf(this.statusCode);
}
@Override
public final HttpHeaders headers() {
return this.headers;
}
@Override
public MultiValueMap<String, Cookie> cookies() {
return this.cookies;
}
@Override
public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response,
Context context) throws ServletException, IOException {
try {
writeStatusAndHeaders(response);
long lastModified = headers().getLastModified();
ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (SAFE_METHODS.contains(httpMethod) &&
servletWebRequest.checkNotModified(headers().getETag(), lastModified)) {
return null;
}
else {
return writeToInternal(request, response, context);
}
} catch (Throwable t) {
return handleError(t, request, response, context);
}
}
private void writeStatusAndHeaders(HttpServletResponse response) {
response.setStatus(this.statusCode);
writeHeaders(response);
writeCookies(response);
}
private void writeHeaders(HttpServletResponse servletResponse) {
this.headers.forEach((headerName, headerValues) -> {
for (String headerValue : headerValues) {
servletResponse.addHeader(headerName, headerValue);
}
});
// HttpServletResponse exposes some headers as properties: we should include those if not already present
if (servletResponse.getContentType() == null && this.headers.getContentType() != null) {
servletResponse.setContentType(this.headers.getContentType().toString());
}
if (servletResponse.getCharacterEncoding() == null &&
this.headers.getContentType() != null &&
this.headers.getContentType().getCharset() != null) {
servletResponse
.setCharacterEncoding(this.headers.getContentType().getCharset().name());
}
}
private void writeCookies(HttpServletResponse servletResponse) {
this.cookies.values().stream()
.flatMap(Collection::stream)
.forEach(servletResponse::addCookie);
}
@Nullable
protected abstract ModelAndView writeToInternal(HttpServletRequest request,
HttpServletResponse response, Context context)
throws ServletException, IOException;
@Nullable
protected ModelAndView handleError(Throwable t, HttpServletRequest servletRequest,
HttpServletResponse servletResponse, Context context) {
return this.errorHandlers.stream()
.filter(errorHandler -> errorHandler.test(t))
.findFirst()
.map(errorHandler -> {
ServerRequest serverRequest =
(ServerRequest) servletRequest
.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE);
ServerResponse serverResponse = errorHandler.handle(t, serverRequest);
try {
return serverResponse.writeTo(servletRequest, servletResponse, context);
}
catch (ServletException ex) {
throw new RuntimeException(ex);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
})
.orElseThrow(() -> new RuntimeException(t));
}
private static class ErrorHandler<T extends ServerResponse> {
private final Predicate<Throwable> predicate;
private final BiFunction<Throwable, ServerRequest, T>
responseProvider;
public ErrorHandler(Predicate<Throwable> predicate,
BiFunction<Throwable, ServerRequest, T> responseProvider) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(responseProvider, "ResponseProvider must not be null");
this.predicate = predicate;
this.responseProvider = responseProvider;
}
public boolean test(Throwable t) {
return this.predicate.test(t);
}
public T handle(Throwable t, ServerRequest serverRequest) {
return this.responseProvider.apply(t, serverRequest);
}
}
}
private static class WriterFunctionResponse extends AbstractServerResponse {
private final BiFunction<HttpServletRequest, HttpServletResponse, ModelAndView> writeFunction;
public WriterFunctionResponse(int statusCode, HttpHeaders headers,
MultiValueMap<String, Cookie> cookies,
BiFunction<HttpServletRequest, HttpServletResponse, ModelAndView> writeFunction) {
super(statusCode, headers, cookies);
Assert.notNull(writeFunction, "WriteFunction must not be null");
this.writeFunction = writeFunction;
}
@Override
protected ModelAndView writeToInternal(HttpServletRequest request,
HttpServletResponse response, Context context) {
return this.writeFunction.apply(request, response);
}
}
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.net.URI;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import javax.servlet.http.Cookie;
import org.reactivestreams.Publisher;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
/**
* Entity-specific subtype of {@link ServerResponse} that exposes entity data.
*
* @author Arjen Poutsma
* @since 5.2
* @param <T> the entity type
*/
public interface EntityResponse<T> extends ServerResponse {
/**
* Return the entity that makes up this response.
*/
T entity();
// Static builder methods
/**
* Create a builder with the given object.
* @param t the object that represents the body of the response
* @param <T> the type of element contained in the publisher
* @return the created builder
*/
static <T> Builder<T> fromObject(T t) {
return DefaultEntityResponseBuilder.fromObject(t);
}
/**
* Create a builder for an asynchronous body supplied by the given {@link CompletionStage}.
* @param completionStage the supplier of the response body
* @param <T> the type of the elements contained in the publisher
* @return the created builder
*/
static <T> Builder<CompletionStage<T>> fromCompletionStage(CompletionStage<T> completionStage) {
return DefaultEntityResponseBuilder.fromCompletionStage(completionStage);
}
/**
* Create a builder for an asynchronous body supplied by the given {@link Publisher}.
* @param publisher the supplier of the response body
* @param <T> the type of the elements contained in the publisher
* @return the created builder
*/
static <T> Builder<Publisher<T>> fromPublisher(Publisher<T> publisher) {
return DefaultEntityResponseBuilder.fromPublisher(publisher);
}
/**
* Defines a builder for {@code EntityResponse}.
*/
interface Builder<T> {
/**
* Add the given header value(s) under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return this builder
* @see HttpHeaders#add(String, String)
*/
Builder<T> header(String headerName, String... headerValues);
/**
* Copy the given headers into the entity's headers map.
* @param headers the existing HttpHeaders to copy from
* @return this builder
* @see HttpHeaders#add(String, String)
*/
Builder<T> headers(HttpHeaders headers);
/**
* Set the HTTP status.
* @param status the response status
* @return this builder
*/
Builder<T> status(HttpStatus status);
/**
* Set the HTTP status.
* @param status the response status
* @return this builder
*/
Builder<T> status(int status);
/**
* Add the given cookie to the response.
* @param cookie the cookie to add
* @return this builder
*/
Builder<T> cookie(Cookie cookie);
/**
* Manipulate this response's cookies with the given consumer. The
* cookies provided to the consumer are "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing cookies,
* {@linkplain MultiValueMap#remove(Object) remove} cookies, or use any of the other
* {@link MultiValueMap} methods.
* @param cookiesConsumer a function that consumes the cookies
* @return this builder
*/
Builder<T> cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer);
/**
* 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)
*/
Builder<T> 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)
*/
Builder<T> allow(Set<HttpMethod> allowedMethods);
/**
* Set the entity tag of the body, as specified by the {@code ETag} header.
* @param etag the new entity tag
* @return this builder
* @see HttpHeaders#setETag(String)
*/
Builder<T> eTag(String etag);
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param lastModified the last modified date
* @return this builder
* @see HttpHeaders#setLastModified(long)
*/
Builder<T> lastModified(ZonedDateTime lastModified);
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param lastModified the last modified date
* @return this builder
* @since 5.1.4
* @see HttpHeaders#setLastModified(long)
*/
Builder<T> lastModified(Instant lastModified);
/**
* Set the location of a resource, as specified by the {@code Location} header.
* @param location the location
* @return this builder
* @see HttpHeaders#setLocation(URI)
*/
Builder<T> location(URI location);
/**
* Set the caching directives for the resource, as specified by the HTTP 1.1
* {@code Cache-Control} header.
* <p>A {@code CacheControl} instance can be built like
* {@code CacheControl.maxAge(3600).cachePublic().noTransform()}.
* @param cacheControl a builder for cache-related HTTP response headers
* @return this builder
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2">RFC-7234 Section 5.2</a>
*/
Builder<T> cacheControl(CacheControl cacheControl);
/**
* Configure one or more request header names (e.g. "Accept-Language") to
* add to the "Vary" response header to inform clients that the response is
* subject to content negotiation and variances based on the value of the
* given request headers. The configured request header names are added only
* if not already present in the response "Vary" header.
* @param requestHeaders request header names
* @return this builder
*/
Builder<T> varyBy(String... requestHeaders);
/**
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* @param contentLength the content length
* @return this builder
* @see HttpHeaders#setContentLength(long)
*/
Builder<T> contentLength(long contentLength);
/**
* Set the {@linkplain MediaType media type} of the body, as specified by the
* {@code Content-Type} header.
* @param contentType the content type
* @return this builder
* @see HttpHeaders#setContentType(MediaType)
*/
Builder<T> contentType(MediaType contentType);
/**
* Build the response.
* @return the built response
*/
EntityResponse<T> build();
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.util.Assert;
/**
* Represents a function that filters a {@linkplain HandlerFunction handler function}.
*
* @author Arjen Poutsma
* @since 5.2
* @param <T> the type of the {@linkplain HandlerFunction handler function} to filter
* @param <R> the type of the response of the function
* @see RouterFunction#filter(HandlerFilterFunction)
*/
@FunctionalInterface
public interface HandlerFilterFunction<T extends ServerResponse, R extends ServerResponse> {
/**
* Apply this filter to the given handler function. The given
* {@linkplain HandlerFunction handler function} represents the next entity in the chain,
* and can be {@linkplain HandlerFunction#handle(ServerRequest) invoked} in order to
* proceed to this entity, or not invoked to block the chain.
* @param request the request
* @param next the next handler or filter function in the chain
* @return the filtered response
*/
R filter(ServerRequest request, HandlerFunction<T> next) throws Exception;
/**
* Return a composed filter function that first applies this filter, and then applies the
* {@code after} filter.
* @param after the filter to apply after this filter is applied
* @return a composed filter that first applies this function and then applies the
* {@code after} function
*/
default HandlerFilterFunction<T, R> andThen(HandlerFilterFunction<T, T> after) {
Assert.notNull(after, "HandlerFilterFunction must not be null");
return (request, next) -> {
HandlerFunction<T> nextHandler = handlerRequest -> after.filter(handlerRequest, next);
return filter(request, nextHandler);
};
}
/**
* Apply this filter to the given handler function, resulting in a filtered handler function.
* @param handler the handler function to filter
* @return the filtered handler function
*/
default HandlerFunction<R> apply(HandlerFunction<T> handler) {
Assert.notNull(handler, "HandlerFunction must not be null");
return request -> this.filter(request, handler);
}
/**
* Adapt the given request processor function to a filter function that only operates
* on the {@code ServerRequest}.
* @param requestProcessor the request processor
* @return the filter adaptation of the request processor
*/
static <T extends ServerResponse> HandlerFilterFunction<T, T>
ofRequestProcessor(Function<ServerRequest, ServerRequest> requestProcessor) {
Assert.notNull(requestProcessor, "Function 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 ServerResponse}.
* @param responseProcessor the response processor
* @return the filter adaptation of the request processor
*/
static <T extends ServerResponse, R extends ServerResponse> HandlerFilterFunction<T, R>
ofResponseProcessor(BiFunction<ServerRequest, T, R> responseProcessor) {
Assert.notNull(responseProcessor, "Function must not be null");
return (request, next) -> responseProcessor.apply(request, next.handle(request));
}
/**
* Adapt the given predicate and response provider function to a filter function that returns
* a {@code ServerResponse} on a given exception.
* @param predicate the predicate to match an exception
* @param errorHandler the response provider
* @return the filter adaption of the error handler
*/
static <T extends ServerResponse> HandlerFilterFunction<T, T>
ofErrorHandler(Predicate<Throwable> predicate, BiFunction<Throwable, ServerRequest, T> errorHandler) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(errorHandler, "ErrorHandler must not be null");
return (request, next) -> {
try {
T t = next.handle(request);
if (t instanceof DefaultServerResponseBuilder.AbstractServerResponse) {
((DefaultServerResponseBuilder.AbstractServerResponse) t)
.addErrorHandler(predicate, errorHandler);
}
return t;
}
catch (Throwable throwable) {
if (predicate.test(throwable)) {
return errorHandler.apply(throwable, request);
}
else {
throw throwable;
}
}
};
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2019 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.servlet.function;
/**
* Represents a function that handles a {@linkplain ServerRequest request}.
*
* @author Arjen Poutsma
* @since 5.2
* @param <T> the type of the response of the function
* @see RouterFunction
*/
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
/**
* Handle the given request.
* @param request the request to handle
* @return the response
*/
T handle(ServerRequest request) throws Exception;
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2002-2019 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.servlet.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.http.server.PathContainer;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
*
* @author Arjen Poutsma
* @since 5.2
*/
class PathResourceLookupFunction implements Function<ServerRequest, Optional<Resource>> {
private static final PathPatternParser PATTERN_PARSER = new PathPatternParser();
private final PathPattern pattern;
private final Resource location;
public PathResourceLookupFunction(String pattern, Resource location) {
Assert.hasLength(pattern, "'pattern' must not be empty");
Assert.notNull(location, "'location' must not be null");
this.pattern = PATTERN_PARSER.parse(pattern);
this.location = location;
}
@Override
public Optional<Resource> apply(ServerRequest request) {
PathContainer pathContainer = request.pathContainer();
if (!this.pattern.matches(pathContainer)) {
return Optional.empty();
}
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
String path = processPath(pathContainer.value());
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
return Optional.empty();
}
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 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 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("..") && StringUtils.cleanPath(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("%") && StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../")) {
return false;
}
return true;
}
@Override
public String toString() {
return this.pattern + " -> " + this.location;
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.Collection;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.http.Cookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
/**
* Rendering-specific subtype of {@link ServerResponse} that exposes model and template data.
*
* @author Arjen Poutsma
* @since 5.2
*/
public interface RenderingResponse extends ServerResponse {
/**
* Return the name of the template to be rendered.
*/
String name();
/**
* Return the unmodifiable model map.
*/
Map<String, Object> model();
// Builder
/**
* Create a builder with the template name, status code, headers and model of the given response.
* @param other the response to copy the values from
* @return the created builder
*/
static Builder from(RenderingResponse other) {
return new DefaultRenderingResponseBuilder(other);
}
/**
* Create a builder with the given template name.
* @param name the name of the template to render
* @return the created builder
*/
static Builder create(String name) {
return new DefaultRenderingResponseBuilder(name);
}
/**
* Defines a builder for {@code RenderingResponse}.
*/
interface Builder {
/**
* Add the supplied attribute to the model using a
* {@linkplain org.springframework.core.Conventions#getVariableName generated name}.
* <p><em>Note: Empty {@link Collection Collections} are not added to
* the model when using this method because we cannot correctly determine
* the true convention name. View code should check for {@code null} rather
* than for empty collections.</em>
* @param attribute the model attribute value (never {@code null})
*/
Builder modelAttribute(Object attribute);
/**
* Add the supplied attribute value under the supplied name.
* @param name the name of the model attribute (never {@code null})
* @param value the model attribute value (can be {@code null})
*/
Builder modelAttribute(String name, @Nullable Object value);
/**
* Copy all attributes in the supplied array into the model,
* using attribute name generation for each element.
* @see #modelAttribute(Object)
*/
Builder modelAttributes(Object... attributes);
/**
* Copy all attributes in the supplied {@code Collection} into the model,
* using attribute name generation for each element.
* @see #modelAttribute(Object)
*/
Builder modelAttributes(Collection<?> attributes);
/**
* Copy all attributes in the supplied {@code Map} into the model.
* @see #modelAttribute(String, Object)
*/
Builder modelAttributes(Map<String, ?> attributes);
/**
* Add the given header value(s) under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return this builder
* @see HttpHeaders#add(String, String)
*/
Builder header(String headerName, String... headerValues);
/**
* Copy the given headers into the entity's headers map.
* @param headers the existing HttpHeaders to copy from
* @return this builder
* @see HttpHeaders#add(String, String)
*/
Builder headers(HttpHeaders headers);
/**
* Set the HTTP status.
* @param status the response status
* @return this builder
*/
Builder status(HttpStatus status);
/**
* Set the HTTP status.
* @param status the response status
* @return this builder
*/
Builder status(int status);
/**
* Add the given cookie to the response.
* @param cookie the cookie to add
* @return this builder
*/
Builder cookie(Cookie cookie);
/**
* Manipulate this response's cookies with the given consumer. The
* cookies provided to the consumer are "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing cookies,
* {@linkplain MultiValueMap#remove(Object) remove} cookies, or use any of the other
* {@link MultiValueMap} methods.
* @param cookiesConsumer a function that consumes the cookies
* @return this builder
*/
Builder cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer);
/**
* Build the response.
* @return the built response
*/
RenderingResponse build();
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.Optional;
/**
* Represents a function that evaluates on a given {@link ServerRequest}.
* Instances of this function that evaluate on common request properties
* can be found in {@link RequestPredicates}.
*
* @author Arjen Poutsma
* @since 5.2
* @see RequestPredicates
* @see RouterFunctions#route(RequestPredicate, HandlerFunction)
* @see RouterFunctions#nest(RequestPredicate, RouterFunction)
*/
@FunctionalInterface
public interface RequestPredicate {
/**
* Evaluate this predicate on the given request.
* @param request the request to match against
* @return {@code true} if the request matches the predicate; {@code false} otherwise
*/
boolean test(ServerRequest request);
/**
* Return a composed request predicate that tests against both this predicate AND
* the {@code other} predicate. When evaluating the composed predicate, if this
* predicate is {@code false}, then the {@code other} predicate is not evaluated.
* @param other a predicate that will be logically-ANDed with this predicate
* @return a predicate composed of this predicate AND the {@code other} predicate
*/
default RequestPredicate and(RequestPredicate other) {
return new RequestPredicates.AndRequestPredicate(this, other);
}
/**
* Return a predicate that represents the logical negation of this predicate.
* @return a predicate that represents the logical negation of this predicate
*/
default RequestPredicate negate() {
return new RequestPredicates.NegateRequestPredicate(this);
}
/**
* Return a composed request predicate that tests against both this predicate OR
* the {@code other} predicate. When evaluating the composed predicate, if this
* predicate is {@code true}, then the {@code other} predicate is not evaluated.
* @param other a predicate that will be logically-ORed with this predicate
* @return a predicate composed of this predicate OR the {@code other} predicate
*/
default RequestPredicate or(RequestPredicate other) {
return new RequestPredicates.OrRequestPredicate(this, other);
}
/**
* Transform the given request into a request used for a nested route. For instance,
* a path-based predicate can return a {@code ServerRequest} with a the path remaining
* after a match.
* <p>The default implementation returns an {@code Optional} wrapping the given request if
* {@link #test(ServerRequest)} evaluates to {@code true}; or {@link Optional#empty()}
* if it evaluates to {@code false}.
* @param request the request to be nested
* @return the nested request
* @see RouterFunctions#nest(RequestPredicate, RouterFunction)
*/
default Optional<ServerRequest> nest(ServerRequest request) {
return (test(request) ? Optional.of(request) : Optional.empty());
}
/**
* Accept the given visitor. Default implementation calls
* {@link RequestPredicates.Visitor#unknown(RequestPredicate)}; composed {@code RequestPredicate}
* implementations are expected to call {@code accept} for all components that make up this
* request predicate.
* @param visitor the visitor to accept
*/
default void accept(RequestPredicates.Visitor visitor) {
visitor.unknown(this);
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2002-2019 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.servlet.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.lang.Nullable;
/**
* Resource-based implementation of {@link HandlerFunction}.
*
* @author Arjen Poutsma
* @since 5.2
*/
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
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 handle(ServerRequest request) {
HttpMethod method = request.method();
if (method != null) {
switch (method) {
case GET:
return EntityResponse.fromObject(this.resource).build();
case HEAD:
Resource headResource = new HeadMethodResource(this.resource);
return EntityResponse.fromObject(headResource).build();
case OPTIONS:
return ServerResponse.ok()
.allow(SUPPORTED_METHODS).build();
}
}
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED)
.allow(SUPPORTED_METHODS).build();
}
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
@Nullable
public String getFilename() {
return this.delegate.getFilename();
}
@Override
public String getDescription() {
return this.delegate.getDescription();
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.Optional;
/**
* Represents a function that routes to a {@linkplain HandlerFunction handler function}.
*
* @author Arjen Poutsma
* @since 5.2
* @param <T> the type of the {@linkplain HandlerFunction handler function} to route to
* @see RouterFunctions
*/
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
/**
* Return the {@linkplain HandlerFunction handler function} that matches the given request.
* @param request the request to route
* @return an {@code Optional} describing the {@code HandlerFunction} that matches this request,
* or an empty {@code Optional} if there is no match
*/
Optional<HandlerFunction<T>> route(ServerRequest request);
/**
* Return a composed routing function that first invokes this function,
* and then invokes the {@code other} function (of the same response type {@code T})
* if this route had {@linkplain Optional#empty() no result}.
* @param other the function of type {@code T} to apply when this function has no result
* @return a composed function that first routes with this function and then the
* {@code other} function if this function has no result
* @see #andOther(RouterFunction)
*/
default RouterFunction<T> and(RouterFunction<T> other) {
return new RouterFunctions.SameComposedRouterFunction<>(this, other);
}
/**
* Return a composed routing function that first invokes this function,
* and then invokes the {@code other} function (of a different response type) if this route had
* {@linkplain Optional#empty() no result}.
* @param other the function to apply when this function has no result
* @return a composed function that first routes with this function and then the
* {@code other} function if this function has no result
* @see #and(RouterFunction)
*/
default RouterFunction<?> andOther(RouterFunction<?> other) {
return new RouterFunctions.DifferentComposedRouterFunction(this, other);
}
/**
* Return a composed routing function that routes to the given handler function if this
* route does not match and the given request predicate applies. This method is a convenient
* combination of {@link #and(RouterFunction)} and
* {@link RouterFunctions#route(RequestPredicate, HandlerFunction)}.
* @param predicate the predicate to test if this route does not match
* @param handlerFunction the handler function to route to if this route does not match and
* the predicate applies
* @return a composed function that route to {@code handlerFunction} if this route does not
* match and if {@code predicate} applies
*/
default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
return and(RouterFunctions.route(predicate, handlerFunction));
}
/**
* Return a composed routing function that routes to the given router function if this
* route does not match and the given request predicate applies. This method is a convenient
* combination of {@link #and(RouterFunction)} and
* {@link RouterFunctions#nest(RequestPredicate, RouterFunction)}.
* @param predicate the predicate to test if this route does not match
* @param routerFunction the router function to route to if this route does not match and
* the predicate applies
* @return a composed function that route to {@code routerFunction} if this route does not
* match and if {@code predicate} applies
*/
default RouterFunction<T> andNest(RequestPredicate predicate, RouterFunction<T> routerFunction) {
return and(RouterFunctions.nest(predicate, routerFunction));
}
/**
* Filter all {@linkplain HandlerFunction handler functions} routed by this function with the given
* {@linkplain HandlerFilterFunction filter function}.
* @param <S> the filter return type
* @param filterFunction the filter to apply
* @return the filtered routing function
*/
default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
return new RouterFunctions.FilteredRouterFunction<>(this, filterFunction);
}
/**
* Accept the given visitor. Default implementation calls
* {@link RouterFunctions.Visitor#unknown(RouterFunction)}; composed {@code RouterFunction}
* implementations are expected to call {@code accept} for all components that make up this
* router function.
* @param visitor the visitor to accept
*/
default void accept(RouterFunctions.Visitor visitor) {
visitor.unknown(this);
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* Default implementation of {@link RouterFunctions.Builder}.
*
* @author Arjen Poutsma
* @since 5.2
*/
class RouterFunctionBuilder implements RouterFunctions.Builder {
private List<RouterFunction<ServerResponse>> routerFunctions = new ArrayList<>();
private List<HandlerFilterFunction<ServerResponse, ServerResponse>> filterFunctions = new ArrayList<>();
@Override
public RouterFunctions.Builder add(RouterFunction<ServerResponse> routerFunction) {
Assert.notNull(routerFunction, "RouterFunction must not be null");
this.routerFunctions.add(routerFunction);
return this;
}
private RouterFunctions.Builder add(RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
this.routerFunctions.add(RouterFunctions.route(predicate, handlerFunction));
return this;
}
@Override
public RouterFunctions.Builder GET(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.GET(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder GET(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.GET(pattern).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder HEAD(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.HEAD(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder HEAD(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.HEAD(pattern).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder POST(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.POST(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder POST(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.POST(pattern).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder PUT(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.PUT(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder PUT(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.PUT(pattern).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder PATCH(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.PATCH(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder PATCH(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.PATCH(pattern).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder DELETE(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.DELETE(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder DELETE(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.DELETE(pattern).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder OPTIONS(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.OPTIONS(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder OPTIONS(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder resources(String pattern, Resource location) {
return add(RouterFunctions.resources(pattern, location));
}
@Override
public RouterFunctions.Builder resources(Function<ServerRequest, Optional<Resource>> lookupFunction) {
return add(RouterFunctions.resources(lookupFunction));
}
@Override
public RouterFunctions.Builder nest(RequestPredicate predicate,
Consumer<RouterFunctions.Builder> builderConsumer) {
Assert.notNull(builderConsumer, "Consumer must not be null");
RouterFunctionBuilder nestedBuilder = new RouterFunctionBuilder();
builderConsumer.accept(nestedBuilder);
RouterFunction<ServerResponse> nestedRoute = nestedBuilder.build();
this.routerFunctions.add(RouterFunctions.nest(predicate, nestedRoute));
return this;
}
@Override
public RouterFunctions.Builder nest(RequestPredicate predicate,
Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier) {
Assert.notNull(routerFunctionSupplier, "RouterFunction Supplier must not be null");
RouterFunction<ServerResponse> nestedRoute = routerFunctionSupplier.get();
this.routerFunctions.add(RouterFunctions.nest(predicate, nestedRoute));
return this;
}
@Override
public RouterFunctions.Builder path(String pattern,
Consumer<RouterFunctions.Builder> builderConsumer) {
return nest(RequestPredicates.path(pattern), builderConsumer);
}
@Override
public RouterFunctions.Builder path(String pattern,
Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier) {
return nest(RequestPredicates.path(pattern), routerFunctionSupplier);
}
@Override
public RouterFunctions.Builder filter(HandlerFilterFunction<ServerResponse, ServerResponse> filterFunction) {
Assert.notNull(filterFunction, "HandlerFilterFunction must not be null");
this.filterFunctions.add(filterFunction);
return this;
}
@Override
public RouterFunctions.Builder before(Function<ServerRequest, ServerRequest> requestProcessor) {
Assert.notNull(requestProcessor, "RequestProcessor must not be null");
return filter(HandlerFilterFunction.ofRequestProcessor(requestProcessor));
}
@Override
public RouterFunctions.Builder after(
BiFunction<ServerRequest, ServerResponse, ServerResponse> responseProcessor) {
Assert.notNull(responseProcessor, "ResponseProcessor must not be null");
return filter(HandlerFilterFunction.ofResponseProcessor(responseProcessor));
}
@Override
public RouterFunctions.Builder onError(Predicate<Throwable> predicate,
BiFunction<Throwable, ServerRequest, ServerResponse> responseProvider) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(responseProvider, "ResponseProvider must not be null");
return filter(HandlerFilterFunction.ofErrorHandler(predicate, responseProvider));
}
@Override
public RouterFunctions.Builder onError(Class<? extends Throwable> exceptionType,
BiFunction<Throwable, ServerRequest, ServerResponse> responseProvider) {
Assert.notNull(exceptionType, "ExceptionType must not be null");
Assert.notNull(responseProvider, "ResponseProvider must not be null");
return filter(HandlerFilterFunction.ofErrorHandler(exceptionType::isInstance,
responseProvider));
}
@Override
public RouterFunction<ServerResponse> build() {
RouterFunction<ServerResponse> result = this.routerFunctions.stream()
.reduce(RouterFunction::and)
.orElseThrow(IllegalStateException::new);
if (this.filterFunctions.isEmpty()) {
return result;
}
else {
HandlerFilterFunction<ServerResponse, ServerResponse> filter =
this.filterFunctions.stream()
.reduce(HandlerFilterFunction::andThen)
.orElseThrow(IllegalStateException::new);
return result.filter(filter);
}
}
}

View File

@@ -0,0 +1,875 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* <strong>Central entry point to Spring's functional web framework.</strong>
* Exposes routing functionality, such as to {@linkplain #route() create} a
* {@code RouterFunction} using a discoverable builder-style API, to
* {@linkplain #route(RequestPredicate, HandlerFunction) create} a {@code RouterFunction}
* given a {@code RequestPredicate} and {@code HandlerFunction}, and to do further
* {@linkplain #nest(RequestPredicate, RouterFunction) subrouting} on an existing routing
* function.
*
* @author Arjen Poutsma
* @since 5.2
*/
public abstract class RouterFunctions {
private static final Log logger = LogFactory.getLog(RouterFunctions.class);
/**
* Name of the request attribute that contains the {@link ServerRequest}.
*/
public static final String REQUEST_ATTRIBUTE = RouterFunctions.class.getName() + ".request";
/**
* Name of the request attribute that contains the URI
* templates map, mapping variable names to values.
*/
public static final String URI_TEMPLATE_VARIABLES_ATTRIBUTE =
RouterFunctions.class.getName() + ".uriTemplateVariables";
/**
* Name of the request attribute that contains the matching pattern, as a
* {@link org.springframework.web.util.pattern.PathPattern}.
*/
public static final String MATCHING_PATTERN_ATTRIBUTE =
RouterFunctions.class.getName() + ".matchingPattern";
/**
* Offers a discoverable way to create router functions through a builder-style interface.
* @return a router function builder
*/
public static Builder route() {
return new RouterFunctionBuilder();
}
/**
* Route to the given handler function if the given request predicate applies.
* <p>For instance, the following example routes GET requests for "/user" to the
* {@code listUsers} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.route(RequestPredicates.GET("/user"), userController::listUsers);
* </pre>
* @param predicate the predicate to test
* @param handlerFunction the handler function to route to if the predicate applies
* @param <T> the type of response returned by the handler function
* @return a router function that routes to {@code handlerFunction} if
* {@code predicate} evaluates to {@code true}
* @see RequestPredicates
*/
public static <T extends ServerResponse> RouterFunction<T> route(
RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
return new DefaultRouterFunction<>(predicate, handlerFunction);
}
/**
* Route to the given router function if the given request predicate applies. This method can be
* used to create <strong>nested routes</strong>, where a group of routes share a common path
* (prefix), header, or other request predicate.
* <p>For instance, the following example first creates a composed route that resolves to
* {@code listUsers} for a GET, and {@code createUser} for a POST. This composed route then gets
* nested with a "/user" path predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; userRoutes =
* RouterFunctions.route(RequestPredicates.method(HttpMethod.GET), this::listUsers)
* .andRoute(RequestPredicates.method(HttpMethod.POST), this::createUser);
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.nest(RequestPredicates.path("/user"), userRoutes);
* </pre>
* @param predicate the predicate to test
* @param routerFunction the nested router function to delegate to if the predicate applies
* @param <T> the type of response returned by the handler function
* @return a router function that routes to {@code routerFunction} if
* {@code predicate} evaluates to {@code true}
* @see RequestPredicates
*/
public static <T extends ServerResponse> RouterFunction<T> nest(
RequestPredicate predicate, RouterFunction<T> routerFunction) {
return new DefaultNestedRouterFunction<>(predicate, routerFunction);
}
/**
* 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/");
* RouterFunction&lt;ServerResponse&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
* @see #resourceLookupFunction(String, Resource)
*/
public static RouterFunction<ServerResponse> resources(String pattern, Resource location) {
return resources(resourceLookupFunction(pattern, location));
}
/**
* Returns the resource lookup function used by {@link #resources(String, Resource)}.
* The returned function can be {@linkplain Function#andThen(Function) composed} on, for
* instance to return a default resource when the lookup function does not match:
* <pre class="code">
* Mono&lt;Resource&gt; defaultResource = Mono.just(new ClassPathResource("index.html"));
* Function&lt;ServerRequest, Mono&lt;Resource&gt;&gt; lookupFunction =
* RouterFunctions.resourceLookupFunction("/resources/**", new FileSystemResource("public-resources/"))
* .andThen(resourceMono -&gt; resourceMono.switchIfEmpty(defaultResource));
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resources(lookupFunction);
* </pre>
* @param pattern the pattern to match
* @param location the location directory relative to which resources should be resolved
* @return the default resource lookup function for the given parameters.
*/
public static Function<ServerRequest, Optional<Resource>> resourceLookupFunction(String pattern, Resource location) {
return 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<ServerResponse> resources(Function<ServerRequest, Optional<Resource>> lookupFunction) {
return new ResourcesRouterFunction(lookupFunction);
}
@SuppressWarnings("unchecked")
static <T extends ServerResponse> HandlerFunction<T> cast(HandlerFunction<?> handlerFunction) {
return (HandlerFunction<T>) handlerFunction;
}
/**
* Represents a discoverable builder for router functions.
* Obtained via {@link RouterFunctions#route()}.
*/
public interface Builder {
/**
* Adds a route to the given handler function that handles all HTTP {@code GET} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code GET} requests that
* match {@code pattern}
* @return this builder
*/
Builder GET(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code GET} requests
* that match the given pattern and predicate.
* <p>For instance, the following example routes GET requests for "/user" that accept JSON
* to the {@code listUsers} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.route()
* .GET("/user", RequestPredicates.accept(MediaType.APPLICATION_JSON), userController::listUsers)
* .build();
* </pre>
* @param pattern the pattern to match to
* @param predicate additional predicate to match
* @param handlerFunction the handler function to handle all {@code GET} requests that
* match {@code pattern}
* @return this builder
* @see RequestPredicates
*/
Builder GET(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code HEAD} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code HEAD} requests that
* match {@code pattern}
* @return this builder
*/
Builder HEAD(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code HEAD} requests
* that match the given pattern and predicate.
* @param pattern the pattern to match to
* @param predicate additional predicate to match
* @param handlerFunction the handler function to handle all {@code HEAD} requests that
* match {@code pattern}
* @return this builder
*/
Builder HEAD(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code POST} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code POST} requests that
* match {@code pattern}
* @return this builder
*/
Builder POST(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code POST} requests
* that match the given pattern and predicate.
* <p>For instance, the following example routes POST requests for "/user" that contain JSON
* to the {@code addUser} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.route()
* .POST("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::addUser)
* .build();
* </pre>
* @param pattern the pattern to match to
* @param predicate additional predicate to match
* @param handlerFunction the handler function to handle all {@code POST} requests that
* match {@code pattern}
* @return this builder
*/
Builder POST(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PUT} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code PUT} requests that
* match {@code pattern}
* @return this builder
*/
Builder PUT(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PUT} requests
* that match the given pattern and predicate.
* <p>For instance, the following example routes PUT requests for "/user" that contain JSON
* to the {@code editUser} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.route()
* .PUT("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::editUser)
* .build();
* </pre>
* @param pattern the pattern to match to
* @param predicate additional predicate to match
* @param handlerFunction the handler function to handle all {@code PUT} requests that
* match {@code pattern}
* @return this builder
*/
Builder PUT(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PATCH} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code PATCH} requests that
* match {@code pattern}
* @return this builder
*/
Builder PATCH(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PATCH} requests
* that match the given pattern and predicate.
* <p>For instance, the following example routes PATCH requests for "/user" that contain JSON
* to the {@code editUser} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.route()
* .PATCH("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::editUser)
* .build();
* </pre>
* @param pattern the pattern to match to
* @param predicate additional predicate to match
* @param handlerFunction the handler function to handle all {@code PATCH} requests that
* match {@code pattern}
* @return this builder
*/
Builder PATCH(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code DELETE} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code DELETE} requests that
* match {@code pattern}
* @return this builder
*/
Builder DELETE(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code DELETE} requests
* that match the given pattern and predicate.
* @param pattern the pattern to match to
* @param predicate additional predicate to match
* @param handlerFunction the handler function to handle all {@code DELETE} requests that
* match {@code pattern}
* @return this builder
*/
Builder DELETE(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code OPTIONS} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code OPTIONS} requests that
* match {@code pattern}
* @return this builder
*/
Builder OPTIONS(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code OPTIONS} requests
* that match the given pattern and predicate.
* @param pattern the pattern to match to
* @param predicate additional predicate to match
* @param handlerFunction the handler function to handle all {@code OPTIONS} requests that
* match {@code pattern}
* @return this builder
*/
Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds the given route to this builder. Can be used to merge externally defined router
* functions into this builder, or can be combined with
* {@link RouterFunctions#route(RequestPredicate, HandlerFunction)}
* to allow for more flexible predicate matching.
* <p>For instance, the following example adds the router function returned from
* {@code OrderController.routerFunction()}.
* to the {@code changeUser} method in {@code userController}:
* <pre class="code">
* RouterFunctionlt;ServerResponsegt; route =
* RouterFunctions.route()
* .GET("/users", userController::listUsers)
* .add(orderController.routerFunction());
* .build();
* </pre>
* @param routerFunction the router function to be added
* @return this builder
* @see RequestPredicates
*/
Builder add(RouterFunction<ServerResponse> routerFunction);
/**
* 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/");
* RouterFunction&lt;ServerResponse&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 this builder
*/
Builder resources(String pattern, Resource 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 this builder
*/
Builder resources(Function<ServerRequest, Optional<Resource>> lookupFunction);
/**
* Route to the supplied router function if the given request predicate applies. This method
* can be used to create <strong>nested routes</strong>, where a group of routes share a
* common path (prefix), header, or other request predicate.
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.route()
* .nest(RequestPredicates.path("/user"), () ->
* RouterFunctions.route()
* .GET(this::listUsers)
* .POST(this::createUser)
* .build())
* .build();
* </pre>
* @param predicate the predicate to test
* @param routerFunctionSupplier supplier for the nested router function to delegate to if
* the predicate applies
* @return this builder
* @see RequestPredicates
*/
Builder nest(RequestPredicate predicate, Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier);
/**
* Route to a built router function if the given request predicate applies.
* This method can be used to create <strong>nested routes</strong>, where a group of routes
* share a common path (prefix), header, or other request predicate.
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.route()
* .nest(RequestPredicates.path("/user"), builder ->
* builder.GET(this::listUsers)
* .POST(this::createUser))
* .build();
* </pre>
* @param predicate the predicate to test
* @param builderConsumer consumer for a {@code Builder} that provides the nested router
* function
* @return this builder
* @see RequestPredicates
*/
Builder nest(RequestPredicate predicate, Consumer<Builder> builderConsumer);
/**
* Route to the supplied router function if the given path prefix pattern applies. This method
* can be used to create <strong>nested routes</strong>, where a group of routes share a
* common path prefix. Specifically, this method can be used to merge externally defined
* router functions under a path prefix.
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate that delegates to the router function defined in {@code userController},
* and with a "/order" path that delegates to {@code orderController}.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.route()
* .path("/user", userController::routerFunction)
* .path("/order", orderController::routerFunction)
* .build();
* </pre>
* @param pattern the pattern to match to
* @param routerFunctionSupplier supplier for the nested router function to delegate to if
* the pattern matches
* @return this builder
*/
Builder path(String pattern, Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier);
/**
* Route to a built router function if the given path prefix pattern applies.
* This method can be used to create <strong>nested routes</strong>, where a group of routes
* share a common path prefix.
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.route()
* .path("/user", builder ->
* builder.GET(this::listUsers)
* .POST(this::createUser))
* .build();
* </pre>
* @param pattern the pattern to match to
* @param builderConsumer consumer for a {@code Builder} that provides the nested router
* function
* @return this builder
*/
Builder path(String pattern, Consumer<Builder> builderConsumer);
/**
* Filters all routes created by this builder with the given filter function. Filter
* functions are typically used to address cross-cutting concerns, such as logging,
* security, etc.
* <p>For instance, the following example creates a filter that returns a 401 Unauthorized
* response if the request does not contain the necessary authentication headers.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.route()
* .GET("/user", this::listUsers)
* .filter((request, next) -> {
* // check for authentication headers
* if (isAuthenticated(request)) {
* return next.handle(request);
* }
* else {
* return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
* }
* })
* .build();
* </pre>
* @param filterFunction the function to filter all routes built by this builder
* @return this builder
*/
Builder filter(HandlerFilterFunction<ServerResponse, ServerResponse> filterFunction);
/**
* Filter the request object for all routes created by this builder with the given request
* processing function. Filters are typically used to address cross-cutting concerns, such
* as logging, security, etc.
* <p>For instance, the following example creates a filter that logs the request before
* the handler function executes.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.route()
* .GET("/user", this::listUsers)
* .before(request -> {
* log(request);
* return request;
* })
* .build();
* </pre>
* @param requestProcessor a function that transforms the request
* @return this builder
*/
Builder before(Function<ServerRequest, ServerRequest> requestProcessor);
/**
* Filter the response object for all routes created by this builder with the given response
* processing function. Filters are typically used to address cross-cutting concerns, such
* as logging, security, etc.
* <p>For instance, the following example creates a filter that logs the response after
* the handler function executes.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.route()
* .GET("/user", this::listUsers)
* .after((request, response) -> {
* log(response);
* return response;
* })
* .build();
* </pre>
* @param responseProcessor a function that transforms the response
* @return this builder
*/
Builder after(BiFunction<ServerRequest, ServerResponse, ServerResponse> responseProcessor);
/**
* Filters all exceptions that match the predicate by applying the given response provider
* function.
* <p>For instance, the following example creates a filter that returns a 500 response
* status when an {@code IllegalStateException} occurs.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.route()
* .GET("/user", this::listUsers)
* .onError(e -> e instanceof IllegalStateException,
* (e, request) -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build())
* .build();
* </pre>
* @param predicate the type of exception to filter
* @param responseProvider a function that creates a response
* @return this builder
*/
Builder onError(Predicate<Throwable> predicate,
BiFunction<Throwable, ServerRequest, ServerResponse> responseProvider);
/**
* Filters all exceptions of the given type by applying the given response provider
* function.
* <p>For instance, the following example creates a filter that returns a 500 response
* status when an {@code IllegalStateException} occurs.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.route()
* .GET("/user", this::listUsers)
* .onError(IllegalStateException.class,
* (e, request) -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build())
* .build();
* </pre>
* @param exceptionType the type of exception to filter
* @param responseProvider a function that creates a response
* @return this builder
*/
Builder onError(Class<? extends Throwable> exceptionType,
BiFunction<Throwable, ServerRequest, ServerResponse> responseProvider);
/**
* Builds the {@code RouterFunction}. All created routes are
* {@linkplain RouterFunction#and(RouterFunction) composed} with one another, and filters
* (if any) are applied to the result.
* @return the built router function
*/
RouterFunction<ServerResponse> build();
}
/**
* Receives notifications from the logical structure of router functions.
*/
public interface Visitor {
/**
* Receive notification of the beginning of a nested router function.
* @param predicate the predicate that applies to the nested router functions
* @see RouterFunctions#nest(RequestPredicate, RouterFunction)
*/
void startNested(RequestPredicate predicate);
/**
* Receive notification of the end of a nested router function.
* @param predicate the predicate that applies to the nested router functions
* @see RouterFunctions#nest(RequestPredicate, RouterFunction)
*/
void endNested(RequestPredicate predicate);
/**
* Receive notification of a standard predicated route to a handler function.
* @param predicate the predicate that applies to the handler function
* @param handlerFunction the handler function.
* @see RouterFunctions#route(RequestPredicate, HandlerFunction)
*/
void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction);
/**
* Receive notification of a resource router function.
* @param lookupFunction the lookup function for the resources
* @see RouterFunctions#resources(Function)
*/
void resources(Function<ServerRequest, Optional<Resource>> lookupFunction);
/**
* Receive notification of an unknown router function. This method is called for router
* functions that were not created via the various {@link RouterFunctions} methods.
* @param routerFunction the router function
*/
void unknown(RouterFunction<?> routerFunction);
}
private abstract static class AbstractRouterFunction<T extends ServerResponse> implements RouterFunction<T> {
@Override
public String toString() {
ToStringVisitor visitor = new ToStringVisitor();
accept(visitor);
return visitor.toString();
}
}
/**
* A composed routing function that first invokes one function, and then invokes the
* another function (of the same response type {@code T}) if this route had
* {@linkplain Optional#empty() no result}.
* @param <T> the server response type
*/
static final class SameComposedRouterFunction<T extends ServerResponse> extends AbstractRouterFunction<T> {
private final RouterFunction<T> first;
private final RouterFunction<T> second;
public SameComposedRouterFunction(RouterFunction<T> first, RouterFunction<T> second) {
this.first = first;
this.second = second;
}
@Override
public Optional<HandlerFunction<T>> route(ServerRequest request) {
Optional<HandlerFunction<T>> firstRoute = this.first.route(request);
if (firstRoute.isPresent()) {
return firstRoute;
}
else {
return this.second.route(request);
}
}
@Override
public void accept(Visitor visitor) {
this.first.accept(visitor);
this.second.accept(visitor);
}
}
/**
* A composed routing function that first invokes one function, and then invokes
* another function (of a different response type) if this route had
* {@linkplain Optional#empty() no result}.
*/
static final class DifferentComposedRouterFunction extends AbstractRouterFunction<ServerResponse> {
private final RouterFunction<?> first;
private final RouterFunction<?> second;
public DifferentComposedRouterFunction(RouterFunction<?> first, RouterFunction<?> second) {
this.first = first;
this.second = second;
}
@Override
@SuppressWarnings("unchecked")
public Optional<HandlerFunction<ServerResponse>> route(ServerRequest request) {
Optional<? extends HandlerFunction<?>> firstRoute = this.first.route(request);
if (firstRoute.isPresent()) {
return (Optional<HandlerFunction<ServerResponse>>) firstRoute;
}
else {
Optional<? extends HandlerFunction<?>> secondRoute = this.second.route(request);
return (Optional<HandlerFunction<ServerResponse>>) secondRoute;
}
}
@Override
public void accept(Visitor visitor) {
this.first.accept(visitor);
this.second.accept(visitor);
}
}
/**
* Filter the specified {@linkplain HandlerFunction handler functions} with the given
* {@linkplain HandlerFilterFunction filter function}.
* @param <T> the type of the {@linkplain HandlerFunction handler function} to filter
* @param <S> the type of the response of the function
*/
static final class FilteredRouterFunction<T extends ServerResponse, S extends ServerResponse>
implements RouterFunction<S> {
private final RouterFunction<T> routerFunction;
private final HandlerFilterFunction<T, S> filterFunction;
public FilteredRouterFunction(
RouterFunction<T> routerFunction,
HandlerFilterFunction<T, S> filterFunction) {
this.routerFunction = routerFunction;
this.filterFunction = filterFunction;
}
@Override
public Optional<HandlerFunction<S>> route(ServerRequest request) {
return this.routerFunction.route(request).map(this.filterFunction::apply);
}
@Override
public void accept(Visitor visitor) {
this.routerFunction.accept(visitor);
}
@Override
public String toString() {
return this.routerFunction.toString();
}
}
private static final class DefaultRouterFunction<T extends ServerResponse>
extends AbstractRouterFunction<T> {
private final RequestPredicate predicate;
private final HandlerFunction<T> handlerFunction;
public DefaultRouterFunction(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(handlerFunction, "HandlerFunction must not be null");
this.predicate = predicate;
this.handlerFunction = handlerFunction;
}
@Override
public Optional<HandlerFunction<T>> route(ServerRequest request) {
if (this.predicate.test(request)) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Predicate \"%s\" matches against \"%s\"", this.predicate, request));
}
return Optional.of(this.handlerFunction);
}
else {
return Optional.empty();
}
}
@Override
public void accept(Visitor visitor) {
visitor.route(this.predicate, this.handlerFunction);
}
}
private static final class DefaultNestedRouterFunction<T extends ServerResponse>
extends AbstractRouterFunction<T> {
private final RequestPredicate predicate;
private final RouterFunction<T> routerFunction;
public DefaultNestedRouterFunction(RequestPredicate predicate, RouterFunction<T> routerFunction) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(routerFunction, "RouterFunction must not be null");
this.predicate = predicate;
this.routerFunction = routerFunction;
}
@Override
public Optional<HandlerFunction<T>> route(ServerRequest serverRequest) {
return this.predicate.nest(serverRequest)
.map(nestedRequest -> {
if (logger.isTraceEnabled()) {
logger.trace(
String.format(
"Nested predicate \"%s\" matches against \"%s\"",
this.predicate, serverRequest));
}
Optional<HandlerFunction<T>> result = this.routerFunction.route(nestedRequest);
if (result.isPresent() && nestedRequest != serverRequest) {
serverRequest.attributes().clear();
serverRequest.attributes().putAll(nestedRequest.attributes());
}
return result;
}
)
.orElseGet(Optional::empty);
}
@Override
public void accept(Visitor visitor) {
visitor.startNested(this.predicate);
this.routerFunction.accept(visitor);
visitor.endNested(this.predicate);
}
}
private static class ResourcesRouterFunction extends AbstractRouterFunction<ServerResponse> {
private final Function<ServerRequest, Optional<Resource>> lookupFunction;
public ResourcesRouterFunction(Function<ServerRequest, Optional<Resource>> lookupFunction) {
Assert.notNull(lookupFunction, "Function must not be null");
this.lookupFunction = lookupFunction;
}
@Override
public Optional<HandlerFunction<ServerResponse>> route(ServerRequest request) {
return this.lookupFunction.apply(request).map(ResourceHandlerFunction::new);
}
@Override
public void accept(Visitor visitor) {
visitor.resources(this.lookupFunction);
}
}
}

View File

@@ -0,0 +1,416 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.function.Consumer;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.PathContainer;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriBuilder;
/**
* Represents a server-side HTTP request, as handled by a {@code HandlerFunction}.
* Access to headers and body is offered by {@link Headers} and
* {@link #body(Class)}, respectively.
*
* @author Arjen Poutsma
* @since 5.2
*/
public interface ServerRequest {
/**
* Get the HTTP method.
* @return the HTTP method as an HttpMethod enum value, or {@code null}
* if not resolvable (e.g. in case of a non-standard HTTP method)
*/
@Nullable
default HttpMethod method() {
return HttpMethod.resolve(methodName());
}
/**
* Get the name of the HTTP method.
* @return the HTTP method as a String
*/
String methodName();
/**
* Get the request URI.
*/
URI uri();
/**
* Get a {@code UriBuilderComponents} from the URI associated with this
* {@code ServerRequest}.
*
* @return a URI builder
*/
UriBuilder uriBuilder();
/**
* Get the request path.
*/
default String path() {
return uri().getRawPath();
}
/**
* Get the request path as a {@code PathContainer}.
*/
default PathContainer pathContainer() {
return PathContainer.parsePath(path());
}
/**
* Get the headers of this request.
*/
Headers headers();
/**
* Get the cookies of this request.
*/
MultiValueMap<String, Cookie> cookies();
/**
* Get the remote address to which this request is connected, if available.
*/
Optional<InetSocketAddress> remoteAddress();
/**
* Get the readers used to convert the body of this request.
*/
List<HttpMessageConverter<?>> messageConverters();
/**
* Extract the body as an object of the given type.
* @param bodyType the type of return value
* @param <T> the body type
* @return the body
*/
<T> T body(Class<T> bodyType) throws ServletException, IOException;
/**
* Extract the body as an object of the given type.
* @param bodyType the type of return value
* @param <T> the body type
* @return the body
*/
<T> T body(ParameterizedTypeReference<T> bodyType) throws ServletException, IOException;
/**
* Get the request attribute value if present.
* @param name the attribute name
* @return the attribute value
*/
default Optional<Object> attribute(String name) {
Map<String, Object> attributes = attributes();
if (attributes.containsKey(name)) {
return Optional.of(attributes.get(name));
}
else {
return Optional.empty();
}
}
/**
* Get a mutable map of request attributes.
* @return the request attributes
*/
Map<String, Object> attributes();
/**
* Get the first query parameter with the given name, if present.
* @param name the parameter name
* @return the parameter value
*/
default Optional<String> param(String name) {
List<String> paramValues = params().get(name);
if (CollectionUtils.isEmpty(paramValues)) {
return Optional.empty();
}
else {
String value = paramValues.get(0);
if (value == null) {
value = "";
}
return Optional.of(value);
}
}
/**
* Get all query parameters for this request.
*/
MultiValueMap<String, String> params();
/**
* Get the path variable with the given name, if present.
* @param name the variable name
* @return the variable value
* @throws IllegalArgumentException if there is no path variable with the given name
*/
default String pathVariable(String name) {
Map<String, String> pathVariables = pathVariables();
if (pathVariables.containsKey(name)) {
return pathVariables().get(name);
}
else {
throw new IllegalArgumentException("No path variable with name \"" + name + "\" available");
}
}
/**
* Get all path variables for this request.
*/
Map<String, String> pathVariables();
/**
* Get the web session for this request. Always guaranteed to
* return an instance either matching to the session id requested by the
* client, or with a new session id either because the client did not
* specify one or because the underlying session had expired. Use of this
* method does not automatically create a session.
*/
HttpSession session();
/**
* Get the authenticated user for the request, if any.
*/
Optional<Principal> principal();
/**
* Get the servlet request that this request is based on.
*/
HttpServletRequest servletRequest();
// Static methods
/**
* Create a new {@code ServerRequest} based on the given {@code {@link HttpServletRequest} and
* message converters.
* @param servletRequest the request
* @param messageReaders the message readers
* @return the created {@code ServerRequest}
*/
static ServerRequest create(HttpServletRequest servletRequest, List<HttpMessageConverter<?>> messageReaders) {
return new DefaultServerRequest(servletRequest, messageReaders);
}
/**
* Create a builder with the status, headers, and cookies of the given request.
* @param other the response to copy the status, headers, and cookies from
* @return the created builder
*/
static Builder from(ServerRequest other) {
return new DefaultServerRequestBuilder(other);
}
/**
* Represents the headers of the HTTP request.
* @see ServerRequest#headers()
*/
interface Headers {
/**
* Get the list of acceptable media types, as specified by the {@code Accept}
* header.
* <p>Returns an empty list if the acceptable media types are unspecified.
*/
List<MediaType> accept();
/**
* Get the list of acceptable charsets, as specified by the
* {@code Accept-Charset} header.
*/
List<Charset> acceptCharset();
/**
* Get the list of acceptable languages, as specified by the
* {@code Accept-Language} header.
*/
List<Locale.LanguageRange> acceptLanguage();
/**
* Get the length of the body in bytes, as specified by the
* {@code Content-Length} header.
*/
OptionalLong contentLength();
/**
* Get the media type of the body, as specified by the
* {@code Content-Type} header.
*/
Optional<MediaType> contentType();
/**
* Get the value of the {@code Host} header, if available.
* <p>If the header value does not contain a port, the
* {@linkplain InetSocketAddress#getPort() port} in the returned address will
* be {@code 0}.
*/
@Nullable
InetSocketAddress host();
/**
* Get the value of the {@code Range} header.
* <p>Returns an empty list when the range is unknown.
*/
List<HttpRange> range();
/**
* Get the header value(s), if any, for the header of the given name.
* <p>Returns an empty list if no header values are found.
* @param headerName the header name
*/
List<String> header(String headerName);
/**
* Get the headers as an instance of {@link HttpHeaders}.
*/
HttpHeaders asHttpHeaders();
}
/**
* Defines a builder for a request.
*/
interface Builder {
/**
* Set the method of the request.
* @param method the new method
* @return this builder
*/
Builder method(HttpMethod method);
/**
* Set the URI of the request.
* @param uri the new URI
* @return this builder
*/
Builder uri(URI uri);
/**
* Add the given header value(s) under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return this builder
* @see HttpHeaders#add(String, String)
*/
Builder header(String headerName, String... headerValues);
/**
* Manipulate this request's headers with the given consumer.
* <p>The headers provided to the consumer are "live", so that the consumer can be used to
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
* {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other
* {@link HttpHeaders} methods.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return this builder
*/
Builder headers(Consumer<HttpHeaders> headersConsumer);
/**
* Add a cookie with the given name and value(s).
* @param name the cookie name
* @param values the cookie value(s)
* @return this builder
*/
Builder cookie(String name, String... values);
/**
* Manipulate this request's cookies with the given consumer.
* <p>The map provided to the consumer is "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing cookies,
* {@linkplain MultiValueMap#remove(Object) remove} cookies, or use any of the other
* {@link MultiValueMap} methods.
* @param cookiesConsumer a function that consumes the cookies map
* @return this builder
*/
Builder cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer);
/**
* Set the body of the request.
* <p>Calling this methods will
* {@linkplain org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer) release}
* the existing body of the builder.
* @param body the new body
* @return this builder
*/
Builder body(byte[] body);
/**
* Set the body of the request to the UTF-8 encoded bytes of the given string.
* <p>Calling this methods will
* {@linkplain org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer) release}
* the existing body of the builder.
* @param body the new body
* @return this builder
*/
Builder body(String body);
/**
* Add an attribute with the given name and value.
* @param name the attribute name
* @param value the attribute value
* @return this builder
*/
Builder attribute(String name, Object value);
/**
* Manipulate this request's attributes with the given consumer.
* <p>The map provided to the consumer is "live", so that the consumer can be used
* to {@linkplain Map#put(Object, Object) overwrite} existing attributes,
* {@linkplain Map#remove(Object) remove} attributes, or use any of the other
* {@link Map} methods.
* @param attributesConsumer a function that consumes the attributes map
* @return this builder
*/
Builder attributes(Consumer<Map<String, Object>> attributesConsumer);
/**
* Build the request.
* @return the built request
*/
ServerRequest build();
}
}

View File

@@ -0,0 +1,422 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.reactivestreams.Publisher;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import org.springframework.web.servlet.ModelAndView;
/**
* Represents a typed server-side HTTP response, as returned
* by a {@linkplain HandlerFunction handler function} or
* {@linkplain HandlerFilterFunction filter function}.
*
* @author Arjen Poutsma
* @since 5.2
*/
public interface ServerResponse {
/**
* Return the status code of this response.
*/
HttpStatus statusCode();
/**
* Return the headers of this response.
*/
HttpHeaders headers();
/**
* Return the cookies of this response.
*/
MultiValueMap<String, Cookie> cookies();
/**
* Write this response to the servlet response (and return {@code null}, or return a
* @param request the current request
* @param response the response to write to
* @param context the context to use when writing
* @return {@code Mono<Void>} to indicate when writing is complete
*/
@Nullable
ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context)
throws ServletException, IOException;
// Static methods
/**
* Create a builder with the status code and headers of the given response.
* @param other the response to copy the status and headers from
* @return the created builder
*/
static BodyBuilder from(ServerResponse other) {
return new DefaultServerResponseBuilder(other);
}
/**
* Create a builder with the given HTTP status.
* @param status the response status
* @return the created builder
*/
static BodyBuilder status(HttpStatus status) {
return new DefaultServerResponseBuilder(status);
}
/**
* Create a builder with the given HTTP status.
* @param status the response status
* @return the created builder
*/
static BodyBuilder status(int status) {
return new DefaultServerResponseBuilder(status);
}
/**
* Create a builder with the status set to {@linkplain HttpStatus#OK 200 OK}.
* @return the created builder
*/
static BodyBuilder ok() {
return status(HttpStatus.OK);
}
/**
* Create a new builder with a {@linkplain HttpStatus#CREATED 201 Created} status
* and a location header set to the given URI.
* @param location the location URI
* @return the created builder
*/
static BodyBuilder created(URI location) {
BodyBuilder builder = status(HttpStatus.CREATED);
return builder.location(location);
}
/**
* Create a builder with an {@linkplain HttpStatus#ACCEPTED 202 Accepted} status.
* @return the created builder
*/
static BodyBuilder accepted() {
return status(HttpStatus.ACCEPTED);
}
/**
* Create a builder with a {@linkplain HttpStatus#NO_CONTENT 204 No Content} status.
* @return the created builder
*/
static HeadersBuilder<?> noContent() {
return status(HttpStatus.NO_CONTENT);
}
/**
* Create a builder with a {@linkplain HttpStatus#SEE_OTHER 303 See Other}
* status and a location header set to the given URI.
* @param location the location URI
* @return the created builder
*/
static BodyBuilder seeOther(URI location) {
BodyBuilder builder = status(HttpStatus.SEE_OTHER);
return builder.location(location);
}
/**
* Create a builder with a {@linkplain HttpStatus#TEMPORARY_REDIRECT 307 Temporary Redirect}
* status and a location header set to the given URI.
* @param location the location URI
* @return the created builder
*/
static BodyBuilder temporaryRedirect(URI location) {
BodyBuilder builder = status(HttpStatus.TEMPORARY_REDIRECT);
return builder.location(location);
}
/**
* Create a builder with a {@linkplain HttpStatus#PERMANENT_REDIRECT 308 Permanent Redirect}
* status and a location header set to the given URI.
* @param location the location URI
* @return the created builder
*/
static BodyBuilder permanentRedirect(URI location) {
BodyBuilder builder = status(HttpStatus.PERMANENT_REDIRECT);
return builder.location(location);
}
/**
* Create a builder with a {@linkplain HttpStatus#BAD_REQUEST 400 Bad Request} status.
* @return the created builder
*/
static BodyBuilder badRequest() {
return status(HttpStatus.BAD_REQUEST);
}
/**
* Create a builder with a {@linkplain HttpStatus#NOT_FOUND 404 Not Found} status.
* @return the created builder
*/
static HeadersBuilder<?> notFound() {
return status(HttpStatus.NOT_FOUND);
}
/**
* Create a builder with an
* {@linkplain HttpStatus#UNPROCESSABLE_ENTITY 422 Unprocessable Entity} status.
* @return the created builder
*/
static BodyBuilder unprocessableEntity() {
return status(HttpStatus.UNPROCESSABLE_ENTITY);
}
/**
* Defines a builder that adds headers to the response.
* @param <B> the builder subclass
*/
interface HeadersBuilder<B extends HeadersBuilder<B>> {
/**
* Add the given header value(s) under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return this builder
* @see HttpHeaders#add(String, String)
*/
B header(String headerName, String... headerValues);
/**
* Manipulate this response's headers with the given consumer. The
* headers provided to the consumer are "live", so that the consumer can be used to
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
* {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other
* {@link HttpHeaders} methods.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return this builder
*/
B headers(Consumer<HttpHeaders> headersConsumer);
/**
* Add the given cookie to the response.
* @param cookie the cookie to add
* @return this builder
*/
B cookie(Cookie cookie);
/**
* Manipulate this response's cookies with the given consumer. The
* cookies provided to the consumer are "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing cookies,
* {@linkplain MultiValueMap#remove(Object) remove} cookies, or use any of the other
* {@link MultiValueMap} methods.
* @param cookiesConsumer a function that consumes the cookies
* @return this builder
*/
B cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer);
/**
* 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(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.
* @param eTag the new entity tag
* @return this builder
* @see HttpHeaders#setETag(String)
*/
B eTag(String eTag);
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @param lastModified the last modified date
* @return this builder
* @see HttpHeaders#setLastModified(long)
*/
B lastModified(ZonedDateTime lastModified);
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @param lastModified the last modified date
* @return this builder
* @see HttpHeaders#setLastModified(long)
*/
B lastModified(Instant lastModified);
/**
* Set the location of a resource, as specified by the {@code Location} header.
* @param location the location
* @return this builder
* @see HttpHeaders#setLocation(URI)
*/
B location(URI location);
/**
* Set the caching directives for the resource, as specified by the HTTP 1.1
* {@code Cache-Control} header.
* <p>A {@code CacheControl} instance can be built like
* {@code CacheControl.maxAge(3600).cachePublic().noTransform()}.
* @param cacheControl a builder for cache-related HTTP response headers
* @return this builder
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2">RFC-7234 Section 5.2</a>
*/
B cacheControl(CacheControl cacheControl);
/**
* Configure one or more request header names (e.g. "Accept-Language") to
* add to the "Vary" response header to inform clients that the response is
* subject to content negotiation and variances based on the value of the
* given request headers. The configured request header names are added only
* if not already present in the response "Vary" header.
* @param requestHeaders request header names
* @return this builder
*/
B varyBy(String... requestHeaders);
/**
* Build the response entity with no body.
*/
ServerResponse build();
/**
* Build the response entity with a custom write function.
* @param writeFunction the function used to write to the {@link HttpServletResponse}
*/
ServerResponse build(BiFunction<HttpServletRequest, HttpServletResponse,
ModelAndView> writeFunction);
}
/**
* Defines a builder that adds a body to the response.
*/
interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
/**
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* @param contentLength the content length
* @return this builder
* @see HttpHeaders#setContentLength(long)
*/
BodyBuilder contentLength(long contentLength);
/**
* Set the {@linkplain MediaType media type} of the body, as specified by the
* {@code Content-Type} header.
* @param contentType the content type
* @return this builder
* @see HttpHeaders#setContentType(MediaType)
*/
BodyBuilder contentType(MediaType contentType);
/**
* Set the body of the response to the given {@code Object} and return it.
* @param body the body of the response
* @return the built response
*/
ServerResponse body(Object body);
/**
* Set the asynchronous body of the response to the given {@link CompletionStage} and
* return it.
* @param asyncBody the body of the response
* @return the built response
*/
ServerResponse asyncBody(CompletionStage<?> asyncBody);
/**
* Set the asynchronous body of the response to the given {@link Publisher} and
* return it.
* @param asyncBody the body of the response
* @return the built response
*/
ServerResponse asyncBody(Publisher<?> asyncBody);
/**
* Render the template with the given {@code name} using the given {@code modelAttributes}.
* The model attributes are mapped under a
* {@linkplain org.springframework.core.Conventions#getVariableName generated name}.
* <p><em>Note: Empty {@link Collection Collections} are not added to
* the model when using this method because we cannot correctly determine
* the true convention name.</em>
* @param name the name of the template to be rendered
* @param modelAttributes the modelAttributes used to render the template
* @return the built response
*/
ServerResponse render(String name, Object... modelAttributes);
/**
* Render the template with the given {@code name} using the given {@code model}.
* @param name the name of the template to be rendered
* @param model the model used to render the template
* @return the built response
*/
ServerResponse render(String name, Map<String, ?> model);
}
/**
* Defines the context used during the {@link #writeTo(HttpServletRequest, HttpServletResponse, Context)}.
*/
interface Context {
/**
* Return the {@link HttpMessageConverter HttpMessageConverters} to be used for response body conversion.
* @return the list of message writers
*/
List<HttpMessageConverter<?>> messageConverters();
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2002-2019 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.servlet.function;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
/**
* Implementation of {@link RouterFunctions.Visitor} that creates a formatted
* string representation of router functions.
*
* @author Arjen Poutsma
* @since 5.2
*/
class ToStringVisitor implements RouterFunctions.Visitor, RequestPredicates.Visitor {
private final StringBuilder builder = new StringBuilder();
private int indent = 0;
// RouterFunctions.Visitor
@Override
public void startNested(RequestPredicate predicate) {
indent();
predicate.accept(this);
this.builder.append(" => {\n");
this.indent++;
}
@Override
public void endNested(RequestPredicate predicate) {
this.indent--;
indent();
this.builder.append("}\n");
}
@Override
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
indent();
predicate.accept(this);
this.builder.append(" -> ");
this.builder.append(handlerFunction).append('\n');
}
@Override
public void resources(Function<ServerRequest, Optional<Resource>> lookupFunction) {
indent();
this.builder.append(lookupFunction).append('\n');
}
@Override
public void unknown(RouterFunction<?> routerFunction) {
indent();
this.builder.append(routerFunction);
}
private void indent() {
for (int i=0; i < this.indent; i++) {
this.builder.append(' ');
}
}
// RequestPredicates.Visitor
@Override
public void method(Set<HttpMethod> methods) {
if (methods.size() == 1) {
this.builder.append(methods.iterator().next());
}
else {
this.builder.append(methods);
}
}
@Override
public void path(String pattern) {
this.builder.append(pattern);
}
@Override
public void pathExtension(String extension) {
this.builder.append(String.format("*.%s", extension));
}
@Override
public void header(String name, String value) {
this.builder.append(String.format("%s: %s", name, value));
}
@Override
public void param(String name, String value) {
this.builder.append(String.format("?%s == %s", name, value));
}
@Override
public void startAnd() {
this.builder.append('(');
}
@Override
public void and() {
this.builder.append(" && ");
}
@Override
public void endAnd() {
this.builder.append(')');
}
@Override
public void startOr() {
this.builder.append('(');
}
@Override
public void or() {
this.builder.append(" || ");
}
@Override
public void endOr() {
this.builder.append(')');
}
@Override
public void startNegate() {
this.builder.append("!(");
}
@Override
public void endNegate() {
this.builder.append(')');
}
@Override
public void unknown(RequestPredicate predicate) {
this.builder.append(predicate);
}
@Override
public String toString() {
String result = this.builder.toString();
if (result.endsWith("\n")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
}

View File

@@ -0,0 +1,9 @@
/**
* Provides the types that make up Spring's functional web framework for Servlet environments.
*/
@NonNullApi
@NonNullFields
package org.springframework.web.servlet.function;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;