Skip to content

Commit 7796c4d

Browse files
committed
Introduce RenderingResponse
This commit introduces the RenderingResponse, a template rendering-specific subtype of ServerResponse that exposes model and template data.
1 parent 71cdd61 commit 7796c4d

14 files changed

+570
-162
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.List;
22+
import java.util.Locale;
23+
import java.util.Optional;
24+
import java.util.function.Function;
2225
import java.util.function.Supplier;
2326
import java.util.stream.Stream;
2427

@@ -52,6 +55,11 @@
5255
*/
5356
class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
5457

58+
static final Function<ServerRequest, Optional<Locale>> DEFAULT_LOCALE_RESOLVER =
59+
request -> request.headers().acceptLanguage().stream()
60+
.map(Locale.LanguageRange::getRange)
61+
.map(Locale::forLanguageTag).findFirst();
62+
5563
private static final boolean jackson2Present =
5664
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
5765
DefaultHandlerStrategiesBuilder.class.getClassLoader()) &&
@@ -69,6 +77,8 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
6977

7078
private final List<ViewResolver> viewResolvers = new ArrayList<>();
7179

80+
private Function<ServerRequest, Optional<Locale>> localeResolver;
81+
7282
public void defaultConfiguration() {
7383
messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
7484
messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
@@ -94,6 +104,7 @@ public void defaultConfiguration() {
94104
else {
95105
messageWriter(new ServerSentEventHttpMessageWriter());
96106
}
107+
localeResolver(DEFAULT_LOCALE_RESOLVER);
97108
}
98109

99110
public void applicationContext(ApplicationContext applicationContext) {
@@ -123,10 +134,17 @@ public HandlerStrategies.Builder viewResolver(ViewResolver viewResolver) {
123134
return this;
124135
}
125136

137+
@Override
138+
public HandlerStrategies.Builder localeResolver(Function<ServerRequest, Optional<Locale>> localeResolver) {
139+
Assert.notNull(localeResolver, "'localeResolver' must not be null");
140+
this.localeResolver = localeResolver;
141+
return this;
142+
}
143+
126144
@Override
127145
public HandlerStrategies build() {
128146
return new DefaultHandlerStrategies(this.messageReaders, this.messageWriters,
129-
this.viewResolvers);
147+
this.viewResolvers, localeResolver);
130148
}
131149

132150
private static class DefaultHandlerStrategies implements HandlerStrategies {
@@ -137,13 +155,18 @@ private static class DefaultHandlerStrategies implements HandlerStrategies {
137155

138156
private final List<ViewResolver> viewResolvers;
139157

158+
private final Function<ServerRequest, Optional<Locale>> localeResolver;
159+
160+
140161
public DefaultHandlerStrategies(
141162
List<HttpMessageReader<?>> messageReaders,
142163
List<HttpMessageWriter<?>> messageWriters,
143-
List<ViewResolver> viewResolvers) {
164+
List<ViewResolver> viewResolvers,
165+
Function<ServerRequest, Optional<Locale>> localeResolver) {
144166
this.messageReaders = unmodifiableCopy(messageReaders);
145167
this.messageWriters = unmodifiableCopy(messageWriters);
146168
this.viewResolvers = unmodifiableCopy(viewResolvers);
169+
this.localeResolver = localeResolver;
147170
}
148171

149172
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
@@ -164,6 +187,11 @@ public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
164187
public Supplier<Stream<ViewResolver>> viewResolvers() {
165188
return this.viewResolvers::stream;
166189
}
190+
191+
@Override
192+
public Function<ServerRequest, Optional<Locale>> localeResolver() {
193+
return this.localeResolver;
194+
}
167195
}
168196

169197
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.reactive.function.server;
18+
19+
import java.util.Arrays;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.LinkedHashMap;
23+
import java.util.Locale;
24+
import java.util.Map;
25+
import java.util.stream.Stream;
26+
27+
import reactor.core.publisher.Flux;
28+
import reactor.core.publisher.Mono;
29+
30+
import org.springframework.core.Conventions;
31+
import org.springframework.http.HttpHeaders;
32+
import org.springframework.http.HttpStatus;
33+
import org.springframework.http.MediaType;
34+
import org.springframework.http.server.reactive.ServerHttpResponse;
35+
import org.springframework.util.Assert;
36+
import org.springframework.web.reactive.result.view.ViewResolver;
37+
import org.springframework.web.server.ServerWebExchange;
38+
39+
/**
40+
* Default {@link RenderingResponse.Builder} implementation.
41+
*
42+
* @author Arjen Poutsma
43+
* @since 5.0
44+
*/
45+
class DefaultRenderingResponseBuilder implements RenderingResponse.Builder {
46+
47+
private final String name;
48+
49+
private final HttpHeaders headers = new HttpHeaders();
50+
51+
private final Map<String, Object> model = new LinkedHashMap<String, Object>();
52+
53+
private HttpStatus status = HttpStatus.OK;
54+
55+
public DefaultRenderingResponseBuilder(String name) {
56+
this.name = name;
57+
}
58+
59+
60+
@Override
61+
public RenderingResponse.Builder modelAttribute(Object attribute) {
62+
Assert.notNull(attribute, "'value' must not be null");
63+
if (attribute instanceof Collection && ((Collection<?>) attribute).isEmpty()) {
64+
return this;
65+
}
66+
return modelAttribute(Conventions.getVariableName(attribute), attribute);
67+
}
68+
69+
@Override
70+
public RenderingResponse.Builder modelAttribute(String name, Object value) {
71+
Assert.notNull(name, "'name' must not be null");
72+
this.model.put(name, value);
73+
return this;
74+
}
75+
76+
@Override
77+
public RenderingResponse.Builder modelAttributes(Object... attributes) {
78+
if (attributes != null) {
79+
modelAttributes(Arrays.asList(attributes));
80+
}
81+
return this;
82+
}
83+
84+
@Override
85+
public RenderingResponse.Builder modelAttributes(Collection<?> attributes) {
86+
if (attributes != null) {
87+
attributes.forEach(this::modelAttribute);
88+
}
89+
return this;
90+
}
91+
92+
@Override
93+
public RenderingResponse.Builder modelAttributes(Map<String, ?> attributes) {
94+
if (attributes != null) {
95+
this.model.putAll(attributes);
96+
}
97+
return this;
98+
}
99+
100+
@Override
101+
public RenderingResponse.Builder header(String headerName, String... headerValues) {
102+
for (String headerValue : headerValues) {
103+
this.headers.add(headerName, headerValue);
104+
}
105+
return this;
106+
}
107+
108+
@Override
109+
public RenderingResponse.Builder headers(HttpHeaders headers) {
110+
if (headers != null) {
111+
this.headers.putAll(headers);
112+
}
113+
return this;
114+
}
115+
116+
@Override
117+
public RenderingResponse.Builder status(HttpStatus status) {
118+
Assert.notNull(status, "'status' must not be null");
119+
this.status = status;
120+
return this;
121+
}
122+
123+
124+
@Override
125+
public Mono<RenderingResponse> build() {
126+
return Mono.just(new DefaultRenderingResponse(this.status, this.headers, this.name,
127+
this.model));
128+
}
129+
130+
static class DefaultRenderingResponse extends DefaultServerResponseBuilder.AbstractServerResponse
131+
implements RenderingResponse {
132+
133+
private final String name;
134+
135+
private final Map<String, Object> model;
136+
137+
public DefaultRenderingResponse(HttpStatus statusCode, HttpHeaders headers, String name,
138+
Map<String, Object> model) {
139+
super(statusCode, headers);
140+
this.name = name;
141+
this.model = unmodifiableCopy(model);
142+
}
143+
144+
private static <K, V> Map<K, V> unmodifiableCopy(Map<? extends K, ? extends V> m) {
145+
return Collections.unmodifiableMap(new LinkedHashMap<>(m));
146+
}
147+
148+
149+
@Override
150+
public String name() {
151+
return this.name;
152+
}
153+
154+
@Override
155+
public Map<String, Object> model() {
156+
return this.model;
157+
}
158+
159+
@Override
160+
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
161+
ServerHttpResponse response = exchange.getResponse();
162+
writeStatusAndHeaders(response);
163+
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
164+
Locale locale = resolveLocale(exchange, strategies);
165+
Stream<ViewResolver> viewResolverStream = strategies.viewResolvers().get();
166+
return Flux.fromStream(viewResolverStream)
167+
.concatMap(viewResolver -> viewResolver.resolveViewName(this.name, locale))
168+
.next()
169+
.otherwiseIfEmpty(Mono.error(new IllegalArgumentException("Could not resolve view with name '" +
170+
this.name +"'")))
171+
.then(view -> view.render(this.model, contentType, exchange));
172+
}
173+
174+
private Locale resolveLocale(ServerWebExchange exchange, HandlerStrategies strategies) {
175+
ServerRequest request =
176+
exchange.<ServerRequest>getAttribute(RouterFunctions.REQUEST_ATTRIBUTE)
177+
.orElseThrow(() -> new IllegalStateException(
178+
"Could not find ServerRequest in exchange attributes"));
179+
180+
return strategies.localeResolver()
181+
.apply(request)
182+
.orElse(Locale.getDefault());
183+
184+
}
185+
}
186+
187+
188+
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.charset.Charset;
2222
import java.util.Collections;
2323
import java.util.List;
24+
import java.util.Locale;
2425
import java.util.Map;
2526
import java.util.Optional;
2627
import java.util.OptionalLong;
@@ -47,7 +48,9 @@
4748

4849
/**
4950
* {@code ServerRequest} implementation based on a {@link ServerWebExchange}.
51+
*
5052
* @author Arjen Poutsma
53+
* @since 5.0
5154
*/
5255
class DefaultServerRequest implements ServerRequest {
5356

@@ -166,6 +169,11 @@ public List<Charset> acceptCharset() {
166169
return delegate().getAcceptCharset();
167170
}
168171

172+
@Override
173+
public List<Locale.LanguageRange> acceptLanguage() {
174+
return delegate().getAcceptLanguage();
175+
}
176+
169177
@Override
170178
public OptionalLong contentLength() {
171179
long value = delegate().getContentLength();

0 commit comments

Comments
 (0)