Skip to content

Commit

Permalink
Qute: extend the @EngineConfiguration support to ParserHook
Browse files Browse the repository at this point in the history
Co-authored-by: George Gastaldi <[email protected]>
  • Loading branch information
mkouba and gastaldi committed Jul 4, 2024
1 parent e9d6790 commit 7c899fe
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 41 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 @@ -618,22 +618,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 @@ -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(
Expand All @@ -2312,7 +2326,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,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;
}

}

}
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,7 +90,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
Event<EngineBuilder> builderReady, Event<Engine> engineReady, ContentTypes contentTypes,
LaunchMode launchMode, LocalesBuildTimeConfig locales, @All List<TemplateLocator> locators,
@All List<SectionHelperFactory<?>> sectionHelperFactories, @All List<ValueResolver> valueResolvers,
@All List<NamespaceResolver> namespaceResolvers) {
@All List<NamespaceResolver> namespaceResolvers, @All List<ParserHook> parserHooks) {
this.contentTypes = contentTypes;
this.suffixes = config.suffixes;
this.templateRoots = context.getTemplateRoots();
Expand Down Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,21 @@
*
* Enables registration of additional components to the preconfigured {@link Engine}.
* <p>
* A top-level or static nested class that implements one of the <b>supported component interfaces</b> and is annotated with
* this
* annotation:
* A non-abstract, top-level or static nested class that implements one of the <b>supported component interfaces</b> and
* is annotated
* with this annotation:
* <ul>
* <li>can be used during validation of templates at build time,</li>
* <li>is automatically registered at runtime (a) to the preconfigured {@link Engine} and (b) as a CDI bean.</li>
* </ul>
*
* 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}.
* <p>
* An annotated class that implements {@link SectionHelperFactory} must declare a no-args constructor that is used to
* instantiate the component at build time.
* <p>
* 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public interface ParserHelper {
/**
* Adds an <em>implicit</em> parameter declaration. This is an alternative approach to <em>explicit</em> parameter
* declarations used directly in the templates, e.g. <code>{@org.acme.Foo foo}</code>.
* <p>
* 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<? extends org.acme.Foo>} is recognized as
* {@code java.util.List<org.acme.Foo> list}. Type variables are not handled in a special way and should never be used.
*
* @param name
* @param type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

/**
* This component can be used to hook into the parser logic.
*
* @see EngineBuilder#addParserHook(ParserHook)
* @see EngineConfiguration
*/
public interface ParserHook {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends SectionHelper> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down

0 comments on commit 7c899fe

Please sign in to comment.