Skip to content

Commit

Permalink
Merge pull request quarkusio#41690 from mkouba/qute-parser-hook-engin…
Browse files Browse the repository at this point in the history
…e-config

Qute: extend the @EngineConfiguration support to ParserHook
  • Loading branch information
gastaldi authored Jul 5, 2024
2 parents 1c1b7ef + 69538fc commit 4e36684
Show file tree
Hide file tree
Showing 16 changed files with 301 additions and 43 deletions.
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,7 @@ public class CustomSectionFactory implements SectionHelperFactory<CustomSectionF
<3> Validate that `foo` parameter is always present; e.g. `{#custom foo='bar' /}` is ok but `{#custom /}` results in a build failure.
<4> Use the injected `Service` during rendering.

The `@EngineConfiguration` annotation can be also used to register ``ValueResolver``s and ``NamespaceResolver``s.
TIP: The `@EngineConfiguration` annotation can be also used to register `ValueResolver`, `NamespaceResolver` and `ParserHook` components.

[[template-locator-registration]]
==== Template Locator Registration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.quarkus.qute.Locate.Locates;
import io.quarkus.qute.Location;
import io.quarkus.qute.NamespaceResolver;
import io.quarkus.qute.ParserHook;
import io.quarkus.qute.SectionHelperFactory;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateEnum;
Expand Down Expand Up @@ -52,6 +53,7 @@ final class Names {
static final DotName SECTION_HELPER_FACTORY = DotName.createSimple(SectionHelperFactory.class.getName());
static final DotName VALUE_RESOLVER = DotName.createSimple(ValueResolver.class.getName());
static final DotName NAMESPACE_RESOLVER = DotName.createSimple(NamespaceResolver.class.getName());
static final DotName PARSER_HOOK = DotName.createSimple(ParserHook.class);

private Names() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,11 @@ private boolean isNotLocatedByCustomTemplateLocator(

@BuildStep
TemplatesAnalysisBuildItem analyzeTemplates(List<TemplatePathBuildItem> templatePaths,
TemplateFilePathsBuildItem filePaths, List<CheckedTemplateBuildItem> checkedTemplates,
List<MessageBundleMethodBuildItem> messageBundleMethods, List<TemplateGlobalBuildItem> globals, QuteConfig config,
TemplateFilePathsBuildItem filePaths,
List<CheckedTemplateBuildItem> checkedTemplates,
List<MessageBundleMethodBuildItem> messageBundleMethods,
List<TemplateGlobalBuildItem> globals, QuteConfig config,
List<ValidationParserHookBuildItem> validationParserHooks,
Optional<EngineConfigurationsBuildItem> engineConfigurations,
BeanArchiveIndexBuildItem beanArchiveIndex,
BuildProducer<CheckedFragmentValidationBuildItem> checkedFragmentValidations) {
Expand All @@ -618,22 +621,32 @@ TemplatesAnalysisBuildItem analyzeTemplates(List<TemplatePathBuildItem> template
}
}

// Register additional section factories
// Register additional section factories and parser hooks
if (engineConfigurations.isPresent()) {
Collection<ClassInfo> sectionFactories = engineConfigurations.get().getConfigurations().stream()
.filter(c -> Types.isImplementorOf(c, Names.SECTION_HELPER_FACTORY, beanArchiveIndex.getIndex()))
.collect(Collectors.toList());
// Use the deployment class loader - it can load application classes; it's non-persistent and isolated
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
for (ClassInfo factoryClass : sectionFactories) {
try {
Class<?> sectionHelperFactoryClass = tccl.loadClass(factoryClass.toString());
SectionHelperFactory<?> factory = (SectionHelperFactory<?>) sectionHelperFactoryClass
.getDeclaredConstructor().newInstance();
builder.addSectionHelper(factory);
LOGGER.debugf("SectionHelperFactory registered during template analysis: " + factoryClass);
} catch (Exception e) {
throw new IllegalStateException("Unable to instantiate SectionHelperFactory: " + factoryClass, e);
IndexView index = beanArchiveIndex.getIndex();

for (ClassInfo engineConfigClass : engineConfigurations.get().getConfigurations()) {
if (Types.isImplementorOf(engineConfigClass, Names.SECTION_HELPER_FACTORY, index)) {
try {
Class<?> sectionHelperFactoryClass = tccl.loadClass(engineConfigClass.toString());
SectionHelperFactory<?> factory = (SectionHelperFactory<?>) sectionHelperFactoryClass
.getDeclaredConstructor().newInstance();
builder.addSectionHelper(factory);
LOGGER.debugf("SectionHelperFactory registered during template analysis: %s", engineConfigClass);
} catch (Exception e) {
throw new IllegalStateException("Unable to instantiate SectionHelperFactory: " + engineConfigClass, e);
}
} else if (Types.isImplementorOf(engineConfigClass, Names.PARSER_HOOK, index)) {
try {
Class<?> parserHookClass = tccl.loadClass(engineConfigClass.toString());
ParserHook parserHook = (ParserHook) parserHookClass.getDeclaredConstructor().newInstance();
builder.addParserHook(parserHook);
LOGGER.debugf("ParserHook registered during template analysis: %s", engineConfigClass);
} catch (Exception e) {
throw new IllegalStateException("Unable to instantiate ParserHook: " + engineConfigClass, e);
}
}
}
}
Expand Down Expand Up @@ -709,6 +722,10 @@ public void beforeParsing(ParserHelper parserHelper) {
parserHelper.addParameter(UserTagSectionHelper.Factory.ARGS,
UserTagSectionHelper.Arguments.class.getName());
}

for (ValidationParserHookBuildItem hook : validationParserHooks) {
hook.accept(parserHelper);
}
}

// If needed add params to message bundle templates
Expand Down Expand Up @@ -2281,28 +2298,32 @@ void collectEngineConfigurations(
for (AnnotationInstance annotation : engineConfigAnnotations) {
AnnotationTarget target = annotation.target();
if (target.kind() == Kind.CLASS) {
ClassInfo targetClass = target.asClass();
ClassInfo clazz = target.asClass();

if (targetClass.nestingType() != NestingType.TOP_LEVEL
&& (targetClass.nestingType() != NestingType.INNER || !Modifier.isStatic(targetClass.flags()))) {
if (clazz.isAbstract()
|| clazz.isInterface()
|| (clazz.nestingType() != NestingType.TOP_LEVEL
&& (clazz.nestingType() != NestingType.INNER || !Modifier.isStatic(clazz.flags())))) {
validationErrors.produce(
new ValidationErrorBuildItem(
new TemplateException(String.format(
"Only top-level and static nested classes may be annotated with @%s: %s",
EngineConfiguration.class.getSimpleName(), targetClass.name()))));
} else if (Types.isImplementorOf(targetClass, Names.SECTION_HELPER_FACTORY, index)) {
if (targetClass.hasNoArgsConstructor()) {
engineConfigClasses.add(targetClass);
"Only non-abstract, top-level or static nested classes may be annotated with @%s: %s",
EngineConfiguration.class.getSimpleName(), clazz.name()))));
} else if (Types.isImplementorOf(clazz, Names.SECTION_HELPER_FACTORY, index)
|| Types.isImplementorOf(clazz, Names.PARSER_HOOK, index)) {
if (clazz.hasNoArgsConstructor()
&& Modifier.isPublic(clazz.flags())) {
engineConfigClasses.add(clazz);
} else {
validationErrors.produce(
new ValidationErrorBuildItem(
new TemplateException(String.format(
"A class annotated with @%s that also implements io.quarkus.qute.SectionHelperFactory must declare a no-args constructor: %s",
EngineConfiguration.class.getSimpleName(), targetClass.name()))));
"A class annotated with @%s that also implements SectionHelperFactory or ParserHelper must be public and declare a no-args constructor: %s",
EngineConfiguration.class.getSimpleName(), clazz.name()))));
}
} else if (Types.isImplementorOf(targetClass, Names.VALUE_RESOLVER, index)
|| Types.isImplementorOf(targetClass, Names.NAMESPACE_RESOLVER, index)) {
engineConfigClasses.add(targetClass);
} else if (Types.isImplementorOf(clazz, Names.VALUE_RESOLVER, index)
|| Types.isImplementorOf(clazz, Names.NAMESPACE_RESOLVER, index)) {
engineConfigClasses.add(clazz);
} else {
validationErrors.produce(
new ValidationErrorBuildItem(
Expand All @@ -2312,7 +2333,7 @@ void collectEngineConfigurations(
new String[] { SectionHelperFactory.class.getName(),
ValueResolver.class.getName(),
NamespaceResolver.class.getName() }),
targetClass.name()))));
clazz.name()))));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.qute.deployment;

