From 9623cdec4883e134c2691cec56c39e59e1b4b80a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 14 Jan 2018 17:45:58 +0100 Subject: [PATCH] Functional response builders allow for non-standard HTTP status codes Issue: SPR-16366 --- .../reactive/AbstractServerHttpResponse.java | 5 +- .../server/DefaultEntityResponseBuilder.java | 30 +++---- .../DefaultRenderingResponseBuilder.java | 49 ++++++----- .../server/DefaultServerResponseBuilder.java | 81 ++++++++++++------- .../function/server/EntityResponse.java | 13 ++- .../function/server/RenderingResponse.java | 21 +++-- .../function/server/ServerResponse.java | 19 +++-- 7 files changed, 137 insertions(+), 81 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index c936aeec5b0..3ad0e937000 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -43,6 +43,7 @@ import org.springframework.util.MultiValueMap; * Base class for {@link ServerHttpResponse} implementations. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @author Sebastien Deleuze * @since 5.0 */ @@ -112,7 +113,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { * @param statusCode the HTTP status as an integer value * @since 5.0.1 */ - public void setStatusCodeValue(Integer statusCode) { + public void setStatusCodeValue(@Nullable Integer statusCode) { this.statusCode = statusCode; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java index 232094f0f95..65b22bc3eef 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -50,6 +50,7 @@ import org.springframework.web.server.ServerWebExchange; * Default {@link EntityResponse.Builder} implementation. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.0 */ class DefaultEntityResponseBuilder implements EntityResponse.Builder { @@ -58,7 +59,7 @@ class DefaultEntityResponseBuilder implements EntityResponse.Builder { private final BodyInserter inserter; - private HttpStatus status = HttpStatus.OK; + private int status = HttpStatus.OK.value(); private final HttpHeaders headers = new HttpHeaders(); @@ -75,22 +76,27 @@ class DefaultEntityResponseBuilder implements EntityResponse.Builder { @Override public EntityResponse.Builder status(HttpStatus status) { - Assert.notNull(status, "'status' must not be null"); + Assert.notNull(status, "HttpStatus must not be null"); + this.status = status.value(); + return this; + } + + @Override + public EntityResponse.Builder status(int status) { this.status = status; return this; } @Override public EntityResponse.Builder cookie(ResponseCookie cookie) { - Assert.notNull(cookie, "'cookie' must not be null"); + Assert.notNull(cookie, "ResponseCookie must not be null"); this.cookies.add(cookie.getName(), cookie); return this; } @Override - public EntityResponse.Builder cookies( - Consumer> cookiesConsumer) { - Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null"); + public EntityResponse.Builder cookies(Consumer> cookiesConsumer) { + Assert.notNull(cookiesConsumer, "Consumer must not be null"); cookiesConsumer.accept(this.cookies); return this; } @@ -182,8 +188,8 @@ class DefaultEntityResponseBuilder implements EntityResponse.Builder { @Override public Mono> build() { - return Mono.just(new DefaultEntityResponse(this.status, this.headers, this.cookies, - this.entity, this.inserter, this.hints)); + return Mono.just(new DefaultEntityResponse( + this.status, this.headers, this.cookies, this.entity, this.inserter, this.hints)); } @@ -197,8 +203,7 @@ class DefaultEntityResponseBuilder implements EntityResponse.Builder { private final Map hints; - - public DefaultEntityResponse(HttpStatus statusCode, HttpHeaders headers, + public DefaultEntityResponse(int statusCode, HttpHeaders headers, MultiValueMap cookies, T entity, BodyInserter inserter, Map hints) { @@ -208,7 +213,6 @@ class DefaultEntityResponseBuilder implements EntityResponse.Builder { this.hints = hints; } - @Override public T entity() { return this.entity; @@ -228,12 +232,10 @@ class DefaultEntityResponseBuilder implements EntityResponse.Builder { public List> messageWriters() { return context.messageWriters(); } - @Override public Optional serverRequest() { return Optional.of(exchange.getRequest()); } - @Override public Map hints() { return hints; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java index 51bbcfd559a..fb2618feacf 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -47,13 +47,14 @@ import org.springframework.web.server.ServerWebExchange; * Default {@link RenderingResponse.Builder} implementation. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.0 */ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { private final String name; - private HttpStatus status = HttpStatus.OK; + private int status = HttpStatus.OK.value(); private final HttpHeaders headers = new HttpHeaders(); @@ -62,6 +63,14 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { private final Map model = new LinkedHashMap<>(); + public DefaultRenderingResponseBuilder(RenderingResponse other) { + 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) { this.name = name; } @@ -69,28 +78,34 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { @Override public RenderingResponse.Builder status(HttpStatus status) { - Assert.notNull(status, "'status' must not be null"); + 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(ResponseCookie cookie) { - Assert.notNull(cookie, "'cookie' must not be null"); + Assert.notNull(cookie, "ResponseCookie must not be null"); this.cookies.add(cookie.getName(), cookie); return this; } @Override public RenderingResponse.Builder cookies(Consumer> cookiesConsumer) { - Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null"); + Assert.notNull(cookiesConsumer, "Consumer must not be null"); cookiesConsumer.accept(this.cookies); return this; } @Override public RenderingResponse.Builder modelAttribute(Object attribute) { - Assert.notNull(attribute, "'value' must not be null"); + Assert.notNull(attribute, "Attribute must not be null"); if (attribute instanceof Collection && ((Collection) attribute).isEmpty()) { return this; } @@ -99,7 +114,7 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { @Override public RenderingResponse.Builder modelAttribute(String name, @Nullable Object value) { - Assert.notNull(name, "'name' must not be null"); + Assert.notNull(name, "Name must not be null"); this.model.put(name, value); return this; } @@ -138,32 +153,26 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { @Override public Mono build() { - return Mono.just(new DefaultRenderingResponse(this.status, this.headers, this.cookies, - this.name, this.model)); + return Mono.just( + new DefaultRenderingResponse(this.status, this.headers, this.cookies, this.name, this.model)); } - private final static class DefaultRenderingResponse - extends DefaultServerResponseBuilder.AbstractServerResponse + private static final class DefaultRenderingResponse extends DefaultServerResponseBuilder.AbstractServerResponse implements RenderingResponse { private final String name; private final Map model; - public DefaultRenderingResponse(HttpStatus statusCode, HttpHeaders headers, - MultiValueMap cookies, - String name, Map model) { + public DefaultRenderingResponse(int statusCode, HttpHeaders headers, + MultiValueMap cookies, String name, Map model) { + super(statusCode, headers, cookies); this.name = name; - this.model = unmodifiableCopy(model); + this.model = Collections.unmodifiableMap(new LinkedHashMap<>(model)); } - private static Map unmodifiableCopy(Map m) { - return Collections.unmodifiableMap(new LinkedHashMap<>(m)); - } - - @Override public String name() { return this.name; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java index be61fb5f0b9..0382ddb93cd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -39,6 +39,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.server.reactive.AbstractServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; @@ -53,11 +54,12 @@ import org.springframework.web.server.ServerWebExchange; * Default {@link ServerResponse.BodyBuilder} implementation. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.0 */ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { - private final HttpStatus statusCode; + private final int statusCode; private final HttpHeaders headers = new HttpHeaders(); @@ -66,7 +68,17 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { private final Map hints = new HashMap<>(); + public DefaultServerResponseBuilder(ServerResponse other) { + this.statusCode = (other instanceof AbstractServerResponse ? + ((AbstractServerResponse) other).statusCode : other.statusCode().value()); + this.headers.addAll(other.headers()); + } + public DefaultServerResponseBuilder(HttpStatus statusCode) { + this.statusCode = statusCode.value(); + } + + public DefaultServerResponseBuilder(int statusCode) { this.statusCode = statusCode; } @@ -81,22 +93,21 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public ServerResponse.BodyBuilder headers(Consumer headersConsumer) { - Assert.notNull(headersConsumer, "'headersConsumer' must not be null"); + Assert.notNull(headersConsumer, "Consumer must not be null"); headersConsumer.accept(this.headers); return this; } @Override public ServerResponse.BodyBuilder cookie(ResponseCookie cookie) { - Assert.notNull(cookie, "'cookie' must not be null"); + Assert.notNull(cookie, "ResponseCookie must not be null"); this.cookies.add(cookie.getName(), cookie); return this; } @Override - public ServerResponse.BodyBuilder cookies( - Consumer> cookiesConsumer) { - Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null"); + public ServerResponse.BodyBuilder cookies(Consumer> cookiesConsumer) { + Assert.notNull(cookiesConsumer, "Consumer must not be null"); cookiesConsumer.accept(this.cookies); return this; } @@ -177,7 +188,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public Mono build(Publisher voidPublisher) { - Assert.notNull(voidPublisher, "'voidPublisher' must not be null"); + Assert.notNull(voidPublisher, "Publisher must not be null"); return build((exchange, handlerStrategies) -> Mono.from(voidPublisher).then(exchange.getResponse().setComplete())); } @@ -186,15 +197,15 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { public Mono build( BiFunction> writeFunction) { - Assert.notNull(writeFunction, "'writeFunction' must not be null"); - return Mono.just(new WriterFunctionServerResponse(this.statusCode, this.headers, - this.cookies, writeFunction)); + Assert.notNull(writeFunction, "BiFunction must not be null"); + return Mono.just( + new WriterFunctionServerResponse(this.statusCode, this.headers, this.cookies, writeFunction)); } @Override public > Mono body(P publisher, Class elementClass) { - Assert.notNull(publisher, "'publisher' must not be null"); - Assert.notNull(elementClass, "'elementClass' must not be null"); + Assert.notNull(publisher, "Publisher must not be null"); + Assert.notNull(elementClass, "Element Class must not be null"); return new DefaultEntityResponseBuilder<>(publisher, BodyInserters.fromPublisher(publisher, elementClass)) @@ -208,8 +219,8 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { public > Mono body(P publisher, ParameterizedTypeReference typeReference) { - Assert.notNull(publisher, "'publisher' must not be null"); - Assert.notNull(typeReference, "'typeReference' must not be null"); + Assert.notNull(publisher, "Publisher must not be null"); + Assert.notNull(typeReference, "ParameterizedTypeReference must not be null"); return new DefaultEntityResponseBuilder<>(publisher, BodyInserters.fromPublisher(publisher, typeReference)) @@ -221,9 +232,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public Mono syncBody(Object body) { - Assert.notNull(body, "'body' must not be null"); - Assert.isTrue(!(body instanceof Publisher), "Please specify the element class by using " + - "body(Publisher, Class)"); + Assert.notNull(body, "Body must not be null"); + Assert.isTrue(!(body instanceof Publisher), + "Please specify the element class by using body(Publisher, Class)"); return new DefaultEntityResponseBuilder<>(body, BodyInserters.fromObject(body)) @@ -235,14 +246,14 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public Mono body(BodyInserter inserter) { - Assert.notNull(inserter, "'inserter' must not be null"); - return Mono.just(new BodyInserterServerResponse<>(this.statusCode, this.headers, - this.cookies, inserter, this.hints)); + Assert.notNull(inserter, "BodyInserter must not be null"); + return Mono.just( + new BodyInserterServerResponse<>(this.statusCode, this.headers, this.cookies, inserter, this.hints)); } @Override public Mono render(String name, Object... modelAttributes) { - Assert.hasLength(name, "'name' must not be empty"); + Assert.hasLength(name, "Name must not be empty"); return new DefaultRenderingResponseBuilder(name) .headers(this.headers) @@ -254,7 +265,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public Mono render(String name, Map model) { - Assert.hasLength(name, "'name' must not be empty"); + Assert.hasLength(name, "Name must not be empty"); return new DefaultRenderingResponseBuilder(name) .headers(this.headers) @@ -267,14 +278,15 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { static abstract class AbstractServerResponse implements ServerResponse { - private final HttpStatus statusCode; + final int statusCode; private final HttpHeaders headers; private final MultiValueMap cookies; - protected AbstractServerResponse(HttpStatus statusCode, HttpHeaders headers, + protected AbstractServerResponse(int statusCode, HttpHeaders headers, MultiValueMap cookies) { + this.statusCode = statusCode; this.headers = readOnlyCopy(headers); this.cookies = readOnlyCopy(cookies); @@ -294,7 +306,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public final HttpStatus statusCode() { - return this.statusCode; + return HttpStatus.valueOf(this.statusCode); } @Override @@ -308,8 +320,17 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } protected void writeStatusAndHeaders(ServerHttpResponse response) { - response.setStatusCode(this.statusCode); - + if (response instanceof AbstractServerHttpResponse) { + ((AbstractServerHttpResponse) response).setStatusCodeValue(this.statusCode); + } + else { + HttpStatus status = HttpStatus.resolve(this.statusCode); + if (status == null) { + throw new IllegalStateException( + "Unresolvable HttpStatus for general ServerHttpResponse: " + this.statusCode); + } + response.setStatusCode(status); + } copy(this.headers, response.getHeaders()); copy(this.cookies, response.getCookies()); } @@ -328,7 +349,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { private final BiFunction> writeFunction; - public WriterFunctionServerResponse(HttpStatus statusCode, HttpHeaders headers, + public WriterFunctionServerResponse(int statusCode, HttpHeaders headers, MultiValueMap cookies, BiFunction> writeFunction) { @@ -350,7 +371,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { private final Map hints; - public BodyInserterServerResponse(HttpStatus statusCode, HttpHeaders headers, + public BodyInserterServerResponse(int statusCode, HttpHeaders headers, MultiValueMap cookies, BodyInserter inserter, Map hints) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java index 7b209310ad6..d54f78f1be9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -41,6 +41,7 @@ import org.springframework.web.reactive.function.BodyInserters; * Entity-specific subtype of {@link ServerResponse} that exposes entity data. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.0 */ public interface EntityResponse extends ServerResponse { @@ -120,12 +121,20 @@ public interface EntityResponse extends ServerResponse { Builder headers(HttpHeaders headers); /** - * Set the status. + * 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 + * @since 5.0.3 + */ + Builder status(int status); + /** * Add the given cookie to the response. * @param cookie the cookie to add diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.java index f1212559d85..af1d5f92a7f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -33,6 +33,7 @@ import org.springframework.util.MultiValueMap; * Rendering-specific subtype of {@link ServerResponse} that exposes model and template data. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.0 */ public interface RenderingResponse extends ServerResponse { @@ -56,12 +57,8 @@ public interface RenderingResponse extends ServerResponse { * @return the created builder */ static Builder from(RenderingResponse other) { - Assert.notNull(other, "'other' must not be null"); - DefaultRenderingResponseBuilder builder = new DefaultRenderingResponseBuilder(other.name()); - builder.status(other.statusCode()); - builder.headers(other.headers()); - builder.modelAttributes(other.model()); - return builder; + Assert.notNull(other, "Other RenderingResponse must not be null"); + return new DefaultRenderingResponseBuilder(other); } /** @@ -136,12 +133,20 @@ public interface RenderingResponse extends ServerResponse { Builder headers(HttpHeaders headers); /** - * Set the status. + * 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 + * @since 5.0.3 + */ + Builder status(int status); + /** * Add the given cookie to the response. * @param cookie the cookie to add diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java index 445e86328dd..acc617f992d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -51,6 +51,7 @@ import org.springframework.web.server.ServerWebExchange; * {@linkplain HandlerFilterFunction filter function}. * * @author Arjen Poutsma + * @author Juergen Hoeller * @author Sebastien Deleuze * @since 5.0 */ @@ -91,12 +92,11 @@ public interface ServerResponse { */ static BodyBuilder from(ServerResponse other) { Assert.notNull(other, "Other ServerResponse must not be null"); - DefaultServerResponseBuilder builder = new DefaultServerResponseBuilder(other.statusCode()); - return builder.headers(headers -> headers.addAll(other.headers())); + return new DefaultServerResponseBuilder(other); } /** - * Create a builder with the given status. + * Create a builder with the given HTTP status. * @param status the response status * @return the created builder */ @@ -105,6 +105,16 @@ public interface ServerResponse { return new DefaultServerResponseBuilder(status); } + /** + * Create a builder with the given HTTP status. + * @param status the response status + * @return the created builder + * @since 5.0.3 + */ + 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 @@ -450,5 +460,4 @@ public interface ServerResponse { List viewResolvers(); } - }