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 committed Aug 7, 2020
1 parent 10af89a commit aae3480
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 46 deletions.
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 @@ -803,7 +808,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 +820,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 aae3480

Please sign in to comment.