Add View, ViewResolver, and ViewResolverResultHandler
This commit adds a View and ViewResolver contracts to support HTML template based rendering. ViewResolverResultHandler applies view resolution by iterating the resolvers to resolve to a view and then use it to render.
This commit is contained in:
@@ -58,7 +58,7 @@ public class HandlerResult {
|
||||
this.handler = handler;
|
||||
this.returnValue = Optional.ofNullable(returnValue);
|
||||
this.returnValueType = returnValueType;
|
||||
this.model = new ExtendedModelMap();
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.reactive;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Contract to render {@link HandlerResult} to the HTTP response.
|
||||
*
|
||||
* <p>In contrast to an {@link org.springframework.core.codec.Encoder Encoder}
|
||||
* which is a singleton and encodes any object of a given type, a {@code View}
|
||||
* is typically selected by name and resolved using a {@link ViewResolver}
|
||||
* which may for example match it to an HTML template. Furthermore a {@code View}
|
||||
* may render based on multiple attributes contained in the model.
|
||||
*
|
||||
* <p>A {@code View} can also choose to select an attribute from the model use
|
||||
* any existing {@code Encoder} to render alternate media types.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public interface View {
|
||||
|
||||
/**
|
||||
* Return the list of media types this encoder supports.
|
||||
*/
|
||||
List<MediaType> getSupportedMediaTypes();
|
||||
|
||||
/**
|
||||
* Render the view based on the given {@link HandlerResult}. Implementations
|
||||
* can access and use the model or only a specific attribute in it.
|
||||
* @param result the result from handler execution
|
||||
* @param contentType the content type selected to render with which should
|
||||
* match one of the {@link #getSupportedMediaTypes() supported media types}.
|
||||
* @param exchange the current exchange
|
||||
* @return the output stream
|
||||
*/
|
||||
Flux<DataBuffer> render(HandlerResult result, Optional<MediaType> contentType, ServerWebExchange exchange);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Contract to resolve a view name to a {@link View} instance. The view name may
|
||||
* correspond to an HTML template or be generated dynamically.
|
||||
*
|
||||
* <p>The process of view resolution is driven through a ViewResolver-based
|
||||
* {@code HandlerResultHandler} implementation called
|
||||
* {@link org.springframework.web.reactive.view.ViewResolverResultHandler
|
||||
* ViewResolverResultHandler}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @see org.springframework.web.reactive.view.ViewResolverResultHandler
|
||||
|
||||
*/
|
||||
public interface ViewResolver {
|
||||
|
||||
/**
|
||||
* Resolve the view name to a View instance.
|
||||
* @param viewName the name of the view to resolve
|
||||
* @param locale the locale for the request
|
||||
* @return the resolved view or an empty stream
|
||||
*/
|
||||
Mono<View> resolveViewName(String viewName, Locale locale);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.reactive.view;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.HandlerResultHandler;
|
||||
import org.springframework.web.reactive.View;
|
||||
import org.springframework.web.reactive.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
|
||||
/**
|
||||
* {@code HandlerResultHandler} that resolves a String return value from a
|
||||
* handler to a {@link View} which is then used to render the response.
|
||||
* A handler may also return a {@code View} instance and/or async variants that
|
||||
* provide a String view name or a {@code View}.
|
||||
*
|
||||
* <p>This result handler should be ordered after others that may also interpret
|
||||
* a String return value for example in combination with {@code @ResponseBody}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ViewResolverResultHandler implements HandlerResultHandler {
|
||||
|
||||
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
|
||||
public ViewResolverResultHandler(List<ViewResolver> resolvers, ConversionService service) {
|
||||
Assert.notEmpty(resolvers, "At least one ViewResolver is required.");
|
||||
Assert.notNull(service, "'conversionService' is required.");
|
||||
this.viewResolvers.addAll(resolvers);
|
||||
this.conversionService = service;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a read-only list of view resolvers.
|
||||
*/
|
||||
public List<ViewResolver> getViewResolvers() {
|
||||
return Collections.unmodifiableList(this.viewResolvers);
|
||||
}
|
||||
|
||||
|
||||
// TODO: @ModelAttribute return value, declared Object return value (either String or View)
|
||||
|
||||
@Override
|
||||
public boolean supports(HandlerResult result) {
|
||||
Class<?> clazz = result.getReturnValueType().getRawClass();
|
||||
if (isViewNameOrViewReference(clazz)) {
|
||||
return true;
|
||||
}
|
||||
if (this.conversionService.canConvert(clazz, Mono.class)) {
|
||||
clazz = result.getReturnValueType().getGeneric(0).getRawClass();
|
||||
return isViewNameOrViewReference(clazz);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isViewNameOrViewReference(Class<?> clazz) {
|
||||
return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
|
||||
|
||||
Mono<?> returnValueMono;
|
||||
if (this.conversionService.canConvert(result.getReturnValueType().getRawClass(), Mono.class)) {
|
||||
returnValueMono = this.conversionService.convert(result.getReturnValue().get(), Mono.class);
|
||||
}
|
||||
else if (result.getReturnValue().isPresent()) {
|
||||
returnValueMono = Mono.just(result.getReturnValue().get());
|
||||
}
|
||||
else {
|
||||
Optional<String> viewName = getDefaultViewName(result, exchange);
|
||||
if (viewName.isPresent()) {
|
||||
returnValueMono = Mono.just(viewName.get());
|
||||
}
|
||||
else {
|
||||
returnValueMono = Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " +
|
||||
"neither returned a view name nor a View object"));
|
||||
}
|
||||
}
|
||||
|
||||
return returnValueMono.then(returnValue -> {
|
||||
if (returnValue instanceof View) {
|
||||
Flux<DataBuffer> body = ((View) returnValue).render(result, Optional.empty(), exchange);
|
||||
return exchange.getResponse().setBody(body);
|
||||
}
|
||||
else if (returnValue instanceof CharSequence) {
|
||||
String viewName = returnValue.toString();
|
||||
Locale locale = Locale.getDefault(); // TODO
|
||||
return Flux.fromIterable(getViewResolvers())
|
||||
.concatMap(resolver -> resolver.resolveViewName(viewName, locale))
|
||||
.next()
|
||||
.then(view -> {
|
||||
Flux<DataBuffer> body = view.render(result, Optional.empty(), exchange);
|
||||
return exchange.getResponse().setBody(body);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Should not happen
|
||||
return Mono.error(new IllegalStateException(
|
||||
"Unexpected return value: " + returnValue.getClass()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Optional<String> getDefaultViewName(HandlerResult result, ServerWebExchange exchange) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Support for result handling through view resolution.
|
||||
*/
|
||||
package org.springframework.web.reactive.view;
|
||||
Reference in New Issue
Block a user