From 2b01ca7b0b3287db87ee98dd62fff2f426bff3e9 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 3 Feb 2021 13:17:50 +0100 Subject: [PATCH] Qute - improve Java array support - make it possible to get the length of the specified array and access the elements directly via an index value - support array-based template parameter declarations --- docs/src/main/asciidoc/qute-reference.adoc | 25 ++++ .../qute/deployment/QuteProcessor.java | 134 ++++++++++++------ .../TemplateExtensionMethodBuildItem.java | 16 +-- .../io/quarkus/qute/deployment/TypeInfos.java | 67 +++++++-- .../io/quarkus/qute/deployment/Types.java | 3 + .../TemplateExtensionMethodsTest.java | 26 +++- .../CheckedTemplateArrayParamTest.java | 37 +++++ .../quarkus/qute/runtime/EngineProducer.java | 2 + .../java/io/quarkus/qute/EngineBuilder.java | 3 +- .../java/io/quarkus/qute/ValueResolvers.java | 52 +++++++ .../io/quarkus/qute/ArrayResolverTest.java | 47 ++++++ 11 files changed, 348 insertions(+), 64 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateArrayParamTest.java create mode 100644 independent-projects/qute/core/src/test/java/io/quarkus/qute/ArrayResolverTest.java diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 2e6380efb31b3..f2ff8c78e4330 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -375,6 +375,30 @@ TIP: The condition in a ternary operator evaluates to `true` if the value is not NOTE: In fact, the operators are implemented as "virtual methods" that consume one parameter and can be used with infix notation, i.e. `{person.name or 'John'}` is translated to `{person.name.or('John')}`. +==== Arrays + +You can iterate over elements of an array with the <>. Moreover, it's also possible to get the length of the specified array and access the elements directly via an index value. + +.Array Examples +[source,html] +---- +

Array of length: {myArray.length}