import java.util.Objects;
import java.util.function.Consumer;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.qute.ParserHelper;

/**
* This build item can be used to hook into the parser logic during validation at build time.
* <p>
* Validation parser hooks are never used at runtime.
*/
public final class ValidationParserHookBuildItem extends MultiBuildItem {

private final Consumer<ParserHelper> hook;

public ValidationParserHookBuildItem(Consumer<ParserHelper> hook) {
this.hook = Objects.requireNonNull(hook);
}

public Consumer<ParserHelper> getHook() {
return hook;
}

public void accept(ParserHelper helper) {
hook.accept(helper);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.quarkus.qute.deployment.engineconfigurations.parserhook;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.EngineConfiguration;
import io.quarkus.qute.ParserHelper;
import io.quarkus.qute.ParserHook;
import io.quarkus.qute.TemplateException;
import io.quarkus.test.QuarkusUnitTest;

public class CustomParserHookBuildTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(
root -> root.addClasses(CustomParserHook.class, Foo.class)
.addAsResource(new StringAsset("{foo.bar}"), "templates/foo.html"))
.assertException(t -> {
Throwable e = t;
TemplateException te = null;
while (e != null) {
if (e instanceof TemplateException) {
te = (TemplateException) e;
break;
}
e = e.getCause();
}
assertNotNull(te);
assertTrue(te.getMessage().contains("Found incorrect expressions (1)"), te.getMessage());
assertTrue(te.getMessage().contains("{foo.bar}"), te.getMessage());
});;

@Test
public void test() {
fail();
}

@EngineConfiguration
public static class CustomParserHook implements ParserHook {

@Override
public void beforeParsing(ParserHelper helper) {
if (helper.getTemplateId().contains("foo")) {
helper.addParameter("foo", Foo.class.getName());
}
}

}

public static class Foo {

// package-private method is ignored
String bar() {
return null;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.quarkus.qute.deployment.engineconfigurations.parserhook;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Engine;
import io.quarkus.qute.EngineConfiguration;
import io.quarkus.qute.ParserHelper;
import io.quarkus.qute.ParserHook;
import io.quarkus.test.QuarkusUnitTest;

public class CustomParserHookRuntimeTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(
root -> root.addClasses(CustomParserHook.class)
.addAsResource(new StringAsset("{foo}"), "templates/foo.html"));

@Inject
Engine engine;

@Test
public void testParserHook() {
assertEquals("42", engine.getTemplate("foo").data("bar", 42).render());
}

@EngineConfiguration
public static class CustomParserHook implements ParserHook {

@Inject
Engine engine;

@Override
public void beforeParsing(ParserHelper helper) {
if (helper.getTemplateId().contains("foo") && engine != null) {
helper.addContentFilter(c -> "{bar}");
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.quarkus.qute.deployment.engineconfigurations.parserhook;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.function.Consumer;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.builder.BuildChainBuilder;
import io.quarkus.builder.BuildContext;
import io.quarkus.builder.BuildStep;
import io.quarkus.qute.TemplateException;
import io.quarkus.qute.deployment.ValidationParserHookBuildItem;
import io.quarkus.test.QuarkusUnitTest;

public class ValidationHookTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(
root -> root.addClasses(Foo.class)
.addAsResource(new StringAsset("{foo.bar}"), "templates/foo.html"))
.assertException(t -> {
Throwable e = t;
TemplateException te = null;
while (e != null) {
if (e instanceof TemplateException) {
te = (TemplateException) e;
break;
}
e = e.getCause();
}
assertNotNull(te);
assertTrue(te.getMessage().contains("Found incorrect expressions (1)"), te.getMessage());
assertTrue(te.getMessage().contains("{foo.bar}"), te.getMessage());
}).addBuildChainCustomizer(buildCustomizer());

static Consumer<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {
@Override
public void accept(BuildChainBuilder builder) {
builder.addBuildStep(new BuildStep() {
@Override
public void execute(BuildContext context) {
context.produce(new ValidationParserHookBuildItem(helper -> {
if (helper.getTemplateId().contains("foo")) {
helper.addParameter("foo", Foo.class.getName());
}
}));
}
}).produces(ValidationParserHookBuildItem.class)
.build();

}
};
}

@Test
public void test() {
fail();
}

public static class Foo {

// package-private method is ignored
String bar() {
return null;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class WrongTargetConstructorTest {
Throwable rootCause = ExceptionUtil.getRootCause(t);
if (rootCause instanceof TemplateException) {
assertTrue(rootCause.getMessage().contains(
"A class annotated with @EngineConfiguration that also implements io.quarkus.qute.SectionHelperFactory must declare a no-args constructor:"),
"A class annotated with @EngineConfiguration that also implements SectionHelperFactory or ParserHelper must be public and declare a no-args constructor"),
rootCause.toString());
} else {
fail("No TemplateException thrown: " + t);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class WrongTargetNestedTypeTest {
Throwable rootCause = ExceptionUtil.getRootCause(t);
if (rootCause instanceof TemplateException) {
assertTrue(rootCause.getMessage().contains(
"Only top-level and static nested classes may be annotated with @EngineConfiguration:"),
"Only non-abstract, top-level or static nested classes may be annotated with @EngineConfiguration:"),
rootCause.toString());
} else {
fail("No TemplateException thrown: " + t);
Expand Down
Loading

0 comments on commit 4e36684

Please sign in to comment.