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 {@code 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 {@code 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;
+ }
+
+
+
+ /**
+ *
+ * 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:
+ * {@code .html}, {@code .htm}, {@code .xhtml},
+ * {@code .xml}, {@code .js}, {@code .json},
+ * {@code .css}, {@code .rss}, {@code .atom}, {@code .txt}.
+ *
+ * Default value is {@code 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:
+ * {@code .html}, {@code .htm}, {@code .xhtml},
+ * {@code .xml}, {@code .js}, {@code .json},
+ * {@code .css}, {@code .rss}, {@code .atom}, {@code .txt}.
+ *
+ * Default value is {@code 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. {@code UTF-8},
+ * {@code 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 {@code 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 {@code "redirect:"} prefix. e.g.:
+ * {@code "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 {@code 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 {@code true}), this will enforce HTTP status
+ * code 302 in any case, i.e. delegate to
+ * {@link jakarta.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 {@code "redirect:"} prefix. e.g.:
+ * {@code "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 {@code 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 {@code viewNames} property.
+ *
+ *
+ * When this flag is set to {@code true} (default value), any view name that starts with the
+ * {@code redirect:} or {@code forward:} prefixes will be resolved by this ViewResolver even if the view names
+ * would not match what is established at the {@code viewNames} property.
+ *
+ *
+ * Note that the behaviour of resolving view names with these prefixes is exactly the same with this
+ * flag set to {@code true} or {@code 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 {@code viewNames} or not.
+ *
+ *
+ * Default value is {@code 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 {@code viewNames} property.
+ *
+ *
+ * When this flag is set to {@code true} (default value), any view name that starts with the
+ * {@code redirect:} or {@code forward:} prefixes will be resolved by this ViewResolver even if the view names
+ * would not match what is established at the {@code viewNames} property.
+ *
+ *
+ * Note that the behaviour of resolving view names with these prefixes is exactly the same with this
+ * flag set to {@code true} or {@code 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 {@code viewNames} or not.
+ *
+ *
+ * Default value is {@code true}.
+ *
+ *
+ * @return whether redirects and forwards will be always processed by this view resolver or else only when they are
+ * matched by the {@code viewNames} property.
+ *
+ */
+ public boolean getAlwaysProcessRedirectAndForward() {
+ return this.alwaysProcessRedirectAndForward;
+ }
+
+
+
+
+ /**
+ *
+ * Returns whether Thymeleaf should start producing output –and sending it to the web server's output
+ * buffers– as soon as possible, outputting partial results while processing as they become available so
+ * that they can potentially be sent to the client (browser) before processing of the whole template has
+ * completely finished.
+ *
+ *
+ * If set to {@code false}, no fragments of template result will be sent to the web server's
+ * output buffers until Thymeleaf completely finishes processing the template and generating
+ * the corresponding output. Only once finished will output start to be written to the web server's
+ * output buffers, and therefore sent to the clients.
+ *
+ *
+ * Note that setting this to {@code false} is not recommended for most
+ * scenarios, as it can (very) significantly increase the amount of memory used per
+ * template execution. Only modify this setting if you know what you are doing. A typical
+ * scenario in which setting this to {@code false} could be of use is when an application is
+ * suffering from UI rendering issues (flickering) at the browser due to incremental
+ * rendering of very large templates.
+ *
+ *
+ * Default value is {@code true}.
+ *
+ *
+ * @return whether to start producing output as soon as possible while processing or not (default: {@code true}).
+ * @since 3.0.10
+ */
+ public boolean getProducePartialOutputWhileProcessing() {
+ return this.producePartialOutputWhileProcessing;
+ }
+
+
+ /**
+ *
+ * Sets whether Thymeleaf should start producing output –and sending it to the web server's output
+ * buffers– as soon as possible, outputting partial results while processing as they become available so
+ * that they can potentially be sent to the client (browser) before processing of the whole template has
+ * completely finished.
+ *
+ *
+ * If set to {@code false}, no fragments of template result will be sent to the web server's
+ * output buffers until Thymeleaf completely finishes processing the template and generating
+ * the corresponding output. Only once finished will output start to be written to the web server's
+ * output buffers, and therefore sent to the clients.
+ *
+ *
+ * Note that setting this to {@code false} is not recommended for most
+ * scenarios, as it can (very) significantly increase the amount of memory used per
+ * template execution. Only modify this setting if you know what you are doing. A typical
+ * scenario in which setting this to {@code false} could be of use is when an application is
+ * suffering from UI rendering issues (flickering) at the browser due to incremental
+ * rendering of very large templates.
+ *
+ *
+ * Default value is {@code true}.
+ *
+ *
+ * @param producePartialOutputWhileProcessing whether to start producing output as soon as possible while
+ * processing or not (default: {@code true}).
+ * @since 3.0.10
+ */
+ public void setProducePartialOutputWhileProcessing(final boolean producePartialOutputWhileProcessing) {
+ this.producePartialOutputWhileProcessing = producePartialOutputWhileProcessing;
+ }
+
+
+
+
+ /**
+ *
+ * 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 {@code order}–
+ * instead.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /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 {@code order}–
+ * instead.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /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, REDIRECT_URL_PREFIX);
+ }
+ // 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, getViewClass().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());
+ }
+ if (!view.isProducePartialOutputWhileProcessingSet()) {
+ view.setProducePartialOutputWhileProcessing(getProducePartialOutputWhileProcessing());
+ }
+
+ return view;
+
+ }
+
+
+}
diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveView.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveView.java
new file mode 100644
index 00000000..55b7083f
--- /dev/null
+++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveView.java
@@ -0,0 +1,677 @@
+/*
+ * =============================================================================
+ *
+ * Copyright (c) 2011-2018, 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.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.reactivestreams.Publisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.web.reactive.HandlerMapping;
+import org.springframework.web.reactive.result.view.AbstractView;
+import org.springframework.web.reactive.result.view.RequestContext;
+import org.springframework.web.server.ServerWebExchange;
+import org.thymeleaf.IEngineConfiguration;
+import org.thymeleaf.context.IContext;
+import org.thymeleaf.exceptions.TemplateProcessingException;
+import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
+import org.thymeleaf.spring5.context.webflux.IReactiveDataDriverContextVariable;
+import org.thymeleaf.spring5.context.webflux.ReactiveDataDriverContextVariable;
+import org.thymeleaf.spring5.context.webflux.SpringWebFluxExpressionContext;
+import org.thymeleaf.spring5.context.webflux.SpringWebFluxThymeleafRequestContext;
+import org.thymeleaf.spring5.expression.ThymeleafEvaluationContext;
+import org.thymeleaf.spring5.naming.SpringContextVariableNames;
+import org.thymeleaf.standard.expression.FragmentExpression;
+import org.thymeleaf.standard.expression.IStandardExpressionParser;
+import org.thymeleaf.standard.expression.StandardExpressions;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+
+/**
+ *
+ * Base implementation of the Spring WebFlux {@link org.springframework.web.reactive.result.view.View}
+ * interface.
+ *
+ *
+ * Views represent a template being executed, after being resolved (and
+ * instantiated) by a {@link org.springframework.web.reactive.result.view.ViewResolver}.
+ *
+ *
+ * This is the default view implementation resolved by {@link ThymeleafReactiveViewResolver}.
+ *
+ *
+ * This view needs a {@link ISpringWebFluxTemplateEngine} for execution, and it will call its
+ * {@link ISpringWebFluxTemplateEngine#processStream(String, Set, IContext, DataBufferFactory, MediaType, Charset, int)}
+ * method to create the reactive data streams to be used for processing the template. See the documentation
+ * of this class to know more about the different operation modes available.
+ *
+ *
+ * @see ThymeleafReactiveViewResolver
+ * @see ISpringWebFluxTemplateEngine
+ * @see ReactiveDataDriverContextVariable
+ * @see IReactiveDataDriverContextVariable
+ *
+ * @author Daniel Fernández
+ *
+ * @since 3.0.3
+ *
+ */
+public class ThymeleafReactiveView extends AbstractView implements BeanNameAware {
+
+
+ protected static final Logger logger = LoggerFactory.getLogger(ThymeleafReactiveView.class);
+
+ /**
+ * By default, no max response chunk size is set. Value = {@link Integer#MAX_VALUE}
+ */
+ public static final int DEFAULT_RESPONSE_CHUNK_SIZE_BYTES = Integer.MAX_VALUE;
+
+
+ /**
+ *
+ * This prefix should be used in order to allow dialects to provide reactive stream objects
+ * that should be resolved (in an unblocked manner) just before the execution of the view. The idea is to allow
+ * these streams to be included in the standard reactive Spring view model resolution mechanisms so that Thymeleaf
+ * does not have to block during the execution of the view in order to obtain the value. The result will be as
+ * if reactive stream objects had been added by the controller methods.
+ *
+ *
+ * The name of the attributes being added to the Model will be the name of the execution attribute minus the
+ * prefix. So {@code ThymeleafReactiveModelAdditions:somedata} will result in a Model attribute called
+ * {@code somedata}.
+ *
+ *
+ * Values of these execution attributes are allowed to be:
+ *
+ *
+ * - {@code Publisher>} (including {@code Flux>} and {@code Mono>}).
+ * - {@code Supplier extends Publisher>>}: The supplier will be called at {@code View}
+ * rendering time and the result will be added to the Model.
+ * - {@code Function>}: The function will be called
+ * at {@code View} rendering time and the result will be added to the Model.
+ *
+ *
+ * Value: {@code "ThymeleafReactiveModelAdditions:"}
+ *
+ *
+ * @since 3.0.10
+ */
+ public static final String REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTE_PREFIX = "ThymeleafReactiveModelAdditions:";
+
+ private static final String WEBFLUX_CONVERSION_SERVICE_NAME = "webFluxConversionService";
+
+
+ private String beanName = null;
+ private ISpringWebFluxTemplateEngine templateEngine = null;
+ private String templateName = null;
+ private Locale locale = null;
+ private Map staticVariables = null;
+
+ // These two flags are meant to determine if these fields have been specifically set a
+ // value for this View object, so that we know that the ViewResolver should not be
+ // overriding them with its own view-resolution-wide values.
+ private boolean defaultCharsetSet = false;
+ private boolean supportedMediaTypesSet = false;
+
+ private Set markupSelectors = null;
+
+
+
+ // This will determine whether we will be throttling or not, and if so the maximum 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 is nullable (and null by default) because it will work as an override of the
+ // value established at the ThymeleafReactiveViewResolver for the same purpose.
+ private Integer responseMaxChunkSizeBytes = null;
+
+
+
+
+
+
+ public ThymeleafReactiveView() {
+ super();
+ }
+
+
+
+ public String getMarkupSelector() {
+ return (this.markupSelectors == null || this.markupSelectors.size() == 0? null : this.markupSelectors.iterator().next());
+ }
+
+
+ public void setMarkupSelector(final String markupSelector) {
+ this.markupSelectors =
+ (markupSelector == null || markupSelector.trim().length() == 0? null : Collections.singleton(markupSelector.trim()));
+ }
+
+
+
+ // This flag is used from the ViewResolver in order to determine if it has to push its own
+ // configuration to the View (which it will do until the View has been specifically configured).
+ boolean isDefaultCharsetSet() {
+ return this.defaultCharsetSet;
+ }
+
+
+ // Implemented at AbstractView, but overridden here in order to set the flag
+ @Override
+ public void setDefaultCharset(final Charset defaultCharset) {
+ super.setDefaultCharset(defaultCharset);
+ this.defaultCharsetSet = true;
+ }
+
+
+
+
+ // This flag is used from the ViewResolver in order to determine if it has to push its own
+ // configuration to the View (which it will do until the View has been specifically configured).
+ boolean isSupportedMediaTypesSet() {
+ return this.supportedMediaTypesSet;
+ }
+
+
+ // Implemented at AbstractView, but overridden here in order to set the flag
+ @Override
+ public void setSupportedMediaTypes(final List supportedMediaTypes) {
+ super.setSupportedMediaTypes(supportedMediaTypes);
+ this.supportedMediaTypesSet = true;
+ }
+
+
+
+
+ public String getBeanName() {
+ return this.beanName;
+ }
+
+
+ public void setBeanName(final String beanName) {
+ this.beanName = beanName;
+ }
+
+
+
+
+ public String getTemplateName() {
+ return this.templateName;
+ }
+
+
+ public void setTemplateName(final String templateName) {
+ this.templateName = templateName;
+ }
+
+
+
+
+ protected Locale getLocale() {
+ return this.locale;
+ }
+
+
+ protected void setLocale(final Locale locale) {
+ this.locale = locale;
+
+ }
+
+
+
+
+ // Default is Integer.MAX_VALUE, which means no explicit limit (note there can still be a limit in
+ // the size of the chunks if execution is data driven, as output will be sent to the server after
+ // the processing of each data-driver buffer).
+ public int getResponseMaxChunkSizeBytes() {
+ return this.responseMaxChunkSizeBytes == null?
+ DEFAULT_RESPONSE_CHUNK_SIZE_BYTES : this.responseMaxChunkSizeBytes.intValue();
+ }
+
+
+ // We need this one at the ViewResolver to determine if a value has been set at all
+ Integer getNullableResponseMaxChunkSize() {
+ return this.responseMaxChunkSizeBytes;
+ }
+
+
+ public void setResponseMaxChunkSizeBytes(final int responseMaxBufferSizeBytes) {
+ this.responseMaxChunkSizeBytes = Integer.valueOf(responseMaxBufferSizeBytes);
+ }
+
+
+
+
+ protected ISpringWebFluxTemplateEngine getTemplateEngine() {
+ return this.templateEngine;
+ }
+
+
+ protected void setTemplateEngine(final ISpringWebFluxTemplateEngine templateEngine) {
+ this.templateEngine = templateEngine;
+ }
+
+
+
+
+ public Map getStaticVariables() {
+ if (this.staticVariables == null) {
+ return Collections.emptyMap();
+ }
+ return Collections.unmodifiableMap(this.staticVariables);
+ }
+
+
+ public void addStaticVariable(final String name, final Object value) {
+ if (this.staticVariables == null) {
+ this.staticVariables = new HashMap(3, 1.0f);
+ }
+ this.staticVariables.put(name, value);
+ }
+
+
+ public void setStaticVariables(final Map variables) {
+ if (variables != null) {
+ if (this.staticVariables == null) {
+ this.staticVariables = new HashMap(3, 1.0f);
+ }
+ this.staticVariables.putAll(variables);
+ }
+ }
+
+
+
+
+ @Override
+ public Mono render(final Map model, final MediaType contentType, final ServerWebExchange exchange) {
+ // We will prepare the model for rendering by checking if the configured dialects have specified any execution
+ // attributes to be added to the model during preparation (e.g. reactive streams that will need to be previously
+ // resolved)
+
+ final ISpringWebFluxTemplateEngine viewTemplateEngine = getTemplateEngine();
+
+ if (viewTemplateEngine == null) {
+ return Mono.error(new IllegalArgumentException("Property 'thymeleafTemplateEngine' is required"));
+ }
+
+ final IEngineConfiguration configuration = viewTemplateEngine.getConfiguration();
+ final Map executionAttributes = configuration.getExecutionAttributes();
+
+ // Process the execution attributes and look for possible reactive objects that should be added for resolution
+
+ Map enrichedModel = null;
+ for (final String executionAttributeName : executionAttributes.keySet()) {
+
+ if (executionAttributeName != null && executionAttributeName.startsWith(REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTE_PREFIX)) {
+ // This execution attribute defines a reactive stream object that should be added to the model for
+ // non-blocking resolution at view rendering time
+
+ final Object executionAttributeValue = executionAttributes.get(executionAttributeName);
+ final String modelAttributeName =
+ executionAttributeName.substring(REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTE_PREFIX.length());
+ Publisher> modelAttributeValue = null;
+
+ if (executionAttributeValue != null) {
+ if (executionAttributeValue instanceof Publisher>) {
+ modelAttributeValue = (Publisher>) executionAttributeValue;
+ } else if (executionAttributeValue instanceof Supplier>){
+ final Supplier> supplier = (Supplier>) executionAttributeValue;
+ modelAttributeValue = supplier.get();
+ } else if (executionAttributeValue instanceof Function,?>) {
+ final Function> function = (Function>) executionAttributeValue;
+ modelAttributeValue = function.apply(exchange);
+ }
+ }
+
+ if (enrichedModel == null) {
+ enrichedModel = new LinkedHashMap<>(model);
+ }
+ enrichedModel.put(modelAttributeName, modelAttributeValue);
+
+ }
+
+ }
+
+ enrichedModel = (enrichedModel != null ? enrichedModel : (Map)model);
+
+ return super.render(enrichedModel, contentType, exchange);
+
+ }
+
+
+
+ @Override
+ protected Mono renderInternal(
+ final Map renderAttributes, final MediaType contentType, final ServerWebExchange exchange) {
+ return renderFragmentInternal(this.markupSelectors, renderAttributes, contentType, exchange);
+ }
+
+
+ protected Mono renderFragmentInternal(
+ final Set markupSelectorsToRender, final Map renderAttributes,
+ final MediaType contentType, final ServerWebExchange exchange) {
+
+ final String viewTemplateName = getTemplateName();
+ final ISpringWebFluxTemplateEngine viewTemplateEngine = getTemplateEngine();
+
+ if (viewTemplateName == null) {
+ return Mono.error(new IllegalArgumentException("Property 'templateName' is required"));
+ }
+ if (getLocale() == null) {
+ return Mono.error(new IllegalArgumentException("Property 'locale' is required"));
+ }
+ if (viewTemplateEngine == null) {
+ return Mono.error(new IllegalArgumentException("Property 'thymeleafTemplateEngine' is required"));
+ }
+
+ final ServerHttpResponse response = exchange.getResponse();
+
+ /*
+ * ----------------------------------------------------------------------------------------------------------
+ * GATHERING OF THE MERGED MODEL
+ * ----------------------------------------------------------------------------------------------------------
+ * - The merged model is the map that will be used for initialising the Thymelef IContext. This context will
+ * contain all the data accessible by the template during its execution.
+ * - The base of the merged model is the ModelMap created by the Controller, but there are some additional
+ * things
+ * ----------------------------------------------------------------------------------------------------------
+ */
+
+ final Map mergedModel = new HashMap<>(30);
+ // First of all, set all the static variables into the mergedModel
+ final Map templateStaticVariables = getStaticVariables();
+ if (templateStaticVariables != null) {
+ mergedModel.putAll(templateStaticVariables);
+ }
+ // Add path variables to merged model (if there are any)
+ final Map pathVars =
+ (Map) exchange.getAttributes().get(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+ if (pathVars != null) {
+ mergedModel.putAll(pathVars);
+ }
+ // Simply dump all the renderAttributes (model coming from the controller) into the merged model
+ if (renderAttributes != null) {
+ mergedModel.putAll(renderAttributes);
+ }
+
+ final ApplicationContext applicationContext = getApplicationContext();
+
+ // Initialize RequestContext (reactive version) and add it to the model as another attribute,
+ // so that it can be retrieved from elsewhere.
+ final RequestContext requestContext = createRequestContext(exchange, mergedModel);
+ final SpringWebFluxThymeleafRequestContext thymeleafRequestContext =
+ new SpringWebFluxThymeleafRequestContext(requestContext, exchange);
+
+ mergedModel.put(SpringContextVariableNames.SPRING_REQUEST_CONTEXT, requestContext);
+ // Add the Thymeleaf RequestContext wrapper that we will be using in this dialect (the bare RequestContext
+ // stays in the context to for compatibility with other dialects)
+ mergedModel.put(SpringContextVariableNames.THYMELEAF_REQUEST_CONTEXT, thymeleafRequestContext);
+
+
+ // Expose Thymeleaf's own evaluation context as a model variable
+ //
+ // Note Spring's EvaluationContexts are NOT THREAD-SAFE (in exchange for SpelExpressions being thread-safe).
+ // That's why we need to create a new EvaluationContext for each request / template execution, even if it is
+ // quite expensive to create because of requiring the initialization of several ConcurrentHashMaps.
+ final ConversionService conversionService =
+ applicationContext.containsBean(WEBFLUX_CONVERSION_SERVICE_NAME)?
+ (ConversionService)applicationContext.getBean(WEBFLUX_CONVERSION_SERVICE_NAME): null;
+ final ThymeleafEvaluationContext evaluationContext =
+ new ThymeleafEvaluationContext(applicationContext, conversionService);
+ mergedModel.put(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME, evaluationContext);
+
+
+ // Determine if we have a data-driver variable, and therefore will need to configure flushing of output chunks
+ final boolean dataDriven = isDataDriven(mergedModel);
+
+
+ /*
+ * ----------------------------------------------------------------------------------------------------------
+ * INSTANTIATION OF THE CONTEXT
+ * ----------------------------------------------------------------------------------------------------------
+ * - Once the model has been merged, we can create the Thymeleaf context object itself.
+ * - The reason it is an ExpressionContext and not a Context is that before executing the template itself,
+ * we might need to use it for computing the markup selectors (if "template :: selector" was specified).
+ * - The reason it is not a WebExpressionContext is that this class is linked to the Servlet API, which
+ * might not be present in a Spring WebFlux environment.
+ * ----------------------------------------------------------------------------------------------------------
+ */
+
+ final IEngineConfiguration configuration = viewTemplateEngine.getConfiguration();
+ final SpringWebFluxExpressionContext context =
+ new SpringWebFluxExpressionContext(
+ configuration, exchange, getReactiveAdapterRegistry(), getLocale(), mergedModel);
+
+
+ /*
+ * ----------------------------------------------------------------------------------------------------------
+ * COMPUTATION OF (OPTIONAL) MARKUP SELECTORS
+ * ----------------------------------------------------------------------------------------------------------
+ * - If view name has been specified with a template selector (in order to execute only a fragment of
+ * the template) like "template :: selector", we will extract it and compute it.
+ * ----------------------------------------------------------------------------------------------------------
+ */
+
+ final String templateName;
+ final Set markupSelectors;
+ if (!viewTemplateName.contains("::")) {
+ // No fragment specified at the template name
+
+ templateName = viewTemplateName;
+ markupSelectors = null;
+
+ } else {
+ // Template name contains a fragment name, so we should parse it as such
+
+ final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
+
+ final FragmentExpression fragmentExpression;
+ try {
+ // By parsing it as a standard expression, we might profit from the expression cache
+ fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
+ } catch (final TemplateProcessingException e) {
+ return Mono.error(
+ new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'"));
+ }
+
+ final FragmentExpression.ExecutedFragmentExpression fragment =
+ FragmentExpression.createExecutedFragmentExpression(context, fragmentExpression);
+
+ templateName = FragmentExpression.resolveTemplateName(fragment);
+ markupSelectors = FragmentExpression.resolveFragments(fragment);
+ final Map nameFragmentParameters = fragment.getFragmentParameters();
+
+ if (nameFragmentParameters != null) {
+
+ if (fragment.hasSyntheticParameters()) {
+ // We cannot allow synthetic parameters because there is no way to specify them at the template
+ // engine execution!
+ return Mono.error(new IllegalArgumentException(
+ "Parameters in a view specification must be named (non-synthetic): '" + viewTemplateName + "'"));
+ }
+
+ context.setVariables(nameFragmentParameters);
+
+ }
+
+ }
+
+ final Set processMarkupSelectors;
+ if (markupSelectors != null && markupSelectors.size() > 0) {
+ if (markupSelectorsToRender != null && markupSelectorsToRender.size() > 0) {
+ return Mono.error(new IllegalArgumentException(
+ "A markup selector has been specified (" + Arrays.asList(markupSelectors) + ") for a view " +
+ "that was already being executed as a fragment (" + Arrays.asList(markupSelectorsToRender) + "). " +
+ "Only one fragment selection is allowed."));
+ }
+ processMarkupSelectors = markupSelectors;
+ } else {
+ if (markupSelectorsToRender != null && markupSelectorsToRender.size() > 0) {
+ processMarkupSelectors = markupSelectorsToRender;
+ } else {
+ processMarkupSelectors = null;
+ }
+ }
+
+
+ /*
+ * ----------------------------------------------------------------------------------------------------------
+ * COMPUTATION OF TEMPLATE PROCESSING PARAMETERS AND HTTP HEADERS
+ * ----------------------------------------------------------------------------------------------------------
+ * - At this point we will compute the final values of the different parameters needed for processing the
+ * template (locale, encoding, buffer sizes, etc.)
+ * ----------------------------------------------------------------------------------------------------------
+ */
+
+ final int templateResponseMaxChunkSizeBytes = getResponseMaxChunkSizeBytes();
+
+ final HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
+ final Locale templateLocale = getLocale();
+ if (templateLocale != null) {
+ responseHeaders.setContentLanguage(templateLocale);
+ }
+
+ // Get the charset from the selected content type (or use default)
+ final Charset charset = getCharset(contentType).orElse(getDefaultCharset());
+
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------
+ * SET (AND RETURN) THE TEMPLATE PROCESSING Flux OBJECTS
+ * -----------------------------------------------------------------------------------------------------------
+ * - There are three possible processing modes, for each of which a Publisher will be created in a
+ * different way:
+ *
+ * 1. FULL: Output chunks not limited in size (templateResponseMaxChunkSizeBytes == Integer.MAX_VALUE) and
+ * no data-driven execution (no context variable of type Publisher driving the template engine
+ * execution): In this case Thymeleaf will be executed unthrottled, in full mode, writing output
+ * to a single DataBuffer chunk instanced before execution, and which will be passed to the output
+ * channels in a single onNext(buffer) call (immediately followed by onComplete()).
+ *
+ * 2. CHUNKED: Output chunks limited in size (responseMaxChunkSizeBytes) but no data-driven
+ * execution (no Publisher driving engine execution). All model attributes are expected to be
+ * fully resolved (in a non-blocking fashion) by WebFlux before engine execution and the Thymeleaf
+ * engine will execute in throttled mode, performing a full-stop each time the chunk reaches the
+ * specified size, sending it to the output channels with onNext(chunk) and then waiting until
+ * these output channels make the engine resume its work with a new request(n) call. This
+ * execution mode will request an output flush from the server after producing each chunk.
+ *
+ * 3. DATA-DRIVEN: one of the model attributes is a Publisher wrapped inside an implementation
+ * of the IReactiveDataDriverContextVariable> interface. In this case, the Thymeleaf engine will
+ * execute as a response to onNext(List) events triggered by this Publisher. The
+ * "bufferSizeElements" specified at the model attribute will define the amount of elements
+ * produced by this Publisher that will be buffered into a List before triggering the template
+ * engine each time (which is why Thymeleaf will react on onNext(List) and not onNext(X)). Thymeleaf
+ * will expect to find a "th:each" iteration on the data-driven variable inside the processed template,
+ * and will be executed in throttled mode for the published elements, sending the resulting DataBuffer
+ * output chunks to the output channels via onNext(chunk) and stopping until a new onNext(List)
+ * event is triggered. When execution is data-driven, a limit in size can be optionally specified for
+ * the output chunks (responseMaxChunkSizeBytes) which will make Thymeleaf never send
+ * to the output channels a chunk bigger than that (thus splitting the output generated for a List
+ * of published elements into several chunks if required). When executing in DATA-DRIVEN mode,
+ * Thymeleaf will always request flushing of the output channels after producing each chunk.
+ * ----------------------------------------------------------------------------------------------------------
+ */
+
+
+ final Publisher stream =
+ viewTemplateEngine.processStream(
+ templateName, processMarkupSelectors, context, response.bufferFactory(), contentType, charset,
+ templateResponseMaxChunkSizeBytes); // FULL/DATADRIVEN if MAX_VALUE, CHUNKED/DATADRIVEN if other
+
+ if (templateResponseMaxChunkSizeBytes == Integer.MAX_VALUE && !dataDriven) {
+
+ // No size limit for output chunks has been set (FULL mode), so we will let the
+ // server apply its standard behaviour ("writeWith").
+ return response.writeWith(stream);
+
+ }
+
+ // Either we are in DATA-DRIVEN mode or a limit for output chunks has been set (CHUNKED mode), so we will
+ // use "writeAndFlushWith" in order to make sure that output is flushed after each buffer.
+ return response.writeAndFlushWith(Flux.from(stream).window(1));
+
+ }
+
+
+
+
+ private static Optional getCharset(final MediaType mediaType) {
+ return mediaType != null ? Optional.ofNullable(mediaType.getCharset()) : Optional.empty();
+ }
+
+
+
+
+ private static boolean isDataDriven(final Map mergedModel) {
+ if (mergedModel == null || mergedModel.size() == 0) {
+ return false;
+ }
+ for (final Object value : mergedModel.values()) {
+ if (value instanceof IReactiveDataDriverContextVariable) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+
+
+ private ReactiveAdapterRegistry getReactiveAdapterRegistry() {
+
+ final ApplicationContext applicationContext = getApplicationContext();
+ if (applicationContext == null) {
+ return null;
+ }
+
+ if (applicationContext != null) {
+ try {
+ return applicationContext.getBean(ReactiveAdapterRegistry.class);
+ } catch (final NoSuchBeanDefinitionException ignored) {
+ // No registry, but note that we can live without it (though limited to Flux and Mono)
+ }
+ }
+ return null;
+
+ }
+
+
+}
diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveViewResolver.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveViewResolver.java
new file mode 100644
index 00000000..698b7696
--- /dev/null
+++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveViewResolver.java
@@ -0,0 +1,1099 @@
+/*
+ * =============================================================================
+ *
+ * Copyright (c) 2011-2018, 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.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+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.BeansException;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.http.MediaType;
+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.ISpringWebFluxTemplateEngine;
+import org.thymeleaf.util.Validate;
+import reactor.core.publisher.Mono;
+
+
+/**
+ *
+ * Implementation of the Spring WebFlux {@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}.
+ *
+ *
+ * In Spring WebFlux 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 ({@link #setResponseMaxChunkSizeBytes(int)})
+ * and no data-driver context variable has been specified. All template output will be generated in memory
+ * as a single chunk (a single {@link org.springframework.core.io.buffer.DataBuffer} object)
+ * and then sent to the server's output channels. In this mode, the Thymeleaf template engine
+ * works unthrottled, which may benefit performance in some scenarios with small templates,
+ * at the cost of a higher memory consumption.
+ * - CHUNKED, when a limit for max chunk size is established ({@link #setResponseMaxChunkSizeBytes(int)})
+ * 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 emitted, 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
+ * this mechanism works single-threaded. When using this execution mode, the response will be configured
+ * by this {@link ViewResolver} so that each output chunk emitted provokes a flush operation at the
+ * server output channels (so that partial content is sent to the browser/client).
+ * - DATA-DRIVEN, when a data-driver variable has been specified at the context
+ * (by implementing {@link org.thymeleaf.spring5.context.webflux.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 throttle
+ * template engine execution by means of back-pressure. When working in this mode, the response will be
+ * configured by this {@link ViewResolver} so that the server output channels are flushed after each
+ * engine execution (which will happen for each buffer (of configurable size) of elements
+ * collected from the data-driver stream. Additionally, if a value has been set for this
+ * {@code responseMaxChunkSizeBytes} property, the emitted
+ * {@link org.springframework.core.io.buffer.DataBuffer} output chunks will never exceed this size,
+ * and therefore more than one chunk could be emitted for each buffer of data-driver elements.
+ *
+ *
+ * Also note that the properties set by means of {@link #setFullModeViewNames(String[])} and
+ * {@link #setChunkedModeViewNames(String[])} also influence and fine-tune which templates are
+ * executed in {@code FULL} or {@code CHUNKED} mode (they have no effect on {@code DATA-DRIVEN}).
+ *
+ *
+ * Also note that {@link ThymeleafReactiveView} objects can be specifically configured to be executed in
+ * {@code CHUNKED} mode by instantiating prototypes of them for the desired view names and setting a
+ * per-view max chunk size by means of {@link ThymeleafReactiveView#setResponseMaxChunkSizeBytes(int)}. If this
+ * is set to {@link Integer#MAX_VALUE}, they will be effectively configured to execute in {@code FULL} mode. This
+ * per-view setting will always have higher precedence than the one performed at the {@link ViewResolver} level.
+ *
+ *
+ * @see ThymeleafReactiveView
+ * @see ISpringWebFluxTemplateEngine
+ *
+ * @author Daniel Fernández
+ *
+ * @since 3.0.3
+ *
+ */
+public class ThymeleafReactiveViewResolver
+ extends ViewResolverSupport
+ implements ViewResolver, ApplicationContextAware {
+
+
+ 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: {@code redirect:}
+ *
+ */
+ public static final String REDIRECT_URL_PREFIX = "redirect:";
+
+ // TODO * Will this exist in future versions of Spring WebFlux? See https://jira.spring.io/browse/SPR-14537
+ public static final String FORWARD_URL_PREFIX = "forward:";
+
+ // Supported media types are all those defined at org.thymeleaf.util.ContentTypeUtils
+ // Note that Spring will automatically perform content type negotiation based on the request query and a (possible)
+ // HTTP Accept header, so there is no additional operation needed at the Thymeleaf side (template mode will
+ // not be forced from the View/ViewResolvers side, but instead will be left to the template resolvers, which
+ // might apply their own file extension suffix-based mechanism for a certain degree of auto-resolution).
+ private static final List SUPPORTED_MEDIA_TYPES =
+ Arrays.asList(new MediaType[] {
+ MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML, // HTML
+ MediaType.APPLICATION_XML, MediaType.TEXT_XML, // XML
+ MediaType.APPLICATION_RSS_XML, // RSS
+ MediaType.APPLICATION_ATOM_XML, // ATOM
+ new MediaType("application", "javascript"), // JAVASCRIPT
+ new MediaType("application", "ecmascript"), //
+ new MediaType("text", "javascript"), //
+ new MediaType("text", "ecmascript"), //
+ MediaType.APPLICATION_JSON, // JSON
+ new MediaType("text", "css"), // CSS
+ MediaType.TEXT_PLAIN, // TEXT
+ MediaType.TEXT_EVENT_STREAM}); // SERVER-SENT EVENTS (SSE)
+
+
+ private ApplicationContext applicationContext;
+
+ // 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 String[] fullModeViewNames = null;
+ private String[] chunkedModeViewNames = null;
+
+ private ISpringWebFluxTemplateEngine templateEngine;
+
+
+
+
+
+
+ /**
+ *
+ * Create an instance of {@code ThymeleafReactiveViewResolver}.
+ *
+ */
+ public ThymeleafReactiveViewResolver() {
+ super();
+ setSupportedMediaTypes(SUPPORTED_MEDIA_TYPES);
+ }
+
+
+
+
+ @Override
+ public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ }
+
+
+ public ApplicationContext getApplicationContext() {
+ return this.applicationContext;
+ }
+
+
+
+
+ /**
+ *
+ * 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 ISpringWebFluxTemplateEngine} to be used for the
+ * execution of templates.
+ *
+ *
+ * @return the template engine being used for processing templates.
+ */
+ public ISpringWebFluxTemplateEngine getTemplateEngine() {
+ return this.templateEngine;
+ }
+
+
+ /**
+ *
+ * Set the template engine object (implementation of {@link ISpringWebFluxTemplateEngine} to be
+ * used for processing templates.
+ *
+ *
+ * @param templateEngine the template engine.
+ */
+ public void setTemplateEngine(final ISpringWebFluxTemplateEngine 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:
+ * {@code ${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:
+ * {@code ${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:
+ * {@code ${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 {@code 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 {@code 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 {@code URL} of the redirect
+ * (as specified in the view name returned by the controller, without the {@code 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 {@code URL} of the redirect
+ * (as specified in the view name returned by the controller, without the {@code 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 {@code viewNames} property.
+ *
+ *
+ * When this flag is set to {@code true} (default value), any view name that starts with the
+ * {@code redirect:} or {@code forward:} prefixes will be resolved by this ViewResolver even if the view names
+ * would not match what is established at the {@code viewNames} property.
+ *
+ *
+ * Note that the behaviour of resolving view names with these prefixes is exactly the same with this
+ * flag set to {@code true} or {@code false} (perform an HTTP redirect or forward to an internal resource).
+ * The only difference is whether the prefixes have to be explicitly specified at {@code viewNames} or not.
+ *
+ *
+ * Default value is {@code 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 {@code viewNames} property.
+ *
+ *
+ * When this flag is set to {@code true} (default value), any view name that starts with the
+ * {@code redirect:} or {@code forward:} prefixes will be resolved by this ViewResolver even if the view names
+ * would not match what is established at the {@code viewNames} property.
+ *
+ *
+ * Note that the behaviour of resolving view names with these prefixes is exactly the same with this
+ * flag set to {@code true} or {@code false} (perform an HTTP redirect or forward to an internal resource).
+ * The only difference is whether the prefixes have to be explicitly specified at {@code viewNames} or not.
+ *
+ *
+ * Default value is {@code true}.
+ *
+ *
+ * @return whether redirects and forwards will be always processed by this view resolver or else only when they are
+ * matched by the {@code 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 WebFlux 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 as a single chunk
+ * (a single {@link org.springframework.core.io.buffer.DataBuffer} object)
+ * and then sent to the server's output channels. In this mode, the Thymeleaf template engine
+ * works unthrottled, which may benefit performance in some scenarios with small templates,
+ * at the cost of a higher memory consumption.
+ * - 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 emitted, 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
+ * this mechanism works single-threaded. When using this execution mode, the response will be configured
+ * by this {@link ViewResolver} so that each output chunk emitted provokes a flush operation at the
+ * server output channels (so that partial content is sent to the browser/client).
+ * - DATA-DRIVEN, when a data-driver variable has been specified at the context
+ * (implementing {@link org.thymeleaf.spring5.context.webflux.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 throttle
+ * template engine execution by means of back-pressure. When working in this mode, the response will be
+ * configured by this {@link ViewResolver} so that the server output channels are flushed after each
+ * engine execution (which will happen for each buffer (of configurable size) of elements
+ * collected from the data-driver stream. Additionally, if a value has been set for this
+ * {@code responseMaxChunkSizeBytes} property, the emitted
+ * {@link org.springframework.core.io.buffer.DataBuffer} output chunks will never exceed this size,
+ * and therefore more than one chunk could be emitted for each buffer of data-driver elements.
+ *
+ *
+ * Also note that the properties set by means of {@link #setFullModeViewNames(String[])} and
+ * {@link #setChunkedModeViewNames(String[])} also influence and fine-tune which templates are
+ * executed in {@code FULL} or {@code CHUNKED} mode (they have no effect on {@code DATA-DRIVEN}).
+ *
+ *
+ * If this property is set to {@code -1} or {@code Integer.MAX_VALUE}, no size limit will be used. Note also
+ * that there is no limit set by default.
+ *
+ *
+ * Also note that this parameter will be ignored when returning SSE (Server-Sent Events), as buffer size in such
+ * case will adapt to the size of each returned element (plus its SSE metadata).
+ *
+ *
+ * @param responseMaxChunkSizeBytes the maximum size in bytes for output chunks
+ * ({@link org.springframework.core.io.buffer.DataBuffer} objects), or
+ * {@code -1} or {@code 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 WebFlux 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 as a single chunk
+ * (a single {@link org.springframework.core.io.buffer.DataBuffer} object)
+ * and then sent to the server's output channels. In this mode, the Thymeleaf template engine
+ * works unthrottled, which may benefit performance in some scenarios with small templates,
+ * at the cost of a higher memory consumption.
+ * - 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 emitted, 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
+ * this mechanism works single-threaded. When using this execution mode, the response will be configured
+ * by this {@link ViewResolver} so that each output chunk emitted provokes a flush operation at the
+ * server output channels (so that partial content is sent to the browser/client).
+ * - DATA-DRIVEN, when a data-driver variable has been specified at the context
+ * (implementing {@link org.thymeleaf.spring5.context.webflux.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 throttle
+ * template engine execution by means of back-pressure. When working in this mode, the response will be
+ * configured by this {@link ViewResolver} so that the server output channels are flushed after each
+ * engine execution (which will happen for each buffer (of configurable size) of elements
+ * collected from the data-driver stream. Additionally, if a value has been set for this
+ * {@code responseMaxChunkSizeBytes} property, the emitted
+ * {@link org.springframework.core.io.buffer.DataBuffer} output chunks will never exceed this size,
+ * and therefore more than one chunk could be emitted for each buffer of data-driver elements.
+ *
+ *
+ * Also note that the properties set by means of {@link #setFullModeViewNames(String[])} and
+ * {@link #setChunkedModeViewNames(String[])} also influence and fine-tune which templates are
+ * executed in {@code FULL} or {@code CHUNKED} mode (they have no effect on {@code DATA-DRIVEN}).
+ *
+ *
+ * Also note that the properties set by means of {@link #setFullModeViewNames(String[])} and
+ * {@link #setChunkedModeViewNames(String[])} also influence and fine-tune which templates are
+ * executed in {@code FULL} or {@code CHUNKED} mode (they have no effect on {@code DATA-DRIVEN}).
+ *
+ *
+ * If this property is set to {@code -1} or {@code Integer.MAX_VALUE}, no size limit will be used. Note also
+ * that there is no limit set by default.
+ *
+ *
+ * Also note that this parameter will be ignored when returning SSE (Server-Sent Events), as buffer size in such
+ * case will adapt to the size of each returned element (plus its SSE metadata).
+ *
+ *
+ * @return the maximum size in bytes for output chunks
+ * ({@link org.springframework.core.io.buffer.DataBuffer} objects), or
+ * {@code -1} or {@code Integer.MAX_VALUE} if no limit is to be used.
+ */
+ public int getResponseMaxChunkSizeBytes() {
+ return this.responseMaxChunkSizeBytes;
+ }
+
+
+
+
+ /**
+ *
+ * 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 {@code order}–
+ * instead.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /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 {@code order}–
+ * instead.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /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 #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 #getViewNames()
+ * @see PatternMatchUtils#simpleMatch(String[], String)
+ */
+ public String[] getExcludedViewNames() {
+ return this.excludedViewNames;
+ }
+
+
+
+
+ /**
+ *
+ * Specify a set of name patterns that be will applied to determine whether a view is to be processed
+ * in {@code FULL} mode even if a maximum response chunk size has been defined.
+ *
+ *
+ * When a response maximum chunk size has been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * this parameter allows the possibility to exclude some views from being applied this maximum size
+ * and therefore be executed in {@code FULL} mode, in just one template engine execution in-memory.
+ *
+ *
+ * This is useful when a maximum chunk size has been set but some pages are actually small enough to benefit
+ * from the performance gain of executing the template engine unthrottled, even if this means
+ * producing the entire output in memory before sending it to the output channels.
+ *
+ *
+ * When a response maximum chunk size has not been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * this parameter has no effect at all.
+ *
+ *
+ * When a response maximum chunk size has been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * but a value has also been set to the {@code chunkedModeViewNames} parameter by means of
+ * {@link #setChunkedModeViewNames(String[])} method, this parameter has no effect at all, as only the views
+ * specified in the latter parameter will be processed in {@code CHUNKED} mode.
+ *
+ *
+ * Also note that, if a view specified here to be executed as {@code FULL} is executed with a
+ * data-driver variable included in the model, the {@code DATA-DRIVEN} execution mode will be
+ * automatically selected instead, and output chunks will be flushed after each execution of the engine for
+ * each buffer of elements obtained from the data-driver stream.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /WEB-INF/templates/orders/*.html}.
+ *
+ *
+ * @param fullModeViewNames the view names (actually view name patterns)
+ * @see #setResponseMaxChunkSizeBytes(int)
+ * @see #setChunkedModeViewNames(String[])
+ * @see PatternMatchUtils#simpleMatch(String[], String)
+ *
+ * @since 3.0.8
+ */
+ public void setFullModeViewNames(final String[] fullModeViewNames) {
+ this.fullModeViewNames = fullModeViewNames;
+ }
+
+
+ /**
+ *
+ * Returns the set of name patterns that will be applied to determine whether a view is to be processed
+ * in {@code FULL} mode even if a maximum response chunk size has been defined.
+ *
+ *
+ * When a response maximum chunk size has been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * this parameter allows the possibility to exclude some views from being applied this maximum size
+ * and therefore be executed in {@code FULL} mode, in just one template engine execution in-memory.
+ *
+ *
+ * This is useful when a maximum chunk size has been set but some pages are actually small enough to benefit
+ * from the performance gain of executing the template engine unthrottled, even if this means
+ * producing the entire output in memory before sending it to the output channels.
+ *
+ *
+ * When a response maximum chunk size has not been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * this parameter has no effect at all.
+ *
+ *
+ * When a response maximum chunk size has been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * but a value has also been set to the {@code chunkedModeViewNames} parameter by means of
+ * {@link #setChunkedModeViewNames(String[])} method, this parameter has no effect at all, as only the views
+ * specified in the latter parameter will be processed in {@code CHUNKED} mode.
+ *
+ *
+ * Also note that, if a view specified here to be executed as {@code FULL} is executed with a
+ * data-driver variable included in the model, the {@code DATA-DRIVEN} execution mode will be
+ * automatically selected instead, and output chunks will be flushed after each execution of the engine for
+ * each buffer of elements obtained from the data-driver stream.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /WEB-INF/templates/orders/*.html}.
+ *
+ *
+ * @return the view name patterns
+ * @see #setResponseMaxChunkSizeBytes(int)
+ * @see #setChunkedModeViewNames(String[])
+ * @see PatternMatchUtils#simpleMatch(String[], String)
+ *
+ * @since 3.0.8
+ */
+ public String[] getFullModeViewNames() {
+ return this.fullModeViewNames;
+ }
+
+
+
+
+ /**
+ *
+ * Specify a set of name patterns that will be applied to determine whether a view is to be processed
+ * in {@code CHUNKED} mode (assuming a maximum response chunk size has been defined).
+ *
+ *
+ * This parameter only has effect if a maximum response chunk size has been set by means of
+ * {@link #setResponseMaxChunkSizeBytes(int)}. If that is the case, then only the views
+ * which name matches the patterns specified here will be executed in {@code CHUNKED} mode using the
+ * maximum output chunk size that has been configured. All other views will be executed in {@code FULL}
+ * mode.
+ *
+ *
+ * This is useful when a maximum chunk size has been set but it is only needed to apply for certain specific
+ * views, normally the larger templates in output size.
+ *
+ *
+ * When a response maximum chunk size has not been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * this parameter has no effect at all.
+ *
+ *
+ * Also note that, if a view specified here to be executed as {@code CHUNKED} is executed with a
+ * data-driver variable included in the model, the {@code DATA-DRIVEN} execution mode will be
+ * automatically selected instead, and output chunks will be flushed after each execution of the engine for
+ * each buffer of elements obtained from the data-driver stream. But in this case, the maximum chunk
+ * size will also apply and, if any of these data-driven chunks exceeds this size, it will be divided into
+ * several output chunks.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /WEB-INF/templates/orders/*.html}.
+ *
+ *
+ * @param chunkedModeViewNames the view names (actually view name patterns)
+ * @see #setResponseMaxChunkSizeBytes(int)
+ * @see #setFullModeViewNames(String[])
+ * @see PatternMatchUtils#simpleMatch(String[], String)
+ *
+ * @since 3.0.8
+ */
+ public void setChunkedModeViewNames(final String[] chunkedModeViewNames) {
+ this.chunkedModeViewNames = chunkedModeViewNames;
+ }
+
+
+ /**
+ *
+ * Return the set of name patterns that will be applied to determine whether a view is to be processed
+ * in {@code CHUNKED} mode (assuming a maximum response chunk size has been defined).
+ *
+ *
+ * This parameter only has effect if a maximum response chunk size has been set by means of
+ * {@link #setResponseMaxChunkSizeBytes(int)}. If that is the case, then only the views
+ * which name matches the patterns specified here will be executed in {@code CHUNKED} mode using the
+ * maximum output chunk size that has been configured. All other views will be executed in {@code FULL}
+ * mode.
+ *
+ *
+ * This is useful when a maximum chunk size has been set but it is only needed to apply for certain specific
+ * views, normally the larger templates in output size.
+ *
+ *
+ * When a response maximum chunk size has not been set by means of {@link #setResponseMaxChunkSizeBytes(int)},
+ * this parameter has no effect at all.
+ *
+ *
+ * Also note that, if a view specified here to be executed as {@code CHUNKED} is executed with a
+ * data-driver variable included in the model, the {@code DATA-DRIVEN} execution mode will be
+ * automatically selected instead, and output chunks will be flushed after each execution of the engine for
+ * each buffer of elements obtained from the data-driver stream. But in this case, the maximum chunk
+ * size will also apply and, if any of these data-driven chunks exceeds this size, it will be divided into
+ * several output chunks.
+ *
+ *
+ * The specified view name patterns can be complete view names, but can also use
+ * the {@code *} wildcard: "{@code index.*}", "{@code user_*}", "{@code 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 {@code orders/*} instead of {@code /WEB-INF/templates/orders/*.html}.
+ *
+ *
+ * @return the view name patterns
+ * @see #setResponseMaxChunkSizeBytes(int)
+ * @see #setFullModeViewNames(String[])
+ * @see PatternMatchUtils#simpleMatch(String[], String)
+ *
+ * @since 3.0.8
+ */
+ public String[] getChunkedModeViewNames() {
+ return this.chunkedModeViewNames;
+ }
+
+
+
+
+ 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)));
+ }
+
+
+ protected boolean shouldUseChunkedExecution(final String viewName) {
+
+ final int viewResponseMaxChunkSizeBytes = getResponseMaxChunkSizeBytes();
+ final String[] viewChunkedModeViewNames = getChunkedModeViewNames();
+ final String[] viewFullModeViewNames = getFullModeViewNames();
+
+ if (viewResponseMaxChunkSizeBytes == ThymeleafReactiveView.DEFAULT_RESPONSE_CHUNK_SIZE_BYTES) {
+ // No response max chunk size has been set, so no possibility to use CHUNKED execution
+ if (viewChunkedModeViewNames != null) {
+ vrlogger.warn("[THYMELEAF] A set of view names to be executed in CHUNKED mode has been specified, " +
+ "but no response max chunk size has been specified, so this configuration parameter " +
+ "has no practical effect (no way to configure CHUNKED mode from the ViewResolver). Please " +
+ "fix your configuration.");
+ }
+ if (viewFullModeViewNames != null) {
+ vrlogger.warn("[THYMELEAF] A set of view names to be executed in FULL mode has been specified, " +
+ "but no response max chunk size has been specified, so the former configuration parameter " +
+ "has no practical effect (all templates will actually be executed as FULL). Please " +
+ "fix your configuration.");
+ }
+ return false;
+ }
+
+ if (viewChunkedModeViewNames != null) {
+ // A specific set of views to be processed in CHUNKED mode has been specified, so only that
+ // set will determine whether CHUNKED should be used or not
+ return PatternMatchUtils.simpleMatch(viewChunkedModeViewNames, viewName);
+ }
+
+ if (viewFullModeViewNames != null) {
+ // A specific set of views to be processed in FULL mode has been specified, so we will not apply
+ // CHUNKED if this view matches the names in such set
+ return !PatternMatchUtils.simpleMatch(viewFullModeViewNames, viewName);
+ }
+
+ return true;
+
+ }
+
+
+
+
+ @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, REDIRECT_URL_PREFIX);
+ 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 WebFlux 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
+ */
+
+ // We determine if there is actually a reason for using chunked execution for this specific view name,
+ // based on the ViewResolver configuration
+ final boolean shouldUseChunkedExecution = shouldUseChunkedExecution(viewName);
+
+ if (shouldUseChunkedExecution && view.getNullableResponseMaxChunkSize() == null) {
+ view.setResponseMaxChunkSizeBytes(getResponseMaxChunkSizeBytes());
+ }
+
+ return Mono.just(view);
+
+ }
+
+}
diff --git a/thymeleaf-spring6/src/main/resources/org/thymeleaf/xml/thymeleaf-spring5/Spring-Standard-Dialect.xml b/thymeleaf-spring6/src/main/resources/org/thymeleaf/xml/thymeleaf-spring5/Spring-Standard-Dialect.xml
new file mode 100755
index 00000000..5c832fd5
--- /dev/null
+++ b/thymeleaf-spring6/src/main/resources/org/thymeleaf/xml/thymeleaf-spring5/Spring-Standard-Dialect.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+ http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#validation-and-error-messages
+ ]]>
+
+
+
+ http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#validation-and-error-messages
+ ]]>
+
+
+
+ http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#validation-and-error-messages
+ ]]>
+
+
+
+ http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#creating-a-form
+ ]]>
+
+
+
+
+ http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#creating-a-form
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/thymeleaf-spring6/src/test/java/README.txt b/thymeleaf-spring6/src/test/java/README.txt
new file mode 100644
index 00000000..d1d9e296
--- /dev/null
+++ b/thymeleaf-spring6/src/test/java/README.txt
@@ -0,0 +1,2 @@
+Tests live at the "thymeleaf-tests" repository in
+https://github.com/thymeleaf/thymeleaf-tests
diff --git a/thymeleaf-spring6/src/test/resources/README.txt b/thymeleaf-spring6/src/test/resources/README.txt
new file mode 100644
index 00000000..d1d9e296
--- /dev/null
+++ b/thymeleaf-spring6/src/test/resources/README.txt
@@ -0,0 +1,2 @@
+Tests live at the "thymeleaf-tests" repository in
+https://github.com/thymeleaf/thymeleaf-tests