From 107a9bf42ef72481d51ed616cbe472d94d4b4eb5 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 7 Aug 2020 15:31:19 +0200 Subject: [PATCH] Qute: template extensions can use regular expressions to match names - also implement a shortcut to access List elements by index - resolves #11275 --- docs/src/main/asciidoc/qute-reference.adoc | 9 +- .../qute/deployment/QuteProcessor.java | 16 ++- .../TemplateExtensionMethodBuildItem.java | 16 ++- .../NamespaceTemplateExtensionTest.java | 7 + .../TemplateExtensionMethodsTest.java | 7 + .../CollectionTemplateExtensions.java | 13 +- .../io/quarkus/qute/TemplateExtension.java | 27 +++- .../quarkus/qute/generator/Descriptors.java | 8 ++ .../generator/ExtensionMethodGenerator.java | 126 +++++++++++++----- .../qute/generator/SimpleGeneratorTest.java | 6 +- 10 files changed, 185 insertions(+), 50 deletions(-) diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index a3490117090f90..7b54724bb969de 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -848,7 +848,8 @@ Extension methods can be used to extend the data classes with new functionality For example, it is possible to add _computed properties_ and _virtual methods_. A value resolver is automatically generated for a method annotated with `@TemplateExtension`. -If a class is annotated with `@TemplateExtension` then a value resolver is generated for every method declared on the class. +If a class is annotated with `@TemplateExtension` then a value resolver is generated for every non-private static method declared on the class. +Method-level annotations override the behavior defined on the class. Methods that do not meet the following requirements are ignored. A template extension method: @@ -860,10 +861,14 @@ A template extension method: The class of the first parameter is used to match the base object unless the namespace is specified. In such case, the namespace is used to match an expression. + The method name is used to match the property name by default. However, it is possible to specify the matching name with `TemplateExtension#matchName()`. +A special constant - `TemplateExtension#ANY` - may be used to specify that the extension method matches any name. +It is also possible to match the name against a regular expression specified in `TemplateExtension#matchRegex()`. +In both cases, a string method parameter is used to pass the property name. +If both `matchName()` and `matchRegex()` are set the regular expression is used for matching. -NOTE: A special constant - `TemplateExtension#ANY` - may be used to specify that the extension method matches any name. In that case, a method parameter is used to pass the property name. If a namespace is specified the method must declare at least one parameter and the first parameter must be a string. If no namespace is specified the method must declare at least two parameters and the second parameter must be a string. 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 3e01ff17c9ebbe..fb0257e4404ce4 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 @@ -625,7 +625,12 @@ private void produceExtensionMethod(IndexView index, BuildProducer() { @Override public boolean test(String name) { - int idx = name.lastIndexOf(ExtensionMethodGenerator.SUFFIX); + int idx = name.lastIndexOf(ExtensionMethodGenerator.NAMESPACE_SUFFIX); + if (idx == -1) { + idx = name.lastIndexOf(ExtensionMethodGenerator.SUFFIX); + } if (idx == -1) { idx = name.lastIndexOf(ValueResolverGenerator.SUFFIX); } @@ -803,7 +811,7 @@ public boolean test(String name) { } else { // Generate ValueResolver per extension method extensionMethodGenerator.generate(templateExtension.getMethod(), templateExtension.getMatchName(), - templateExtension.getPriority()); + templateExtension.getMatchRegex(), templateExtension.getPriority()); } } @@ -815,7 +823,7 @@ public boolean test(String name) { .createNamespaceResolver(methods.get(0).getMethod().declaringClass(), methods.get(0).getNamespace())) { try (ResolveCreator resolveCreator = namespaceResolverCreator.implementResolve()) { for (TemplateExtensionMethodBuildItem method : methods) { - resolveCreator.addMethod(method.getMethod(), method.getMatchName()); + resolveCreator.addMethod(method.getMethod(), method.getMatchName(), method.getMatchRegex()); } } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java index 588cabd15966f2..76e9936ffae4d6 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java @@ -1,5 +1,7 @@ package io.quarkus.qute.deployment; +import java.util.regex.Pattern; + import org.jboss.jandex.ClassInfo; import org.jboss.jandex.MethodInfo; @@ -15,17 +17,22 @@ public final class TemplateExtensionMethodBuildItem extends MultiBuildItem { private final MethodInfo method; private final String matchName; + private final String matchRegex; + private final Pattern matchPattern; private final ClassInfo matchClass; private final int priority; private final String namespace; - public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, ClassInfo matchClass, int priority, + public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, String matchRegex, ClassInfo matchClass, + int priority, String namespace) { this.method = method; this.matchName = matchName; + this.matchRegex = matchRegex; this.matchClass = matchClass; this.priority = priority; this.namespace = namespace; + this.matchPattern = (matchRegex == null || matchRegex.isEmpty()) ? null : Pattern.compile(matchRegex); } public MethodInfo getMethod() { @@ -36,6 +43,10 @@ public String getMatchName() { return matchName; } + public String getMatchRegex() { + return matchRegex; + } + public ClassInfo getMatchClass() { return matchClass; } @@ -53,6 +64,9 @@ boolean matchesClass(ClassInfo clazz) { } boolean matchesName(String name) { + if (matchPattern != null) { + return matchPattern.matcher(name).matches(); + } return TemplateExtension.ANY.equals(matchName) ? true : matchName.equals(name); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/NamespaceTemplateExtensionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/NamespaceTemplateExtensionTest.java index 1b724d268310ee..8976448987672f 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/NamespaceTemplateExtensionTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/NamespaceTemplateExtensionTest.java @@ -30,6 +30,8 @@ public void testTemplateExtensions() { engine.parse("{str:format('%s:%s','hello', 1)}").render()); assertEquals("olleh", engine.parse("{str:reverse('hello')}").render()); + assertEquals("foolish:olleh", + engine.parse("{str:foolish('hello')}").render()); assertEquals("ONE=ONE", engine.parse("{MyEnum:ONE}={MyEnum:one}").render()); } @@ -45,6 +47,11 @@ static String reverse(String val) { return new StringBuilder(val).reverse().toString(); } + @TemplateExtension(namespace = "str", matchRegex = "foo.*") + static String foo(String name, String val) { + return name + ":" + new StringBuilder(val).reverse().toString(); + } + } public enum MyEnum { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java index f732fedfc24614..ed867eb3792a65 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java @@ -5,6 +5,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -80,6 +81,12 @@ public void testPriority() { assertEquals("bravo::baz", engine.getTemplate("priority").data("foo", new Foo("baz", 10l)).render()); } + @Test + public void testListGetByIndex() { + assertEquals("true=true=NOT_FOUND", + engine.parse("{list.0}={list[0]}={list[100]}").data("list", Collections.singletonList(true)).render()); + } + @TemplateExtension public static class Extensions { diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java index c6c02d26b7b693..d9f73edbd92774 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java @@ -2,6 +2,7 @@ import java.util.List; +import io.quarkus.qute.Results.Result; import io.quarkus.qute.TemplateExtension; @TemplateExtension @@ -10,4 +11,14 @@ public class CollectionTemplateExtensions { static Object get(List list, int index) { return list.get(index); } -} \ No newline at end of file + + @TemplateExtension(matchRegex = "\\d{1,10}") + static Object getByIndex(List list, String index) { + int idx = Integer.parseInt(index); + if (idx >= list.size()) { + // Be consistent with property resolvers + return Result.NOT_FOUND; + } + return list.get(idx); + } +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java index bdab4354d43dca..2615a969fcec25 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java @@ -9,23 +9,29 @@ /** * A value resolver is automatically generated for a method annotated with this annotation. If declared on a class a value - * resolver is generated for every non-private static method declared on the class. Methods that do not meet the following - * requirements are ignored. + * resolver is generated for every non-private static method declared on the class. Method-level annotations override the + * behavior defined on the class. + *

