Skip to content

Commit be7e613

Browse files
authored
Merge pull request #46258 from mkouba/eval-ns-resolver
Qute: add some more built-in string extensions
2 parents 4100928 + aae8420 commit be7e613

File tree

17 files changed

+377
-49
lines changed

17 files changed

+377
-49
lines changed

docs/src/main/asciidoc/qute-reference.adoc

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,12 +2367,34 @@ TIP: A list element can be accessed directly via an index: `{list.10}` or even `
23672367
* `fmt` or `format`: Formats the string instance via `java.lang.String.format()`
23682368
** `{myStr.fmt("arg1","arg2")}`
23692369
** `{myStr.format(locale,arg1)}`
2370+
2371+
* `+`: Infix notation for concatenation, works with `String` and `StringBuilder` base objects
2372+
** `{item.name + '_' + mySuffix}`
2373+
** `{name + 10}`
2374+
2375+
* `str:['<value>']`: Returns the string value, e.g. to easily concatenate another string value
2376+
** `{str:['/path/to/'] + fileName}`
2377+
23702378
* `str:fmt` or `str:format`: Formats the supplied string value via `java.lang.String.format()`
23712379
** `{str:format("Hello %s!",name)}`
23722380
** `{str:fmt(locale,'%tA',now)}`
2373-
* `+`: Concatenation
2374-
** `{item.name + '_' + mySuffix}`
2375-
** `{name + 10}`
2381+
** `{str:fmt('/path/to/%s', fileName)}`
2382+
2383+
* `str:concat`: Concatenates the string representations of the specified arguments.
2384+
** `{str:concat("Hello ",name,"!")}` yields `Hello Foo!` if `name` resolves to `Foo`
2385+
** `{str:concat('/path/to/', fileName)}`
2386+
2387+
* `str:join`: Joins the string representations of the specified arguments together with a delimiter.
2388+
** `{str:join('_','Qute','is','cool')}` yields `Qute_is_cool`
2389+
2390+
* `str:builder`: Returns a new string builder.
2391+
** `{str:builder('Qute').append("is").append("cool!")}` yields `Qute is cool!`
2392+
** `{str:builder('Qute') + "is" + whatisqute + "!"}` yields `Qute is cool!` if `whatisqute` resolves to `cool`
2393+
2394+
* `str:eval`: Evaluates the string representation of the first argument as a template in the <<current_context_object,current context>>.
2395+
** `{str:eval('Hello {name}!')` yields `Hello lovely!` if `name` resolves to `lovely`
2396+
** `{str:eval(myTemplate)}` yields `Hello lovely!` if `myTemplate` resolves to `Hello {name}!` and `name` resolves to `lovely`
2397+
** `{str:eval('/path/to/{fileName}')}` yields `/path/to/file.txt` if `fileName` resolves to `file.txt`
23762398

23772399
===== Config
23782400

extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/StringTemplateExtensionsTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,43 @@ public void testTemplateExtensions() {
4646
engine.parse("{foo + 'bar' + 1}")
4747
.data("foo", "bar")
4848
.render());
49+
assertEquals("barbar1",
50+
engine.parse("{str:concat(foo, 'bar', 1)}")
51+
.data("foo", "bar")
52+
.render());
53+
assertEquals("barbar1",
54+
engine.parse("{str:builder(foo).append('bar').append(1)}")
55+
.data("foo", "bar")
56+
.render());
57+
assertEquals("barbar1",
58+
engine.parse("{str:builder.append(foo).append('bar').append(1)}")
59+
.data("foo", "bar")
60+
.render());
61+
assertEquals("barbar1",
62+
engine.parse("{str:builder(foo) + 'bar' + 1}")
63+
.data("foo", "bar")
64+
.render());
65+
assertEquals("barbar1",
66+
engine.parse("{str:builder + foo + 'bar' + 1}")
67+
.data("foo", "bar")
68+
.render());
69+
assertEquals("Qute-is-cool",
70+
engine.parse("{str:join('-', 'Qute', 'is', foo)}")
71+
.data("foo", "cool")
72+
.render());
73+
assertEquals("Qute is cool!",
74+
engine.parse("{str:Qute + ' is ' + foo + '!'}")
75+
.data("foo", "cool")
76+
.render());
77+
assertEquals("Qute is cool!",
78+
engine.parse("{str:['Qute'] + ' is ' + foo + '!'}")
79+
.data("foo", "cool")
80+
.render());
81+
// note that this is not implemented as a template extension but a ns resolver
82+
assertEquals("Hello fool!",
83+
engine.parse("{str:eval('Hello {name}!')}")
84+
.data("name", "fool")
85+
.render());
4986
}
5087

5188
}

extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import io.quarkus.qute.Resolver;
5050
import io.quarkus.qute.Results;
5151
import io.quarkus.qute.SectionHelperFactory;
52+
import io.quarkus.qute.StrEvalNamespaceResolver;
5253
import io.quarkus.qute.Template;
5354
import io.quarkus.qute.TemplateGlobalProvider;
5455
import io.quarkus.qute.TemplateInstance;
@@ -194,6 +195,9 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
194195
for (NamespaceResolver namespaceResolver : namespaceResolvers) {
195196
builder.addNamespaceResolver(namespaceResolver);
196197
}
198+
// str:eval
199+
StrEvalNamespaceResolver strEvalNamespaceResolver = new StrEvalNamespaceResolver();
200+
builder.addNamespaceResolver(strEvalNamespaceResolver);
197201

198202
// Add generated resolvers
199203
for (String resolverClass : context.getResolverClasses()) {
@@ -269,6 +273,9 @@ public void run() {
269273

270274
engine = builder.build();
271275

276+
// Init resolver for str:eval
277+
strEvalNamespaceResolver.setEngine(engine);
278+
272279
// Load discovered template files
273280
Map<String, List<Template>> discovered = new HashMap<>();
274281
for (String path : context.getTemplatePaths()) {

extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/StringTemplateExtensions.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.quarkus.qute.runtime.extensions;
22

3+
import static io.quarkus.qute.TemplateExtension.ANY;
4+
35
import java.util.Locale;
6+
import java.util.Objects;
47

58
import jakarta.enterprise.inject.Vetoed;
69

@@ -72,4 +75,64 @@ static String plus(String str, Object val) {
7275
return str + val;
7376
}
7477

78+
/**
79+
* E.g. {@code str:concat("Hello ",name)}. The priority must be lower than {@link #fmt(String, String, Object...)}.
80+
*
81+
* @param args
82+
*/
83+
@TemplateExtension(namespace = STR, priority = 1)
84+
static String concat(Object... args) {
85+
StringBuilder b = new StringBuilder(args.length * 10);
86+
for (Object obj : args) {
87+
b.append(obj.toString());
88+
}
89+
return b.toString();
90+
}
91+
92+
/**
93+
* E.g. {@code str:join("_", "Hello",name)}. The priority must be lower than {@link #concat(Object...)}.
94+
*
95+
* @param delimiter
96+
* @param args
97+
*/
98+
@TemplateExtension(namespace = STR, priority = 0)
99+
static String join(String delimiter, Object... args) {
100+
CharSequence[] elements = new CharSequence[args.length];
101+
for (int i = 0; i < args.length; i++) {
102+
elements[i] = args[i].toString();
103+
}
104+
return String.join(delimiter, elements);
105+
}
106+
107+
/**
108+
* E.g. {@code str:builder}. The priority must be lower than {@link #join(String, Object...)}.
109+
*/
110+
@TemplateExtension(namespace = STR, priority = -1)
111+
static StringBuilder builder() {
112+
return new StringBuilder();
113+
}
114+
115+
/**
116+
* E.g. {@code str:builder('Hello')}. The priority must be lower than {@link #builder()}.
117+
*/
118+
@TemplateExtension(namespace = STR, priority = -2)
119+
static StringBuilder builder(Object val) {
120+
return new StringBuilder(Objects.toString(val));
121+
}
122+
123+
/**
124+
* E.g. {@code str:['Foo and bar']}. The priority must be lower than any other {@code str:} resolver.
125+
*
126+
* @param name
127+
*/
128+
@TemplateExtension(namespace = STR, priority = -10, matchName = ANY)
129+
static String self(String name) {
130+
return name;
131+
}
132+
133+
@TemplateExtension(matchName = "+")
134+
static StringBuilder plus(StringBuilder builder, Object val) {
135+
return builder.append(val);
136+
}
137+
75138
}

independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ public interface EvalContext {
5757
*/
5858
Object getAttribute(String key);
5959

60+
/**
61+
*
62+
* @return the current resolution context
63+
*/
64+
ResolutionContext resolutionContext();
65+
6066
}

independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalSectionHelper.java

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
public class EvalSectionHelper implements SectionHelper {
1313

14+
public static final String EVAL = "eval";
1415
private static final String TEMPLATE = "template";
1516

1617
private final Map<String, Expression> parameters;
@@ -23,52 +24,69 @@ public EvalSectionHelper(Map<String, Expression> parameters, Engine engine) {
2324

2425
@Override
2526
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
26-
CompletableFuture<ResultNode> result = new CompletableFuture<>();
27-
context.evaluate(parameters).whenComplete((evaluatedParams, t1) -> {
28-
if (t1 != null) {
29-
result.completeExceptionally(t1);
30-
} else {
31-
try {
27+
CompletableFuture<ResultNode> ret = new CompletableFuture<>();
28+
if (parameters.size() > 1) {
29+
context.evaluate(parameters).whenComplete((evaluatedParams, t1) -> {
30+
if (t1 != null) {
31+
ret.completeExceptionally(t1);
32+
} else {
3233
// Parse the template and execute with the params as the root context object
33-
String templateStr = evaluatedParams.get(TEMPLATE).toString();
34-
TemplateImpl template;
35-
try {
36-
template = (TemplateImpl) engine.parse(templateStr);
37-
} catch (TemplateException e) {
38-
Origin origin = parameters.get(TEMPLATE).getOrigin();
39-
throw TemplateException.builder()
40-
.message(
41-
"Parser error in the evaluated template: {templateId} line {line}:\\n\\t{originalMessage}")
42-
.code(Code.ERROR_IN_EVALUATED_TEMPLATE)
43-
.argument("templateId",
44-
origin.hasNonGeneratedTemplateId() ? " template [" + origin.getTemplateId() + "]"
45-
: "")
46-
.argument("line", origin.getLine())
47-
.argument("originalMessage", e.getMessage())
48-
.build();
49-
}
50-
template.root
51-
.resolve(context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null))
52-
.whenComplete((resultNode, t2) -> {
53-
if (t2 != null) {
54-
result.completeExceptionally(t2);
55-
} else {
56-
result.complete(resultNode);
57-
}
58-
});
59-
} catch (Throwable e) {
60-
result.completeExceptionally(e);
34+
String contents = evaluatedParams.get(TEMPLATE).toString();
35+
parseAndResolve(ret, contents,
36+
context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null));
6137
}
38+
});
39+
} else {
40+
Expression contents = parameters.get(TEMPLATE);
41+
if (contents.isLiteral()) {
42+
parseAndResolve(ret, contents.getLiteral().toString(), context.resolutionContext());
43+
} else {
44+
context.evaluate(contents).whenComplete((r, t) -> {
45+
if (t != null) {
46+
ret.completeExceptionally(t);
47+
} else {
48+
parseAndResolve(ret, r.toString(), context.resolutionContext());
49+
}
50+
});
6251
}
63-
});
64-
return result;
52+
}
53+
54+
return ret;
55+
}
56+
57+
private void parseAndResolve(CompletableFuture<ResultNode> ret, String contents, ResolutionContext resolutionContext) {
58+
TemplateImpl template;
59+
try {
60+
template = (TemplateImpl) engine.parse(contents);
61+
template.root
62+
.resolve(resolutionContext)
63+
.whenComplete((resultNode, t2) -> {
64+
if (t2 != null) {
65+
ret.completeExceptionally(t2);
66+
} else {
67+
ret.complete(resultNode);
68+
}
69+
});
70+
} catch (TemplateException e) {
71+
Origin origin = parameters.get(TEMPLATE).getOrigin();
72+
ret.completeExceptionally(TemplateException.builder()
73+
.message(
74+
"Parser error in the evaluated template: {templateId} line {line}:\\n\\t{originalMessage}")
75+
.code(Code.ERROR_IN_EVALUATED_TEMPLATE)
76+
.argument("templateId",
77+
origin.hasNonGeneratedTemplateId() ? " template [" + origin.getTemplateId() + "]"
78+
: "")
79+
.argument("line", origin.getLine())
80+
.argument("originalMessage", e.getMessage())
81+
.build());
82+
}
6583
}
6684

6785
public static class Factory implements SectionHelperFactory<EvalSectionHelper> {
6886

6987
@Override
7088
public List<String> getDefaultAliases() {
71-
return ImmutableList.of("eval");
89+
return ImmutableList.of(EVAL);
7290
}
7391

7492
@Override

independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ public Object getAttribute(String key) {
291291
return resolutionContext.getAttribute(key);
292292
}
293293

294+
@Override
295+
public ResolutionContext resolutionContext() {
296+
return resolutionContext;
297+
}
298+
294299
@Override
295300
public String toString() {
296301
StringBuilder builder = new StringBuilder();
@@ -405,6 +410,11 @@ boolean tryParent() {
405410
return true;
406411
}
407412

413+
@Override
414+
public ResolutionContext resolutionContext() {
415+
return resolutionContext;
416+
}
417+
408418
@Override
409419
public String toString() {
410420
StringBuilder builder = new StringBuilder();

independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ static Object getLiteralValue(String literal) {
2424
return value;
2525
}
2626
if (isStringLiteral(literal)) {
27-
value = literal.substring(1, literal.length() - 1);
27+
value = extractStringValue(literal);
2828
} else if (literal.equals("true")) {
2929
value = Boolean.TRUE;
3030
} else if (literal.equals("false")) {
@@ -85,6 +85,10 @@ static boolean isStringLiteralSeparatorDouble(char character) {
8585
return character == '"';
8686
}
8787

88+
static String extractStringValue(String strLiteral) {
89+
return strLiteral.substring(1, strLiteral.length() - 1);
90+
}
91+
8892
static boolean isStringLiteral(String value) {
8993
if (value == null || value.isEmpty()) {
9094
return false;

independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public interface ResolutionContext {
6969
*/
7070
Object getAttribute(String key);
7171

72+
/**
73+
* @return the current template
74+
*/
75+
Template getTemplate();
76+
7277
/**
7378
*
7479
* @return the evaluator

0 commit comments

Comments
 (0)