Skip to content

Commit

Permalink
Qute: template extensions can use regular expressions to match names
Browse files Browse the repository at this point in the history
- also implement a shortcut to access List elements by index
- resolves quarkusio#11275
  • Loading branch information
mkouba authored and gsmet committed Aug 24, 2020
1 parent 9be3505 commit 107a9bf
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 50 deletions.
9 changes: 7 additions & 2 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,12 @@ private void produceExtensionMethod(IndexView index, BuildProducer<TemplateExten
if (namespaceValue != null) {
namespace = namespaceValue.asString();
}
extensionMethods.produce(new TemplateExtensionMethodBuildItem(method, matchName,
String matchRegex = null;
AnnotationValue matchRegexValue = extensionAnnotation.value(ExtensionMethodGenerator.MATCH_REGEX);
if (matchRegexValue != null) {
matchRegex = matchRegexValue.asString();
}
extensionMethods.produce(new TemplateExtensionMethodBuildItem(method, matchName, matchRegex,
index.getClassByName(method.parameters().get(0).name()), priority, namespace));
}

Expand Down Expand Up @@ -718,7 +723,10 @@ void generateValueResolvers(QuteConfig config, BuildProducer<GeneratedClassBuild
ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Predicate<String>() {
@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);
}
Expand Down Expand Up @@ -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());
}
}

Expand All @@ -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());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.qute.deployment;

import java.util.regex.Pattern;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.MethodInfo;

Expand All @@ -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() {
Expand All @@ -36,6 +43,10 @@ public String getMatchName() {
return matchName;
}

public String getMatchRegex() {
return matchRegex;
}

public ClassInfo getMatchClass() {
return matchClass;
}
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import io.quarkus.qute.Results.Result;
import io.quarkus.qute.TemplateExtension;

@TemplateExtension
Expand All @@ -10,4 +11,14 @@ public class CollectionTemplateExtensions {
static Object get(List<?> list, int index) {
return list.get(index);
}
}

@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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* Methods that do not meet the following requirements are ignored.
* <p>
* A template extension method:
* <ul>
* <li>must be static,</li>
* <li>must not return {@code void},</li>
* <li>must accept at least one parameter, unless the namespace is specified.</li>
* <p>
* </ul>
* 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.
* <p>
* 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.
* <p>
* If both {@link #matchName()} and {@link #matchRegex()} are set the regular expression is used for matching.
* <p>
* 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.
*
* <pre>
* {@literal @}TemplateExtension
Expand Down Expand Up @@ -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
Expand All @@ -70,7 +82,8 @@
* If not empty a namespace resolver is generated instead.
* <p>
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 107a9bf

Please sign in to comment.