org.thymeleaf.spring4.view.ThymeleafViewResolver Maven / Gradle / Ivy
Show all versions of thymeleaf-spring4 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.spring4.view;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
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.core.Ordered;
import org.springframework.util.PatternMatchUtils;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractCachingViewResolver;
import org.springframework.web.servlet.view.InternalResourceView;
import org.springframework.web.servlet.view.RedirectView;
import org.thymeleaf.ITemplateEngine;
/**
*
* Implementation of the Spring MVC {@link org.springframework.web.servlet.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 subclasses of
* {@link AbstractThymeleafView}. By default, {@link ThymeleafView} is used.
*
*
* @author Daniel Fernández
*
* @since 1.0
*
*/
public class ThymeleafViewResolver
extends AbstractCachingViewResolver
implements Ordered {
private static final Logger vrlogger = LoggerFactory.getLogger(ThymeleafViewResolver.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:";
/**
*
* Prefix to be used in view names (returned by controllers) for specifying an
* HTTP forward.
*
*
* Value: forward:
*
*/
public static final String FORWARD_URL_PREFIX = "forward:";
private boolean redirectContextRelative = true;
private boolean redirectHttp10Compatible = true;
private boolean alwaysProcessRedirectAndForward = true;
private Class extends AbstractThymeleafView> viewClass = ThymeleafView.class;
private String[] viewNames = null;
private String[] excludedViewNames = null;
private int order = Integer.MAX_VALUE;
private final Map staticVariables = new LinkedHashMap(10);
private String contentType = null;
private String characterEncoding = null;
private ITemplateEngine templateEngine;
/**
*
* Create an instance of ThymeleafViewResolver.
*
*/
public ThymeleafViewResolver() {
super();
}
/**
*
* Set the view class that should be used to create views. This must be a subclass
* of {@link AbstractThymeleafView}. The default value is {@link ThymeleafView}.
*
*
* @param viewClass class that is assignable to the required view class
* (by default, ThmeleafView).
*
* @since 2.0.9
*
*/
public void setViewClass(final Class extends AbstractThymeleafView> viewClass) {
if (viewClass == null || !AbstractThymeleafView.class.isAssignableFrom(viewClass)) {
throw new IllegalArgumentException(
"Given view class [" + (viewClass != null ? viewClass.getName() : null) +
"] is not of type [" + AbstractThymeleafView.class.getName() + "]");
}
this.viewClass = viewClass;
}
/**
*
* Return the view class to be used to create views.
*
*
* @return the view class.
*
* @since 2.0.9
*
*/
protected Class extends AbstractThymeleafView> getViewClass() {
return this.viewClass;
}
/**
*
* Returns the Thymeleaf template engine instance to be used for the
* execution of templates.
*
*
* @return the template engine being used for processing templates.
*/
public ITemplateEngine getTemplateEngine() {
return this.templateEngine;
}
/**
*
* Sets the Template Engine instance to be used for processing
* templates.
*
*
* @param templateEngine the template engine to be used
*/
public void setTemplateEngine(final ITemplateEngine 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 MVC 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 MVC 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 content type to be used when rendering views.
*
*
* This content type acts as a default, so that every view
* resolved by this resolver will use this content type unless there
* is a bean defined for such view that specifies a different content type.
*
*
* Therefore, individual views are allowed to specify their own content type
* regardless of the application-wide setting established here.
*
*
* If a content type is not specified (either here or at a specific view definition),
* {@link AbstractThymeleafView#DEFAULT_CONTENT_TYPE} will be used.
*
*
* @param contentType the content type to be used.
*/
public void setContentType(final String contentType) {
this.contentType = contentType;
}
/**
*
* Returns the content type that will be set into views resolved by this
* view resolver.
*
*
* This content type acts as a default, so that every view
* resolved by this resolver will use this content type unless there
* is a bean defined for such view that specifies a different content type.
*
*
* Therefore, individual views are allowed to specify their own content type
* regardless of the application-wide setting established here.
*
*
* If a content type is not specified (either at the view resolver or at a specific
* view definition), {@link AbstractThymeleafView#DEFAULT_CONTENT_TYPE} will be used.
*
*
* @return the content type currently configured
*/
public String getContentType() {
return this.contentType;
}
/**
*
* Specifies the character encoding to be set into the response when
* the view is rendered.
*
*
* Many times, character encoding is specified as a part of the content
* type, using the {@link #setContentType(String)} or
* {@link AbstractThymeleafView#setContentType(String)}, but this is not mandatory,
* and it could be that only the MIME type is specified that way, thus allowing
* to set the character encoding using this method.
*
*
* As with {@link #setContentType(String)}, the value specified here acts as a
* default in case no character encoding has been specified at the view itself.
* If a view bean exists with the name of the view to be processed, and this
* view has been set a value for its {@link AbstractThymeleafView#setCharacterEncoding(String)}
* method, the value specified at the view resolver has no effect.
*
*
* @param characterEncoding the character encoding to be used (e.g. UTF-8,
* ISO-8859-1, etc.)
*/
public void setCharacterEncoding(final String characterEncoding) {
this.characterEncoding = characterEncoding;
}
/**
*
* Returns the character encoding set to be used for all views resolved by
* this view resolver.
*
*
* Many times, character encoding is specified as a part of the content
* type, using the {@link #setContentType(String)} or
* {@link AbstractThymeleafView#setContentType(String)}, but this is not mandatory,
* and it could be that only the MIME type is specified that way, thus allowing
* to set the character encoding using the {@link #setCharacterEncoding(String)}
* counterpart of this getter method.
*
*
* As with {@link #setContentType(String)}, the value specified here acts as a
* default in case no character encoding has been specified at the view itself.
* If a view bean exists with the name of the view to be processed, and this
* view has been set a value for its {@link AbstractThymeleafView#setCharacterEncoding(String)}
* method, the value specified at the view resolver has no effect.
*
*
* @return the character encoding to be set at a view-resolver-wide level.
*/
public String getCharacterEncoding() {
return this.characterEncoding;
}
/**
*
* Set whether to interpret a given redirect URL that starts with a slash ("/")
* as relative to the current ServletContext, i.e. as relative to the web application root.
*
*
* Default is true: A redirect URL that starts with a slash will be interpreted
* as relative to the web application root, i.e. the context path will be prepended to the URL.
*
*
* Redirect URLs can be specified via the "redirect:" prefix. e.g.:
* "redirect:myAction.do".
*
*
* @param redirectContextRelative whether redirect URLs should be considered context-relative or not.
* @see RedirectView#setContextRelative(boolean)
*/
public void setRedirectContextRelative(final boolean redirectContextRelative) {
this.redirectContextRelative = redirectContextRelative;
}
/**
*
* Return whether to interpret a given redirect URL that starts with a slash ("/")
* as relative to the current ServletContext, i.e. as relative to the web application root.
*
*
* Default is true.
*
*
* @return true if redirect URLs will be considered relative to context, false if not.
* @see RedirectView#setContextRelative(boolean)
*/
public boolean isRedirectContextRelative() {
return this.redirectContextRelative;
}
/**
*
* Set whether redirects should stay compatible with HTTP 1.0 clients.
*
*
* In the default implementation (default is true), this will enforce HTTP status
* code 302 in any case, i.e. delegate to
* {@link javax.servlet.http.HttpServletResponse#sendRedirect(String)}. Turning this off
* will send HTTP status code 303, which is the correct code for HTTP 1.1 clients, but not understood
* by HTTP 1.0 clients.
*
*
* Many HTTP 1.1 clients treat 302 just like 303, not making any difference. However, some clients
* depend on 303 when redirecting after a POST request; turn this flag off in such a scenario.
*
*
* Redirect URLs can be specified via the "redirect:" prefix. e.g.:
* "redirect:myAction.do"
*
*
* @param redirectHttp10Compatible true if redirects should stay compatible with HTTP 1.0 clients,
* false if not.
* @see RedirectView#setHttp10Compatible(boolean)
*/
public void setRedirectHttp10Compatible(final boolean redirectHttp10Compatible) {
this.redirectHttp10Compatible = redirectHttp10Compatible;
}
/**
*
* Return whether redirects should stay compatible with HTTP 1.0 clients.
*
*
* Default is true.
*
*
* @return whether redirect responses should stay compatible with HTTP 1.0 clients.
* @see RedirectView#setHttp10Compatible(boolean)
*/
public boolean isRedirectHttp10Compatible() {
return this.redirectHttp10Compatible;
}
/**
*
* 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 JSP 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.
*
* @since 3.0.0
*
*/
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 JSP 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.
*
* @since 3.0.0
*
*/
public boolean getAlwaysProcessRedirectAndForward() {
return this.alwaysProcessRedirectAndForward;
}
/**
*
* Specify a set of name patterns that will applied to determine whether a view name
* returned by a controller will be resolved by this resolver or not.
*
*
* In applications configuring several view resolvers –for example, one for Thymeleaf
* and another one for JSP+JSTL legacy pages–, this property establishes when
* a view will be considered to be resolved by this view resolver and when Spring should
* simply ask the next resolver in the chain –according to its order–
* instead.
*
*
* The specified view name patterns can be complete view names, but can also use
* the * wildcard: "index.*", "user_*", "admin/*", etc.
*
*
* Also note that these view name patterns are checked before applying any prefixes
* or suffixes to the view name, so they should not include these. Usually therefore, you
* would specify orders/* instead of /WEB-INF/templates/orders/*.html.
*
*
* @param viewNames the view names (actually view name patterns)
* @see PatternMatchUtils#simpleMatch(String[], String)
*/
public void setViewNames(final String[] viewNames) {
this.viewNames = viewNames;
}
/**
*
* Return the set of name patterns that will applied to determine whether a view name
* returned by a controller will be resolved by this resolver or not.
*
*
* In applications configuring several view resolvers –for example, one for Thymeleaf
* and another one for JSP+JSTL legacy pages–, this property establishes when
* a view will be considered to be resolved by this view resolver and when Spring should
* simply ask the next resolver in the chain –according to its order–
* instead.
*
*
* The specified view name patterns can be complete view names, but can also use
* the * wildcard: "index.*", "user_*", "admin/*", etc.
*
*
* Also note that these view name patterns are checked before applying any prefixes
* or suffixes to the view name, so they should not include these. Usually therefore, you
* would specify orders/* instead of /WEB-INF/templates/orders/*.html.
*
*
* @return the view name patterns
* @see PatternMatchUtils#simpleMatch(String[], String)
*/
public String[] getViewNames() {
return this.viewNames;
}
/**
*
* Specify names of views –patterns, in fact– that cannot
* be handled by this view resolver.
*
*
* These patterns can be specified in the same format as those in
* {@link #setViewNames(String[])}, but work as an exclusion list.
*
*
* @param excludedViewNames the view names to be excluded (actually view name patterns)
* @see ThymeleafViewResolver#setViewNames(String[])
* @see PatternMatchUtils#simpleMatch(String[], String)
*/
public void setExcludedViewNames(final String[] excludedViewNames) {
this.excludedViewNames = excludedViewNames;
}
/**
*
* Returns the names of views –patterns, in fact– that cannot
* be handled by this view resolver.
*
*
* These patterns can be specified in the same format as those in
* {@link #setViewNames(String[])}, but work as an exclusion list.
*
*
* @return the excluded view name patterns
* @see ThymeleafViewResolver#getViewNames()
* @see PatternMatchUtils#simpleMatch(String[], String)
*/
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
protected View createView(final String viewName, final Locale locale) throws Exception {
// 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 ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
}
// Process redirects (HTTP redirects)
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length(), viewName.length());
final RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
}
// Process forwards (to JSP resources)
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
// The "forward:" prefix will actually create a Servlet/JSP view, and that's precisely its aim per the Spring
// documentation. See http://docs.spring.io/spring-framework/docs/4.2.4.RELEASE/spring-framework-reference/html/mvc.html#mvc-redirecting-forward-prefix
vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
final String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length(), viewName.length());
return new InternalResourceView(forwardUrl);
}
// 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 ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
}
vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a " +
"{} instance will be created for it", viewName, this.viewClass.getSimpleName());
return loadView(viewName, locale);
}
@Override
protected View loadView(final String viewName, final Locale locale) throws Exception {
final AutowireCapableBeanFactory beanFactory = getApplicationContext().getAutowireCapableBeanFactory();
final boolean viewBeanExists = beanFactory.containsBean(viewName);
final Class> viewBeanType = viewBeanExists? beanFactory.getType(viewName) : null;
final AbstractThymeleafView view;
if (viewBeanExists && viewBeanType != null && AbstractThymeleafView.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 AbstractThymeleafView viewInstance = BeanUtils.instantiateClass(getViewClass());
view = (AbstractThymeleafView) beanFactory.configureBean(viewInstance, viewName);
} else {
// This is a prototype bean. Use it as such.
view = (AbstractThymeleafView) beanFactory.getBean(viewName);
}
} else {
final AbstractThymeleafView 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 = (AbstractThymeleafView) 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 = (AbstractThymeleafView) 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);
}
if (!view.isContentTypeSet() && getContentType() != null) {
view.setContentType(getContentType());
}
if (view.getLocale() == null && locale != null) {
view.setLocale(locale);
}
if (view.getCharacterEncoding() == null && getCharacterEncoding() != null) {
view.setCharacterEncoding(getCharacterEncoding());
}
return view;
}
}