Skip to content

Commit

Permalink
Qute - support multiple NamespaceResolvers for the same namespace
Browse files Browse the repository at this point in the history
- resolves quarkusio#16759
- also add built-in StringTemplateExtensions
  • Loading branch information
mkouba committed Apr 26, 2021
1 parent a2276ae commit 42ed240
Show file tree
Hide file tree
Showing 14 changed files with 362 additions and 87 deletions.
12 changes: 11 additions & 1 deletion docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,8 @@ These extension methods can be used as follows.
<1> The output is `Hello world!`
<2> The output is `olleh`

==== Built-in Template Extension
[[built-in-template-extension]]
==== Built-in Template Extensions

Quarkus provides a set of built-in extension methods.

Expand Down Expand Up @@ -1396,6 +1397,15 @@ TIP: A list element can be accessed directly: `{list.10}` or `{list[10]}`.
* `mod`: Modulo operation
** `{#if counter.mod(5) == 0}`

==== Strings

* `fmt` or `format`: format the string instance via `java.lang.String.format()`
** `{myStr.fmt("arg1","arg2")}`
** `{myStr.format(locale,arg1)}`
* `str:fmt` or `str:format`: format the supplied string value via `java.lang.String.format()`
** `{str:format("Hello %s!",name)}`
** `{str:fmt(locale,'%tA',now)}`

===== Config

* `config:<name>` or `config:[<name>]`: Returns the config value for the given property name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
Expand Down Expand Up @@ -123,6 +122,7 @@
import io.quarkus.qute.runtime.extensions.ConfigTemplateExtensions;
import io.quarkus.qute.runtime.extensions.MapTemplateExtensions;
import io.quarkus.qute.runtime.extensions.NumberTemplateExtensions;
import io.quarkus.qute.runtime.extensions.StringTemplateExtensions;
import io.quarkus.qute.runtime.extensions.TimeTemplateExtensions;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
Expand Down Expand Up @@ -209,7 +209,7 @@ AdditionalBeanBuildItem additionalBeans() {
.addBeanClasses(EngineProducer.class, TemplateProducer.class, ContentTypes.class, Template.class,
TemplateInstance.class, CollectionTemplateExtensions.class,
MapTemplateExtensions.class, NumberTemplateExtensions.class, ConfigTemplateExtensions.class,
TimeTemplateExtensions.class)
TimeTemplateExtensions.class, StringTemplateExtensions.class)
.build();
}

Expand Down Expand Up @@ -1038,19 +1038,28 @@ public Function<FieldInfo, String> apply(ClassInfo clazz) {
}
}

