diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 2330d4b525c6c..bd7886bc1cbea 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1383,6 +1383,9 @@ TIP: A map value can be also accessed directly: `{map.myKey}`. Use the bracket n * `get(index)`: Returns the element at the specified position in a list ** `{list.get(0)}` +* `reversed`: Returns a reversed iterator over a list +** `{#for r in recordsList.reversed}` + TIP: A list element can be accessed directly: `{list.10}` or `{list[10]}`. ===== Numbers 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 c93625a39f8ad..dfed7671a02cf 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 @@ -59,6 +59,7 @@ import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; @@ -651,6 +652,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI } AnnotationTarget member = null; + TemplateExtensionMethodBuildItem extensionMethod = null; if (!match.isPrimitive()) { Set membersUsed = implicitClassToMembersUsed.get(match.type().name()); @@ -679,9 +681,12 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI if (member == null) { // Then try to find an etension method - member = findTemplateExtensionMethod(info, match.type(), templateExtensionMethods, expression, + extensionMethod = findTemplateExtensionMethod(info, match.type(), templateExtensionMethods, expression, index, templateIdToPathFun, results); + if (extensionMethod != null) { + member = extensionMethod.getMethod(); + } } if (member == null) { @@ -707,7 +712,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI match.clearValues(); break; } else { - Type type = resolveType(member, match, index); + Type type = resolveType(member, match, index, extensionMethod); ClassInfo clazz = null; if (type.kind() == Type.Kind.CLASS || type.kind() == Type.Kind.PARAMETERIZED_TYPE) { clazz = index.getClassByName(type.name()); @@ -1268,7 +1273,8 @@ private Object translate(JsonObject testData) { return map; } - private static Type resolveType(AnnotationTarget member, Match match, IndexView index) { + private static Type resolveType(AnnotationTarget member, Match match, IndexView index, + TemplateExtensionMethodBuildItem extensionMethod) { Type matchType; if (member.kind() == Kind.FIELD) { matchType = member.asField().type(); @@ -1283,17 +1289,64 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView Set closure = Types.getTypeClosure(match.clazz, Types.buildResolvedMap( match.getParameterizedTypeArguments(), match.getTypeParameters(), new HashMap<>(), index), index); - DotName declaringClassName = member.kind() == Kind.METHOD ? member.asMethod().declaringClass().name() - : member.asField().declaringClass().name(); + + DotName declaringClassName = null; + Type extensionMatchBase = null; + if (member.kind() == Kind.METHOD) { + MethodInfo method = member.asMethod(); + List typeParameters = method.typeParameters(); + if (extensionMethod != null && !extensionMethod.hasNamespace() && !typeParameters.isEmpty()) { + // Special handling for extension methods with type parameters + // For example "static Iterator reversed(List list)" + // 1. identify the type used to match the base object; List + // 2. resolve this type; List + // 3. if needed apply to the return type; Iterator + List params = method.parameters(); + Set attributeAnnotations = Annotations.getAnnotations(Kind.METHOD_PARAMETER, + ExtensionMethodGenerator.TEMPLATE_ATTRIBUTE, method.annotations()); + if (attributeAnnotations.isEmpty()) { + extensionMatchBase = params.get(0); + } else { + for (int i = 0; i < params.size(); i++) { + int position = i; + if (attributeAnnotations.stream() + .noneMatch(a -> a.target().asMethodParameter().position() == position)) { + // The first parameter that is not annotated with @TemplateAttribute is used to match the base object + extensionMatchBase = params.get(i); + break; + } + } + } + if (extensionMatchBase != null && Types.containsTypeVariable(extensionMatchBase)) { + declaringClassName = extensionMatchBase.name(); + } + } else { + declaringClassName = method.declaringClass().name(); + } + } else if (member.kind() == Kind.FIELD) { + declaringClassName = member.asField().declaringClass().name(); + } // Then find the declaring type with resolved type variables - Type declaringType = closure.stream() - .filter(t -> t.name().equals(declaringClassName)).findAny() - .orElse(null); + Type declaringType = null; + if (declaringClassName != null) { + for (Type type : closure) { + if (type.name().equals(declaringClassName)) { + declaringType = type; + break; + } + } + } if (declaringType != null && declaringType.kind() == org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE) { + List typeParameters; + if (extensionMatchBase != null) { + typeParameters = extensionMethod.getMethod().typeParameters(); + } else { + typeParameters = index.getClassByName(declaringType.name()).typeParameters(); + } matchType = Types.resolveTypeParam(matchType, Types.buildResolvedMap(declaringType.asParameterizedType().arguments(), - index.getClassByName(declaringType.name()).typeParameters(), + typeParameters, Collections.emptyMap(), index), index); @@ -1482,7 +1535,7 @@ void autoExtractType() { } } - private static AnnotationTarget findTemplateExtensionMethod(Info info, Type matchType, + private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info info, Type matchType, List templateExtensionMethods, Expression expression, IndexView index, Function templateIdToPathFun, Map results) { if (!info.isProperty() && !info.isVirtualMethod()) { @@ -1563,7 +1616,7 @@ private static AnnotationTarget findTemplateExtensionMethod(Info info, Type matc continue; } } - return extensionMethod.getMethod(); + return extensionMethod; } return null; } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/CollectionTemplateExtensionsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/CollectionTemplateExtensionsTest.java new file mode 100644 index 0000000000000..d11a1d4a87010 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/CollectionTemplateExtensionsTest.java @@ -0,0 +1,46 @@ +package io.quarkus.qute.deployment.extensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +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 CollectionTemplateExtensionsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + Engine engine; + + @Test + public void testListGetByIndex() { + assertEquals("true=true=NOT_FOUND", + engine.parse("{@java.util.List list}{list.0.booleanValue}={list[0]}={list[100]}") + .data("list", Collections.singletonList(true)).render()); + } + + @Test + public void testListReversed() { + List names = new ArrayList<>(); + names.add("alpha"); + names.add("bravo"); + names.add("charlie"); + assertEquals("CHARLIE::BRAVO::ALPHA::", + engine.parse("{@java.util.List list}{#each list.reversed}{it.toUpperCase}::{/each}").data("list", names) + .render()); + } + +} 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 374eb293914f9..82ee0cd38ec4c 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 @@ -5,7 +5,6 @@ import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -91,12 +90,6 @@ 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()); - } - @Test public void testMatchTypeAssignability() { assertEquals("20", diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java index 8124ab958cb08..7d141106df09a 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/CollectionTemplateExtensions.java @@ -1,6 +1,8 @@ package io.quarkus.qute.runtime.extensions; +import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import javax.enterprise.inject.Vetoed; @@ -11,17 +13,35 @@ @TemplateExtension public class CollectionTemplateExtensions { - static Object get(List list, int index) { + static T get(List list, int index) { return list.get(index); } + @SuppressWarnings("unchecked") @TemplateExtension(matchRegex = "\\d{1,10}") - static Object getByIndex(List list, String index) { + static T getByIndex(List list, String index) { int idx = Integer.parseInt(index); if (idx >= list.size()) { // Be consistent with property resolvers - return Result.NOT_FOUND; + return (T) Result.NOT_FOUND; } return list.get(idx); } + + static Iterator reversed(List list) { + ListIterator it = list.listIterator(list.size()); + return new Iterator() { + + @Override + public boolean hasNext() { + return it.hasPrevious(); + } + + @Override + public T next() { + return it.previous(); + } + }; + } + } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/MapTemplateExtensions.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/MapTemplateExtensions.java index 83afd7ab9725e..a8fd08d6eaee1 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/MapTemplateExtensions.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/MapTemplateExtensions.java @@ -33,7 +33,7 @@ static Object map(Map map, String name) { } } - static Object get(Map map, Object key) { + static V get(Map map, Object key) { return map.get(key); }