Simplify TemplateEngine due to upgrade of thymeleaf (#3146)

#### What type of PR is this?

/kind improvement
/area core

#### What this PR does / why we need it:

When we [implemented theme mechanism](https://github.com/halo-dev/halo/pull/2280), some problems of thymeleaf came up in front of us, especially TemplateEngine. So we copied full code of SpringTemplateEngine and SpringWebFluxTemplateEngine and made minor changes to adapt our requirements.

Fortunately, issue https://github.com/thymeleaf/thymeleaf/issues/901 I fired has been fixed, and we have a chance to optimize this part of the code.

See https://github.com/halo-dev/halo/issues/3070 for more.

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3070

#### Special notes for your reviewer:

Hope reviewers test various endpoints.

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/3152/head
John Niang 2023-01-14 15:22:13 +08:00 committed by GitHub
parent 5c29ab5750
commit d6ff0fd83c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 1355 deletions

View File

@ -18,7 +18,7 @@ import reactor.core.publisher.Mono;
import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.theme.dialect.HaloProcessorDialect; import run.halo.app.theme.dialect.HaloProcessorDialect;
import run.halo.app.theme.engine.SpringWebFluxTemplateEngine; import run.halo.app.theme.engine.HaloTemplateEngine;
import run.halo.app.theme.message.ThemeMessageResolver; import run.halo.app.theme.message.ThemeMessageResolver;
/** /**
@ -93,9 +93,8 @@ public class TemplateEngineManager {
} }
private ISpringWebFluxTemplateEngine templateEngineGenerator(ThemeContext theme) { private ISpringWebFluxTemplateEngine templateEngineGenerator(ThemeContext theme) {
var engine = new SpringWebFluxTemplateEngine(); var engine = new HaloTemplateEngine(new ThemeMessageResolver(theme));
engine.setEnableSpringELCompiler(thymeleafProperties.isEnableSpringElCompiler()); engine.setEnableSpringELCompiler(thymeleafProperties.isEnableSpringElCompiler());
engine.setMessageResolver(new ThemeMessageResolver(theme));
engine.setLinkBuilder(new ThemeLinkBuilder(theme, externalUrlSupplier)); engine.setLinkBuilder(new ThemeLinkBuilder(theme, externalUrlSupplier));
engine.setRenderHiddenMarkersBeforeCheckboxes( engine.setRenderHiddenMarkersBeforeCheckboxes(
thymeleafProperties.isRenderHiddenMarkersBeforeCheckboxes()); thymeleafProperties.isRenderHiddenMarkersBeforeCheckboxes());

View File

@ -0,0 +1,53 @@
package run.halo.app.theme.engine;
import java.nio.charset.Charset;
import java.util.Set;
import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.thymeleaf.context.IContext;
import org.thymeleaf.messageresolver.IMessageResolver;
import org.thymeleaf.spring6.SpringWebFluxTemplateEngine;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
/**
* Default template engine implementation to be used in Halo.
*
* @author johnniang
*/
public class HaloTemplateEngine extends SpringWebFluxTemplateEngine {
private final IMessageResolver messageResolver;
public HaloTemplateEngine(IMessageResolver messageResolver) {
this.messageResolver = messageResolver;
}
@Override
protected void initializeSpringSpecific() {
// Before initialization, thymeleaf will overwrite message resolvers.
// So we need to add own message resolver at here.
addMessageResolver(messageResolver);
}
@Override
public Publisher<DataBuffer> processStream(String template, Set<String> markupSelectors,
IContext context, DataBufferFactory bufferFactory,
MediaType mediaType, Charset charset, int responseMaxChunkSizeBytes) {
var publisher = super.processStream(template, markupSelectors, context, bufferFactory,
mediaType, charset, responseMaxChunkSizeBytes);
// We have to subscribe on blocking thread, because some blocking operations will be present
// while processing.
if (publisher instanceof Mono<DataBuffer> mono) {
return mono.subscribeOn(Schedulers.boundedElastic());
}
if (publisher instanceof Flux<DataBuffer> flux) {
return flux.subscribeOn(Schedulers.boundedElastic());
}
return publisher;
}
}

View File

@ -1,330 +0,0 @@
package run.halo.app.theme.engine;
import java.util.Set;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.messageresolver.IMessageResolver;
import org.thymeleaf.messageresolver.StandardMessageResolver;
import org.thymeleaf.spring6.ISpringTemplateEngine;
import org.thymeleaf.spring6.dialect.SpringStandardDialect;
import org.thymeleaf.spring6.messageresolver.SpringMessageResolver;
/**
* <p>
* Implementation of {@link ISpringTemplateEngine} meant for Spring-enabled applications,
* that establishes by default an instance of {@link SpringStandardDialect}
* as a dialect (instead of an instance of {@link org.thymeleaf.standard.StandardDialect}.
* </p>
* <p>
* It also configures a {@link SpringMessageResolver} as message resolver, and
* implements the {@link MessageSourceAware} interface in order to let Spring
* automatically setting the {@link MessageSource} used at the application
* (bean needs to have id {@code "messageSource"}). If this Spring standard setting
* needs to be overridden, the {@link #setTemplateEngineMessageSource(MessageSource)} can
* be used.
* </p>
* <p>
* Code from
* <a href="https://github.com/thymeleaf/thymeleaf/blob/3.1-master/lib/thymeleaf-spring6/src/main/java/org/thymeleaf/spring6/SpringTemplateEngine.java">Thymeleaf SpringTemplateEngine</a>
* </p>
*
* @author Daniel Fern&aacute;ndez
* @author guqing
* @see org.thymeleaf.spring6.SpringTemplateEngine
* @since 2.0.0
*/
public class SpringTemplateEngine extends TemplateEngine
implements ISpringTemplateEngine, MessageSourceAware {
private static final SpringStandardDialect SPRINGSTANDARD_DIALECT = new SpringStandardDialect();
private MessageSource messageSource = null;
private MessageSource templateEngineMessageSource = null;
public SpringTemplateEngine() {
super();
// This will set the SpringStandardDialect, overriding the Standard one set in the super
// constructor
super.setDialect(SPRINGSTANDARD_DIALECT);
}
/**
* <p>
* Implementation of the {@link MessageSourceAware#setMessageSource(MessageSource)}
* method at the {@link MessageSourceAware} interface, provided so that
* Spring is able to automatically set the currently configured {@link MessageSource} into
* this template engine.
* </p>
* <p>
* If several {@link MessageSource} implementation beans exist, Spring will inject here
* the one with id {@code "messageSource"}.
* </p>
* <p>
* This property <b>should not be set manually</b> in most scenarios (see
* {@link #setTemplateEngineMessageSource(MessageSource)} instead).
* </p>
*
* @param messageSource the message source to be used by the message resolver
*/
@Override
public void setMessageSource(final MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* <p>
* Convenience method for setting the message source that will
* be used by this template engine, overriding the one automatically set by
* Spring at the {@link #setMessageSource(MessageSource)} method.
* </p>
*
* @param templateEngineMessageSource the message source to be used by the message resolver
*/
@Override
public void setTemplateEngineMessageSource(final MessageSource templateEngineMessageSource) {
this.templateEngineMessageSource = templateEngineMessageSource;
}
/**
* <p>
* Returns whether the SpringEL compiler should be enabled in SpringEL expressions or not.
* </p>
* <p>
* (This is just a convenience method, equivalent to calling
* {@link SpringStandardDialect#getEnableSpringELCompiler()} on the dialect instance itself.
* It is provided
* here in order to allow users to enable the SpEL compiler without
* having to directly create instances of the {@link SpringStandardDialect})
* </p>
* <p>
* Expression compilation can significantly improve the performance of Spring EL expressions,
* but
* might not be adequate for every environment. Read
* <a href="http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html#expressions-spel-compilation">the
* official Spring documentation</a> for more detail.
* </p>
* <p>
* Also note that although Spring includes a SpEL compiler since Spring 4.1, most expressions
* in Thymeleaf templates will only be able to properly benefit from this compilation step
* when at least
* Spring Framework version 4.2.4 is used.
* </p>
* <p>
* This flag is set to {@code false} by default.
* </p>
*
* @return {@code true} if SpEL expressions should be compiled if possible, {@code false} if
* not.
*/
public boolean getEnableSpringELCompiler() {
final Set<IDialect> dialects = getDialects();
for (final IDialect dialect : dialects) {
if (dialect instanceof SpringStandardDialect) {
return ((SpringStandardDialect) dialect).getEnableSpringELCompiler();
}
}
return false;
}
/**
* <p>
* Sets whether the SpringEL compiler should be enabled in SpringEL expressions or not.
* </p>
* <p>
* (This is just a convenience method, equivalent to calling
* {@link SpringStandardDialect#setEnableSpringELCompiler(boolean)} on the dialect instance
* itself. It is provided
* here in order to allow users to enable the SpEL compiler without
* having to directly create instances of the {@link SpringStandardDialect})
* </p>
* <p>
* Expression compilation can significantly improve the performance of Spring EL expressions,
* but
* might not be adequate for every environment. Read
* <a href="http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html#expressions-spel-compilation">the
* official Spring documentation</a> for more detail.
* </p>
* <p>
* Also note that although Spring includes a SpEL compiler since Spring 4.1, most expressions
* in Thymeleaf templates will only be able to properly benefit from this compilation step
* when at least
* Spring Framework version 4.2.4 is used.
* </p>
* <p>
* This flag is set to {@code false} by default.
* </p>
*
* @param enableSpringELCompiler {@code true} if SpEL expressions should be compiled if
* possible, {@code false} if not.
*/
public void setEnableSpringELCompiler(final boolean enableSpringELCompiler) {
final Set<IDialect> dialects = getDialects();
for (final IDialect dialect : dialects) {
if (dialect instanceof SpringStandardDialect) {
((SpringStandardDialect) dialect).setEnableSpringELCompiler(enableSpringELCompiler);
}
}
}
/**
* <p>
* Returns whether the {@code <input type="hidden" ...>} marker tags rendered to signal the
* presence
* of checkboxes in forms when unchecked should be rendered <em>before</em> the checkbox tag
* itself,
* or after (default).
* </p>
* <p>
* (This is just a convenience method, equivalent to calling
* {@link SpringStandardDialect#getRenderHiddenMarkersBeforeCheckboxes()} on the dialect
* instance
* itself. It is provided here in order to allow users to modify this behaviour without
* having to directly create instances of the {@link SpringStandardDialect})
* </p>
* <p>
* A number of CSS frameworks and style guides assume that the {@code <label ...>} for a
* checkbox
* will appear in markup just after the {@code <input type="checkbox" ...>} tag itself, and
* so the
* default behaviour of rendering an {@code <input type="hidden" ...>} after the checkbox can
* lead to
* bad application of styles. By tuning this flag, developers can modify this behaviour and
* make the hidden
* tag appear before the checkbox (and thus allow the lable to truly appear right after the
* checkbox).
* </p>
* <p>
* Note this hidden field is introduced in order to signal the existence of the field in the
* form being sent,
* even if the checkbox is unchecked (no URL parameter is added for unchecked check boxes).
* </p>
* <p>
* This flag is set to {@code false} by default.
* </p>
*
* @return {@code true} if hidden markers should be rendered before the checkboxes, {@code
* false} if not.
* @since 3.0.10
*/
public boolean getRenderHiddenMarkersBeforeCheckboxes() {
final Set<IDialect> dialects = getDialects();
for (final IDialect dialect : dialects) {
if (dialect instanceof SpringStandardDialect) {
return ((SpringStandardDialect) dialect).getRenderHiddenMarkersBeforeCheckboxes();
}
}
return false;
}
/**
* <p>
* Sets whether the {@code <input type="hidden" ...>} marker tags rendered to signal the
* presence
* of checkboxes in forms when unchecked should be rendered <em>before</em> the checkbox tag
* itself,
* or after (default).
* </p>
* <p>
* (This is just a convenience method, equivalent to calling
* {@link SpringStandardDialect#setRenderHiddenMarkersBeforeCheckboxes(boolean)} on the
* dialect instance
* itself. It is provided here in order to allow users to modify this behaviour without
* having to directly create instances of the {@link SpringStandardDialect})
* </p>
* <p>
* A number of CSS frameworks and style guides assume that the {@code <label ...>} for a
* checkbox
* will appear in markup just after the {@code <input type="checkbox" ...>} tag itself, and
* so the
* default behaviour of rendering an {@code <input type="hidden" ...>} after the checkbox can
* lead to
* bad application of styles. By tuning this flag, developers can modify this behaviour and
* make the hidden
* tag appear before the checkbox (and thus allow the lable to truly appear right after the
* checkbox).
* </p>
* <p>
* Note this hidden field is introduced in order to signal the existence of the field in the
* form being sent,
* even if the checkbox is unchecked (no URL parameter is added for unchecked check boxes).
* </p>
* <p>
* This flag is set to {@code false} by default.
* </p>
*
* @param renderHiddenMarkersBeforeCheckboxes {@code true} if hidden markers should be rendered
* before the checkboxes, {@code false} if not.
* @since 3.0.10
*/
public void setRenderHiddenMarkersBeforeCheckboxes(
final boolean renderHiddenMarkersBeforeCheckboxes) {
final Set<IDialect> dialects = getDialects();
for (final IDialect dialect : dialects) {
if (dialect instanceof SpringStandardDialect) {
((SpringStandardDialect) dialect).setRenderHiddenMarkersBeforeCheckboxes(
renderHiddenMarkersBeforeCheckboxes);
}
}
}
@Override
protected void initializeSpecific() {
// First of all, give the opportunity to subclasses to apply their own configurations
initializeSpringSpecific();
// Once the subclasses have had their opportunity, compute configurations belonging to
// SpringTemplateEngine
super.initializeSpecific();
final MessageSource messageSource =
this.templateEngineMessageSource == null ? this.messageSource
: this.templateEngineMessageSource;
final IMessageResolver messageResolver;
if (messageSource != null) {
final SpringMessageResolver springMessageResolver = new SpringMessageResolver();
springMessageResolver.setMessageSource(messageSource);
messageResolver = springMessageResolver;
} else {
messageResolver = new StandardMessageResolver();
}
super.addMessageResolver(messageResolver);
}
/**
* <p>
* This method performs additional initializations required for a
* {@code SpringTemplateEngine} subclass instance. This method
* is called before the first execution of
* {@link TemplateEngine#process(String, org.thymeleaf.context.IContext)}
* or {@link TemplateEngine#processThrottled(String, org.thymeleaf.context.IContext)}
* in order to create all the structures required for a quick execution of
* templates.
* </p>
* <p>
* THIS METHOD IS INTERNAL AND SHOULD <b>NEVER</b> BE CALLED DIRECTLY.
* </p>
* <p>
* The implementation of this method does nothing, and it is designed
* for being overridden by subclasses of {@code SpringTemplateEngine}.
* </p>
*/
protected void initializeSpringSpecific() {
// Nothing to be executed here. Meant for extension
}
}