From 7c899fe8bc953e9fd897a74b2a9a2b735b800b3f Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 4 Jul 2024 14:44:22 +0200 Subject: [PATCH 1/2] Qute: extend the `@EngineConfiguration` support to ParserHook Co-authored-by: George Gastaldi --- docs/src/main/asciidoc/qute-reference.adoc | 2 +- .../io/quarkus/qute/deployment/Names.java | 2 + .../qute/deployment/QuteProcessor.java | 68 +++++++++++------- .../parserhook/CustomParserHookBuildTest.java | 71 +++++++++++++++++++ .../CustomParserHookRuntimeTest.java | 48 +++++++++++++ .../section/WrongTargetConstructorTest.java | 2 +- .../section/WrongTargetNestedTypeTest.java | 2 +- .../quarkus/qute/runtime/EngineProducer.java | 7 +- .../io/quarkus/qute/EngineConfiguration.java | 19 +++-- .../io/quarkus/qute/NamespaceResolver.java | 1 + .../java/io/quarkus/qute/ParserHelper.java | 5 ++ .../main/java/io/quarkus/qute/ParserHook.java | 3 + .../io/quarkus/qute/SectionHelperFactory.java | 1 + .../java/io/quarkus/qute/ValueResolver.java | 2 + 14 files changed, 192 insertions(+), 41 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookRuntimeTest.java diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index dff500a62efb2..43687701a925f 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1626,7 +1626,7 @@ public class CustomSectionFactory implements SectionHelperFactory 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 diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java index aeef20ef729b7..e0d2afbf4a999 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java @@ -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; @@ -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() { } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index f591dcfb3cf27..f0958213ff94a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -618,22 +618,32 @@ TemplatesAnalysisBuildItem analyzeTemplates(List template } } - // Register additional section factories + // Register additional section factories and parser hooks if (engineConfigurations.isPresent()) { - Collection 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); + } } } } @@ -2281,28 +2291,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( @@ -2312,7 +2326,7 @@ void collectEngineConfigurations( new String[] { SectionHelperFactory.class.getName(), ValueResolver.class.getName(), NamespaceResolver.class.getName() }), - targetClass.name())))); + clazz.name())))); } } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java new file mode 100644 index 0000000000000..b2b7f2af94323 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java @@ -0,0 +1,71 @@ +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 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.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()); + });; + + @Inject + Engine engine; + + @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; + } + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookRuntimeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookRuntimeTest.java new file mode 100644 index 0000000000000..859389e76b0d0 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookRuntimeTest.java @@ -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}"); + } + } + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetConstructorTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetConstructorTest.java index 82e3feb4f4655..7a2dd4255b0af 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetConstructorTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetConstructorTest.java @@ -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); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetNestedTypeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetNestedTypeTest.java index 2a8b66d6804ca..7fe1a5eae1bd4 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetNestedTypeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/section/WrongTargetNestedTypeTest.java @@ -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); diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index 5e513a98d04c5..bc3d9c95225df 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -41,6 +41,7 @@ import io.quarkus.qute.Expression; import io.quarkus.qute.HtmlEscaper; import io.quarkus.qute.NamespaceResolver; +import io.quarkus.qute.ParserHook; import io.quarkus.qute.Qute; import io.quarkus.qute.ReflectionValueResolver; import io.quarkus.qute.Resolver; @@ -89,7 +90,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig Event builderReady, Event engineReady, ContentTypes contentTypes, LaunchMode launchMode, LocalesBuildTimeConfig locales, @All List locators, @All List> sectionHelperFactories, @All List valueResolvers, - @All List namespaceResolvers) { + @All List namespaceResolvers, @All List parserHooks) { this.contentTypes = contentTypes; this.suffixes = config.suffixes; this.templateRoots = context.getTemplateRoots(); @@ -205,6 +206,10 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig builder.addLocator(this::locate); registerCustomLocators(builder, locators); + // Add parser hooks + for (ParserHook parserHook : parserHooks) { + builder.addParserHook(parserHook); + } // Add a special parser hook for Qute.fmt() methods builder.addParserHook(new Qute.IndexedArgumentsParserHook()); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineConfiguration.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineConfiguration.java index f103b54208d46..7457801fe8d80 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineConfiguration.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineConfiguration.java @@ -13,22 +13,21 @@ * * Enables registration of additional components to the preconfigured {@link Engine}. *

- * A top-level or static nested class that implements one of the supported component interfaces and is annotated with - * this - * annotation: + * A non-abstract, top-level or static nested class that implements one of the supported component interfaces and + * is annotated + * with this annotation: *

    *
  • can be used during validation of templates at build time,
  • *
  • is automatically registered at runtime (a) to the preconfigured {@link Engine} and (b) as a CDI bean.
  • *
* - * The list of supported component interfaces includes: {@link SectionHelperFactory}, {@link ValueResolver} and - * {@link NamespaceResolver}. + * The list of supported component interfaces includes: {@link SectionHelperFactory}, {@link ValueResolver}, + * {@link NamespaceResolver} and {@link ParserHook}. *

- * An annotated class that implements {@link SectionHelperFactory} must declare a no-args constructor that is used to - * instantiate the component at build time. - *

