All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.thymeleaf.spring4.view.ThymeleafViewResolver Maven / Gradle / Ivy

/*
 * =============================================================================
 * 
 *   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 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 boolean forceContentType = false; 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 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 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 establishes 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 establishes 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; } /** *

* Returns whether the configured content type should be forced instead of attempting * a smart content type application based on template name. *

*

* When forced, the configured content type ({@link #setForceContentType(boolean)}) will * be applied even if the template name ends in a known suffix: * .html, .htm, .xhtml, * .xml, .js, .json, * .css, .rss, .atom, .txt. *

*

Default value is false

. * * @return whether the content type will be forced or not. * @since 3.0.6 */ public boolean getForceContentType() { return this.forceContentType; } /** *

* Sets whether the configured content type should be forced instead of attempting * a smart content type application based on template name. *

*

* When forced, the configured content type ({@link #setForceContentType(boolean)}) will * be applied even if the template name ends in a known suffix: * .html, .htm, .xhtml, * .xml, .js, .json, * .css, .rss, .atom, .txt. *

*

Default value is false

. * * @param forceContentType whether the configured template mode should be forced or not. * @since 3.0.6 */ public void setForceContentType(final boolean forceContentType) { this.forceContentType = forceContentType; } /** *

* 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.isForceContentTypeSet()) { view.setForceContentType(getForceContentType()); } 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy