Skip to content

Commit

Permalink
Provide ElementTagPostProcessor to handle element tag in plugin
Browse files Browse the repository at this point in the history
Signed-off-by: JohnNiang <[email protected]>
  • Loading branch information
JohnNiang committed Sep 18, 2024
1 parent 3fda9e6 commit 3fd3df7
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package run.halo.app.theme.dialect;

import org.pf4j.ExtensionPoint;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import reactor.core.publisher.Mono;

/**
* An extension point for post-processing element tag.
*
* @author johnniang
* @since 2.20.0
*/
public interface ElementTagPostProcessor extends ExtensionPoint {

/**
* <p>
* Execute the processor.
* </p>
* <p>
* The {@link IProcessableElementTag} object argument is immutable, so all modifications to
* this object or any
* instructions to be given to the engine should be done through the specified
* {@link IElementTagStructureHandler} handler.
* </p>
*
* @param context the execution context.
* @param tag the event this processor is executing on.
* @param structureHandler the handler that will centralise modifications and commands to the
* engine.
* @return a {@link Mono} that will complete when processing finishes or empty mono if
* not support.
*/
Mono<Void> process(
final ITemplateContext context,
final IProcessableElementTag tag,
final IElementTagStructureHandler structureHandler);

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.plugin.HaloPluginManager;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.theme.dialect.HaloProcessorDialect;
import run.halo.app.theme.engine.HaloTemplateEngine;
import run.halo.app.theme.engine.PluginClassloaderTemplateResolver;
Expand Down Expand Up @@ -56,16 +57,22 @@ public class TemplateEngineManager {

private final ThemeResolver themeResolver;

private final ExtensionGetter extensionGetter;

public TemplateEngineManager(ThymeleafProperties thymeleafProperties,
ExternalUrlSupplier externalUrlSupplier,
HaloPluginManager haloPluginManager, ObjectProvider<ITemplateResolver> templateResolvers,
ObjectProvider<IDialect> dialects, ThemeResolver themeResolver) {
HaloPluginManager haloPluginManager,
ObjectProvider<ITemplateResolver> templateResolvers,
ObjectProvider<IDialect> dialects,
ThemeResolver themeResolver,
ExtensionGetter extensionGetter) {
this.thymeleafProperties = thymeleafProperties;
this.externalUrlSupplier = externalUrlSupplier;
this.haloPluginManager = haloPluginManager;
this.templateResolvers = templateResolvers;
this.dialects = dialects;
this.themeResolver = themeResolver;
this.extensionGetter = extensionGetter;
engineCache = new ConcurrentLruCache<>(CACHE_SIZE_LIMIT, this::templateEngineGenerator);
}

Expand Down Expand Up @@ -134,7 +141,7 @@ public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {
return ReactiveSpelVariableExpressionEvaluator.INSTANCE;
}
});
engine.addDialect(new HaloProcessorDialect());
engine.addDialect(new HaloProcessorDialect(extensionGetter));

templateResolvers.orderedStream().forEach(engine::addTemplateResolver);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package run.halo.app.theme.dialect;

import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.templatemode.TemplateMode;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;

