16
16
17
17
package org .springframework .web .reactive .result .view ;
18
18
19
- import java .lang .reflect .Method ;
20
19
import java .util .ArrayList ;
21
20
import java .util .Collections ;
22
21
import java .util .List ;
23
22
import java .util .Locale ;
24
23
import java .util .Map ;
25
24
import java .util .Optional ;
25
+ import java .util .stream .Collectors ;
26
26
27
27
import reactor .core .publisher .Flux ;
28
28
import reactor .core .publisher .Mono ;
29
29
30
30
import org .springframework .beans .BeanUtils ;
31
- import org .springframework .core .Conventions ;
32
- import org .springframework .core .GenericTypeResolver ;
33
31
import org .springframework .core .MethodParameter ;
34
32
import org .springframework .core .Ordered ;
35
33
import org .springframework .core .ReactiveAdapter ;
38
36
import org .springframework .core .annotation .AnnotationAwareOrderComparator ;
39
37
import org .springframework .http .MediaType ;
40
38
import org .springframework .ui .Model ;
39
+ import org .springframework .util .ClassUtils ;
41
40
import org .springframework .util .StringUtils ;
42
41
import org .springframework .web .bind .annotation .ModelAttribute ;
43
42
import org .springframework .web .reactive .HandlerResult ;
77
76
public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
78
77
implements HandlerResultHandler , Ordered {
79
78
79
+ private static final Object NO_VALUE = new Object ();
80
+
81
+ private static final Mono <Object > NO_VALUE_MONO = Mono .just (NO_VALUE );
82
+
83
+
80
84
private final List <ViewResolver > viewResolvers = new ArrayList <>(4 );
81
85
82
86
private final List <View > defaultViews = new ArrayList <>(4 );
@@ -172,89 +176,81 @@ private boolean isSupportedType(Class<?> clazz) {
172
176
}
173
177
174
178
@ Override
179
+ @ SuppressWarnings ("unchecked" )
175
180
public Mono <Void > handleResult (ServerWebExchange exchange , HandlerResult result ) {
176
181
177
- Mono <Object > valueMono ;
182
+ Mono <Object > returnValueMono ;
178
183
ResolvableType elementType ;
179
- ResolvableType returnType = result .getReturnType ();
184
+ ResolvableType parameterType = result .getReturnType ();
180
185
181
186
Optional <Object > optional = result .getReturnValue ();
182
- ReactiveAdapter adapter = getAdapterRegistry ().getAdapterFrom (returnType .getRawClass (), optional );
187
+ ReactiveAdapter adapter = getAdapterRegistry ().getAdapterFrom (parameterType .getRawClass (), optional );
188
+
183
189
if (adapter != null ) {
184
190
if (optional .isPresent ()) {
185
191
Mono <?> converted = adapter .toMono (optional );
186
- valueMono = converted .map (o -> o );
192
+ returnValueMono = converted .map (o -> o );
187
193
}
188
194
else {
189
- valueMono = Mono .empty ();
195
+ returnValueMono = Mono .empty ();
190
196
}
191
197
elementType = adapter .getDescriptor ().isNoValue () ?
192
- ResolvableType .forClass (Void .class ) : returnType .getGeneric (0 );
198
+ ResolvableType .forClass (Void .class ) : parameterType .getGeneric (0 );
193
199
}
194
200
else {
195
- valueMono = Mono .justOrEmpty (result .getReturnValue ());
196
- elementType = returnType ;
201
+ returnValueMono = Mono .justOrEmpty (result .getReturnValue ());
202
+ elementType = parameterType ;
197
203
}
198
204
199
- Mono <Object > viewMono ;
200
- if (isViewNameOrReference (elementType , result )) {
201
- Mono <Object > viewName = getDefaultViewNameMono (exchange , result );
202
- viewMono = valueMono .otherwiseIfEmpty (viewName );
203
- }
204
- else {
205
- viewMono = valueMono .map (value -> updateModel (value , result ))
206
- .defaultIfEmpty (result .getModel ())
207
- .then (model -> getDefaultViewNameMono (exchange , result ));
208
- }
209
- Map <String , ?> model = result .getModel ().asMap ();
210
- return viewMono .then (view -> {
211
- updateResponseStatus (result .getReturnTypeSource (), exchange );
212
- if (view instanceof View ) {
213
- return ((View ) view ).render (model , null , exchange );
214
- }
215
- else if (view instanceof CharSequence ) {
216
- String viewName = view .toString ();
217
- Locale locale = Locale .getDefault (); // TODO
218
- return resolveAndRender (viewName , locale , model , exchange );
205
+ return returnValueMono
206
+ .otherwiseIfEmpty (exchange .isNotModified () ? Mono .empty () : NO_VALUE_MONO )
207
+ .then (returnValue -> {
219
208
220
- }
221
- else {
222
- // Should not happen
223
- return Mono .error (new IllegalStateException ("Unexpected view type" ));
224
- }
225
- });
226
- }
209
+ updateResponseStatus (result .getReturnTypeSource (), exchange );
227
210
228
- private boolean isViewNameOrReference (ResolvableType elementType , HandlerResult result ) {
229
- Class <?> clazz = elementType .getRawClass ();
230
- return (View .class .isAssignableFrom (clazz ) ||
231
- (CharSequence .class .isAssignableFrom (clazz ) && !hasModelAttributeAnnotation (result )));
232
- }
211
+ Mono <List <View >> viewsMono ;
212
+ Model model = result .getModel ();
213
+ Locale locale = Locale .getDefault (); // TODO
233
214
234
- private Mono <Object > getDefaultViewNameMono (ServerWebExchange exchange , HandlerResult result ) {
235
- if (exchange .isNotModified ()) {
236
- return Mono .empty ();
237
- }
238
- String defaultViewName = getDefaultViewName (result , exchange );
239
- if (defaultViewName != null ) {
240
- return Mono .just (defaultViewName );
241
- }
242
- else {
243
- return Mono .error (new IllegalStateException (
244
- "Handler [" + result .getHandler () + "] " +
245
- "neither returned a view name nor a View object" ));
246
- }
215
+ Class <?> clazz = elementType .getRawClass ();
216
+ if (clazz == null ) {
217
+ clazz = returnValue .getClass ();
218
+ }
219
+
220
+ if (returnValue == NO_VALUE || Void .class .equals (clazz ) || void .class .equals (clazz )) {
221
+ viewsMono = resolveViews (getDefaultViewName (result , exchange ), locale );
222
+ }
223
+ else if (Model .class .isAssignableFrom (clazz )) {
224
+ model .addAllAttributes (((Model ) returnValue ).asMap ());
225
+ viewsMono = resolveViews (getDefaultViewName (result , exchange ), locale );
226
+ }
227
+ else if (Map .class .isAssignableFrom (clazz )) {
228
+ model .addAllAttributes ((Map <String , ?>) returnValue );
229
+ viewsMono = resolveViews (getDefaultViewName (result , exchange ), locale );
230
+ }
231
+ else if (View .class .isAssignableFrom (clazz )) {
232
+ viewsMono = Mono .just (Collections .singletonList ((View ) returnValue ));
233
+ }
234
+ else if (CharSequence .class .isAssignableFrom (clazz ) && !hasModelAttributeAnnotation (result )) {
235
+ viewsMono = resolveViews (returnValue .toString (), locale );
236
+ }
237
+ else {
238
+ String name = getNameForReturnValue (clazz , result .getReturnTypeSource ());
239
+ model .addAttribute (name , returnValue );
240
+ viewsMono = resolveViews (getDefaultViewName (result , exchange ), locale );
241
+ }
242
+
243
+ return resolveAsyncAttributes (model .asMap ())
244
+ .then (viewsMono )
245
+ .then (views -> render (views , model .asMap (), exchange ));
246
+ });
247
247
}
248
248
249
249
/**
250
- * Translate the given request into a default view name. This is useful when
251
- * the application leaves the view name unspecified.
252
- * <p>The default implementation strips the leading and trailing slash from
253
- * the as well as any extension and uses that as the view name.
254
- * @return the default view name to use; if {@code null} is returned
255
- * processing will result in an IllegalStateException.
250
+ * Select a default view name when a controller leaves the view unspecified.
251
+ * The default implementation strips the leading and trailing slash from the
252
+ * as well as any extension and uses that as the view name.
256
253
*/
257
- @ SuppressWarnings ("UnusedParameters" )
258
254
protected String getDefaultViewName (HandlerResult result , ServerWebExchange exchange ) {
259
255
String path = this .pathHelper .getLookupPathForRequest (exchange );
260
256
if (path .startsWith ("/" )) {
@@ -266,79 +262,87 @@ protected String getDefaultViewName(HandlerResult result, ServerWebExchange exch
266
262
return StringUtils .stripFilenameExtension (path );
267
263
}
268
264
269
- @ SuppressWarnings ("unchecked" )
270
- private Object updateModel (Object value , HandlerResult result ) {
271
- if (value instanceof Model ) {
272
- result .getModel ().addAllAttributes (((Model ) value ).asMap ());
273
- }
274
- else if (value instanceof Map ) {
275
- result .getModel ().addAllAttributes ((Map <String , ?>) value );
276
- }
277
- else {
278
- MethodParameter returnType = result .getReturnTypeSource ();
279
- String name = getNameForReturnValue (value , returnType );
280
- result .getModel ().addAttribute (name , value );
281
- }
282
- return value ;
265
+ private Mono <List <View >> resolveViews (String viewName , Locale locale ) {
266
+ return Flux .fromIterable (getViewResolvers ())
267
+ .concatMap (resolver -> resolver .resolveViewName (viewName , locale ))
268
+ .collectList ()
269
+ .map (views -> {
270
+ if (views .isEmpty ()) {
271
+ throw new IllegalStateException (
272
+ "Could not resolve view with name '" + viewName + "'." );
273
+ }
274
+ views .addAll (getDefaultViews ());
275
+ return views ;
276
+ });
283
277
}
284
278
285
279
/**
286
- * Derive the model attribute name for the given return value using one of:
287
- * <ol>
288
- * <li>The method {@code ModelAttribute} annotation value
289
- * <li>The declared return type if it is more specific than {@code Object}
290
- * <li>The actual return value type
291
- * </ol>
292
- * @param returnValue the value returned from a method invocation
293
- * @param returnType the return type of the method
294
- * @return the model name, never {@code null} nor empty
280
+ * Return the name of a model attribute return value based on the method
281
+ * {@code @ModelAttribute} annotation, if present, or derived from the type
282
+ * of the return value otherwise.
295
283
*/
296
- private static String getNameForReturnValue (Object returnValue , MethodParameter returnType ) {
284
+ private String getNameForReturnValue (Class <?> returnValueType , MethodParameter returnType ) {
297
285
ModelAttribute annotation = returnType .getMethodAnnotation (ModelAttribute .class );
298
286
if (annotation != null && StringUtils .hasText (annotation .value ())) {
299
287
return annotation .value ();
300
288
}
301
- else {
302
- Method method = returnType .getMethod ();
303
- Class <?> containingClass = returnType .getContainingClass ();
304
- Class <?> resolvedType = GenericTypeResolver .resolveReturnType (method , containingClass );
305
- return Conventions .getVariableNameForReturnType (method , resolvedType , returnValue );
306
- }
289
+ // TODO: Conventions does not deal with async wrappers
290
+ return ClassUtils .getShortNameAsProperty (returnValueType );
307
291
}
308
292
309
- private Mono <? extends Void > resolveAndRender (String viewName , Locale locale ,
310
- Map <String , ?> model , ServerWebExchange exchange ) {
293
+ private Mono <Void > resolveAsyncAttributes (Map <String , Object > model ) {
311
294
312
- return Flux .fromIterable (getViewResolvers ())
313
- .concatMap (resolver -> resolver .resolveViewName (viewName , locale ))
314
- .switchIfEmpty (Mono .error (
315
- new IllegalStateException (
316
- "Could not resolve view with name '" + viewName + "'." )))
317
- .collectList ()
318
- .then (views -> {
319
- views .addAll (getDefaultViews ());
295
+ List <String > names = new ArrayList <>();
296
+ List <Mono <Object >> valueMonos = new ArrayList <>();
320
297
321
- List <MediaType > producibleTypes = getProducibleMediaTypes (views );
322
- MediaType bestMediaType = selectMediaType (exchange , () -> producibleTypes );
298
+ for (Map .Entry <String , ?> entry : model .entrySet ()) {
299
+ ReactiveAdapter adapter = getAdapterRegistry ().getAdapterFrom (null , entry .getValue ());
300
+ if (adapter != null ) {
301
+ names .add (entry .getKey ());
302
+ valueMonos .add (adapter .toMono (entry .getValue ()).defaultIfEmpty (NO_VALUE ));
303
+ }
304
+ }
305
+
306
+ if (names .isEmpty ()) {
307
+ return Mono .empty ();
308
+ }
323
309
324
- if (bestMediaType != null ) {
325
- for (View view : views ) {
326
- for (MediaType supported : view .getSupportedMediaTypes ()) {
327
- if (supported .isCompatibleWith (bestMediaType )) {
328
- return view .render (model , bestMediaType , exchange );
329
- }
330
- }
310
+ return Mono .when (valueMonos ,
311
+ values -> {
312
+ for (int i =0 ; i < values .length ; i ++) {
313
+ if (values [i ] != NO_VALUE ) {
314
+ model .put (names .get (i ), values [i ]);
315
+ }
316
+ else {
317
+ model .remove (names .get (i ));
331
318
}
332
319
}
320
+ return NO_VALUE ;
321
+ })
322
+ .then ();
323
+ }
333
324
334
- return Mono .error (new NotAcceptableStatusException (producibleTypes ));
335
- });
325
+ private Mono <? extends Void > render (List <View > views , Map <String , Object > model ,
326
+ ServerWebExchange exchange ) {
327
+
328
+ List <MediaType > mediaTypes = getMediaTypes (views );
329
+ MediaType bestMediaType = selectMediaType (exchange , () -> mediaTypes );
330
+ if (bestMediaType != null ) {
331
+ for (View view : views ) {
332
+ for (MediaType mediaType : view .getSupportedMediaTypes ()) {
333
+ if (mediaType .isCompatibleWith (bestMediaType )) {
334
+ return view .render (model , mediaType , exchange );
335
+ }
336
+ }
337
+ }
338
+ }
339
+ throw new NotAcceptableStatusException (mediaTypes );
336
340
}
337
341
338
- private List <MediaType > getProducibleMediaTypes (List <View > views ) {
339
- List < MediaType > result = new ArrayList <>();
340
- views . forEach (view -> result . addAll ( view .getSupportedMediaTypes ()));
341
- return result ;
342
+ private List <MediaType > getMediaTypes (List <View > views ) {
343
+ return views . stream ()
344
+ . flatMap (view -> view .getSupportedMediaTypes (). stream ())
345
+ . collect ( Collectors . toList ()) ;
342
346
}
343
347
344
348
}
0 commit comments