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:
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ServerResponse> 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<ServerResponse> userRoutes =
|
||||
* RouterFunctions.route(RequestPredicates.method(HttpMethod.GET), this::listUsers)
|
||||
* .andRoute(RequestPredicates.method(HttpMethod.POST), this::createUser);
|
||||
* RouterFunction<ServerResponse> 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<ServerResponse> 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<Resource> defaultResource = Mono.just(new ClassPathResource("index.html"));
|
||||
* Function<ServerRequest, Mono<Resource>> lookupFunction =
|
||||
* RouterFunctions.resourceLookupFunction("/resources/**", new FileSystemResource("public-resources/"))
|
||||
* .andThen(resourceMono -> resourceMono.switchIfEmpty(defaultResource));
|
||||
* RouterFunction<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user