From 1728e218044a38f42fde985599db81909952da12 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 8 Nov 2017 08:29:49 -0600 Subject: [PATCH] Update Thymeleaf We can remove PatchThymleeafReactiveView now that it is fixed and released in Thymeleaf. Issue gh-4802 --- gradle/dependency-management.gradle | 2 +- .../src/main/java/sample/ThymeleafConfig.java | 2 - .../thymeleaf/PatchThymeleafReactiveView.java | 341 ------------------ 3 files changed, 1 insertion(+), 344 deletions(-) delete mode 100644 samples/javaconfig/webflux-form/src/main/java/thymeleaf/PatchThymeleafReactiveView.java diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index abbc0a33da..d36c2ad623 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -197,7 +197,7 @@ dependencyManagement { dependency 'org.slf4j:slf4j-nop:1.7.25' dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111' dependency 'org.springframework.ldap:spring-ldap-core:2.3.1.RELEASE' - dependency 'org.thymeleaf:thymeleaf-spring5:3.0.8.RELEASE' + dependency 'org.thymeleaf:thymeleaf-spring5:3.0.9.RELEASE' dependency 'org.unbescape:unbescape:1.1.4.RELEASE' dependency 'org.w3c.css:sac:1.3' dependency 'xalan:serializer:2.7.2' diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java b/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java index 440be68161..6a37a82a05 100644 --- a/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java +++ b/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java @@ -26,7 +26,6 @@ import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver; import org.thymeleaf.templatemode.TemplateMode; -import thymeleaf.PatchThymeleafReactiveView; /** * @author Rob Winch @@ -68,7 +67,6 @@ public class ThymeleafConfig implements WebFluxConfigurer { viewResolver.setTemplateEngine(thymeleafTemplateEngine()); viewResolver.setOrder(1); viewResolver.setResponseMaxChunkSizeBytes(8192); // OUTPUT BUFFER size limit - viewResolver.setViewClass(PatchThymeleafReactiveView.class); return viewResolver; } diff --git a/samples/javaconfig/webflux-form/src/main/java/thymeleaf/PatchThymeleafReactiveView.java b/samples/javaconfig/webflux-form/src/main/java/thymeleaf/PatchThymeleafReactiveView.java deleted file mode 100644 index a8c1b7b9af..0000000000 --- a/samples/javaconfig/webflux-form/src/main/java/thymeleaf/PatchThymeleafReactiveView.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package thymeleaf; - -import org.reactivestreams.Publisher; -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.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.RequestContext; -import org.springframework.web.server.ServerWebExchange; -import org.thymeleaf.IEngineConfiguration; -import org.thymeleaf.exceptions.TemplateProcessingException; -import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine; -import org.thymeleaf.spring5.context.webflux.IReactiveDataDriverContextVariable; -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.spring5.view.reactive.ThymeleafReactiveView; -import org.thymeleaf.standard.expression.FragmentExpression; -import org.thymeleaf.standard.expression.IStandardExpressionParser; -import org.thymeleaf.standard.expression.StandardExpressionExecutionContext; -import org.thymeleaf.standard.expression.StandardExpressions; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -/** - * @author Rob Winch - * @since 5.0 - */ -public class PatchThymeleafReactiveView extends ThymeleafReactiveView { - private static final String WEBFLUX_CONVERSION_SERVICE_NAME = "webFluxConversionService"; - @Override - protected Mono renderFragmentInternal( - Set markupSelectorsToRender, Map renderAttributes, - MediaType contentType, 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, StandardExpressionExecutionContext.NORMAL); - - 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 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; - - } - - private static Optional getCharset(final MediaType mediaType) { - return mediaType != null ? Optional.ofNullable(mediaType.getCharset()) : Optional.empty(); - } -}