From 7917f25e963ddc7e44363400f2dfd59fc00d79a1 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Tue, 10 Mar 2020 14:37:47 +0100 Subject: [PATCH] Qute: add parser hook to set up parser before it parses --- .../io/quarkus/deployment/util/AsmUtil.java | 13 + .../quarkus/deployment/util/JandexUtil.java | 26 ++ docs/src/main/asciidoc/mailer.adoc | 33 +++ docs/src/main/asciidoc/qute.adoc | 242 ++++++++++++++++-- extensions/mailer/deployment/pom.xml | 1 + .../MailTemplateInstanceAdaptor.java | 28 ++ .../mailer/deployment/MailerProcessor.java | 6 + .../java/io/quarkus/mailer/InjectionTest.java | 15 ++ .../mailer/runtime/MailTemplateProducer.java | 10 + .../deployment/CheckedTemplateAdapter.java | 11 + .../CheckedTemplateAdapterBuildItem.java | 13 + .../deployment/CheckedTemplateBuildItem.java | 17 ++ .../NativeCheckedTemplateEnhancer.java | 143 +++++++++++ .../qute/deployment/QuteProcessor.java | 134 +++++++++- .../io/quarkus/qute/api/CheckedTemplate.java | 59 +++++ .../qute/runtime/TemplateProducer.java | 11 + extensions/resteasy-qute/deployment/pom.xml | 1 + .../resteasy/deployment/HelloResource.java | 63 ++++- .../deployment/MissingTemplateResource.java | 19 ++ .../deployment/MissingTemplateTest.java | 27 ++ .../TemplateResponseFilterTest.java | 19 ++ .../qute/resteasy/deployment/Templates.java | 9 + .../deployment/TypeErrorResource.java | 22 ++ .../resteasy/deployment/TypeErrorTest.java | 30 +++ .../resteasy/deployment/TypeErrorTest3.java | 26 ++ .../templates/HelloResource/hello.txt | 1 + .../templates/HelloResource/typeError.txt | 1 + .../templates/HelloResource/typeError2.txt | 1 + .../HelloResource/typedTemplate.html | 1 + .../templates/HelloResource/typedTemplate.txt | 1 + .../HelloResource/typedTemplatePrimitives.txt | 1 + .../MissingTemplateResource/hello.txt | 1 + .../TypeErrorResource/typeError3.txt | 1 + .../src/test/resources/templates/toplevel.txt | 1 + .../quarkus/resteasy/qute/RestTemplate.java | 31 +++ .../java/io/quarkus/qute/EngineBuilder.java | 9 +- .../main/java/io/quarkus/qute/EngineImpl.java | 17 +- .../java/io/quarkus/qute/ExpressionImpl.java | 2 +- .../java/io/quarkus/qute/IfSectionHelper.java | 5 +- .../io/quarkus/qute/LoopSectionHelper.java | 18 +- .../src/main/java/io/quarkus/qute/Parser.java | 64 ++--- .../java/io/quarkus/qute/ParserHelper.java | 5 + .../main/java/io/quarkus/qute/ParserHook.java | 7 + .../src/main/java/io/quarkus/qute/Scope.java | 41 +++ .../io/quarkus/qute/SectionHelperFactory.java | 6 +- .../io/quarkus/qute/SetSectionHelper.java | 14 +- .../io/quarkus/qute/WithSectionHelper.java | 8 +- .../java/io/quarkus/qute/TestEvalContext.java | 5 +- 48 files changed, 1122 insertions(+), 97 deletions(-) create mode 100644 extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailTemplateInstanceAdaptor.java create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapter.java create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapterBuildItem.java create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateBuildItem.java create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/NativeCheckedTemplateEnhancer.java create mode 100644 extensions/qute/runtime/src/main/java/io/quarkus/qute/api/CheckedTemplate.java create mode 100644 extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateResource.java create mode 100644 extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java create mode 100644 extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/Templates.java create mode 100644 extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorResource.java create mode 100644 extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest.java create mode 100644 extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest3.java create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/hello.txt create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError.txt create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError2.txt create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.html create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.txt create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplatePrimitives.txt create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/MissingTemplateResource/hello.txt create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/TypeErrorResource/typeError3.txt create mode 100644 extensions/resteasy-qute/deployment/src/test/resources/templates/toplevel.txt create mode 100644 extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/RestTemplate.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHook.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/AsmUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/AsmUtil.java index 9d4c638346b86..b1d43433b0780 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/AsmUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/AsmUtil.java @@ -542,4 +542,17 @@ public static int getParameterSize(Type paramType) { return 1; } + /** + * Prints the value pushed on the stack (must be an Object) by the given valuePusher + * to STDERR. + * + * @param mv The MethodVisitor to forward printing to. + * @param valuePusher The function to invoke to push an Object to print on the stack. + */ + public static void printValueOnStderr(MethodVisitor mv, Runnable valuePusher) { + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;"); + valuePusher.run(); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", + "(Ljava/lang/Object;)V", false); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java index 590c9bac233b3..f85928fd3a0b8 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java @@ -318,4 +318,30 @@ public static boolean isSubclassOf(IndexView index, ClassInfo info, DotName pare } return isSubclassOf(index, superClass, parentName); } + + @SuppressWarnings("incomplete-switch") + public static String getBoxedTypeName(Type type) { + switch (type.kind()) { + case PRIMITIVE: + switch (type.asPrimitiveType().primitive()) { + case BOOLEAN: + return "java.lang.Boolean"; + case BYTE: + return "java.lang.Byte"; + case CHAR: + return "java.lang.Character"; + case DOUBLE: + return "java.lang.Double"; + case FLOAT: + return "java.lang.Float"; + case INT: + return "java.lang.Integer"; + case LONG: + return "java.lang.Long"; + case SHORT: + return "java.lang.Short"; + } + } + return type.toString(); + } } diff --git a/docs/src/main/asciidoc/mailer.adoc b/docs/src/main/asciidoc/mailer.adoc index d744ca942d689..10a134dbd1fc0 100644 --- a/docs/src/main/asciidoc/mailer.adoc +++ b/docs/src/main/asciidoc/mailer.adoc @@ -230,6 +230,39 @@ When you want to reference your attachment, for instance in the `src` attribute, It's also possible to inject a mail template, where the message body is created automatically using link:qute[Qute templates]. +[source, java] +---- +@Path("") +public class MailingResource { + + @CheckedTemplate + class Templates { + public static native MailTemplateInstance hello(String name); <1> + } + + @GET + @Path("/mail") + public CompletionStage send() { + // the template looks like: Hello {name}! <2> + return Templates.hello("John") + .to("to@acme.org") <3> + .subject("Hello from Qute template") + .send() <4> + .subscribeAsCompletionStage() + .thenApply(x -> Response.accepted().build()); + } +} +---- +<1> By convention, the enclosing class name and method names are used to locate the template. In this particular case, +we will use the `MailingResource/hello.html` and `MailingResource/hello.txt` templates to create the message body. +<2> Set the data used in the template. +<3> Create a mail template instance and set the recipient. +<4> `MailTemplate.send()` triggers the rendering and, once finished, sends the e-mail via a `Mailer` instance. + +TIP: Injected mail templates are validated during build. If there is no matching template in `src/main/resources/templates` the build fails. + +You can also do this without type-safe templates: + [source, java] ---- @Inject diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc index fa4fb810ffb34..5c17b15ee56f9 100644 --- a/docs/src/main/asciidoc/qute.adoc +++ b/docs/src/main/asciidoc/qute.adoc @@ -81,12 +81,115 @@ $ curl -w "\n" http://localhost:8080/hello?name=Martin Hello Martin! ---- -== Parameter Declarations and Template Extension Methods +== Type-safe templates + +There's an alternate way to declare your templates in your Java code, which relies on the following convention: + +- Organise your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if + your `ItemResource` class references two templates `hello` and `goodbye`, place them at `/src/main/resources/templates/ItemResource/hello.txt` + and `/src/main/resources/templates/ItemResource/goodbye.txt`. Grouping templates per resource class makes it easier to navigate to them. +- In each of your resource class, declare a `@CheckedTemplate static class Template {}` class within your resource class. +- Declare one `public static native TemplateInstance method();` per template file for your resource. +- Use those static methods to build your template instances. + +Here's the previous example, rewritten using this style: + +We'll start with a very simple template: + +.HelloResource/hello.txt +[source] +---- +Hello {name}! <1> +---- +<1> `{name}` is a value expression that is evaluated when the template is rendered. + +Now let's declare and use those templates in the resource class. + +.HelloResource.java +[source,java] +---- +package org.acme.quarkus.sample; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; + +@Path("hello") +public class HelloResource { + + @CheckedTemplate + class Templates { + public static native TemplateInstance hello(); <1> + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance get(@QueryParam("name") String name) { + return Templates.hello().data("name", name); <2> <3> + } +} +---- +<1> This declares a template with path `templates/HelloResource/hello.txt`. +<2> `Templates.hello()` returns a new template instance that can be customized before the actual rendering is triggered. In this case, we put the name value under the key `name`. The data map is accessible during rendering. +<3> Note that we don't trigger the rendering - this is done automatically by a special `ContainerResponseFilter` implementation. + +NOTE: Once you have declared a `@CheckedTemplate` class, we will check that all its methods point to existing templates, so if you try to use a template +from your Java code and you forgot to add it, we will let you know at build time :) + +Keep in mind this style of declaration allows you to reference templates declared in other resources too: + +.HelloResource.java +[source,java] +---- +package org.acme.quarkus.sample; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import io.quarkus.qute.TemplateInstance; + +@Path("goodbye") +public class GoodbyeResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance get(@QueryParam("name") String name) { + return HelloResource.Templates.hello().data("name", name); + } +} +---- + +=== Toplevel type-safe templates + +Naturally, if you want to declare templates at the toplevel, directly in `/src/main/resources/templates/hello.txt`, for example, +you can declare them in a toplevel (non-nested) `Templates` class: + +.HelloResource.java +[source,java] +---- +package org.acme.quarkus.sample; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Template; +import io.quarkus.qute.api.CheckedTemplate; + +@CheckedTemplate +public class Templates { + public static native TemplateInstance hello(); <1> +} +---- +<1> This declares a template with path `templates/hello.txt`. + + +== Template Parameter Declarations -Qute has many useful features. -In this example, we'll demonstrate two of them. If you declare a *parameter declaration* in a template then Qute attempts to validate all expressions that reference this parameter and if an incorrect expression is found the build fails. -*Template extension methods* are used to extend the set of accessible properties of data objects. Let's suppose we have a simple class like this: @@ -99,9 +202,69 @@ public class Item { } ---- -And we'd like to render a simple HTML page that contains the item name, price and also a discounted price. -The discounted price is sometimes called a "computed property". -We will implement a template extension method to render this property easily. +And we'd like to render a simple HTML page that contains the item name and price. + +Let's start again with the template: + +.ItemResource/item.html +[source,html] +---- + + + + +{item.name} <1> + + +

