org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver Maven / Gradle / Ivy
Show all versions of thymeleaf-spring5 Show documentation
/*
* =============================================================================
*
* Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org)
*
* 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.thymeleaf.spring5.view.reactive;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.util.PatternMatchUtils;
import org.springframework.web.reactive.result.view.RedirectView;
import org.springframework.web.reactive.result.view.View;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.reactive.result.view.ViewResolverSupport;
import org.thymeleaf.spring5.ISpringWebReactiveTemplateEngine;
import org.thymeleaf.util.Validate;
import reactor.core.publisher.Mono;
/**
*
* Implementation of the Spring Web Reactive {@link ViewResolver}
* interface.
*
*
* View resolvers execute after the controller ends its execution. They receive the name
* of the view to be processed and are in charge of creating (and configuring) the
* corresponding {@link View} object for it.
*
*
* The {@link View} implementations managed by this class are of type {@link ThymeleafReactiveView}.
*
*
* @see ThymeleafReactiveView
* @see ISpringWebReactiveTemplateEngine
*
* @author Daniel Fernández
*
* @since 3.0.3
*
*/
public class ThymeleafReactiveViewResolver extends ViewResolverSupport implements ViewResolver {
private static final Logger vrlogger = LoggerFactory.getLogger(ThymeleafReactiveViewResolver.class);
/**
*
* Prefix to be used in view names (returned by controllers) for specifying an
* HTTP redirect.
*
*
* Value: redirect:
*
*/
public static final String REDIRECT_URL_PREFIX = "redirect:";
// TODO * Will this still exist in Spring Web Reactive? See https://jira.spring.io/browse/SPR-14537
public static final String FORWARD_URL_PREFIX = "forward:";
// This provider function for redirect mirrors what is done at the reactive version of UrlBasedViewResolver
private Function redirectViewProvider = url -> new RedirectView(url);
private boolean alwaysProcessRedirectAndForward = true;
private Class extends ThymeleafReactiveView> viewClass = ThymeleafReactiveView.class;
private String[] viewNames = null;
private String[] excludedViewNames = null;
private int order = Integer.MAX_VALUE;
private final Map staticVariables = new LinkedHashMap(10);
// This will determine whether we will be throttling or not, and if so the size of the chunks that will be produced
// by the throttled engine each time the back-pressure mechanism asks for a new "unit" (a new DataBuffer)
//
// The value established here will be a default value, which can be overridden by specific views at the
// ThymeleafReactiveView class
private int responseMaxChunkSizeBytes = ThymeleafReactiveView.DEFAULT_RESPONSE_CHUNK_SIZE_BYTES;
private ISpringWebReactiveTemplateEngine templateEngine;
/**
*
* Create an instance of ThymeleafReactiveViewResolver.
*
*/
public ThymeleafReactiveViewResolver() {
super();
}
/**
*
* Set the view class that should be used to create views. This must be a subclass
* of {@link ThymeleafReactiveView}.
*
*
* @param viewClass class that is assignable to the required view class
* (by default, ThymeleafReactiveView).
*/
public void setViewClass(final Class extends ThymeleafReactiveView> viewClass) {
if (viewClass == null || !ThymeleafReactiveView.class.isAssignableFrom(viewClass)) {
throw new IllegalArgumentException(
"Given view class [" + (viewClass != null ? viewClass.getName() : null) +
"] is not of type [" + ThymeleafReactiveView.class.getName() + "]");
}
this.viewClass = viewClass;
}
protected Class extends ThymeleafReactiveView> getViewClass() {
return this.viewClass;
}
/**
*
* Returns the Thymeleaf template engine instance
* (implementation of {@link ISpringWebReactiveTemplateEngine} to be used for the
* execution of templates.
*
*
* @return the template engine being used for processing templates.
*/
public ISpringWebReactiveTemplateEngine getTemplateEngine() {
return this.templateEngine;
}
/**
*
* Set the template engine object (implementation of {@link ISpringWebReactiveTemplateEngine} to be
* used for processing templates.
*
*
* @param templateEngine the template engine.
*/
public void setTemplateEngine(final ISpringWebReactiveTemplateEngine templateEngine) {
this.templateEngine = templateEngine;
}
/**
*
* Return the static variables, which will be available at the context
* every time a view resolved by this ViewResolver is processed.
*
*
* These static variables are added to the context by the view resolver
* before every view is processed, so that they can be referenced from
* the context like any other context variables, for example:
* ${myStaticVar}.
*
*
* @return the map of static variables to be set into views' execution.
*/
public Map getStaticVariables() {
return Collections.unmodifiableMap(this.staticVariables);
}
/**
*
* Add a new static variable.
*
*
* These static variables are added to the context by the view resolver
* before every view is processed, so that they can be referenced from
* the context like any other context variables, for example:
* ${myStaticVar}.
*
*
* @param name the name of the static variable
* @param value the value of the static variable
*/
public void addStaticVariable(final String name, final Object value) {
this.staticVariables.put(name, value);
}
/**
*
* Sets a set of static variables, which will be available at the context
* every time a view resolved by this ViewResolver is processed.
*
*
* This method does not overwrite the existing static variables, it
* simply adds the ones specify to any variables already registered.
*
*
* These static variables are added to the context by the view resolver
* before every view is processed, so that they can be referenced from
* the context like any other context variables, for example:
* ${myStaticVar}.
*
*
*
* @param variables the set of variables to be added.
*/
public void setStaticVariables(final Map variables) {
if (variables != null) {
for (final Map.Entry entry : variables.entrySet()) {
addStaticVariable(entry.getKey(), entry.getValue());
}
}
}
/**
*
* Specify the order in which this view resolver will be queried.
*
*
* Spring Web applications can have several view resolvers configured,
* and this order property established the order in which
* they will be queried for view resolution.
*
*
* @param order the order in which this view resolver will be asked to resolve
* the view.
*/
public void setOrder(final int order) {
this.order = order;
}
/**
*
* Returns the order in which this view resolver will be queried.
*
*
* Spring Web applications can have several view resolvers configured,
* and this order property established the order in which
* they will be queried for view resolution.
*
*
* @return the order
*/
public int getOrder() {
return this.order;
}
/**
*
* Sets the provider function for creating {@link RedirectView} instances when a redirect
* request is passed to the view resolver.
*
*
* Note the parameter specified to the function will be the URL of the redirect
* (as specified in the view name returned by the controller, without the redirect:
* prefix).
*
*
* @param redirectViewProvider the redirect-view provider function.
*/
public void setRedirectViewProvider(final Function redirectViewProvider) {
Validate.notNull(redirectViewProvider, "RedirectView provider cannot be null");
this.redirectViewProvider = redirectViewProvider;
}
/**
*
* Returns the provider function for creating {@link RedirectView} instances when a redirect
* request is passed to the view resolver.
*
*
* Note the parameter specified to the function will be the URL of the redirect
* (as specified in the view name returned by the controller, without the redirect:
* prefix).
*
*
* @return the redirect-view provider function.
*/
public Function getRedirectViewProvider() {
return this.redirectViewProvider;
}
/**
*
* Set whether this view resolver should always process forwards and redirects independently of the value of
* the viewNames property.
*
*
* When this flag is set to true (default value), any view name that starts with the
* redirect: or forward: prefixes will be resolved by this ViewResolver even if the view names
* would not match what is established at the viewNames property.
*
*
* Note that the behaviour of resolving view names with these prefixes is exactly the same with this
* flag set to true or false (perform an HTTP redirect or forward to an internal resource).
* The only difference is whether the prefixes have to be explicitly specified at viewNames or not.
*
*
* Default value is true.
*
*
* @param alwaysProcessRedirectAndForward true if redirects and forwards are always processed, false if this will
* depend on what is established at the viewNames property.
*/
public void setAlwaysProcessRedirectAndForward(final boolean alwaysProcessRedirectAndForward) {
this.alwaysProcessRedirectAndForward = alwaysProcessRedirectAndForward;
}
/**
*
* Return whether this view resolver should always process forwards and redirects independently of the value of
* the viewNames property.
*
*
* When this flag is set to true (default value), any view name that starts with the
* redirect: or forward: prefixes will be resolved by this ViewResolver even if the view names
* would not match what is established at the viewNames property.
*
*
* Note that the behaviour of resolving view names with these prefixes is exactly the same with this
* flag set to true or false (perform an HTTP redirect or forward to an internal resource).
* The only difference is whether the prefixes have to be explicitly specified at viewNames or not.
*
*
* Default value is true.
*
*
* @return whether redirects and forwards will be always processed by this view resolver or else only when they are
* matched by the viewNames property.
*
*/
public boolean getAlwaysProcessRedirectAndForward() {
return this.alwaysProcessRedirectAndForward;
}
/**
*
* Set the maximum size (in bytes) allowed for the chunks ({@link org.springframework.core.io.buffer.DataBuffer})
* that are produced by the Thymeleaf engine and passed to the server as output.
*
*
* In Spring Web Reactive applications, Thymeleaf has three modes of operation depending on whether a limit
* has been set for the output chunk size and/or data-driver context variables have been specified:
*
*
* - FULL, when no limit for max chunk size is established and no data-driver context variable
* has been specified. All template output will be generated in memory and then sent to the server's
* output channels as a single {@link org.springframework.core.io.buffer.DataBuffer}.
* - CHUNKED, when a limit for max chunk size is established but no data-driver context
* variable has been specified. Template output will be generated in chunks of a size equal or less
* than the specified limit (in bytes) and then sent to the server's output channels. After each chunk
* is sent to output, the template engine will stop (thanks to its throttling mechanism), and
* wait for the server to request more chunks by means of reactive backpressure. Note all of
* this mechanism works single-threaded. This execution mode will also force the server to perform
* output flush operations after each chunk is sent from Thymeleaf.
* - DATA-DRIVEN, when a data-driver variable has been specified at the context
* (implementing {@link org.thymeleaf.spring5.context.reactive.IReactiveDataDriverContextVariable}). This
* variable is expected to contain a data stream (usually in the shape of a
* {@link org.reactivestreams.Publisher} that Thymeleaf will consume, creating markup output as data
* is streamed from this data-driver and letting the output channels of the server throttle
* template engine execution by means of back-pressure. Additionally, depending on whether a value has
* been specified for this property or not, Thymeleaf will never generate
* {@link org.springframework.core.io.buffer.DataBuffer} output chunks larger than the specified size,
* and will request the server to perform an output flush operation after each chunk is produced.
*
*
* If this property is set to -1 or Integer.MAX_VALUE, no size limit will be used. Note also
* that there is no limit set by default.
*
*
* @param responseMaxChunkSizeBytes the maximum size in bytes for output chunks
* ({@link org.springframework.core.io.buffer.DataBuffer} objects), or
* -1 or Integer.MAX_VALUE if no limit is to be used.
*/
public void setResponseMaxChunkSizeBytes(final int responseMaxChunkSizeBytes) {
this.responseMaxChunkSizeBytes = responseMaxChunkSizeBytes;
}
/**
*
* Return the maximum size (in bytes) allowed for the chunks
* ({@link org.springframework.core.io.buffer.DataBuffer}) that are produced by the Thymeleaf engine and passed
* to the server as output.
*
*
* In Spring Web Reactive applications, Thymeleaf has three modes of operation depending on whether a limit
* has been set for the output chunk size and/or data-driver context variables have been specified:
*
*
* - FULL, when no limit for max chunk size is established and no data-driver context variable
* has been specified. All template output will be generated in memory and then sent to the server's
* output channels as a single {@link org.springframework.core.io.buffer.DataBuffer}.
* - CHUNKED, when a limit for max chunk size is established but no data-driver context
* variable has been specified. Template output will be generated in chunks of a size equal or less
* than the specified limit (in bytes) and then sent to the server's output channels. After each chunk
* is sent to output, the template engine will stop (thanks to its throttling mechanism), and
* wait for the server to request more chunks by means of reactive backpressure. Note all of
* this mechanism works single-threaded. This execution mode will also force the server to perform
* output flush operations after each chunk is sent from Thymeleaf.
* - DATA-DRIVEN, when a data-driver variable has been specified at the context
* (implementing {@link org.thymeleaf.spring5.context.reactive.IReactiveDataDriverContextVariable}). This
* variable is expected to contain a data stream (usually in the shape of a
* {@link org.reactivestreams.Publisher} that Thymeleaf will consume, creating markup output as data
* is streamed from this data-driver and letting the output channels of the server throttle
* template engine execution by means of back-pressure. Additionally, depending on whether a value has
* been specified for this property or not, Thymeleaf will never generate
* {@link org.springframework.core.io.buffer.DataBuffer} output chunks larger than the specified size,
* and will request the server to perform an output flush operation after each chunk is produced.
*
*
* If this property is set to -1 or Integer.MAX_VALUE, no size limit will be used. Note also
* that there is no limit set by default.
*
*
* @return the maximum size in bytes for output chunks
* ({@link org.springframework.core.io.buffer.DataBuffer} objects), or
* -1 or Integer.MAX_VALUE if no limit is to be used.
*/
public int getResponseMaxChunkSizeBytes() {
return this.responseMaxChunkSizeBytes;
}
public void setViewNames(final String[] viewNames) {
this.viewNames = viewNames;
}
public String[] getViewNames() {
return this.viewNames;
}
public void setExcludedViewNames(final String[] excludedViewNames) {
this.excludedViewNames = excludedViewNames;
}
public String[] getExcludedViewNames() {
return this.excludedViewNames;
}
protected boolean canHandle(final String viewName, @SuppressWarnings("unused") final Locale locale) {
final String[] viewNamesToBeProcessed = getViewNames();
final String[] viewNamesNotToBeProcessed = getExcludedViewNames();
return ((viewNamesToBeProcessed == null || PatternMatchUtils.simpleMatch(viewNamesToBeProcessed, viewName)) &&
(viewNamesNotToBeProcessed == null || !PatternMatchUtils.simpleMatch(viewNamesNotToBeProcessed, viewName)));
}
@Override
public Mono resolveViewName(final String viewName, final Locale locale) {
// First possible call to check "viewNames": before processing redirects and forwards
if (!this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafReactiveViewResolver. Passing on to the next resolver in the chain.", viewName);
return Mono.empty();
}
// Process redirects (HTTP redirects)
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafReactiveViewResolver.", viewName);
final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
final RedirectView view = this.redirectViewProvider.apply(redirectUrl);
final RedirectView initializedView =
(RedirectView) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
return Mono.just(initializedView);
}
// Process forwards (to JSP resources)
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafReactiveViewResolver.", viewName);
// TODO * No view forwarding in Spring Reactive yet. See https://jira.spring.io/browse/SPR-14537
return Mono.error(new UnsupportedOperationException("Forwards are not currently supported by ThymeleafReactiveViewResolver"));
}
// Second possible call to check "viewNames": after processing redirects and forwards
if (this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafReactiveViewResolver. Passing on to the next resolver in the chain.", viewName);
return Mono.empty();
}
vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafReactiveViewResolver and a " +
"{} instance will be created for it", viewName, getViewClass().getSimpleName());
return loadView(viewName, locale);
}
protected Mono loadView(final String viewName, final Locale locale) {
final AutowireCapableBeanFactory beanFactory = getApplicationContext().getAutowireCapableBeanFactory();
final boolean viewBeanExists = beanFactory.containsBean(viewName);
final Class> viewBeanType = viewBeanExists? beanFactory.getType(viewName) : null;
final ThymeleafReactiveView view;
if (viewBeanExists && viewBeanType != null && ThymeleafReactiveView.class.isAssignableFrom(viewBeanType)) {
// AppCtx has a bean with name == viewName, and it is a View bean. So let's use it as a prototype!
//
// This can mean two things: if the bean has been defined with scope "prototype", we will just use it.
// If it hasn't we will create a new instance of the view class and use its properties in order to
// configure this view instance (so that we don't end up using the same bean from several request threads).
//
// Note that, if Java-based configuration is used, using @Scope("prototype") would be the only viable
// possibility here.
final BeanDefinition viewBeanDefinition =
(beanFactory instanceof ConfigurableListableBeanFactory ?
((ConfigurableListableBeanFactory)beanFactory).getBeanDefinition(viewName) :
null);
if (viewBeanDefinition == null || !viewBeanDefinition.isPrototype()) {
// No scope="prototype", so we will just apply its properties. This should only happen with XML config.
final ThymeleafReactiveView viewInstance = BeanUtils.instantiateClass(getViewClass());
view = (ThymeleafReactiveView) beanFactory.configureBean(viewInstance, viewName);
} else {
// This is a prototype bean. Use it as such.
view = (ThymeleafReactiveView) beanFactory.getBean(viewName);
}
} else {
final ThymeleafReactiveView viewInstance = BeanUtils.instantiateClass(getViewClass());
if (viewBeanExists && viewBeanType == null) {
// AppCtx has a bean with name == viewName, but it is an abstract bean. We still can use it as a prototype.
// The AUTOWIRE_NO mode applies autowiring only through annotations
beanFactory.autowireBeanProperties(viewInstance, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
// A bean with this name exists, so we apply its properties
beanFactory.applyBeanPropertyValues(viewInstance, viewName);
// Finally, we let Spring do the remaining initializations (incl. proxifying if needed)
view = (ThymeleafReactiveView) beanFactory.initializeBean(viewInstance, viewName);
} else {
// Either AppCtx has no bean with name == viewName, or it is of an incompatible class. No prototyping done.
// The AUTOWIRE_NO mode applies autowiring only through annotations
beanFactory.autowireBeanProperties(viewInstance, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
// Finally, we let Spring do the remaining initializations (incl. proxifying if needed)
view = (ThymeleafReactiveView) beanFactory.initializeBean(viewInstance, viewName);
}
}
view.setTemplateEngine(getTemplateEngine());
view.setStaticVariables(getStaticVariables());
// We give view beans the opportunity to specify the template name to be used
if (view.getTemplateName() == null) {
view.setTemplateName(viewName);
}
// We set the media types from the view resolver only if no value has already been set at the view def.
if (!view.isSupportedMediaTypesSet()) {
view.setSupportedMediaTypes(getSupportedMediaTypes());
}
// We set the default charset from the view resolver only if no value has already been set at the view def.
if (!view.isDefaultCharsetSet()) {
view.setDefaultCharset(getDefaultCharset());
}
// We set the locale from the view resolver only if no value has already been set at the view def.
if (locale != null && view.getLocale() == null) {
view.setLocale(locale);
}
/*
* Set the reactive operation-related flags
*/
if (getResponseMaxChunkSizeBytes() != ThymeleafReactiveView.DEFAULT_RESPONSE_CHUNK_SIZE_BYTES && view.getNullableResponseMaxChunkSize() == null) {
view.setResponseMaxChunkSizeBytes(getResponseMaxChunkSizeBytes());
}
return Mono.just(view);
}
}