<1> + +
    + {#for element in myArray} +
  1. {element}
  2. + {/for} +
+---- +<1> Outputs the length of the array. +<2> Outputs the first element of the array. +<3> Outputs the second element of the array using the bracket notation. +<4> Outputs the third element of the array via the virtual method `get()`. + ==== Character Escapes For HTML and XML templates the `'`, `"`, `<`, `>`, `&` characters are escaped by default if a template variant is set. @@ -482,6 +506,7 @@ A section helper that defines the logic of a section can "execute" any of the bl {/if} ---- +[[loop_section]] ==== Loop Section The loop section makes it possible to iterate over an instance of `Iterable`, `Map` 's entry set, `Stream` and an Integer. 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 eb1475a4bb5b6..fff3d51b8d1f9 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 @@ -28,6 +28,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -45,6 +46,7 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.PrimitiveType; import org.jboss.jandex.Type; import org.jboss.jandex.TypeVariable; import org.jboss.logging.Logger; @@ -572,55 +574,100 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI } while (iterator.hasNext()) { - // Now iterate over all parts of the expression and check each part against the current "match class" + // Now iterate over all parts of the expression and check each part against the current match type Info info = iterator.next(); if (!match.isEmpty()) { - // By default, we only consider properties - Set membersUsed = implicitClassToMembersUsed.get(match.clazz().name()); - if (membersUsed == null) { - membersUsed = new HashSet<>(); - implicitClassToMembersUsed.put(match.clazz().name(), membersUsed); + + // Arrays are handled specifically + // We use the built-in resolver at runtime because the extension methods cannot be used to cover all combinations of dimensions and component types + if (match.isArray()) { + if (info.isProperty()) { + String name = info.asProperty().name; + if (name.equals("length")) { + // myArray.length + match.setValues(null, PrimitiveType.INT); + continue; + } else { + // myArray[0], myArray.1 + try { + Integer.parseInt(name); + match.setValues(null, match.type().asArrayType().component()); + continue; + } catch (NumberFormatException e) { + // not an integer index + } + } + } else if (info.isVirtualMethod() && info.asVirtualMethod().name.equals("get")) { + // array.get(84) + List params = info.asVirtualMethod().part.asVirtualMethod().getParameters(); + if (params.size() == 1) { + Expression param = params.get(0); + Object literalValue; + try { + literalValue = param.getLiteralValue().get(); + } catch (InterruptedException | ExecutionException e) { + literalValue = null; + } + if (literalValue == null || literalValue instanceof Integer) { + match.setValues(null, match.type().asArrayType().component()); + continue; + } + } + } } + AnnotationTarget member = null; - // First try to find a java member - if (info.isVirtualMethod()) { - member = findMethod(info.part.asVirtualMethod(), match.clazz(), expression, index, templateIdToPathFun, - results); - if (member != null) { - membersUsed.add(member.asMethod().name()); + + if (!match.isPrimitive()) { + Set membersUsed = implicitClassToMembersUsed.get(match.type().name()); + if (membersUsed == null) { + membersUsed = new HashSet<>(); + implicitClassToMembersUsed.put(match.type().name(), membersUsed); + } + // First try to find a java member + if (match.clazz() != null) { + if (info.isVirtualMethod()) { + member = findMethod(info.part.asVirtualMethod(), match.clazz(), expression, index, + templateIdToPathFun, + results); + if (member != null) { + membersUsed.add(member.asMethod().name()); + } + } else if (info.isProperty()) { + member = findProperty(info.asProperty().name, match.clazz(), index); + if (member != null) { + membersUsed + .add(member.kind() == Kind.FIELD ? member.asField().name() : member.asMethod().name()); + } + } } - } else if (info.isProperty()) { - member = findProperty(info.asProperty().name, match.clazz(), index); - if (member != null) { - membersUsed.add(member.kind() == Kind.FIELD ? member.asField().name() : member.asMethod().name()); + if (member == null) { + // Then try to find an etension method + member = findTemplateExtensionMethod(info, match.type(), templateExtensionMethods, expression, + index, + templateIdToPathFun, results); } - } - if (member == null) { - // Then try to find an etension method - member = findTemplateExtensionMethod(info, match.clazz(), templateExtensionMethods, expression, - index, - templateIdToPathFun, results); - } - - if (member == null) { - // Test whether the validation should be skipped - TypeCheck check = new TypeCheck(info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name, - match.clazz(), - info.part.isVirtualMethod() ? info.part.asVirtualMethod().getParameters().size() : -1); - if (isExcluded(check, excludes)) { - LOGGER.debugf( - "Expression part [%s] excluded from validation of [%s] against class [%s]", - info.value, - expression.toOriginalString(), match.clazz()); - match.clearValues(); - break; + if (member == null) { + // Test whether the validation should be skipped + TypeCheck check = new TypeCheck( + info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name, + match.clazz(), + info.part.isVirtualMethod() ? info.part.asVirtualMethod().getParameters().size() : -1); + if (isExcluded(check, excludes)) { + LOGGER.debugf( + "Expression part [%s] excluded from validation of [%s] against type [%s]", + info.value, + expression.toOriginalString(), match.type()); + match.clearValues(); + break; + } } } if (member == null) { // No member found - incorrect expression incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - info.value, match.clazz().toString(), expression.getOrigin())); + info.value, match.type().toString(), expression.getOrigin())); match.clearValues(); break; } else { @@ -638,9 +685,6 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI incorrectExpressions); } } - if (match.isPrimitive()) { - break; - } } } else { LOGGER.debugf( @@ -736,8 +780,7 @@ private void produceExtensionMethod(IndexView index, BuildProducer templateExtensionMethods, Expression expression, IndexView index, Function templateIdToPathFun, Map results) { if (!info.isProperty() && !info.isVirtualMethod()) { @@ -1383,7 +1427,7 @@ private static AnnotationTarget findTemplateExtensionMethod(Info info, ClassInfo // Skip namespace extensions continue; } - if (!Types.isAssignableFrom(extensionMethod.getMatchClass().name(), matchClass.name(), index)) { + if (!Types.isAssignableFrom(extensionMethod.getMatchType(), matchType, index)) { // If "Bar extends Foo" then Bar should be matched for the extension method "int get(Foo)" continue; } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java index 76e9936ffae4d..ba215f97e930d 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java @@ -4,6 +4,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.qute.TemplateExtension; @@ -19,17 +20,16 @@ public final class TemplateExtensionMethodBuildItem extends MultiBuildItem { private final String matchName; private final String matchRegex; private final Pattern matchPattern; - private final ClassInfo matchClass; + private final Type matchType; private final int priority; private final String namespace; - public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, String matchRegex, ClassInfo matchClass, - int priority, - String namespace) { + public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, String matchRegex, Type matchType, + int priority, String namespace) { this.method = method; this.matchName = matchName; this.matchRegex = matchRegex; - this.matchClass = matchClass; + this.matchType = matchType; this.priority = priority; this.namespace = namespace; this.matchPattern = (matchRegex == null || matchRegex.isEmpty()) ? null : Pattern.compile(matchRegex); @@ -47,8 +47,8 @@ public String getMatchRegex() { return matchRegex; } - public ClassInfo getMatchClass() { - return matchClass; + public Type getMatchType() { + return matchType; } public int getPriority() { @@ -60,7 +60,7 @@ public String getNamespace() { } boolean matchesClass(ClassInfo clazz) { - return matchClass.name().equals(clazz.name()); + return matchType.name().equals(clazz.name()); } boolean matchesName(String name) { 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 1f4aba5bcd58f..c1471cfbe9252 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 @@ -5,10 +5,12 @@ import java.util.List; import java.util.function.Function; +import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.PrimitiveType; import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; @@ -19,6 +21,8 @@ final class TypeInfos { + private static final String ARRAY_DIM = "[]"; + static List create(Expression expression, IndexView index, Function templateIdToPathFun) { if (expression.isLiteral()) { Expression.Part literalPart = expression.getParts().get(0); @@ -53,16 +57,24 @@ static Info create(String typeInfo, Expression.Part part, IndexView index, Funct if (classStr.equals(Expressions.TYPECHECK_NAMESPACE_PLACEHOLDER)) { return new Info(typeInfo, part); } else { - DotName rawClassName = rawClassName(classStr); - ClassInfo rawClass = index.getClassByName(rawClassName); - if (rawClass == null) { - throw new TemplateException( - "Class [" + rawClassName + "] used in the parameter declaration in template [" - + templateIdToPathFun.apply(expressionOrigin.getTemplateGeneratedId()) + "] on line " - + expressionOrigin.getLine() - + " was not found in the application index. Make sure it is spelled correctly."); + // TODO make the parsing logic more robust + ClassInfo rawClass; + Type resolvedType; + int idx = classStr.indexOf(ARRAY_DIM); + if (idx > 0) { + // int[], java.lang.String[][], etc. + String componentTypeStr = classStr.substring(0, idx); + Type componentType = decodePrimitive(componentTypeStr); + if (componentType == null) { + componentType = resolveType(componentTypeStr); + } + String[] dimensions = classStr.substring(idx, classStr.length()).split("\\]"); + rawClass = null; + resolvedType = ArrayType.create(componentType, dimensions.length); + } else { + rawClass = getClassInfo(classStr, index, templateIdToPathFun, expressionOrigin); + resolvedType = resolveType(classStr); } - Type resolvedType = resolveType(classStr); return new TypeInfo(typeInfo, part, helperHint(typeInfo.substring(endIdx, typeInfo.length())), resolvedType, rawClass); } @@ -78,6 +90,43 @@ static Info create(String typeInfo, Expression.Part part, IndexView index, Funct } } + private static ClassInfo getClassInfo(String val, IndexView index, Function templateIdToPathFun, + Origin expressionOrigin) { + DotName rawClassName = rawClassName(val); + ClassInfo clazz = index.getClassByName(rawClassName); + if (clazz == null) { + throw new TemplateException( + "Class [" + rawClassName + "] used in the parameter declaration in template [" + + templateIdToPathFun.apply(expressionOrigin.getTemplateGeneratedId()) + "] on line " + + expressionOrigin.getLine() + + " was not found in the application index. Make sure it is spelled correctly."); + } + return clazz; + } + + private static PrimitiveType decodePrimitive(String val) { + switch (val) { + case "byte": + return PrimitiveType.BYTE; + case "char": + return PrimitiveType.CHAR; + case "double": + return PrimitiveType.DOUBLE; + case "float": + return PrimitiveType.FLOAT; + case "int": + return PrimitiveType.INT; + case "long": + return PrimitiveType.LONG; + case "short": + return PrimitiveType.SHORT; + case "boolean": + return PrimitiveType.BOOLEAN; + default: + return null; + } + } + static final String LEFT_ANGLE = "<"; static final String RIGHT_ANGLE = ">"; static final String TYPE_INFO_SEPARATOR = "" + Expressions.TYPE_INFO_SEPARATOR; diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java index 2920cc80e3f1f..e4d859e790fe9 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java @@ -113,6 +113,9 @@ static boolean containsTypeVariable(Type type) { static boolean isAssignableFrom(Type type1, Type type2, IndexView index) { // TODO consider type params in assignability rules + if (type1.kind() == Kind.ARRAY && type2.kind() == Kind.ARRAY) { + return isAssignableFrom(type1.asArrayType().component(), type2.asArrayType().component(), index); + } return Types.isAssignableFrom(box(type1).name(), box(type2).name(), index); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java index 5bbaf035f2c09..374eb293914f9 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionMethodsTest.java @@ -36,7 +36,11 @@ public class TemplateExtensionMethodsTest { .addAsResource(new StringAsset("{anyInt.foo('bing')}"), "templates/any.txt") .addAsResource(new StringAsset("{foo.pong}::{foo.name}"), - "templates/priority.txt")); + "templates/priority.txt") + .addAsResource(new StringAsset("{num.twice}"), + "templates/assignability.txt") + .addAsResource(new StringAsset("{myArray.getLast}"), + "templates/arrays.txt")); @Inject Template foo; @@ -93,6 +97,18 @@ public void testListGetByIndex() { engine.parse("{list.0}={list[0]}={list[100]}").data("list", Collections.singletonList(true)).render()); } + @Test + public void testMatchTypeAssignability() { + assertEquals("20", + engine.getTemplate("assignability").data("num", 10.1).render()); + } + + @Test + public void testArrayMatchType() { + assertEquals("last", + engine.getTemplate("arrays").data("myArray", new String[] { "first", "second", "last" }).render()); + } + @TemplateExtension public static class Extensions { @@ -130,6 +146,14 @@ static int defaultScale(BigDecimal val) { static String fooRegex(Foo foo, String name) { return name.toUpperCase(); } + + static int twice(Number number) { + return number.intValue() * 2; + } + + static Object getLast(Object[] array) { + return array[array.length - 1]; + } } @TemplateExtension(matchName = "pong") diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateArrayParamTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateArrayParamTest.java new file mode 100644 index 0000000000000..0538c5022d941 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateArrayParamTest.java @@ -0,0 +1,37 @@ +package io.quarkus.qute.deployment.typesafe; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; +import io.quarkus.test.QuarkusUnitTest; + +public class CheckedTemplateArrayParamTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Templates.class) + .addAsResource(new StringAsset("Hello {myArray[1]}!"), + "templates/CheckedTemplateArrayParamTest/arrays.txt")); + + @Test + public void testBasePath() { + assertEquals("Hello 1!", + Templates.arrays(new int[] { 0, 1 }).render()); + } + + @CheckedTemplate + public static class Templates { + + static native TemplateInstance arrays(int[] myArray); + + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index 06dc8d016b58c..a9df70d9c1b5b 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -77,6 +77,8 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig builder.addValueResolver(ValueResolvers.logicalAndResolver()); builder.addValueResolver(ValueResolvers.logicalOrResolver()); builder.addValueResolver(ValueResolvers.orEmpty()); + // Note that arrays are handled specifically during validation + builder.addValueResolver(ValueResolvers.arrayResolver()); // If needed use a specific result mapper for the selected strategy switch (runtimeConfig.propertyNotFoundStrategy) { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java index 89e315f1de803..3d7919831b69c 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java @@ -82,7 +82,8 @@ public EngineBuilder addDefaultValueResolvers() { return addValueResolvers(ValueResolvers.mapResolver(), ValueResolvers.mapperResolver(), ValueResolvers.mapEntryResolver(), ValueResolvers.collectionResolver(), ValueResolvers.listResolver(), ValueResolvers.thisResolver(), ValueResolvers.orResolver(), ValueResolvers.trueResolver(), - ValueResolvers.logicalAndResolver(), ValueResolvers.logicalOrResolver(), ValueResolvers.orEmpty()); + ValueResolvers.logicalAndResolver(), ValueResolvers.logicalOrResolver(), ValueResolvers.orEmpty(), + ValueResolvers.arrayResolver()); } public EngineBuilder addDefaults() { 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 c83e61b8bc5ae..f75046c764690 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 @@ -3,6 +3,7 @@ import static io.quarkus.qute.Booleans.isFalsy; import io.quarkus.qute.Results.Result; +import java.lang.reflect.Array; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -10,6 +11,7 @@ import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; import java.util.function.Function; /** @@ -261,6 +263,56 @@ public Object apply(Object booleanParam) { }; } + public static ValueResolver arrayResolver() { + return new ValueResolver() { + + public boolean appliesTo(EvalContext context) { + return context.getBase() != null && context.getBase().getClass().isArray(); + } + + @Override + public CompletionStage resolve(EvalContext context) { + String name = context.getName(); + if (name.equals("length")) { + return CompletableFuture.completedFuture(Array.getLength(context.getBase())); + } else if (name.equals("get")) { + if (context.getParams().isEmpty()) { + throw new IllegalArgumentException("Index parameter is missing"); + } + Expression indexExpr = context.getParams().get(0); + if (indexExpr.isLiteral()) { + Object literalValue; + try { + literalValue = indexExpr.getLiteralValue().get(); + if (literalValue instanceof Integer) { + return CompletableFuture.completedFuture(Array.get(context.getBase(), (Integer) literalValue)); + } + return Results.NOT_FOUND; + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } else { + return context.evaluate(indexExpr).thenCompose(idx -> { + if (idx instanceof Integer) { + return CompletableFuture.completedFuture(Array.get(context.getBase(), (Integer) idx)); + } + return Results.NOT_FOUND; + }); + } + } else { + // Try to use the name as an index + int index; + try { + index = Integer.parseInt(name); + } catch (NumberFormatException e) { + return Results.NOT_FOUND; + } + return CompletableFuture.completedFuture(Array.get(context.getBase(), index)); + } + } + }; + } + // helper methods private static CompletionStage collectionResolveAsync(EvalContext context) { diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ArrayResolverTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ArrayResolverTest.java new file mode 100644 index 0000000000000..60951bcf53327 --- /dev/null +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ArrayResolverTest.java @@ -0,0 +1,47 @@ +package io.quarkus.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +public class ArrayResolverTest { + + @Test + public void testArrays() { + Engine engine = Engine.builder().addDefaults().build(); + + int[] int1 = { 1, 2, 3 }; + + assertEquals("3", engine.parse("{array.length}").data("array", int1).render()); + assertEquals("2", engine.parse("{array.1}").data("array", int1).render()); + assertEquals("3", engine.parse("{array.get(2)}").data("array", int1).render()); + assertEquals("NOT_FOUND", engine.parse("{array.get('foo')}").data("array", int1).render()); + assertEquals("3", engine.parse("{array.get(last)}").data("array", int1, "last", 2).render()); + assertEquals("1", engine.parse("{array[0]}").data("array", int1).render()); + + long[][] long2 = { { 1l, 2l }, { 3l, 4l }, {} }; + + assertEquals("3", engine.parse("{array.length}").data("array", long2).render()); + assertEquals("2", engine.parse("{array.1.length}").data("array", long2).render()); + assertEquals("2", engine.parse("{array.get(0).get(1)}").data("array", long2).render()); + assertEquals("1", engine.parse("{array[0][0]}").data("array", long2).render()); + assertEquals("0", engine.parse("{array[2].length}").data("array", long2).render()); + + try { + engine.parse("{array.get(10)}").data("array", long2).render(); + fail(); + } catch (Exception expected) { + assertTrue(expected instanceof ArrayIndexOutOfBoundsException, expected.toString()); + } + + try { + engine.parse("{array.get()}").data("array", long2).render(); + fail(); + } catch (Exception expected) { + assertTrue(expected instanceof IllegalArgumentException, expected.toString()); + } + } + +}