{item.name}

+
Price: {item.price}
<2> + + +---- +<1> This expression is validated. Try to change the expression to `{item.nonSense}` and the build should fail. +<2> This is also validated. + +Finally, let's create a resource class with type-safe templates: + +.ItemResource.java +[source,java] +---- +package org.acme.quarkus.sample; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Template; +import io.quarkus.qute.api.CheckedTemplate; + +@Path("item") +public class ItemResource { + + @CheckedTemplate + class Templates { + public static native TemplateInstance item(Item item); <1> + } + + @GET + @Path("{id}") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance get(@PathParam("id") Integer id) { + return Templates.item(service.findItem(id)); <2> + } +} +---- +<1> Declare a method that gives us a `TemplateInstance` for `templates/ItemResource/item.html` and declare its `Item item` parameter so we can validate the template. +<2> Make the `Item` object accessible in the template. + +=== Template parameter declaration inside the template itself + +Alternatively, you can declare your template parameters in the template file itself. + Let's start again with the template: .item.html @@ -117,16 +280,11 @@ Let's start again with the template:

{item.name}

Price: {item.price}
- {#if item.price > 100} <3> -
Discounted Price: {item.discountedPrice}
<4> - {/if} ---- <1> Optional parameter declaration. Qute attempts to validate all expressions that reference the parameter `item`. <2> This expression is validated. Try to change the expression to `{item.nonSense}` and the build should fail. -<3> `if` is a basic control flow section. -<4> This expression is also validated against the `Item` class and obviously there is no such property declared. However, there is a template extension method declared on the `ItemResource` class - see below. Finally, let's create a resource class. @@ -160,16 +318,66 @@ public class ItemResource { public TemplateInstance get(@PathParam("id") Integer id) { return item.data("item", service.findItem(id)); <2> } +} +---- +<1> Inject the template with path `templates/item.html`. +<2> Make the `Item` object accessible in the template. + +== Template Extension Methods + +*Template extension methods* are used to extend the set of accessible properties of data objects. + +Sometimes, you're not in control of the classes that you want to use in your template, and you cannot add methods +to them. Template extension methods allows you to declare new method for those classes that will be available +from your templates just as if they belonged to the target class. + +Let's keep extending on our simple HTML page that contains the item name, price and add a discounted price. +The discounted price is sometimes called a "computed property". +We will implement a template extension method to render this property easily. +Let's update our template: + +.HelloResource/item.html +[source,html] +---- + + + + +{item.name} + + +

{item.name}

+
Price: {item.price}
+ {#if item.price > 100} <1> +
Discounted Price: {item.discountedPrice}
<2> + {/if} + + +---- +<1> `if` is a basic control flow section. +<2> This expression is also validated against the `Item` class and obviously there is no such property declared. However, there is a template extension method declared on the `TemplateExtensions` class - see below. - @TemplateExtension <3> - static BigDecimal discountedPrice(Item item) { +Finally, let's create a class where we put all our extension methods: + +.TemplateExtensions.java +[source,java] +---- +package org.acme.quarkus.sample; + +import io.quarkus.qute.TemplateExtension; + +@TemplateExtension +public class TemplateExtensions { + + public static BigDecimal discountedPrice(Item item) { <1> return item.price.multiply(new BigDecimal("0.9")); } } ---- -<1> Inject the template with path `templates/item.html`. -<2> Make the `Item` object accessible in the template. -<3> A static template extension method can be used to add "computed properties" to a data class. The class of the first parameter is used to match the base object and the method name is used to match the property name. +<1> A static template extension method can be used to add "computed properties" to a data class. The class of the first parameter is used to match the base object and the method name is used to match the property name. + +NOTE: you can place template extension methods in every class if you annotate them with `@TemplateExtension` but we advise to keep them either +grouped by target type, or in a single `TemplateExtensions` class by convention. == Rendering Periodic Reports diff --git a/extensions/mailer/deployment/pom.xml b/extensions/mailer/deployment/pom.xml index d4a416a277938..1a74549c8fb2e 100644 --- a/extensions/mailer/deployment/pom.xml +++ b/extensions/mailer/deployment/pom.xml @@ -46,6 +46,7 @@ maven-compiler-plugin + true io.quarkus diff --git a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailTemplateInstanceAdaptor.java b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailTemplateInstanceAdaptor.java new file mode 100644 index 0000000000000..b876da5036570 --- /dev/null +++ b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailTemplateInstanceAdaptor.java @@ -0,0 +1,28 @@ +package io.quarkus.mailer.deployment; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.mailer.runtime.MailTemplateProducer; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.deployment.CheckedTemplateAdapter; + +public class MailTemplateInstanceAdaptor implements CheckedTemplateAdapter { + + @Override + public String templateInstanceBinaryName() { + return MailTemplateInstance.class.getName().replace('.', '/'); + } + + @Override + public void convertTemplateInstance(MethodVisitor mv) { + // we have a TemplateInstance on the stack, we need to turn it into a MailTemplateInstance + mv.visitMethodInsn(Opcodes.INVOKESTATIC, MailTemplateProducer.class.getName().replace('.', '/'), + "getMailTemplateInstance", + "(L" + TemplateInstance.class.getName().replace('.', '/') + ";)L" + + MailTemplateInstance.class.getName().replace('.', '/') + ";", + false); + } + +} diff --git a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java index 3105ef45de2e0..0b05e8cec6ba2 100644 --- a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java +++ b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java @@ -27,6 +27,7 @@ import io.quarkus.mailer.runtime.MockMailboxImpl; import io.quarkus.mailer.runtime.MutinyMailerImpl; import io.quarkus.mailer.runtime.ReactiveMailerImpl; +import io.quarkus.qute.deployment.CheckedTemplateAdapterBuildItem; import io.quarkus.qute.deployment.QuteProcessor; import io.quarkus.qute.deployment.TemplatePathBuildItem; @@ -48,6 +49,11 @@ AdditionalBeanBuildItem registerMailers() { .build(); } + @BuildStep + CheckedTemplateAdapterBuildItem registerCheckedTemplateAdaptor() { + return new CheckedTemplateAdapterBuildItem(new MailTemplateInstanceAdaptor()); + } + @BuildStep ExtensionSslNativeSupportBuildItem activateSslNativeSupport() { return new ExtensionSslNativeSupportBuildItem(Feature.MAILER); diff --git a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java index 988ed552166bc..b4c5c64e278c1 100644 --- a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java +++ b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java @@ -13,6 +13,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; import io.quarkus.qute.api.ResourcePath; import io.quarkus.test.QuarkusUnitTest; import io.vertx.ext.mail.MailClient; @@ -32,6 +34,10 @@ public class InjectionTest { + "{name}"), "templates/test1.html") .addAsResource(new StringAsset("" + "{name}"), "templates/test1.txt") + .addAsResource(new StringAsset("" + + "{name}"), "templates/MailTemplates/testNative.html") + .addAsResource(new StringAsset("" + + "{name}"), "templates/MailTemplates/testNative.txt") .addAsResource(new StringAsset("" + "{name}"), "templates/mails/test2.html")); @@ -70,6 +76,7 @@ public void testInjection() { beanUsingLegacyReactiveMailer.verify().toCompletableFuture().join(); templates.send1(); templates.send2().toCompletableFuture().join(); + templates.sendNative().toCompletableFuture().join(); } @ApplicationScoped @@ -153,6 +160,11 @@ void verify() { @Singleton static class MailTemplates { + @CheckedTemplate + static class Templates { + public static native MailTemplateInstance testNative(String name); + } + @Inject MailTemplate test1; @@ -167,5 +179,8 @@ CompletionStage send2() { return testMail.to("quarkus@quarkus.io").subject("Test").data("name", "Lu").send(); } + CompletionStage sendNative() { + return Templates.testNative("John").to("quarkus@quarkus.io").subject("Test").send(); + } } } diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateProducer.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateProducer.java index e07748449ec5e..15b501dd55859 100644 --- a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateProducer.java +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateProducer.java @@ -13,8 +13,10 @@ import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.mailer.MailTemplate; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.api.ResourcePath; @Singleton @@ -75,4 +77,12 @@ public MailTemplateInstance instance() { } }; } + + /** + * Called by MailTemplateInstanceAdaptor + */ + public static MailTemplate.MailTemplateInstance getMailTemplateInstance(TemplateInstance instance) { + MutinyMailerImpl mailerImpl = Arc.container().instance(MutinyMailerImpl.class).get(); + return new MailTemplateInstanceImpl(mailerImpl, instance); + } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapter.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapter.java new file mode 100644 index 0000000000000..f7b9bececcb40 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapter.java @@ -0,0 +1,11 @@ +package io.quarkus.qute.deployment; + +import org.objectweb.asm.MethodVisitor; + +public interface CheckedTemplateAdapter { + + String templateInstanceBinaryName(); + + void convertTemplateInstance(MethodVisitor nativeMethodVisitor); + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapterBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapterBuildItem.java new file mode 100644 index 0000000000000..75463a13eacdc --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateAdapterBuildItem.java @@ -0,0 +1,13 @@ +package io.quarkus.qute.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class CheckedTemplateAdapterBuildItem extends MultiBuildItem { + + public final CheckedTemplateAdapter adapter; + + public CheckedTemplateAdapterBuildItem(CheckedTemplateAdapter adapter) { + this.adapter = adapter; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateBuildItem.java new file mode 100644 index 0000000000000..0512043705740 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/CheckedTemplateBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.qute.deployment; + +import java.util.Map; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class CheckedTemplateBuildItem extends MultiBuildItem { + + public final String templateId; + public final Map bindings; + + public CheckedTemplateBuildItem(String templateId, Map bindings) { + this.templateId = templateId; + this.bindings = bindings; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/NativeCheckedTemplateEnhancer.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/NativeCheckedTemplateEnhancer.java new file mode 100644 index 0000000000000..b3e824dff5cff --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/NativeCheckedTemplateEnhancer.java @@ -0,0 +1,143 @@ +package io.quarkus.qute.deployment; + +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.quarkus.deployment.util.AsmUtil; +import io.quarkus.gizmo.Gizmo; +import io.quarkus.qute.runtime.TemplateProducer; + +public class NativeCheckedTemplateEnhancer implements BiFunction { + + private static class NativeMethod { + private final MethodInfo methodInfo; + private final String templateId; + private final List parameterNames; + private final CheckedTemplateAdapter adaptor; + + public NativeMethod(MethodInfo methodInfo, String templatePath, List parameterNames, + CheckedTemplateAdapter adaptor) { + this.methodInfo = methodInfo; + this.templateId = templatePath; + this.parameterNames = parameterNames; + this.adaptor = adaptor; + } + } + + private final Map methods = new HashMap<>(); + + public void implement(MethodInfo methodInfo, String templatePath, List parameterNames, + CheckedTemplateAdapter adaptor) { + // FIXME: this should support overloading by using the method signature as key, but requires moving JandexUtil stuff around + methods.put(methodInfo.name(), new NativeMethod(methodInfo, templatePath, parameterNames, adaptor)); + } + + @Override + public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { + return new DynamicTemplateClassVisitor(className, methods, outputClassVisitor); + } + + public static class DynamicTemplateClassVisitor extends ClassVisitor { + + private final Map methods; + + public DynamicTemplateClassVisitor(String className, Map methods, + ClassVisitor outputClassVisitor) { + super(Gizmo.ASM_API_VERSION, outputClassVisitor); + this.methods = methods; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + NativeMethod nativeMethod = methods.get(name); + if (nativeMethod != null) { + // remove the native bit + access = access & ~Modifier.NATIVE; + } + MethodVisitor ret = super.visitMethod(access, name, descriptor, signature, exceptions); + if (nativeMethod != null) { + return new NativeMethodVisitor(nativeMethod, ret); + } + return ret; + } + + public static class NativeMethodVisitor extends MethodVisitor { + + private NativeMethod nativeMethod; + + public NativeMethodVisitor(NativeMethod nativeMethod, MethodVisitor outputVisitor) { + super(Gizmo.ASM_API_VERSION, outputVisitor); + this.nativeMethod = nativeMethod; + } + + @Override + public void visitEnd() { + visitCode(); + /* + * Template template = + * Arc.container().instance(TemplateProducer.class).get().getInjectableTemplate("HelloResource/typedTemplate"); + */ + visitMethodInsn(Opcodes.INVOKESTATIC, "io/quarkus/arc/Arc", "container", "()Lio/quarkus/arc/ArcContainer;", + false); + visitLdcInsn(org.objectweb.asm.Type.getType(TemplateProducer.class)); + visitLdcInsn(0); + visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/annotation/Annotation"); + visitMethodInsn(Opcodes.INVOKEINTERFACE, "io/quarkus/arc/ArcContainer", "instance", + "(Ljava/lang/Class;[Ljava/lang/annotation/Annotation;)Lio/quarkus/arc/InstanceHandle;", true); + visitMethodInsn(Opcodes.INVOKEINTERFACE, "io/quarkus/arc/InstanceHandle", "get", + "()Ljava/lang/Object;", true); + visitTypeInsn(Opcodes.CHECKCAST, "io/quarkus/qute/runtime/TemplateProducer"); + visitLdcInsn(nativeMethod.templateId); + visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/quarkus/qute/runtime/TemplateProducer", "getInjectableTemplate", + "(Ljava/lang/String;)Lio/quarkus/qute/Template;", false); + + /* + * TemplateInstance instance = template.instance(); + */ + // we store it on the stack because local vars are too much trouble + visitMethodInsn(Opcodes.INVOKEINTERFACE, "io/quarkus/qute/Template", "instance", + "()Lio/quarkus/qute/TemplateInstance;", true); + + String templateInstanceBinaryName = "io/quarkus/qute/TemplateInstance"; + // some adaptors are required to return a different type such as MailTemplateInstance + if (nativeMethod.adaptor != null) { + nativeMethod.adaptor.convertTemplateInstance(this); + templateInstanceBinaryName = nativeMethod.adaptor.templateInstanceBinaryName(); + } + + int slot = 0; // arg slots start at 0 for static methods + List parameters = nativeMethod.methodInfo.parameters(); + for (int i = 0; i < nativeMethod.parameterNames.size(); i++) { + Type parameterType = parameters.get(i); + /* + * instance = instance.data("name", name); + */ + visitLdcInsn(nativeMethod.parameterNames.get(i)); // first arg name + visitVarInsn(AsmUtil.getLoadOpcode(parameterType), slot); // slot-th arg value + AsmUtil.boxIfRequired(this, parameterType); + + visitMethodInsn(Opcodes.INVOKEINTERFACE, templateInstanceBinaryName, "data", + "(Ljava/lang/String;Ljava/lang/Object;)L" + templateInstanceBinaryName + ";", true); + + slot += AsmUtil.getParameterSize(parameterType); + } + /* + * return instance; + */ + visitInsn(Opcodes.ARETURN); + + visitMaxs(0, 0); + super.visitEnd(); + } + } + } +} 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 e170676b4fcb6..da0e9d46e959f 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 @@ -61,6 +61,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; @@ -69,12 +70,15 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.util.JandexUtil; import io.quarkus.gizmo.ClassOutput; import io.quarkus.qute.Engine; import io.quarkus.qute.EngineBuilder; import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.VirtualMethodPart; import io.quarkus.qute.LoopSectionHelper; +import io.quarkus.qute.ParserHelper; +import io.quarkus.qute.ParserHook; import io.quarkus.qute.PublisherFactory; import io.quarkus.qute.ResultNode; import io.quarkus.qute.SectionHelper; @@ -86,6 +90,7 @@ import io.quarkus.qute.TemplateLocator; import io.quarkus.qute.UserTagSectionHelper; import io.quarkus.qute.Variant; +import io.quarkus.qute.api.CheckedTemplate; import io.quarkus.qute.api.ResourcePath; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; import io.quarkus.qute.deployment.TypeCheckExcludeBuildItem.Check; @@ -118,6 +123,9 @@ public class QuteProcessor { static final DotName COLLECTION = DotName.createSimple(Collection.class.getName()); static final DotName STRING = DotName.createSimple(String.class.getName()); + static final DotName DOTNAME_CHECKED_TEMPLATE = DotName.createSimple(CheckedTemplate.class.getName()); + static final DotName DOTNAME_TEMPLATE_INSTANCE = DotName.createSimple(TemplateInstance.class.getName()); + private static final String MATCH_NAME = "matchName"; private static final String PRIORITY = "priority"; @@ -167,7 +175,115 @@ AdditionalBeanBuildItem additionalBeans() { } @BuildStep - TemplatesAnalysisBuildItem analyzeTemplates(List templatePaths) { + List collectTemplateTypeInfo(BeanArchiveIndexBuildItem index, + BuildProducer transformers, + List templatePaths, + List templateAdaptorBuildItems) { + List ret = new ArrayList<>(); + + Map adaptors = new HashMap<>(); + for (CheckedTemplateAdapterBuildItem templateAdaptorBuildItem : templateAdaptorBuildItems) { + adaptors.put(DotName.createSimple(templateAdaptorBuildItem.adapter.templateInstanceBinaryName().replace('/', '.')), + templateAdaptorBuildItem.adapter); + } + String supportedAdaptors; + if (adaptors.isEmpty()) { + supportedAdaptors = DOTNAME_TEMPLATE_INSTANCE + " is supported"; + } else { + StringBuffer strbuf = new StringBuffer(DOTNAME_TEMPLATE_INSTANCE.toString()); + List adaptorsList = new ArrayList<>(adaptors.size()); + for (DotName name : adaptors.keySet()) { + adaptorsList.add(name.toString()); + } + Collections.sort(adaptorsList); + for (String name : adaptorsList) { + strbuf.append(", ").append(name); + } + supportedAdaptors = strbuf.append(" are supported").toString(); + } + + for (AnnotationInstance annotation : index.getIndex().getAnnotations(DOTNAME_CHECKED_TEMPLATE)) { + if (annotation.target().kind() != Kind.CLASS) + continue; + ClassInfo classInfo = annotation.target().asClass(); + NativeCheckedTemplateEnhancer enhancer = new NativeCheckedTemplateEnhancer(); + for (MethodInfo methodInfo : classInfo.methods()) { + // only keep native public static methods + if (!Modifier.isPublic(methodInfo.flags()) + || !Modifier.isStatic(methodInfo.flags()) + || !Modifier.isNative(methodInfo.flags())) + continue; + // check its return type + if (methodInfo.returnType().kind() != Type.Kind.CLASS) { + throw new TemplateException("Incompatible checked template return type: " + methodInfo.returnType() + + " only " + supportedAdaptors); + } + DotName returnTypeName = methodInfo.returnType().asClassType().name(); + CheckedTemplateAdapter adaptor = null; + // if it's not the default template instance, try to find an adapter + if (!returnTypeName.equals(DOTNAME_TEMPLATE_INSTANCE)) { + adaptor = adaptors.get(returnTypeName); + if (adaptor == null) + throw new TemplateException("Incompatible checked template return type: " + methodInfo.returnType() + + " only " + supportedAdaptors); + } + String methodName = methodInfo.name(); + String templatePath; + if (classInfo.enclosingClass() != null) { + ClassInfo enclosingClass = index.getIndex().getClassByName(classInfo.enclosingClass()); + String className = enclosingClass.simpleName(); + templatePath = className + "/" + methodName; + } else { + templatePath = methodName; + } + checkTemplatePath(templatePath, templatePaths, classInfo, methodInfo); + + Map bindings = new HashMap<>(); + List parameters = methodInfo.parameters(); + List parameterNames = new ArrayList<>(parameters.size()); + for (int i = 0; i < parameters.size(); i++) { + Type type = parameters.get(i); + String name = methodInfo.parameterName(i); + if (name == null) { + throw new TemplateException("Parameter names not recorded for " + classInfo.name() + + ": compile the class with -parameters"); + } + bindings.put(name, JandexUtil.getBoxedTypeName(type)); + parameterNames.add(name); + } + ret.add(new CheckedTemplateBuildItem(templatePath, bindings)); + enhancer.implement(methodInfo, templatePath, parameterNames, adaptor); + } + transformers.produce(new BytecodeTransformerBuildItem(classInfo.name().toString(), + enhancer)); + } + + return ret; + } + + private void checkTemplatePath(String templatePath, List templatePaths, ClassInfo enclosingClass, + MethodInfo methodInfo) { + for (TemplatePathBuildItem templatePathBuildItem : templatePaths) { + // perfect match + if (templatePathBuildItem.getPath().equals(templatePath)) { + return; + } + // if our templatePath is "Foo/hello", make it match "Foo/hello.txt" + // if they're not equal and they start with our path, there must be something left after + if (templatePathBuildItem.getPath().startsWith(templatePath) + // check that we have an extension, let variant matching work later + && templatePathBuildItem.getPath().charAt(templatePath.length()) == '.') { + return; + } + } + throw new TemplateException( + "Declared template " + templatePath + " could not be found. Either add it or delete its declaration in " + + enclosingClass.name().toString('.') + "." + methodInfo.name()); + } + + @BuildStep + TemplatesAnalysisBuildItem analyzeTemplates(List templatePaths, + List dynamicTemplates) { long start = System.currentTimeMillis(); List analysis = new ArrayList<>(); @@ -226,7 +342,21 @@ public Optional getVariant() { } return Optional.empty(); } - }); + }).addParserHook(new ParserHook() { + + @Override + public void beforeParsing(ParserHelper parserHelper, String id) { + for (CheckedTemplateBuildItem dynamicTemplate : dynamicTemplates) { + // FIXME: check for dot/extension? + if (id.startsWith(dynamicTemplate.templateId)) { + for (Entry entry : dynamicTemplate.bindings.entrySet()) { + parserHelper.addParameter(entry.getKey(), entry.getValue()); + } + } + } + } + + }).build(); Engine dummyEngine = builder.build(); diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/CheckedTemplate.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/CheckedTemplate.java new file mode 100644 index 0000000000000..5d67cb64c24d9 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/CheckedTemplate.java @@ -0,0 +1,59 @@ +package io.quarkus.qute.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.quarkus.qute.TemplateInstance; + +/** + *

+ * If you place this annotation on a class, all its native static methods will be used to declare + * templates and the list of parameters they require. + *

+ * If this is placed on an inner class of the class X, a native static method of the name + * foo will refer to a template at the path X/foo (template file extensions are + * not part of the method name) relative to your templates root. + *

+ * If this is placed on a toplevel class, a native static method of the name + * foo will refer to a template at the path foo (template file extensions are + * not part of the method name) at the toplevel of your templates root. + *

+ * Each parameter of the native static will be used to validate the template at build time, to + * make sure that those parameters are used properly in a type-safe manner. The return type of each + * native static method should be {@link TemplateInstance}. + *

+ * Example: + *

+ * + *

+ * {
+ *     @code
+ *     @Path("item")
+ *     public class ItemResource {
+ * 
+ *         @CheckedTemplate
+ *         class Templates {
+ *             // defines a template at ItemResource/item, taking an Item parameter named item
+ *             public static native TemplateInstance item(Item item);
+ *         }
+ * 
+ *         @GET
+ *         @Path("{id}")
+ *         @Produces(MediaType.TEXT_HTML)
+ *         public TemplateInstance get(@PathParam("id") Integer id) {
+ *             // instantiate that template and pass it the required template parameter
+ *             return Templates.item(service.findItem(id));
+ *         }
+ *     }
+ * }
+ * 
+ */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CheckedTemplate { + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index fd6faf4098382..ca53db23bd609 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -87,6 +87,13 @@ Template getTemplate(InjectionPoint injectionPoint) { return new InjectableTemplate(path.value(), templateVariants, engine); } + /** + * Used by NativeCheckedTemplateEnhancer to inject calls to this method in the native type-safe methods. + */ + public Template getInjectableTemplate(String path) { + return new InjectableTemplate(path, templateVariants, engine); + } + static class InjectableTemplate implements Template { private final String path; @@ -182,6 +189,10 @@ public TemplateVariants(Map variants, String defaultTemplate) { this.defaultTemplate = defaultTemplate; } + @Override + public String toString() { + return "TemplateVariants{default=" + defaultTemplate + ", variants=" + variantToTemplate + "}"; + } } static String parseMediaType(String suffix) { diff --git a/extensions/resteasy-qute/deployment/pom.xml b/extensions/resteasy-qute/deployment/pom.xml index 96c27db38fde3..774ed24a7ac69 100644 --- a/extensions/resteasy-qute/deployment/pom.xml +++ b/extensions/resteasy-qute/deployment/pom.xml @@ -51,6 +51,7 @@ ${project.version}
+ true
diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/HelloResource.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/HelloResource.java index 9f4e68ecaaf46..ebcbe9328ef4e 100644 --- a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/HelloResource.java +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/HelloResource.java @@ -1,25 +1,84 @@ package io.quarkus.qute.resteasy.deployment; +import java.util.Map; + import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; + +import org.jboss.resteasy.annotations.jaxrs.QueryParam; import io.quarkus.qute.Template; import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; +import io.quarkus.resteasy.qute.RestTemplate; @Path("hello") public class HelloResource { + @CheckedTemplate + public static class Templates { + // GENERATED + // { + // Template template = Arc.container().instance(Engine.class).get().getTemplate("HelloResource/typedTemplate"); + // TemplateInstance instance = template.instance(); + // instance.data("name", name); + // return instance; + // } + + public static native TemplateInstance typedTemplate(String name, Map other); + + public static native TemplateInstance typedTemplatePrimitives(boolean bool, byte b, short s, int i, long l, char c, + float f, double d); + } + @Inject Template hello; @GET - public TemplateInstance get(@QueryParam("name") String name) { + public TemplateInstance get(@QueryParam String name) { if (name == null) { name = "world"; } return hello.data("name", name); } + @Path("no-injection") + @GET + public TemplateInstance hello(@QueryParam String name) { + if (name == null) { + name = "world"; + } + return RestTemplate.data("name", name); + } + + @Path("type-error") + @GET + public TemplateInstance typeError() { + return RestTemplate.data("name", "world"); + } + + @Path("native/typed-template-primitives") + @GET + public TemplateInstance typedTemplatePrimitives() { + return Templates.typedTemplatePrimitives(true, (byte) 0, (short) 1, 2, 3, 'a', 4.0f, 5.0); + } + + @Path("native/typed-template") + @GET + public TemplateInstance nativeTypedTemplate(@QueryParam String name) { + if (name == null) { + name = "world"; + } + return Templates.typedTemplate(name, null); + } + + @Path("native/toplevel") + @GET + public TemplateInstance nativeToplevelTypedTemplate(@QueryParam String name) { + if (name == null) { + name = "world"; + } + return io.quarkus.qute.resteasy.deployment.Templates.toplevel(name); + } } diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateResource.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateResource.java new file mode 100644 index 0000000000000..b20f034f1b2ea --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateResource.java @@ -0,0 +1,19 @@ +package io.quarkus.qute.resteasy.deployment; + +import java.util.Map; + +import javax.ws.rs.Path; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; + +@Path("missing-template") +public class MissingTemplateResource { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance hello(String name, Map other); + + public static native TemplateInstance missingTemplate(); + } +} diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java new file mode 100644 index 0000000000000..99e063bafd447 --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java @@ -0,0 +1,27 @@ +package io.quarkus.qute.resteasy.deployment; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.wildfly.common.Assert; + +import io.quarkus.test.QuarkusUnitTest; + +public class MissingTemplateTest { + + @RegisterExtension + static final QuarkusUnitTest configError = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(MissingTemplateResource.class) + .addAsResource("templates/MissingTemplateResource/hello.txt")) + .assertException(t -> { + t.printStackTrace(); + Assert.assertTrue(t.getMessage().contains( + "Declared template MissingTemplateResource/missingTemplate could not be found. Either add it or delete its declaration in io.quarkus.qute.resteasy.deployment.MissingTemplateResource$Templates.missingTemplate")); + }); + + @Test + public void emptyTest() { + } +} diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TemplateResponseFilterTest.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TemplateResponseFilterTest.java index a5bef18d4b9f4..a7d616aa83b48 100644 --- a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TemplateResponseFilterTest.java +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TemplateResponseFilterTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; public class TemplateResponseFilterTest { @@ -17,12 +19,29 @@ public class TemplateResponseFilterTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClass(HelloResource.class) + .addClass(Templates.class) + .addAsResource("templates/toplevel.txt") + .addAsResource("templates/HelloResource/hello.txt") + .addAsResource("templates/HelloResource/typedTemplate.txt") + .addAsResource("templates/HelloResource/typedTemplate.html") + .addAsResource("templates/HelloResource/typedTemplatePrimitives.txt") .addAsResource(new StringAsset("Hello {name}!"), "templates/hello.txt")); @Test public void testFilter() { when().get("/hello").then().body(Matchers.is("Hello world!")); when().get("/hello?name=Joe").then().body(Matchers.is("Hello Joe!")); + when().get("/hello/no-injection").then().body(Matchers.is("Salut world!")); + when().get("/hello/no-injection?name=Joe").then().body(Matchers.is("Salut Joe!")); + RestAssured.given().accept(ContentType.TEXT).get("/hello/native/typed-template").then() + .body(Matchers.is("Salut world!")); + RestAssured.given().accept(ContentType.TEXT).get("/hello/native/typed-template?name=Joe").then() + .body(Matchers.is("Salut Joe!")); + RestAssured.given().accept(ContentType.HTML).get("/hello/native/typed-template?name=Joe").then() + .body(Matchers.is("Salut Joe!")); + when().get("/hello/native/typed-template-primitives").then() + .body(Matchers.is("Byte: 0 Short: 1 Int: 2 Long: 3 Char: a Boolean: true Float: 4.0 Double: 5.0")); + when().get("/hello/native/toplevel?name=Joe").then().body(Matchers.is("Salut Joe!")); } } diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/Templates.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/Templates.java new file mode 100644 index 0000000000000..31bfedf82c895 --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/Templates.java @@ -0,0 +1,9 @@ +package io.quarkus.qute.resteasy.deployment; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; + +@CheckedTemplate +public class Templates { + public static native TemplateInstance toplevel(String name); +} diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorResource.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorResource.java new file mode 100644 index 0000000000000..a8bc79aca65be --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorResource.java @@ -0,0 +1,22 @@ +package io.quarkus.qute.resteasy.deployment; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; + +@Path("type-error") +public class TypeErrorResource { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance typeError3(String name); + } + + @Path("native/type-error3") + @GET + public TemplateInstance nativeTypeError3() { + return Templates.typeError3("world"); + } +} diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest.java new file mode 100644 index 0000000000000..2b6a881e37dea --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest.java @@ -0,0 +1,30 @@ +package io.quarkus.qute.resteasy.deployment; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.wildfly.common.Assert; + +import io.quarkus.test.QuarkusUnitTest; + +public class TypeErrorTest { + + @RegisterExtension + static final QuarkusUnitTest configError = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(HelloResource.class) + .addAsResource("templates/HelloResource/hello.txt") + .addAsResource("templates/HelloResource/typeError.txt") + .addAsResource("templates/HelloResource/typedTemplate.txt") + .addAsResource("templates/HelloResource/typedTemplatePrimitives.txt") + .addAsResource(new StringAsset("Hello {name}!"), "templates/hello.txt")) + .assertException(t -> { + Assert.assertTrue(t.getMessage().contains("Incorrect expression: name.foo()")); + }); + + @Test + public void emptyTest() { + } +} diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest3.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest3.java new file mode 100644 index 0000000000000..ae72ca17de1eb --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/TypeErrorTest3.java @@ -0,0 +1,26 @@ +package io.quarkus.qute.resteasy.deployment; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.wildfly.common.Assert; + +import io.quarkus.test.QuarkusUnitTest; + +public class TypeErrorTest3 { + + @RegisterExtension + static final QuarkusUnitTest configError = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(TypeErrorResource.class) + .addAsResource("templates/TypeErrorResource/typeError3.txt")) + .assertException(t -> { + t.printStackTrace(); + Assert.assertTrue(t.getMessage().contains("Incorrect expression: name.foo()")); + }); + + @Test + public void emptyTest() { + } +} diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/hello.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/hello.txt new file mode 100644 index 0000000000000..8c2a722b8984a --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/hello.txt @@ -0,0 +1 @@ +Salut {name}! \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError.txt new file mode 100644 index 0000000000000..7b7d4ddcdae5e --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError.txt @@ -0,0 +1 @@ +{@java.lang.String name}Salut {name.foo()}! \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError2.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError2.txt new file mode 100644 index 0000000000000..e6a6bf71b26d6 --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typeError2.txt @@ -0,0 +1 @@ +Salut {name.foo()}! \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.html b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.html new file mode 100644 index 0000000000000..f8f74faf3cf34 --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.html @@ -0,0 +1 @@ +Salut {name}! \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.txt new file mode 100644 index 0000000000000..8c2a722b8984a --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplate.txt @@ -0,0 +1 @@ +Salut {name}! \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplatePrimitives.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplatePrimitives.txt new file mode 100644 index 0000000000000..534874e943163 --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/HelloResource/typedTemplatePrimitives.txt @@ -0,0 +1 @@ +Byte: {b} Short: {s} Int: {i} Long: {l} Char: {c} Boolean: {bool} Float: {f} Double: {d} \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/MissingTemplateResource/hello.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/MissingTemplateResource/hello.txt new file mode 100644 index 0000000000000..8c2a722b8984a --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/MissingTemplateResource/hello.txt @@ -0,0 +1 @@ +Salut {name}! \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/TypeErrorResource/typeError3.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/TypeErrorResource/typeError3.txt new file mode 100644 index 0000000000000..e6a6bf71b26d6 --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/TypeErrorResource/typeError3.txt @@ -0,0 +1 @@ +Salut {name.foo()}! \ No newline at end of file diff --git a/extensions/resteasy-qute/deployment/src/test/resources/templates/toplevel.txt b/extensions/resteasy-qute/deployment/src/test/resources/templates/toplevel.txt new file mode 100644 index 0000000000000..8c2a722b8984a --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/resources/templates/toplevel.txt @@ -0,0 +1 @@ +Salut {name}! \ No newline at end of file diff --git a/extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/RestTemplate.java b/extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/RestTemplate.java new file mode 100644 index 0000000000000..dc8780c303b2e --- /dev/null +++ b/extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/RestTemplate.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.qute; + +import javax.ws.rs.container.ResourceInfo; + +import org.jboss.resteasy.core.ResteasyContext; + +import io.quarkus.arc.Arc; +import io.quarkus.qute.Engine; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; + +public final class RestTemplate { + + private RestTemplate() { + } + + private static String getActionName() { + ResourceInfo resourceMethod = ResteasyContext.getContextData(ResourceInfo.class); + return resourceMethod.getResourceClass().getSimpleName() + "/" + resourceMethod.getResourceMethod().getName(); + } + + public static TemplateInstance data(String name, Object value) { + Template template = Arc.container().instance(Engine.class).get().getTemplate(getActionName()); + return template.data(name, value); + } + + public static TemplateInstance data(Object data) { + Template template = Arc.container().instance(Engine.class).get().getTemplate(getActionName()); + return template.data(data); + } +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java index 4dbe55c7bd6dd..fa09b5c8ce9e6 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java @@ -27,6 +27,7 @@ public final class EngineBuilder { private final List locators; private final List resultMappers; private Function> sectionHelperFunc; + private final List parserHooks; EngineBuilder() { this.sectionHelperFactories = new HashMap<>(); @@ -34,6 +35,7 @@ public final class EngineBuilder { this.namespaceResolvers = new ArrayList<>(); this.locators = new ArrayList<>(); this.resultMappers = new ArrayList<>(); + this.parserHooks = new ArrayList<>(); } public EngineBuilder addSectionHelper(SectionHelperFactory factory) { @@ -115,6 +117,11 @@ public EngineBuilder addLocator(TemplateLocator locator) { return this; } + public EngineBuilder addParserHook(ParserHook parserHook) { + this.parserHooks.add(parserHook); + return this; + } + /** * * @param resultMapper @@ -132,7 +139,7 @@ public EngineBuilder computeSectionHelper(Function resultMappers; private final PublisherFactory publisherFactory; private final AtomicLong idGenerator = new AtomicLong(0); + private final List parserHooks; EngineImpl(Map> sectionHelperFactories, List valueResolvers, List namespaceResolvers, List locators, - List resultMappers, Function> sectionHelperFunc) { + List resultMappers, Function> sectionHelperFunc, + List parserHooks) { this.sectionHelperFactories = Collections.unmodifiableMap(new HashMap<>(sectionHelperFactories)); this.valueResolvers = sort(valueResolvers); this.namespaceResolvers = ImmutableList.copyOf(namespaceResolvers); @@ -64,12 +66,21 @@ class EngineImpl implements Engine { } this.resultMappers = sort(resultMappers); this.sectionHelperFunc = sectionHelperFunc; + this.parserHooks = parserHooks; } @Override public Template parse(String content, Variant variant) { String generatedId = generateId(); - return new Parser(this).parse(new StringReader(content), Optional.ofNullable(variant), generatedId, generatedId); + return newParser(null).parse(new StringReader(content), Optional.ofNullable(variant), generatedId, generatedId); + } + + private Parser newParser(String id) { + Parser parser = new Parser(this); + for (ParserHook parserHook : parserHooks) { + parserHook.beforeParsing(parser, id); + } + return parser; } @Override @@ -132,7 +143,7 @@ private Template load(String id) { Optional location = locator.locate(id); if (location.isPresent()) { try (Reader r = location.get().read()) { - return new Parser(this).parse(ensureBufferedReader(r), location.get().getVariant(), id, generateId()); + return newParser(id).parse(ensureBufferedReader(r), location.get().getVariant(), id, generateId()); } catch (IOException e) { LOGGER.warn("Unable to close the reader for " + id, e); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java index aa182335f3cee..75897a8bbf107 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java @@ -138,7 +138,7 @@ static ExpressionImpl from(String value) { if (value == null || value.isEmpty()) { return EMPTY; } - return Parser.parseExpression(value, Collections.emptyMap(), Parser.SYNTHETIC_ORIGIN); + return Parser.parseExpression(value, Scope.EMPTY, Parser.SYNTHETIC_ORIGIN); } static ExpressionImpl literalFrom(String literal) { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java index 2b033790e3864..87c575a9c716b 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java @@ -13,7 +13,6 @@ import java.util.Iterator; import java.util.List; import java.util.ListIterator; -import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -100,7 +99,7 @@ public IfSectionHelper initialize(SectionInitContext context) { } @Override - public Map initializeBlock(Map outerNameTypeInfos, BlockInfo block) { + public Scope initializeBlock(Scope previousScope, BlockInfo block) { List params = null; if (MAIN_BLOCK_NAME.equals(block.getLabel())) { params = parseParams(new ArrayList<>(block.getParameters().values()), block); @@ -113,7 +112,7 @@ public Map initializeBlock(Map outerNameTypeInfo } addExpressions(params, block); // {#if} never changes the scope - return Collections.emptyMap(); + return previousScope; } @SuppressWarnings("unchecked") diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java index 36e8526dac45e..ad7554cce88b7 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java @@ -5,8 +5,6 @@ import io.quarkus.qute.Results.Result; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -126,7 +124,7 @@ public LoopSectionHelper initialize(SectionInitContext context) { } @Override - public Map initializeBlock(Map outerNameTypeInfos, BlockInfo block) { + public Scope initializeBlock(Scope previousScope, BlockInfo block) { if (block.getLabel().equals(MAIN_BLOCK_NAME)) { String iterable = block.getParameters().get(ITERABLE); if (iterable == null) { @@ -136,17 +134,17 @@ public Map initializeBlock(Map outerNameTypeInfo String alias = block.getParameters().get(ALIAS); if (iterableExpr.getParts().get(0).getTypeInfo() != null) { alias = alias.equals(Parameter.EMPTY) ? DEFAULT_ALIAS : alias; - Map typeInfos = new HashMap(outerNameTypeInfos); - typeInfos.put(alias, iterableExpr.collectTypeInfo() + HINT); - return typeInfos; + Scope newScope = new Scope(previousScope); + newScope.put(alias, iterableExpr.collectTypeInfo() + HINT); + return newScope; } else { - Map typeInfos = new HashMap(outerNameTypeInfos); // Make sure we do not try to validate against the parent context - typeInfos.put(alias, null); - return typeInfos; + Scope newScope = new Scope(previousScope); + newScope.put(alias, null); + return newScope; } } else { - return Collections.emptyMap(); + return previousScope; } } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index b96899989989d..2805f505c961d 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -8,9 +8,7 @@ import java.io.Reader; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.Deque; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -25,7 +23,7 @@ /** * Simple non-reusable parser. */ -class Parser implements Function { +class Parser implements Function, ParserHelper { private static final Logger LOGGER = Logger.getLogger(Parser.class); private static final String ROOT_HELPER_NAME = "$root"; @@ -58,7 +56,7 @@ class Parser implements Function { private final Deque sectionStack; private final Deque sectionBlockStack; private final Deque paramsStack; - private final Deque> typeInfoStack; + private final Deque scopeStack; private int sectionBlockIdx; private boolean ignoreContent; private String id; @@ -91,8 +89,8 @@ public CompletionStage resolve(SectionResolutionContext context) { this.sectionBlockIdx = 0; this.paramsStack = new ArrayDeque<>(); this.paramsStack.addFirst(ParametersInfo.EMPTY); - this.typeInfoStack = new ArrayDeque<>(); - this.typeInfoStack.addFirst(new HashMap<>()); + this.scopeStack = new ArrayDeque<>(); + this.scopeStack.addFirst(new Scope(null)); this.line = 1; this.lineCharacter = 1; } @@ -302,16 +300,9 @@ private void flushTag() { processParams(tag, sectionName, iter); // Initialize the block - Map typeInfos = typeInfoStack.peek(); - Map result = sectionStack.peek().factory.initializeBlock(typeInfos, block); - if (!result.isEmpty()) { - Map newTypeInfos = new HashMap<>(); - newTypeInfos.putAll(typeInfos); - newTypeInfos.putAll(result); - typeInfoStack.addFirst(newTypeInfos); - } else { - typeInfoStack.addFirst(typeInfos); - } + Scope currentScope = scopeStack.peek(); + Scope newScope = sectionStack.peek().factory.initializeBlock(currentScope, block); + scopeStack.addFirst(newScope); // A new block - stop ignoring the block content ignoreContent = false; @@ -330,8 +321,8 @@ private void flushTag() { processParams(tag, SectionHelperFactory.MAIN_BLOCK_NAME, iter); // Init section block - Map typeInfos = typeInfoStack.peek(); - Map result = factory.initializeBlock(typeInfos, mainBlock); + Scope currentScope = scopeStack.peek(); + Scope newScope = factory.initializeBlock(currentScope, mainBlock); SectionNode.Builder sectionNode = SectionNode .builder(sectionName, origin()) .setEngine(engine) @@ -346,15 +337,7 @@ private void flushTag() { // Add node to the parent block sectionBlockStack.peek().addNode(sectionNode.build()); } else { - if (!result.isEmpty()) { - // The section modifies the type info stack - Map newTypeInfos = new HashMap<>(); - newTypeInfos.putAll(typeInfos); - newTypeInfos.putAll(result); - typeInfoStack.addFirst(newTypeInfos); - } else { - typeInfoStack.addFirst(typeInfos); - } + scopeStack.addFirst(newScope); sectionStack.addFirst(sectionNode); } } @@ -390,16 +373,16 @@ private void flushTag() { } // Remove the last type info map from the stack - typeInfoStack.pop(); + scopeStack.pop(); } else if (content.charAt(0) == Tag.PARAM.command) { // {@org.acme.Foo foo} - Map typeInfos = typeInfoStack.peek(); + Scope currentScope = scopeStack.peek(); int spaceIdx = content.indexOf(" "); String key = content.substring(spaceIdx + 1, content.length()); String value = content.substring(1, spaceIdx); - typeInfos.put(key, Expressions.TYPE_INFO_SEPARATOR + value + Expressions.TYPE_INFO_SEPARATOR); + currentScope.put(key, Expressions.TYPE_INFO_SEPARATOR + value + Expressions.TYPE_INFO_SEPARATOR); } else { sectionBlockStack.peek().addNode(new ExpressionNode(apply(content), engine, origin())); @@ -598,13 +581,10 @@ enum State { } - static ExpressionImpl parseExpression(String value, Map typeInfos, Origin origin) { + static ExpressionImpl parseExpression(String value, Scope scope, Origin origin) { if (value == null || value.isEmpty()) { return ExpressionImpl.EMPTY; } - if (typeInfos == null) { - typeInfos = Collections.emptyMap(); - } String namespace = null; int namespaceIdx = value.indexOf(':'); int spaceIdx = value.indexOf(' '); @@ -629,7 +609,7 @@ static ExpressionImpl parseExpression(String value, Map typeInfo List parts = new ArrayList<>(strParts.size()); Part first = null; for (String strPart : strParts) { - Part part = createPart(namespace, first, strPart, typeInfos, origin); + Part part = createPart(namespace, first, strPart, scope, origin); if (first == null) { first = part; } @@ -638,13 +618,13 @@ static ExpressionImpl parseExpression(String value, Map typeInfo return new ExpressionImpl(namespace, parts, Result.NOT_FOUND, origin); } - private static Part createPart(String namespace, Part first, String value, Map typeInfos, Origin origin) { + private static Part createPart(String namespace, Part first, String value, Scope scope, Origin origin) { if (Expressions.isVirtualMethod(value)) { String name = Expressions.parseVirtualMethodName(value); List strParams = new ArrayList<>(Expressions.parseVirtualMethodParams(value)); List params = new ArrayList<>(strParams.size()); for (String strParam : strParams) { - params.add(parseExpression(strParam.trim(), typeInfos, origin)); + params.add(parseExpression(strParam.trim(), scope, origin)); } return new ExpressionImpl.VirtualMethodExpressionPartImpl(name, params); } @@ -652,7 +632,7 @@ private static Part createPart(String namespace, Part first, String value, Map bindings; + + public Scope(Scope parentScope) { + this.parentScope = parentScope; + } + + public void put(String binding, String type) { + if (bindings == null) + bindings = new HashMap<>(); + bindings.put(binding, type); + } + + public String getBindingType(String binding) { + // we can contain null types to override outer scopes + if (bindings != null + && bindings.containsKey(binding)) + return bindings.get(binding); + return parentScope != null ? parentScope.getBindingType(binding) : null; + } + + public String getBindingTypeOrDefault(String binding, String defaultValue) { + String type = getBindingType(binding); + return type != null ? type : defaultValue; + } + +} 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 f0c2270dc991d..98a466adf38b9 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 @@ -62,11 +62,11 @@ default boolean treatUnknownSectionsAsBlocks() { /** * Initialize a section block. * - * @return a map of name to type infos + * @return a new scope if this section introduces a new scope, or the outer scope * @see BlockInfo#addExpression(String, String) */ - default Map initializeBlock(Map outerNameTypeInfos, BlockInfo block) { - return Collections.emptyMap(); + default Scope initializeBlock(Scope outerScope, BlockInfo block) { + return outerScope; } interface ParserDelegate { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java index c6e9586a2d4fc..c26802e6065c3 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java @@ -2,8 +2,6 @@ import static io.quarkus.qute.Futures.evaluateParams; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -65,18 +63,18 @@ public SetSectionHelper initialize(SectionInitContext context) { } @Override - public Map initializeBlock(Map outerNameTypeInfos, BlockInfo block) { + public Scope initializeBlock(Scope previousScope, BlockInfo block) { if (block.getLabel().equals(MAIN_BLOCK_NAME)) { - Map typeInfos = new HashMap(outerNameTypeInfos); + Scope newScope = new Scope(previousScope); for (Entry entry : block.getParameters().entrySet()) { Expression expr = block.addExpression(entry.getKey(), entry.getValue()); - typeInfos.put(entry.getKey(), expr.collectTypeInfo()); + newScope.put(entry.getKey(), expr.collectTypeInfo()); } - return typeInfos; + return newScope; } else { - return Collections.emptyMap(); + return previousScope; } } } -} \ No newline at end of file +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/WithSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/WithSectionHelper.java index 07931868edcc5..dfe53994bb2f8 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/WithSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/WithSectionHelper.java @@ -1,9 +1,7 @@ package io.quarkus.qute; import io.quarkus.qute.SectionHelperFactory.SectionInitContext; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletionStage; /** @@ -48,16 +46,16 @@ public WithSectionHelper initialize(SectionInitContext context) { } @Override - public Map initializeBlock(Map outerNameTypeInfos, BlockInfo block) { + public Scope initializeBlock(Scope previousScope, BlockInfo block) { if (block.getLabel().equals(MAIN_BLOCK_NAME)) { String object = block.getParameters().get(OBJECT); if (object == null) { throw new IllegalStateException("Object param not present"); } block.addExpression(OBJECT, object); - return outerNameTypeInfos; + return previousScope; } else { - return Collections.emptyMap(); + return previousScope; } } diff --git a/independent-projects/qute/generator/src/test/java/io/quarkus/qute/TestEvalContext.java b/independent-projects/qute/generator/src/test/java/io/quarkus/qute/TestEvalContext.java index 6b29f506aaf77..bca029cb2c42e 100644 --- a/independent-projects/qute/generator/src/test/java/io/quarkus/qute/TestEvalContext.java +++ b/independent-projects/qute/generator/src/test/java/io/quarkus/qute/TestEvalContext.java @@ -1,7 +1,6 @@ package io.quarkus.qute; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.concurrent.CompletionStage; import java.util.function.Function; @@ -18,7 +17,7 @@ public TestEvalContext(Object base, String name, Function> evaluate, String... params) { this.base = base; this.name = name; - this.params = Arrays.stream(params).map(p -> Parser.parseExpression(p, Collections.emptyMap(), null)) + this.params = Arrays.stream(params).map(p -> Parser.parseExpression(p, Scope.EMPTY, null)) .collect(Collectors.toList()); this.evaluate = evaluate; } @@ -40,7 +39,7 @@ public List getParams() { @Override public CompletionStage evaluate(String expression) { - return evaluate(Parser.parseExpression(expression, Collections.emptyMap(), null)); + return evaluate(Parser.parseExpression(expression, Scope.EMPTY, null)); } @Override