// Generate a namespace resolver for extension methods declared on the same class
for (Entry<DotName, Map<String, List<TemplateExtensionMethodBuildItem>>> entry1 : classToNamespaceExtensions
// Generate a namespace resolver for extension methods declared on the same class and of the same priority
for (Entry<DotName, Map<String, List<TemplateExtensionMethodBuildItem>>> classEntry : classToNamespaceExtensions
.entrySet()) {
Map<String, List<TemplateExtensionMethodBuildItem>> namespaceToMethods = entry1.getValue();
for (Entry<String, List<TemplateExtensionMethodBuildItem>> entry2 : namespaceToMethods.entrySet()) {
List<TemplateExtensionMethodBuildItem> methods = entry2.getValue();
// Methods with higher priority take precedence
methods.sort(Comparator.comparingInt(TemplateExtensionMethodBuildItem::getPriority).reversed());
try (NamespaceResolverCreator namespaceResolverCreator = extensionMethodGenerator
.createNamespaceResolver(methods.get(0).getMethod().declaringClass(), entry2.getKey())) {
try (ResolveCreator resolveCreator = namespaceResolverCreator.implementResolve()) {
for (TemplateExtensionMethodBuildItem method : methods) {
resolveCreator.addMethod(method.getMethod(), method.getMatchName(), method.getMatchRegex());
Map<String, List<TemplateExtensionMethodBuildItem>> namespaceToMethods = classEntry.getValue();
for (Entry<String, List<TemplateExtensionMethodBuildItem>> nsEntry : namespaceToMethods.entrySet()) {
Map<Integer, List<TemplateExtensionMethodBuildItem>> priorityToMethods = new HashMap<>();
for (TemplateExtensionMethodBuildItem method : nsEntry.getValue()) {
List<TemplateExtensionMethodBuildItem> methods = priorityToMethods.get(method.getPriority());
if (methods == null) {
methods = new ArrayList<>();
priorityToMethods.put(method.getPriority(), methods);
}
methods.add(method);
}
for (Entry<Integer, List<TemplateExtensionMethodBuildItem>> priorityEntry : priorityToMethods.entrySet()) {
try (NamespaceResolverCreator namespaceResolverCreator = extensionMethodGenerator
.createNamespaceResolver(priorityEntry.getValue().get(0).getMethod().declaringClass(),
nsEntry.getKey(), priorityEntry.getKey())) {
try (ResolveCreator resolveCreator = namespaceResolverCreator.implementResolve()) {
for (TemplateExtensionMethodBuildItem method : priorityEntry.getValue()) {
resolveCreator.addMethod(method.getMethod(), method.getMatchName(), method.getMatchRegex());
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ static String quark() {
return "Quark!";
}

@TemplateExtension(namespace = "str", matchName = ANY, priority = 1)
@TemplateExtension(namespace = "str", matchName = ANY, priority = 4)
static String quarkAny(String key) {
return key.toUpperCase() + "!";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.quarkus.qute.deployment.extensions;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.LocalDateTime;
import java.util.Locale;

import javax.inject.Inject;

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 io.quarkus.qute.Engine;
import io.quarkus.test.QuarkusUnitTest;

public class StringTemplateExtensionsTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class));

@Inject
Engine engine;

@Test
public void testTemplateExtensions() {
assertEquals("hello:1",
engine.parse("{str:format('%s:%s',greeting, 1)}").data("greeting", "hello").render());
assertEquals("1",
engine.parse("{str:fmt('%s',1)}").render());
assertEquals(" d c b a",
engine.parse("{myStr.fmt('a','b','c','d')}").data("myStr", "%4$2s %3$2s %2$2s %1$2s").render());
assertEquals("%s",
engine.parse("{myStr.fmt(myStr)}").data("myStr", "%s").render());
assertEquals("Hello Dorka!",
engine.parse("{myStr.format(name)}").data("myStr", "Hello %s!", "name", "Dorka").render());
assertEquals("Dienstag",
engine.parse("{myStr.fmt(locale,now)}")
.data("myStr", "%tA", "now", LocalDateTime.of(2016, 7, 26, 12, 0), "locale", Locale.GERMAN)
.render());
assertEquals("Dienstag",
engine.parse("{str:fmt(locale,'%tA',now)}")
.data("now", LocalDateTime.of(2016, 7, 26, 12, 0), "locale", Locale.GERMAN)
.render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.List;
Expand Down Expand Up @@ -162,10 +163,11 @@ private Resolver createResolver(String resolverClassName) {
Class<?> resolverClazz = Thread.currentThread()
.getContextClassLoader().loadClass(resolverClassName);
if (Resolver.class.isAssignableFrom(resolverClazz)) {
return (Resolver) resolverClazz.newInstance();
return (Resolver) resolverClazz.getDeclaredConstructor().newInstance();
}
throw new IllegalStateException("Not a resolver: " + resolverClassName);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
throw new IllegalStateException("Unable to create resolver: " + resolverClassName, e);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.quarkus.qute.runtime.extensions;

import java.util.Locale;

import javax.enterprise.inject.Vetoed;

import io.quarkus.qute.TemplateExtension;

@Vetoed // Make sure no bean is created from this class
public class StringTemplateExtensions {

static final String STR = "str";

/**
* E.g. {@code strVal.fmt(name,surname)}. The priority must be lower than
* {@link #fmtInstance(String, String, Locale, Object...)}.
*
* @param format
* @param ignoredPropertyName
* @param args
* @return the formatted value
*/
@TemplateExtension(matchRegex = "fmt|format", priority = 2)
static String fmtInstance(String format, String ignoredPropertyName, Object... args) {
return String.format(format, args);
}

/**
* E.g. {@code strVal.format(locale,name)}. The priority must be higher than
* {@link #fmtInstance(String, String, Object...)}.
*
* @param format
* @param ignoredPropertyName
* @param locale
* @param args
* @return the formatted value
*/
@TemplateExtension(matchRegex = "fmt|format", priority = 3)
static String fmtInstance(String format, String ignoredPropertyName, Locale locale, Object... args) {
return String.format(locale, format, args);
}

/**
* E.g. {@cde str:fmt("Hello %s",name)}. The priority must be lower than {@link #fmt(String, Locale, String, Object...)}.
*
* @param ignoredPropertyName
* @param format
* @param args
* @return the formatted value
*/
@TemplateExtension(namespace = STR, matchRegex = "fmt|format", priority = 2)
static String fmt(String ignoredPropertyName, String format, Object... args) {
return String.format(format, args);
}

/**
* E.g. {@code str:fmt(locale,"Hello %s",name)}. The priority must be higher than {@link #fmt(String, String, Object...)}.
*
* @param ignoredPropertyName
* @param locale
* @param format
* @param args
* @return the formatted value
*/
@TemplateExtension(namespace = STR, matchRegex = "fmt|format", priority = 3)
static String fmt(String ignoredPropertyName, Locale locale, String format, Object... args) {
return String.format(locale, format, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,13 @@ public EngineBuilder addDefaults() {
}

public EngineBuilder addNamespaceResolver(NamespaceResolver resolver) {
for (NamespaceResolver namespaceResolver : namespaceResolvers) {
if (namespaceResolver.getNamespace().equals(resolver.getNamespace())) {
for (NamespaceResolver nsResolver : namespaceResolvers) {
if (nsResolver.getNamespace().equals(resolver.getNamespace())
&& resolver.getPriority() == nsResolver.getPriority()) {
throw new IllegalArgumentException(
String.format("Namespace %s is already handled by %s", resolver.getNamespace(), namespaceResolver));
String.format(
"Namespace [%s] may not be handled by multiple resolvers of the same priority [%s]: %s and %s",
resolver.getNamespace(), resolver.getPriority(), nsResolver, resolver));
}
}
this.namespaceResolvers.add(resolver);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,13 @@ public boolean parameterTypesMatch(boolean varargs, Class<?>[] types) throws Int
} else {
if (varargs) {
int diff = types.length - results.length;
if (diff == 1) {
// varargs may be empty
return true;
} else if (diff > 1) {
if (diff > 1) {
return false;
} else if (diff < 1) {
Class<?> varargsType = types[types.length - 1];
types[types.length - 1] = varargsType.getComponentType();
}
// diff < 1
Class<?> varargsType = types[types.length - 1];
types[types.length - 1] = varargsType.getComponentType();
// if diff == 1 then vargs may be empty and we need to compare the result types
} else {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
import io.quarkus.qute.ExpressionImpl.PartImpl;
import io.quarkus.qute.Results.Result;
import io.smallrye.mutiny.Uni;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import org.jboss.logging.Logger;
Expand All @@ -19,38 +24,56 @@ class EvaluatorImpl implements Evaluator {
private static final Logger LOGGER = Logger.getLogger(EvaluatorImpl.class);

private final List<ValueResolver> resolvers;
private final List<NamespaceResolver> namespaceResolvers;
private final Map<String, List<NamespaceResolver>> namespaceResolvers;

EvaluatorImpl(List<ValueResolver> valueResolvers, List<NamespaceResolver> namespaceResolvers) {
this.resolvers = valueResolvers;
this.namespaceResolvers = namespaceResolvers;
Map<String, List<NamespaceResolver>> namespaceResolversMap = new HashMap<>();
for (NamespaceResolver namespaceResolver : namespaceResolvers) {
List<NamespaceResolver> matching = namespaceResolversMap.get(namespaceResolver.getNamespace());
if (matching == null) {
matching = new ArrayList<>();
namespaceResolversMap.put(namespaceResolver.getNamespace(), matching);
}
matching.add(namespaceResolver);
}
for (Entry<String, List<NamespaceResolver>> entry : namespaceResolversMap.entrySet()) {
List<NamespaceResolver> list = entry.getValue();
if (list.size() == 1) {
entry.setValue(Collections.singletonList(list.get(0)));
} else {
// Sort by priority - higher priority wins
list.sort(Comparator.comparingInt(WithPriority::getPriority).reversed());
}
}
this.namespaceResolvers = namespaceResolversMap;
}

@Override
public CompletionStage<Object> evaluate(Expression expression, ResolutionContext resolutionContext) {
Iterator<Part> parts;
if (expression.hasNamespace()) {
parts = expression.getParts().iterator();
NamespaceResolver resolver = null;
for (NamespaceResolver namespaceResolver : namespaceResolvers) {
if (namespaceResolver.getNamespace().equals(expression.getNamespace())) {
resolver = namespaceResolver;
break;
}
}
if (resolver == null) {
LOGGER.errorf("No namespace resolver found for: %s", expression.getNamespace());
return Futures.failure(new TemplateException("No resolver for namespace: " + expression.getNamespace()));
List<NamespaceResolver> matching = namespaceResolvers.get(expression.getNamespace());
if (matching == null) {
String msg = "No namespace resolver found for: " + expression.getNamespace();
LOGGER.errorf(msg);
return Futures.failure(new TemplateException(msg));
}
EvalContext context = new EvalContextImpl(false, null, parts.next(), resolutionContext);
LOGGER.debugf("Found '%s' namespace resolver: %s", expression.getNamespace(), resolver.getClass());
return resolver.resolve(context).thenCompose(r -> {
if (parts.hasNext()) {
return resolveReference(false, r, parts, resolutionContext);
} else {
return toCompletionStage(r);
}
});
if (matching.size() == 1) {
// Very often a single matching resolver will be found
return matching.get(0).resolve(context).thenCompose(r -> {
if (parts.hasNext()) {
return resolveReference(false, r, parts, resolutionContext);
} else {
return toCompletionStage(r);
}
});
} else {
// Multiple namespace resolvers match
return resolveNamespace(context, resolutionContext, parts, matching.iterator());
}
} else {
if (expression.isLiteral()) {
return expression.getLiteralValue();
Expand All @@ -61,6 +84,24 @@ public CompletionStage<Object> evaluate(Expression expression, ResolutionContext
}
}

private CompletionStage<Object> resolveNamespace(EvalContext context, ResolutionContext resolutionContext,
Iterator<Part> parts, Iterator<NamespaceResolver> resolvers) {
NamespaceResolver resolver = resolvers.next();
return resolver.resolve(context).thenCompose(r -> {
if (Result.NOT_FOUND.equals(r)) {
if (resolvers.hasNext()) {
return resolveNamespace(context, resolutionContext, parts, resolvers);
} else {
return Results.NOT_FOUND;
}
} else if (parts.hasNext()) {
return resolveReference(false, r, parts, resolutionContext);
} else {
return toCompletionStage(r);
}
});
}

private CompletionStage<Object> resolveReference(boolean tryParent, Object ref, Iterator<Part> parts,
ResolutionContext resolutionContext) {
Part part = parts.next();
Expand Down
Loading

0 comments on commit 42ed240

Please sign in to comment.