/**
* Element tag processors.
*
* @author johnniang
* @since 2.20.0
*/
@Slf4j
public class ElementTagPostProcessors extends AbstractElementTagProcessor {

private static final int PRECEDENCE = Integer.MAX_VALUE;

private final ExtensionGetter extensionGetter;

public ElementTagPostProcessors(ExtensionGetter extensionGetter) {
super(TemplateMode.HTML,
null,
null,
false,
null,
false,
PRECEDENCE);
this.extensionGetter = extensionGetter;
}

@Override
protected void doProcess(
ITemplateContext context,
IProcessableElementTag tag,
IElementTagStructureHandler structureHandler
) {
extensionGetter.getExtensions(ElementTagPostProcessor.class)
.concatMap(processor -> processor.process(context, tag, structureHandler)
.doOnSuccess(v -> {
if (log.isDebugEnabled()) {
log.debug("Processed tag [{}] with processor [{}]",
tag.getElementCompleteName(), processor.getClass().getName()
);
}
})
)
.then()
.block(Duration.ofSeconds(20));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.thymeleaf.expression.IExpressionObjectFactory;
import org.thymeleaf.processor.IProcessor;
import org.thymeleaf.standard.StandardDialect;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;

/**
* Thymeleaf processor dialect for Halo.
Expand All @@ -21,10 +22,13 @@ public class HaloProcessorDialect extends AbstractProcessorDialect implements
private static final IExpressionObjectFactory HALO_EXPRESSION_OBJECTS_FACTORY =
new HaloExpressionObjectFactory();

public HaloProcessorDialect() {
private final ExtensionGetter extensionGetter;

public HaloProcessorDialect(ExtensionGetter extensionGetter) {
// We will set this dialect the same "dialect processor" precedence as
// the Standard Dialect, so that processor executions can interleave.
super(DIALECT_NAME, "halo", StandardDialect.PROCESSOR_PRECEDENCE);
this.extensionGetter = extensionGetter;
}

@Override
Expand All @@ -36,6 +40,7 @@ public Set<IProcessor> getProcessors(String dialectPrefix) {
processors.add(new JsonNodePropertyAccessorBoundariesProcessor());
processors.add(new CommentElementTagProcessor(dialectPrefix));
processors.add(new CommentEnabledVariableProcessor());
processors.add(new ElementTagPostProcessors(extensionGetter));
return processors;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
Expand All @@ -29,6 +30,8 @@
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.theme.dialect.ElementTagPostProcessor;
import run.halo.app.theme.dialect.HaloProcessorDialect;

/**
Expand All @@ -47,11 +50,15 @@ public class ReactiveFinderExpressionParserTests {
@Mock
private SystemConfigurableEnvironmentFetcher environmentFetcher;

@Mock
ExtensionGetter extensionGetter;

private TemplateEngine templateEngine;

@BeforeEach
void setUp() {
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ class CommentElementTagProcessorTest {

@BeforeEach
void setUp() {
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new TestTemplateResolver());
lenient().when(applicationContext.getBean(eq(ExtensionGetter.class)))
.thenReturn(extensionGetter);
when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class ContentTemplateHeadProcessorIntegrationTest {

@BeforeEach
void setUp() {
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new TestTemplateResolver());
Expand Down Expand Up @@ -114,6 +114,7 @@ void setUp() {
lenient().when(extensionGetter.getExtensions(TemplateHeadProcessor.class)).thenReturn(
Flux.fromIterable(map.values()).sort(AnnotationAwareOrderComparator.INSTANCE)
);
when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
.thenReturn(fetcher);
lenient().when(fetcher.fetchComment()).thenReturn(Mono.just(new SystemSetting.Comment()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package run.halo.app.theme.dialect;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.model.IProcessableElementTag;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;

@ExtendWith(MockitoExtension.class)
class ElementTagPostProcessorsTest {

@Mock
ExtensionGetter extensionGetter;

@InjectMocks
ElementTagPostProcessors elementTagPostProcessors;

@Test
void shouldDoNothingIfNoProcessorsFound() {
when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
elementTagPostProcessors.process(null, null, null);
}

@Test
void shouldProcessWithOneProcessor() {
var processor = mock(ElementTagPostProcessor.class);
var tag = mock(IProcessableElementTag.class);
doReturn(Mono.empty()).when(processor).process(null, tag, null);
when(extensionGetter.getExtensions(ElementTagPostProcessor.class))
.thenReturn(Flux.just(processor));
elementTagPostProcessors.process(null, tag, null);
}

@Test
void shouldProcessWithTwoProcessors() {
var processor1 = mock(ElementTagPostProcessor.class);
var processor2 = mock(ElementTagPostProcessor.class);
var tag = mock(IProcessableElementTag.class);
doReturn(Mono.empty()).when(processor1).process(null, tag, null);
doReturn(Mono.empty()).when(processor2).process(null, tag, null);
when(extensionGetter.getExtensions(ElementTagPostProcessor.class))
.thenReturn(Flux.just(processor1, processor2));
elementTagPostProcessors.process(null, tag, null);
var inOrder = inOrder(processor1, processor2);
inOrder.verify(processor1).process(null, tag, null);
inOrder.verify(processor2).process(null, tag, null);
}

@Test
void shouldProcessWithFailure() {
var tag = mock(IProcessableElementTag.class);
var processor1 = mock(ElementTagPostProcessor.class);
var processor2 = mock(ElementTagPostProcessor.class);
doReturn(Mono.error(new IllegalStateException("failed to process")))
.when(processor1).process(null, tag, null);
when(extensionGetter.getExtensions(ElementTagPostProcessor.class))
.thenReturn(Flux.just(processor1, processor2));

var e = assertThrows(TemplateProcessingException.class,
() -> elementTagPostProcessors.process(null, tag, null)
);
assertInstanceOf(IllegalStateException.class, e.getCause());
assertEquals("failed to process", e.getCause().getMessage());
verify(processor2, never()).process(null, tag, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class HaloProcessorDialectTest {

@BeforeEach
void setUp() {
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new TestTemplateResolver());
Expand Down Expand Up @@ -113,6 +113,7 @@ void setUp() {
lenient().when(extensionGetter.getExtensions(TemplateHeadProcessor.class)).thenReturn(
Flux.fromIterable(map.values()).sort(AnnotationAwareOrderComparator.INSTANCE)
);
when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());

lenient().when(fetcher.fetchComment())
.thenReturn(Mono.just(new SystemSetting.Comment()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class TemplateFooterElementTagProcessorTest {

@BeforeEach
void setUp() {
HaloProcessorDialect haloProcessorDialect = new MockHaloProcessorDialect();
HaloProcessorDialect haloProcessorDialect = new MockHaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new MockTemplateResolver());
Expand Down Expand Up @@ -111,6 +111,10 @@ protected ITemplateResource computeTemplateResource(IEngineConfiguration configu
}

static class MockHaloProcessorDialect extends HaloProcessorDialect {
MockHaloProcessorDialect(ExtensionGetter extensionGetter) {
super(extensionGetter);
}

@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
var processors = new HashSet<IProcessor>();
Expand Down

0 comments on commit 3fd3df7

Please sign in to comment.