Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SpringStandardDialect doesn't allow to use custom IStandardConversionService. #258

Closed
kozel-stas opened this issue Aug 8, 2021 · 0 comments

Comments

@kozel-stas
Copy link

kozel-stas commented Aug 8, 2021

Hi,

IStandardConversionService docs:

Common interface for all implementations of a conversion service, to be used during template execution.
Thymeleaf conversion services work in a way similar to Spring Framework's ConversionService interface, but this is a generic mechanism (not dependent on Spring).
Default implementation —registered by org.thymeleaf.standard.StandardDialect— is StandardConversionService, which performs some standard conversions, but the Spring Standard Dialect used by the Thymeleaf + Spring integration module automatically registers an implementation of this interface that delegates on any existing Spring ConversionService objects (thus using the Converters and Formatters registered at the Spring Application Context).
Important: there is one conversion that implementations of this interface should always implement, because it is heavily used at the Thymeleaf core: conversion of any Object to String.
The implementation of this interface that should be used is specified as an execution attribute by the Standard Dialects (see org.thymeleaf.standard.StandardDialect.getExecutionAttributes()).
Implementations of this interface should be thread-safe.
Since:
2.1.0
Author:
Daniel Fernández

Per docs, it looks like we can customize IStandardConversionService for further usages. I know that we can register Formatters and it will be applied for toString calls. But I want to have two template engines that will allow me to use different rules for formatting. In that case, I create a new SpringTemplateEngine for internal usages. And register SpringStandardDialect witgh customized IStandardConversionService.
It looks like

SpringTemplateEngine engine = new SpringTemplateEngine();
SpringStandardDialect dialect = new SpringStandardDialect();
dialect.setConversionService(new CustomConversionService());
engine.setDialects(Set.of(dialect);
engine.setEnableSpringELCompiler(true);
engine.setCacheManager(null);

thymeleaf/thymeleaf#223 implements conversions for placeholders like this "${{placeholder}}".
So CustomConversionService should be called for processing toString call for my placeholder.

Surprise!
SpringStandardDialect has its own property of IStandardConversionService.

public class SpringStandardDialect extends StandardDialect {

    public static final String NAME = "SpringStandard";
    public static final String PREFIX = "th";
    public static final int PROCESSOR_PRECEDENCE = 1000;

    public static final boolean DEFAULT_ENABLE_SPRING_EL_COMPILER = false;
    public static final boolean DEFAULT_RENDER_HIDDEN_MARKERS_BEFORE_CHECKBOXES = false;

    private boolean enableSpringELCompiler = DEFAULT_ENABLE_SPRING_EL_COMPILER;
    private boolean renderHiddenMarkersBeforeCheckboxes = DEFAULT_RENDER_HIDDEN_MARKERS_BEFORE_CHECKBOXES;

    private static final Map<String,Object> REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTES;

    // This execution attribute will force the asynchronous resolution of the WebSession (not the real creation of a
    // persisted session) before view execution. This will avoid the need to block in order to obtain the WebSession
    // from the ServerWebExchange during template execution.
    // NOTE here we are not using the constant from the ReactiveThymeleafView class (instead we replicate its same
    // value "ThymeleafReactiveModelAdditions:" so that we don't force the initialisation of that class in
    // non-WebFlux environments.
    private static final String WEB_SESSION_EXECUTION_ATTRIBUTE_NAME =
            "ThymeleafReactiveModelAdditions:" + SpringContextUtils.WEB_SESSION_ATTRIBUTE_NAME;

    // These variables will be initialized lazily following the model applied in the extended StandardDialect.
    private IExpressionObjectFactory expressionObjectFactory = null;
    private IStandardConversionService conversionService = null;

But StandardDialect has its own property too. So invoking the setter for SpringStandardDialect will set the property to StandardDialect.

public class StandardDialect
            extends AbstractProcessorDialect
            implements IExecutionAttributeDialect, IExpressionObjectDialect {

    public static final String NAME = "Standard";
    public static final String PREFIX = "th";
    public static final int PROCESSOR_PRECEDENCE = 1000;


    // We will avoid setting this variableExpressionEvaluator variable to "OgnlVariableExpressionEvaluator.INSTANCE"
    // in order to not cause this OGNL-related class to initialize, therefore introducing a forced dependency on OGNL
    // to Spring users (who don't need OGNL at all).
    private IStandardVariableExpressionEvaluator variableExpressionEvaluator = null;

    // These variables will be initialized lazily if needed (because no value is set to them via their setters)
    // This should improve startup times (esp. Jackson for the JS serializer) and avoid losing time initializing
    // objects that might not be used after all if they are overridden via setter.
    private IStandardExpressionParser expressionParser = null;
    private IStandardConversionService conversionService = null;
    private IStandardJavaScriptSerializer javaScriptSerializer = null;
    private IStandardCSSSerializer cssSerializer = null;

    // Note this is not settable - just lazily initialized
    private IExpressionObjectFactory expressionObjectFactory = null;

So calling of getConversionService() method will not return the value of set property due to the getter implementation.

    @Override
    public IStandardConversionService getConversionService() {
        if (this.conversionService == null) {
            this.conversionService = new SpringStandardConversionService();
        }
        return this.conversionService;
    }

In that case, the calling of setConversionService has no effect... It will use SpringStandardConversionService as a conversion service.
In my opinion, such behaviour is unclear... Better to throw an exception if you don't want to allow to set your own conversion service.

The workaround is overriding getter:

public class CustomStandardDialect extends SpringStandardDialect {

    private final IStandardConversionService conversionService = new CustomConversionService();

    @Override
    public IStandardConversionService getConversionService() {
        return conversionService;
    }

}

Version: The latest version of thymeleaf
https://github.com/thymeleaf/thymeleaf-spring/blob/3.0-master/thymeleaf-spring5/src/main/java/org/thymeleaf/spring5/dialect/SpringStandardDialect.java

@danielfernandez danielfernandez self-assigned this Nov 27, 2021
@danielfernandez danielfernandez added this to the Thymeleaf 3.0.13 milestone Nov 27, 2021
danielfernandez added a commit that referenced this issue Nov 28, 2021

Verified

This commit was signed with the committer’s verified signature. The key has expired.
…ardConversionService
hthole pushed a commit to hthole/thymeleaf that referenced this issue Jul 18, 2023
…allow to use custom IStandardConversionService
chriskellet pushed a commit to KedosConsultingLtd/thymeleaf-spring that referenced this issue Apr 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants