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 9c5e6cbd62078..df5467af0ad78 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 @@ -557,15 +557,15 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI if (root.isTypeInfo()) { // E.g. |org.acme.Item| match.setValues(root.asTypeInfo().rawClass, root.asTypeInfo().resolvedType); - if (root.asTypeInfo().hint != null) { - processHints(templateAnalysis, root.asTypeInfo().hint, match, index, expression, generatedIdsToMatches, + if (root.asTypeInfo().hasHints()) { + processHints(templateAnalysis, root.asTypeInfo().hints, match, index, expression, generatedIdsToMatches, incorrectExpressions); } } else { - if (root.isProperty() && root.asProperty().hint != null) { + if (root.isProperty() && root.asProperty().hasHints()) { // Root is not a type info but a property with hint // E.g. 'it' and 'STATUS' - if (processHints(templateAnalysis, root.asProperty().hint, match, index, expression, + if (processHints(templateAnalysis, root.asProperty().hints, match, index, expression, generatedIdsToMatches, incorrectExpressions)) { // In some cases it's necessary to reset the iterator iterator = parts.iterator(); @@ -687,13 +687,10 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI clazz = index.getClassByName(type.name()); } match.setValues(clazz, type); - if (info.isProperty()) { - String hint = info.asProperty().hint; - if (hint != null) { - // For example a loop section needs to validate the type of an element - processHints(templateAnalysis, hint, match, index, expression, generatedIdsToMatches, - incorrectExpressions); - } + if (info.isProperty() && info.asProperty().hasHints()) { + // For example a loop section needs to validate the type of an element + processHints(templateAnalysis, info.asProperty().hints, match, index, expression, generatedIdsToMatches, + incorrectExpressions); } } } else { @@ -1278,7 +1275,7 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView /** * @param templateAnalysis - * @param helperHint + * @param helperHints * @param match * @param index * @param expression @@ -1286,41 +1283,43 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView * @param incorrectExpressions * @return {@code true} if it is necessary to reset the type info part iterator */ - static boolean processHints(TemplateAnalysis templateAnalysis, String helperHint, Match match, IndexView index, + static boolean processHints(TemplateAnalysis templateAnalysis, List helperHints, Match match, IndexView index, Expression expression, Map generatedIdsToMatches, BuildProducer incorrectExpressions) { - if (helperHint == null || helperHint.isEmpty()) { + if (helperHints == null || helperHints.isEmpty()) { return false; } - if (helperHint.equals(LoopSectionHelper.Factory.HINT_ELEMENT)) { - // Iterable, Stream => Item - // Map => Entry - processLoopElementHint(match, index, expression, incorrectExpressions); - } else if (helperHint.startsWith(LoopSectionHelper.Factory.HINT_PREFIX)) { - Expression valueExpr = findExpression(helperHint, LoopSectionHelper.Factory.HINT_PREFIX, templateAnalysis); - if (valueExpr != null) { - Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId()); - if (valueExprMatch != null) { - match.setValues(valueExprMatch.clazz, valueExprMatch.type); + for (String helperHint : helperHints) { + if (helperHint.equals(LoopSectionHelper.Factory.HINT_ELEMENT)) { + // Iterable, Stream => Item + // Map => Entry + processLoopElementHint(match, index, expression, incorrectExpressions); + } else if (helperHint.startsWith(LoopSectionHelper.Factory.HINT_PREFIX)) { + Expression valueExpr = findExpression(helperHint, LoopSectionHelper.Factory.HINT_PREFIX, templateAnalysis); + if (valueExpr != null) { + Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId()); + if (valueExprMatch != null) { + match.setValues(valueExprMatch.clazz, valueExprMatch.type); + } } - } - } else if (helperHint.startsWith(WhenSectionHelper.Factory.HINT_PREFIX)) { - // If a value expression resolves to an enum we attempt to use the enum type to validate the enum constant - // This basically transforms the type info "ON" into something like "|org.acme.Status|.ON" - Expression valueExpr = findExpression(helperHint, WhenSectionHelper.Factory.HINT_PREFIX, templateAnalysis); - if (valueExpr != null) { - Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId()); - if (valueExprMatch != null && valueExprMatch.clazz.isEnum()) { - match.setValues(valueExprMatch.clazz, valueExprMatch.type); - return true; + } else if (helperHint.startsWith(WhenSectionHelper.Factory.HINT_PREFIX)) { + // If a value expression resolves to an enum we attempt to use the enum type to validate the enum constant + // This basically transforms the type info "ON" into something like "|org.acme.Status|.ON" + Expression valueExpr = findExpression(helperHint, WhenSectionHelper.Factory.HINT_PREFIX, templateAnalysis); + if (valueExpr != null) { + Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId()); + if (valueExprMatch != null && valueExprMatch.clazz.isEnum()) { + match.setValues(valueExprMatch.clazz, valueExprMatch.type); + return true; + } } - } - } else if (helperHint.startsWith(SetSectionHelper.Factory.HINT_PREFIX)) { - Expression valueExpr = findExpression(helperHint, SetSectionHelper.Factory.HINT_PREFIX, templateAnalysis); - if (valueExpr != null) { - Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId()); - if (valueExprMatch != null) { - match.setValues(valueExprMatch.clazz, valueExprMatch.type); + } else if (helperHint.startsWith(SetSectionHelper.Factory.HINT_PREFIX)) { + Expression valueExpr = findExpression(helperHint, SetSectionHelper.Factory.HINT_PREFIX, templateAnalysis); + if (valueExpr != null) { + Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId()); + if (valueExprMatch != null) { + match.setValues(valueExprMatch.clazz, valueExprMatch.type); + } } } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java index c1471cfbe9252..b3c6865834e0a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java @@ -4,6 +4,8 @@ import java.util.Collections; import java.util.List; import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; @@ -210,11 +212,27 @@ public String toString() { static abstract class HintInfo extends Info { - final String hint; + static final Pattern HINT_PATTERN = Pattern.compile("\\<[a-zA-Z_0-9#-]+\\>"); - public HintInfo(String value, Expression.Part part, String hint) { + // , , etc. + final List hints; + + HintInfo(String value, Expression.Part part, String hintStr) { super(value, part); - this.hint = hint; + if (hintStr != null) { + List found = new ArrayList<>(); + Matcher m = HINT_PATTERN.matcher(hintStr); + while (m.find()) { + found.add(m.group()); + } + this.hints = found; + } else { + this.hints = Collections.emptyList(); + } + } + + boolean hasHints() { + return !hints.isEmpty(); } } @@ -224,7 +242,7 @@ static class TypeInfo extends HintInfo { final Type resolvedType; final ClassInfo rawClass; - public TypeInfo(String value, Expression.Part part, String hint, Type resolvedType, ClassInfo rawClass) { + TypeInfo(String value, Expression.Part part, String hint, Type resolvedType, ClassInfo rawClass) { super(value, part, hint); this.resolvedType = resolvedType; this.rawClass = rawClass; @@ -246,7 +264,7 @@ static class PropertyInfo extends HintInfo { final String name; - public PropertyInfo(String name, Expression.Part part, String hint) { + PropertyInfo(String name, Expression.Part part, String hint) { super(name, part, hint); this.name = name; } @@ -267,7 +285,7 @@ static class VirtualMethodInfo extends Info { final String name; - public VirtualMethodInfo(String value, Expression.VirtualMethodPart part) { + VirtualMethodInfo(String value, Expression.VirtualMethodPart part) { super(value, part); this.name = part.getName(); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeInfosTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeInfosTest.java new file mode 100644 index 0000000000000..6ec6fce4efe8d --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeInfosTest.java @@ -0,0 +1,31 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; + +import org.junit.jupiter.api.Test; + +public class TypeInfosTest { + + @Test + public void testHintPattern() { + assertHints("", ""); + assertHints("", "", ""); + assertHints("", "", "", ""); + assertHints("loop-element>", ""); + } + + private void assertHints(String hintStr, String... expectedHints) { + Matcher m = TypeInfos.HintInfo.HINT_PATTERN.matcher(hintStr); + List hints = new ArrayList<>(); + while (m.find()) { + hints.add(m.group()); + } + assertEquals(Arrays.asList(expectedHints), hints); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java index 032f93a57d71b..7842dbe7b069d 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java @@ -41,11 +41,22 @@ public class TypeSafeLoopTest { + "::" + "{#for item in items}{#each item.tags('foo')}{it}{/each}{/for}" + "::" - + "{fooList.get(0).tags.size}={#each fooList.get(0).tags}{it}{/each}"), "templates/foo.html")); + + "{fooList.get(0).tags.size}={#each fooList.get(0).tags}{it}{/each}"), "templates/foo.html") + .addAsResource(new StringAsset("{@java.util.List list}" + + "{#for foo in list}" + + "{#let name=foo.name}" + + "{#for char in name.toCharArray}" + + "{char}:" + + "{/for}" + + "{/let}" + + "{/for}"), "templates/nested.html")); @Inject Template foo; + @Inject + Template nested; + @Test public void testValidation() { List foos = Collections.singletonList(new Foo("bravo", 10l)); @@ -55,6 +66,13 @@ public void testValidation() { foo.data("list", foos, "fooList", myFoos, "items", items).render()); } + @Test + public void testNestedHintsValidation() { + List foos = Collections.singletonList(new Foo("boom", 10l)); + assertEquals("b:o:o:m:", + nested.data("list", foos).render()); + } + static class Item { } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java index 25440044ba4c9..ed8e2c100d13f 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java @@ -41,7 +41,7 @@ public CompletionStage resolve(SectionResolutionContext context) { // Execute the template with the params as the root context object TemplateImpl tagTemplate = (TemplateImpl) templateSupplier.get(); tagTemplate.root - .resolve(context.resolutionContext().createChild(evaluatedParams, extendingBlocks)) + .resolve(context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), extendingBlocks)) .whenComplete((resultNode, t2) -> { if (t2 != null) { result.completeExceptionally(t2); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java index 14d702c0b081a..999562da72add 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java @@ -2,8 +2,8 @@ import static io.quarkus.qute.Parameter.EMPTY; +import java.lang.reflect.Array; import java.util.ArrayList; -import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -81,7 +81,13 @@ private Iterator extractIterator(Object it) { } else if (it instanceof Integer) { return IntStream.rangeClosed(1, (Integer) it).iterator(); } else if (it.getClass().isArray()) { - return Arrays.stream((Object[]) it).iterator(); + int length = Array.getLength(it); + List elements = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + // The val is automatically wrapped for primitive types + elements.add(Array.get(it, i)); + } + return elements.iterator(); } else { throw new TemplateException(String.format( "Loop section error in template %s on line %s: [%s] resolved to [%s] which is not iterable", @@ -130,8 +136,7 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) { if (iterable == null) { iterable = ValueResolvers.THIS; } - // foo.items - // > |org.acme.Foo|.items + // foo.items becomes |org.acme.Foo|.items previousScope.setLastPartHint(HINT_ELEMENT); Expression iterableExpr = block.addExpression(ITERABLE, iterable); previousScope.setLastPartHint(null); @@ -139,8 +144,7 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) { String alias = block.getParameters().get(ALIAS); if (iterableExpr.hasTypeInfo()) { - // it.name - // > it.name + // it.name becomes it.name alias = alias.equals(Parameter.EMPTY) ? DEFAULT_ALIAS : alias; Scope newScope = new Scope(previousScope); newScope.putBinding(alias, alias + HINT_PREFIX + iterableExpr.getGeneratedId() + ">"); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java index 8d34b892f6a4a..1d5a42fd28330 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java @@ -1,5 +1,6 @@ package io.quarkus.qute; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -19,4 +20,33 @@ default CompletionStage getAsync(String key) { return CompletableFuture.completedFuture(get(key)); } + /** + * + * @param key + * @return {@code true} if the mapper should be applied to the specified key + */ + default boolean appliesTo(String key) { + return true; + } + + /** + * + * @param map + * @return a mapper that wraps the given map + */ + static Mapper wrap(Map map) { + return new Mapper() { + + @Override + public boolean appliesTo(String key) { + return map.containsKey(key); + } + + @Override + public Object get(String key) { + return map.get(key); + } + }; + } + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java index e839304b629f4..a35663a29d979 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java @@ -31,7 +31,7 @@ public CompletionStage resolve(SectionResolutionContext context) { result.completeExceptionally(t); } else { // Execute the main block with the params as the current context object - context.execute(context.resolutionContext().createChild(r, null)).whenComplete((r2, t2) -> { + context.execute(context.resolutionContext().createChild(Mapper.wrap(r), null)).whenComplete((r2, t2) -> { if (t2 != null) { result.completeExceptionally(t2); } else { @@ -71,6 +71,7 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) { for (Entry entry : block.getParameters().entrySet()) { Expression expr = block.addExpression(entry.getKey(), entry.getValue()); if (expr.hasTypeInfo()) { + // item.name becomes item.name newScope.putBinding(entry.getKey(), entry.getKey() + HINT_PREFIX + expr.getGeneratedId() + ">"); } else { newScope.putBinding(entry.getKey(), null); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java index 6072cabe437ea..12bb5e4befdb8 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java @@ -36,12 +36,12 @@ public CompletionStage resolve(SectionResolutionContext context) { // Execute the nested content first and make it accessible via the "nested-content" key evaluatedParams.put(NESTED_CONTENT, context.execute( - context.resolutionContext().createChild(new HashMap<>(evaluatedParams), null))); + context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null))); } try { // Execute the template with the params as the root context object TemplateImpl tagTemplate = (TemplateImpl) templateSupplier.get(); - tagTemplate.root.resolve(context.resolutionContext().createChild(evaluatedParams, null)) + tagTemplate.root.resolve(context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null)) .whenComplete((resultNode, t2) -> { if (t2 != null) { result.completeExceptionally(t2); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolvers.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolvers.java index 789ed41a0cc62..dc8c643c83164 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolvers.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolvers.java @@ -189,7 +189,10 @@ public static ValueResolver mapperResolver() { return new ValueResolver() { public boolean appliesTo(EvalContext context) { - return context.getBase() instanceof Mapper; + if (context.getBase() instanceof Mapper && context.getParams().isEmpty()) { + return ((Mapper) context.getBase()).appliesTo(context.getName()); + } + return false; } @Override diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java index 1ae65ce5bbf5d..7b359a20ae8c0 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java @@ -11,6 +11,7 @@ import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -303,6 +304,65 @@ public void testStringLiteralWithTagEndDelimiter() { 1); } + @Test + public void testNestedHintValidation() { + Engine engine = Engine.builder().addDefaults().addValueResolver(new ReflectionValueResolver()).build(); + Template loopLetLet = engine.parse("{@org.acme.Foo foo}" + + "{#for item in foo.items}" + + "{#let names=item.names}" + + "{#let size=names.size}" + + "{size}" + + "{/let}" + + "{/let}" + + "{/for}"); + List expressions = loopLetLet.getExpressions(); + assertExpr(expressions, "foo.items", 2, "|org.acme.Foo|.items"); + Expression itemNames = find(expressions, "item.names"); + assertExpr(expressions, "names.size", 2, "names.size"); + Expression namesSize = find(expressions, "names.size"); + assertExpr(expressions, "size", 1, "size"); + assertEquals("2", loopLetLet.data("foo", new Foo()).render()); + + Template loopLetLoopLet = engine.parse("{@org.acme.Foo foo}" + + "{#for item in foo.items}" + + "{#let names=item.names}" + + "{#for name in names}" + + "{#let upperCase=name.toUpperCase}" + + ":{upperCase.length}" + + "{/let}" + + "{/for}" + + "{/let}" + + "{/for}"); + expressions = loopLetLoopLet.getExpressions(); + assertExpr(expressions, "foo.items", 2, "|org.acme.Foo|.items"); + Expression fooItems = find(expressions, "foo.items"); + assertExpr(expressions, "item.names", 2, "item.names"); + itemNames = find(expressions, "item.names"); + // Note the 2 hints... + assertExpr(expressions, "names", 1, "names"); + Expression names = find(expressions, "names"); + assertExpr(expressions, "name.toUpperCase", 2, "name.toUpperCase"); + Expression nameToUpperCase = find(expressions, "name.toUpperCase"); + assertExpr(expressions, "upperCase.length", 2, "upperCase.length"); + assertEquals(":3:5", loopLetLoopLet.data("foo", new Foo()).render()); + } + + public static class Foo { + + public List getItems() { + return Collections.singletonList(new Item()); + } + + } + + public static class Item { + + public List getNames() { + return Arrays.asList("foo", "bzink"); + } + + } + private void assertParserError(String template, String message, int line) { Engine engine = Engine.builder().addDefaultSectionHelpers().build(); try { diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java index 870fd1787dc03..24f6519ee8d1d 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java @@ -10,14 +10,14 @@ public class SetSectionTest { public void testSet() { Engine engine = Engine.builder().addDefaults().build(); assertEquals("NOT_FOUND - true:mix", - engine.parse("{foo} - {#set foo=true bar='mix'}{foo}:{bar}{/}").instance().render()); + engine.parse("{foo} - {#set foo=true bar='mix'}{foo}:{bar}{/}").render()); } @Test public void testLet() { Engine engine = Engine.builder().addDefaults().build(); - assertEquals("NOT_FOUND - true:mix:what?!", - engine.parse("{foo} - {#let foo=true bar='mix'}{foo}:{bar}:{baz}{/}").data("baz", "what?!").render()); + assertEquals("NOT_FOUND:what?! - true:mix:what?!", + engine.parse("{foo}:{baz} - {#let foo=true bar='mix'}{foo}:{bar}:{baz}{/}").data("baz", "what?!").render()); } }