+ * Methods that do not meet the following requirements are ignored. *

* A template extension method: *

* The class of the first parameter is used to match the base object unless the namespace is specified. In such case, the * namespace is used to match an expression. *

* By default, the method name is used to match the property name. However, it is possible to specify the matching name with * {@link #matchName()}. A special constant - {@link #ANY} - may be used to specify that the extension method matches any name. - * In that case, a method parameter is used to pass the property name. If a namespace is specified the method must declare at - * least one parameter and the first parameter must be a string. If no namespace is specified the method must declare at least - * two parameters and the second parameter must be a string. + * It is also possible to match the name against a regular expression specified in {@link #matchRegex()}. In both cases, a + * string method parameter is used to pass the property name. + *

+ * If both {@link #matchName()} and {@link #matchRegex()} are set the regular expression is used for matching. + *

+ * If a namespace is specified the method must declare at least one parameter and the first parameter must be a string. If no + * namespace is specified the method must declare at least two parameters and the second parameter must be a string. * *

  * {@literal @}TemplateExtension
@@ -60,6 +66,12 @@
      */
     String matchName() default METHOD_NAME;
 
+    /**
+     * 
+     * @return the regex is used to match the property name
+     */
+    String matchRegex() default "";
+
     /**
      * 
      * @return the priority used by the generated value resolver
@@ -70,7 +82,8 @@
      * If not empty a namespace resolver is generated instead.
      * 

* Template extension methods that share the same namespace and are declared on the same class are grouped in one resolver - * ordered by {@link #priority()}. The first matching extension method is used to resolve an expression. Template extension + * and ordered by {@link #priority()}. The first matching extension method is used to resolve an expression. Template + * extension * methods declared on different classes cannot share the same namespace. * * @return the namespace diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java index 26a5a58c32b0bc..521262bd63eccb 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java @@ -12,6 +12,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public final class Descriptors { @@ -66,6 +68,12 @@ private Descriptors() { static final MethodDescriptor EVALUATED_PARAMS_GET_VARARGS_RESULTS = MethodDescriptor.ofMethod(EvaluatedParams.class, "getVarargsResults", Object.class, int.class, Class.class); + public static final MethodDescriptor PATTERN_COMPILE = MethodDescriptor.ofMethod(Pattern.class, "compile", Pattern.class, + String.class); + public static final MethodDescriptor PATTERN_MATCHER = MethodDescriptor.ofMethod(Pattern.class, "matcher", Matcher.class, + CharSequence.class); + public static final MethodDescriptor MATCHER_MATCHES = MethodDescriptor.ofMethod(Matcher.class, "matches", boolean.class); + public static final MethodDescriptor OBJECT_CONSTRUCTOR = MethodDescriptor.ofConstructor(Object.class); public static final FieldDescriptor RESULTS_NOT_FOUND = FieldDescriptor.of(Results.class, "NOT_FOUND", CompletionStage.class); diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java index 2aa6ea6098c7c1..4a187ddcd02f4e 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java @@ -3,6 +3,8 @@ import static io.quarkus.qute.generator.ValueResolverGenerator.generatedNameFromTarget; import static io.quarkus.qute.generator.ValueResolverGenerator.packageName; import static io.quarkus.qute.generator.ValueResolverGenerator.simpleName; +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import io.quarkus.gizmo.AssignableResultHandle; @@ -11,6 +13,7 @@ import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; @@ -31,6 +34,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.BiConsumer; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; @@ -52,10 +56,13 @@ public class ExtensionMethodGenerator { static final DotName STRING = DotName.createSimple(String.class.getName()); public static final String SUFFIX = "_Extension" + ValueResolverGenerator.SUFFIX; + public static final String NAMESPACE_SUFFIX = "_Namespace" + SUFFIX; public static final String MATCH_NAME = "matchName"; + public static final String MATCH_REGEX = "matchRegex"; public static final String PRIORITY = "priority"; public static final String NAMESPACE = "namespace"; + public static final String PATTERN = "pattern"; private final Set generatedTypes; private final ClassOutput classOutput; @@ -81,7 +88,7 @@ public static void validate(MethodInfo method, List parameters, String nam } } - public void generate(MethodInfo method, String matchName, Integer priority) { + public void generate(MethodInfo method, String matchName, String matchRegex, Integer priority) { AnnotationInstance extensionAnnotation = method.annotation(TEMPLATE_EXTENSION); List parameters = method.parameters(); @@ -120,6 +127,13 @@ public void generate(MethodInfo method, String matchName, Integer priority) { priority = TemplateExtension.DEFAULT_PRIORITY; } + if (matchRegex == null && extensionAnnotation != null) { + AnnotationValue matchRegexValue = extensionAnnotation.value(MATCH_REGEX); + if (matchRegexValue != null) { + matchRegex = matchRegexValue.asString(); + } + } + String baseName; if (declaringClass.enclosingClass() != null) { baseName = simpleName(declaringClass.enclosingClass()) + ValueResolverGenerator.NESTED_SEPARATOR @@ -136,9 +150,22 @@ public void generate(MethodInfo method, String matchName, Integer priority) { ClassCreator valueResolver = ClassCreator.builder().classOutput(classOutput).className(generatedName) .interfaces(ValueResolver.class).build(); + FieldDescriptor patternField = null; + if (matchRegex != null && !matchRegex.isEmpty()) { + patternField = valueResolver.getFieldCreator(PATTERN, Pattern.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL).getFieldDescriptor(); + MethodCreator constructor = valueResolver.getMethodCreator("", "V"); + // Invoke super() + constructor.invokeSpecialMethod(Descriptors.OBJECT_CONSTRUCTOR, constructor.getThis()); + // Compile the regex pattern + constructor.writeInstanceField(patternField, constructor.getThis(), + constructor.invokeStaticMethod(Descriptors.PATTERN_COMPILE, constructor.load(matchRegex))); + constructor.returnValue(null); + } + implementGetPriority(valueResolver, priority); - implementAppliesTo(valueResolver, method, matchName); - implementResolve(valueResolver, declaringClass, method, matchName); + implementAppliesTo(valueResolver, method, matchName, patternField); + implementResolve(valueResolver, declaringClass, method, matchName, patternField); valueResolver.close(); } @@ -159,22 +186,23 @@ private void implementGetPriority(ClassCreator valueResolver, int priority) { getPriority.returnValue(getPriority.load(priority)); } - private void implementResolve(ClassCreator valueResolver, ClassInfo declaringClass, MethodInfo method, String matchName) { + private void implementResolve(ClassCreator valueResolver, ClassInfo declaringClass, MethodInfo method, String matchName, + FieldDescriptor patternField) { MethodCreator resolve = valueResolver.getMethodCreator("resolve", CompletionStage.class, EvalContext.class) .setModifiers(ACC_PUBLIC); ResultHandle evalContext = resolve.getMethodParam(0); ResultHandle base = resolve.invokeInterfaceMethod(Descriptors.GET_BASE, evalContext); - boolean matchAny = matchName.equals(TemplateExtension.ANY); + boolean matchAnyOrRegex = patternField != null || matchName.equals(TemplateExtension.ANY); List parameters = method.parameters(); ResultHandle ret; int paramSize = parameters.size(); - if (paramSize == 1 || (paramSize == 2 && matchAny)) { - // Single parameter or two parameters and matches any name - the first param is the base object and the second param is the name + if (paramSize == 1 || (paramSize == 2 && matchAnyOrRegex)) { + // Single parameter or two parameters and matches any name or regex - the first param is the base object and the second param is the name ResultHandle[] args = new ResultHandle[paramSize]; args[0] = base; - if (matchAny) { + if (matchAnyOrRegex) { args[1] = resolve.invokeInterfaceMethod(Descriptors.GET_NAME, evalContext); } ret = resolve.invokeStaticMethod(Descriptors.COMPLETED_FUTURE, resolve @@ -186,7 +214,7 @@ private void implementResolve(ClassCreator valueResolver, ClassInfo declaringCla ret = resolve .newInstance(MethodDescriptor.ofConstructor(CompletableFuture.class)); // The real number of evaluated params, i.e. skip the base object and name if matchAny==true - int realParamSize = paramSize - (matchAny ? 2 : 1); + int realParamSize = paramSize - (matchAnyOrRegex ? 2 : 1); // Evaluate params first ResultHandle name = resolve.invokeInterfaceMethod(Descriptors.GET_NAME, evalContext); // The CompletionStage upon which we invoke whenComplete() @@ -202,7 +230,7 @@ private void implementResolve(ClassCreator valueResolver, ClassInfo declaringCla AssignableResultHandle whenBase = whenComplete.createVariable(Object.class); whenComplete.assign(whenBase, base); AssignableResultHandle whenName = null; - if (matchAny) { + if (matchAnyOrRegex) { whenName = whenComplete.createVariable(String.class); whenComplete.assign(whenName, name); } @@ -250,7 +278,7 @@ private void implementResolve(ClassCreator valueResolver, ClassInfo declaringCla // Base object args[shift] = whenBase; shift++; - if (matchAny) { + if (matchAnyOrRegex) { args[shift] = whenName; shift++; } @@ -293,14 +321,15 @@ private void implementResolve(ClassCreator valueResolver, ClassInfo declaringCla resolve.returnValue(ret); } - private void implementAppliesTo(ClassCreator valueResolver, MethodInfo method, String matchName) { + private void implementAppliesTo(ClassCreator valueResolver, MethodInfo method, String matchName, + FieldDescriptor patternField) { MethodCreator appliesTo = valueResolver.getMethodCreator("appliesTo", boolean.class, EvalContext.class) .setModifiers(ACC_PUBLIC); List parameters = method.parameters(); - boolean matchAny = matchName.equals(TemplateExtension.ANY); + boolean matchAny = patternField == null && matchName.equals(TemplateExtension.ANY); boolean isVarArgs = ValueResolverGenerator.isVarArgs(method); - int realParamSize = parameters.size() - (matchAny ? 2 : 1); + int realParamSize = parameters.size() - (matchAny || patternField != null ? 2 : 1); ResultHandle evalContext = appliesTo.getMethodParam(0); ResultHandle base = appliesTo.invokeInterfaceMethod(Descriptors.GET_BASE, evalContext); ResultHandle name = appliesTo.invokeInterfaceMethod(Descriptors.GET_NAME, evalContext); @@ -317,10 +346,21 @@ private void implementAppliesTo(ClassCreator valueResolver, MethodInfo method, S // Test property name if (!matchAny) { - ResultHandle nameTest = appliesTo.invokeVirtualMethod(Descriptors.EQUALS, name, - appliesTo.load(matchName)); - BytecodeCreator nameNotMatched = appliesTo.ifTrue(nameTest).falseBranch(); - nameNotMatched.returnValue(nameNotMatched.load(false)); + if (patternField != null) { + // if (!pattern.matcher(value).match()) { + // return false; + // } + ResultHandle pattern = appliesTo.readInstanceField(patternField, appliesTo.getThis()); + ResultHandle matcher = appliesTo.invokeVirtualMethod(Descriptors.PATTERN_MATCHER, pattern, name); + BytecodeCreator nameNotMatched = appliesTo + .ifFalse(appliesTo.invokeVirtualMethod(Descriptors.MATCHER_MATCHES, matcher)).trueBranch(); + nameNotMatched.returnValue(appliesTo.load(false)); + } else { + ResultHandle nameTest = appliesTo.invokeVirtualMethod(Descriptors.EQUALS, name, + appliesTo.load(matchName)); + BytecodeCreator nameNotMatched = appliesTo.ifTrue(nameTest).falseBranch(); + nameNotMatched.returnValue(nameNotMatched.load(false)); + } } // Test number of parameters @@ -354,7 +394,7 @@ public NamespaceResolverCreator(ClassInfo declaringClass, String namespace) { } String targetPackage = packageName(declaringClass.name()); - String suffix = "_Namespace" + SUFFIX + sha1(namespace); + String suffix = NAMESPACE_SUFFIX + sha1(namespace); String generatedName = generatedNameFromTarget(targetPackage, baseName, suffix); generatedTypes.add(generatedName.replace('/', '.')); @@ -376,6 +416,7 @@ public void close() { public class ResolveCreator implements AutoCloseable { private final MethodCreator resolve; + private final MethodCreator constructor; private final ResultHandle evalContext; private final ResultHandle name; private final ResultHandle params; @@ -388,20 +429,33 @@ public ResolveCreator() { this.name = resolve.invokeInterfaceMethod(Descriptors.GET_NAME, evalContext); this.params = resolve.invokeInterfaceMethod(Descriptors.GET_PARAMS, evalContext); this.paramsCount = resolve.invokeInterfaceMethod(Descriptors.COLLECTION_SIZE, params); + this.constructor = namespaceResolver.getMethodCreator("", "V"); + // Invoke super() + this.constructor.invokeSpecialMethod(Descriptors.OBJECT_CONSTRUCTOR, constructor.getThis()); } - public void addMethod(MethodInfo method, String matchName) { + public void addMethod(MethodInfo method, String matchName, String matchRegex) { List parameters = method.parameters(); int paramSize = parameters.size(); - boolean matchAny = matchName.equals(TemplateExtension.ANY); + + FieldDescriptor patternField = null; + if (matchRegex != null && !matchRegex.isEmpty()) { + patternField = namespaceResolver.getFieldCreator(PATTERN + "_" + sha1(method.toString()), Pattern.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL).getFieldDescriptor(); + constructor.writeInstanceField(patternField, constructor.getThis(), + constructor.invokeStaticMethod(Descriptors.PATTERN_COMPILE, constructor.load(matchRegex))); + } + + boolean matchAnyOrRegex = patternField != null || matchName.equals(TemplateExtension.ANY); // The real number of evaluated params, i.e. skip the name if matchAny==true - int realParamSize = paramSize - (matchAny ? 1 : 0); + int realParamSize = paramSize - (matchAnyOrRegex ? 1 : 0); - BytecodeCreator matchScope = createNamespaceExtensionMatchScope(resolve, method, realParamSize, matchName, name, + BytecodeCreator matchScope = createNamespaceExtensionMatchScope(resolve, method, realParamSize, matchName, + patternField, name, params, paramsCount); ResultHandle ret = matchScope.newInstance(MethodDescriptor.ofConstructor(CompletableFuture.class)); - if (paramSize == 1 && matchAny) { + if (paramSize == 1 && matchAnyOrRegex) { // Single parameter and matches any name - the first param is the name ResultHandle[] args = new ResultHandle[1]; args[0] = name; @@ -421,7 +475,7 @@ public void addMethod(MethodInfo method, String matchName) { matchScope.invokeInterfaceMethod(Descriptors.CF_WHEN_COMPLETE, paramsReady, whenCompleteFun.getInstance()); BytecodeCreator whenComplete = whenCompleteFun.getBytecode(); AssignableResultHandle whenName = null; - if (matchAny) { + if (matchAnyOrRegex) { whenName = whenComplete.createVariable(String.class); whenComplete.assign(whenName, name); } @@ -465,7 +519,7 @@ public void addMethod(MethodInfo method, String matchName) { // n minus 1 - adapted arg for varargs methods ResultHandle[] args = new ResultHandle[paramSize]; int shift = 0; - if (matchAny) { + if (matchAnyOrRegex) { args[shift] = whenName; shift++; } @@ -508,6 +562,7 @@ public void addMethod(MethodInfo method, String matchName) { @Override public void close() { + constructor.returnValue(null); resolve.returnValue(resolve.readStaticField(Descriptors.RESULTS_NOT_FOUND)); } @@ -516,19 +571,26 @@ public void close() { } private BytecodeCreator createNamespaceExtensionMatchScope(BytecodeCreator bytecodeCreator, MethodInfo method, - int realParamSize, String matchName, ResultHandle name, ResultHandle params, + int realParamSize, String matchName, FieldDescriptor patternField, ResultHandle name, ResultHandle params, ResultHandle paramsCount) { - boolean matchAny = matchName.equals(TemplateExtension.ANY); + boolean matchAny = patternField == null && matchName.equals(TemplateExtension.ANY); boolean isVarArgs = ValueResolverGenerator.isVarArgs(method); BytecodeCreator matchScope = bytecodeCreator.createScope(); // Test property name if (!matchAny) { - matchScope.ifNonZero(matchScope.invokeVirtualMethod(Descriptors.EQUALS, - matchScope.load(matchName), - name)) - .falseBranch().breakScope(matchScope); + if (patternField != null) { + ResultHandle pattern = matchScope.readInstanceField(patternField, matchScope.getThis()); + ResultHandle matcher = matchScope.invokeVirtualMethod(Descriptors.PATTERN_MATCHER, pattern, name); + matchScope.ifFalse(matchScope.invokeVirtualMethod(Descriptors.MATCHER_MATCHES, matcher)).trueBranch() + .breakScope(matchScope); + } else { + matchScope.ifTrue(matchScope.invokeVirtualMethod(Descriptors.EQUALS, + matchScope.load(matchName), + name)) + .falseBranch().breakScope(matchScope); + } } // Test number of parameters if (!isVarArgs || realParamSize > 1) { diff --git a/independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/SimpleGeneratorTest.java b/independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/SimpleGeneratorTest.java index 7d7a08dcf4ceca..2d57978619e776 100644 --- a/independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/SimpleGeneratorTest.java +++ b/independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/SimpleGeneratorTest.java @@ -50,15 +50,15 @@ public static void init() throws IOException { MethodInfo extensionMethod = index.getClassByName(DotName.createSimple(MyService.class.getName())).method( "getDummy", Type.create(myServiceClazz.name(), Kind.CLASS), PrimitiveType.INT, Type.create(DotName.createSimple(String.class.getName()), Kind.CLASS)); - extensionMethodGenerator.generate(extensionMethod, null, null); + extensionMethodGenerator.generate(extensionMethod, null, null, null); extensionMethod = index.getClassByName(DotName.createSimple(MyService.class.getName())).method( "getDummy", Type.create(myServiceClazz.name(), Kind.CLASS), PrimitiveType.INT, PrimitiveType.LONG); - extensionMethodGenerator.generate(extensionMethod, null, null); + extensionMethodGenerator.generate(extensionMethod, null, null, null); extensionMethod = index.getClassByName(DotName.createSimple(MyService.class.getName())).method( "getDummyVarargs", Type.create(myServiceClazz.name(), Kind.CLASS), PrimitiveType.INT, Type.create(DotName.createSimple("[L" + String.class.getName() + ";"), Kind.ARRAY)); - extensionMethodGenerator.generate(extensionMethod, null, null); + extensionMethodGenerator.generate(extensionMethod, null, null, null); generatedTypes.addAll(extensionMethodGenerator.getGeneratedTypes()); }