- * At runtime, a CDI bean instance is used. This means that the factory can define injection points. If no CDI scope is defined - * then {@code javax.enterprise.context.Dependent} is used. + * An annotated class that implements {@link SectionHelperFactory} or {@link ParserHook} must be public and declare a no-args + * constructor that is used to instantiate the component at build time. At runtime, a CDI bean instance is used. This means that + * the factory can declare injection points. However, these injection points are only injected at runtime. If no CDI scope is + * defined then {@code javax.enterprise.context.Dependent} is used. * * @see EngineBuilder#addSectionHelper(SectionHelperFactory) * @see EngineBuilder#addValueResolver(ValueResolver) diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/NamespaceResolver.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/NamespaceResolver.java index 04301d1546e07..16e4f1455a8e1 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/NamespaceResolver.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/NamespaceResolver.java @@ -10,6 +10,7 @@ * For example the expression {@code data:colors} declares a namespace {@code data}. * * @see EngineBuilder#addNamespaceResolver(NamespaceResolver) + * @see EngineConfiguration */ public interface NamespaceResolver extends Resolver, WithPriority { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java index d44fb05de3c85..1bf0e915a0433 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java @@ -17,6 +17,11 @@ public interface ParserHelper { /** * Adds an implicit parameter declaration. This is an alternative approach to explicit parameter * declarations used directly in the templates, e.g. {@org.acme.Foo foo}. + *

+ * The type is a fully qualified class name. The package name is optional for JDK types from the {@code java.lang} + * package. Parameterized types are supported, however wildcards are always ignored - only the upper/lower bound is taken + * into account. For example, the type info {@code java.util.List} is recognized as + * {@code java.util.List list}. Type variables are not handled in a special way and should never be used. * * @param name * @param type diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHook.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHook.java index 8b02e424f3a68..78b1e60af4218 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHook.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHook.java @@ -2,6 +2,9 @@ /** * This component can be used to hook into the parser logic. + * + * @see EngineBuilder#addParserHook(ParserHook) + * @see EngineConfiguration */ public interface ParserHook { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java index 849170227e8ec..7e81f7b583a70 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java @@ -14,6 +14,7 @@ * Factory to create a new {@link SectionHelper} based on the {@link SectionInitContextImpl}. * * @see EngineBuilder#addSectionHelper(SectionHelperFactory) + * @see EngineConfiguration */ public interface SectionHelperFactory { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolver.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolver.java index 267d4f2529db1..918b82f31f496 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolver.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolver.java @@ -7,6 +7,8 @@ * to resolve the data. If {@link Results#isNotFound(Object)} is returned the next available resolver is tried. * * @see EvalContext + * @see EngineBuilder#addValueResolver(ValueResolver) + * @see EngineConfiguration */ public interface ValueResolver extends Resolver, WithPriority { From 69538fc93d71bc3bfbc95a8e9e8501b0c40136f0 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 4 Jul 2024 15:04:02 +0200 Subject: [PATCH 2/2] Qute: introduce ValidationParserHookBuildItem - that can be used to hook into the parser logic during validation at build time --- .../qute/deployment/QuteProcessor.java | 11 ++- .../ValidationParserHookBuildItem.java | 30 ++++++++ .../parserhook/CustomParserHookBuildTest.java | 6 -- .../parserhook/ValidationHookTest.java | 76 +++++++++++++++++++ 4 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/ValidationParserHookBuildItem.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/ValidationHookTest.java diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index f0958213ff94a..f40f1eeb724aa 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -591,8 +591,11 @@ private boolean isNotLocatedByCustomTemplateLocator( @BuildStep TemplatesAnalysisBuildItem analyzeTemplates(List templatePaths, - TemplateFilePathsBuildItem filePaths, List checkedTemplates, - List messageBundleMethods, List globals, QuteConfig config, + TemplateFilePathsBuildItem filePaths, + List checkedTemplates, + List messageBundleMethods, + List globals, QuteConfig config, + List validationParserHooks, Optional engineConfigurations, BeanArchiveIndexBuildItem beanArchiveIndex, BuildProducer checkedFragmentValidations) { @@ -719,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 diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/ValidationParserHookBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/ValidationParserHookBuildItem.java new file mode 100644 index 0000000000000..656a17a4d0d09 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/ValidationParserHookBuildItem.java @@ -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. + *

+ * Validation parser hooks are never used at runtime. + */ +public final class ValidationParserHookBuildItem extends MultiBuildItem { + + private final Consumer hook; + + public ValidationParserHookBuildItem(Consumer hook) { + this.hook = Objects.requireNonNull(hook); + } + + public Consumer getHook() { + return hook; + } + + public void accept(ParserHelper helper) { + hook.accept(helper); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java index b2b7f2af94323..62b4192bcb31c 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/CustomParserHookBuildTest.java @@ -4,13 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -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; @@ -39,9 +36,6 @@ public class CustomParserHookBuildTest { assertTrue(te.getMessage().contains("{foo.bar}"), te.getMessage()); });; - @Inject - Engine engine; - @Test public void test() { fail(); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/ValidationHookTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/ValidationHookTest.java new file mode 100644 index 0000000000000..e76dc7b8c851a --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/parserhook/ValidationHookTest.java @@ -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 buildCustomizer() { + return new Consumer() { + @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; + } + + } + +}