Browse Source
We can remove PatchThymleeafReactiveView now that it is fixed and released in Thymeleaf. Issue gh-4802pull/4820/head
3 changed files with 1 additions and 344 deletions
@ -1,341 +0,0 @@
@@ -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<Void> renderFragmentInternal( |
||||
Set<String> markupSelectorsToRender, Map<String, Object> 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<String, Object> mergedModel = new HashMap<>(30); |
||||
// First of all, set all the static variables into the mergedModel
|
||||
final Map<String, Object> templateStaticVariables = getStaticVariables(); |
||||
if (templateStaticVariables != null) { |
||||
mergedModel.putAll(templateStaticVariables); |
||||
} |
||||
// Add path variables to merged model (if there are any)
|
||||
final Map<String, Object> pathVars = |
||||
(Map<String, Object>) 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<String> 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<String,Object> 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<String> 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<DataBuffer> OBJECTS |
||||
* ----------------------------------------------------------------------------------------------------------- |
||||
* - There are three possible processing modes, for each of which a Publisher<DataBuffer> 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<X> 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<X> 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<X> wrapped inside an implementation |
||||
* of the IReactiveDataDriverContextVariable<?> interface. In this case, the Thymeleaf engine will |
||||
* execute as a response to onNext(List<X>) 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<X> before triggering the template |
||||
* engine each time (which is why Thymeleaf will react on onNext(List<X>) 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<X>) |
||||
* 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<X> |
||||
* 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<DataBuffer> 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<String,Object> 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<Charset> getCharset(final MediaType mediaType) { |
||||
return mediaType != null ? Optional.ofNullable(mediaType.getCharset()) : Optional.empty(); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue