diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 10fee619dbe1e..9f4b2ad377920 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -372,6 +372,14 @@ public String apply(String id) { .filter(TemplateDataBuildItem::hasNamespace) .collect(Collectors.toMap(TemplateDataBuildItem::getNamespace, Function.identity())); + Map> namespaceExtensionMethods = templateExtensionMethods.stream() + .filter(TemplateExtensionMethodBuildItem::hasNamespace) + .sorted(Comparator.comparingInt(TemplateExtensionMethodBuildItem::getPriority).reversed()) + .collect(Collectors.groupingBy(TemplateExtensionMethodBuildItem::getNamespace)); + + List regularExtensionMethods = templateExtensionMethods.stream() + .filter(Predicate.not(TemplateExtensionMethodBuildItem::hasNamespace)).collect(Collectors.toUnmodifiableList()); + LookupConfig lookupConfig = new QuteProcessor.FixedLookupConfig(index, QuteProcessor.initDefaultMembersFilter(), false); // bundle name -> (key -> method) @@ -474,9 +482,10 @@ public String apply(String id) { if (param.hasTypeInfo()) { Map results = new HashMap<>(); QuteProcessor.validateNestedExpressions(exprEntry.getKey(), defaultBundleInterface, - results, templateExtensionMethods, excludes, incorrectExpressions, expression, - index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, - checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData); + results, excludes, incorrectExpressions, expression, index, + implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, + checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, + regularExtensionMethods, namespaceExtensionMethods); Match match = results.get(param.toOriginalString()); if (match != null && !match.isEmpty() && !Types.isAssignableFrom(match.type(), methodParams.get(idx), index)) { 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 3ace4467e18a1..b6f8d7884a839 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 @@ -16,6 +16,7 @@ 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; @@ -499,6 +500,14 @@ public String apply(String id) { .filter(TemplateDataBuildItem::hasNamespace) .collect(Collectors.toMap(TemplateDataBuildItem::getNamespace, Function.identity())); + Map> namespaceExtensionMethods = templateExtensionMethods.stream() + .filter(TemplateExtensionMethodBuildItem::hasNamespace) + .sorted(Comparator.comparingInt(TemplateExtensionMethodBuildItem::getPriority).reversed()) + .collect(Collectors.groupingBy(TemplateExtensionMethodBuildItem::getNamespace)); + + List regularExtensionMethods = templateExtensionMethods.stream() + .filter(Predicate.not(TemplateExtensionMethodBuildItem::hasNamespace)).collect(Collectors.toUnmodifiableList()); + LookupConfig lookupConfig = new FixedLookupConfig(index, initDefaultMembersFilter(), false); for (TemplateAnalysis templateAnalysis : templatesAnalysis.getAnalysis()) { @@ -512,10 +521,10 @@ public String apply(String id) { if (expression.isLiteral()) { continue; } - Match match = validateNestedExpressions(templateAnalysis, null, new HashMap<>(), templateExtensionMethods, - excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed, - templateIdToPathFun, generatedIdsToMatches, checkedTemplate, - lookupConfig, namedBeans, namespaceTemplateData); + Match match = validateNestedExpressions(templateAnalysis, null, new HashMap<>(), excludes, incorrectExpressions, + expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, + checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, + namespaceExtensionMethods); generatedIdsToMatches.put(expression.getGeneratedId(), match); } expressionMatches @@ -573,21 +582,24 @@ static String buildIgnorePattern(Iterable names) { } static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassInfo rootClazz, Map results, - List templateExtensionMethods, List excludes, - BuildProducer incorrectExpressions, Expression expression, IndexView index, + List excludes, BuildProducer incorrectExpressions, + Expression expression, IndexView index, Map> implicitClassToMembersUsed, Function templateIdToPathFun, Map generatedIdsToMatches, CheckedTemplateBuildItem checkedTemplate, LookupConfig lookupConfig, Map namedBeans, - Map namespaceTemplateData) { + Map namespaceTemplateData, + List regularExtensionMethods, + Map> namespaceExtensionMethods) { // Validate the parameters of nested virtual methods for (Expression.Part part : expression.getParts()) { if (part.isVirtualMethod()) { for (Expression param : part.asVirtualMethod().getParameters()) { if (!results.containsKey(param.toOriginalString())) { - validateNestedExpressions(templateAnalysis, null, results, templateExtensionMethods, excludes, + validateNestedExpressions(templateAnalysis, null, results, excludes, incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun, - generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData); + generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, + regularExtensionMethods, namespaceExtensionMethods); } } } @@ -597,6 +609,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI String namespace = expression.getNamespace(); TemplateDataBuildItem templateData = null; + List extensionMethods = null; if (namespace != null) { if (namespace.equals(INJECT_NAMESPACE)) { @@ -618,8 +631,11 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI filter = filter.and(templateData::filter); lookupConfig = new FirstPassLookupConfig(lookupConfig, filter, true); } else { - // All other namespaces are ignored - return putResult(match, results, expression); + extensionMethods = namespaceExtensionMethods.get(namespace); + if (extensionMethods == null) { + // All other namespaces are ignored + return putResult(match, results, expression); + } } } } @@ -645,7 +661,29 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI Iterator iterator = parts.iterator(); Info root = iterator.next(); - if (rootClazz == null) { + if (extensionMethods != null) { + // Namespace is used and at least one namespace extension method exists for the given namespace + TemplateExtensionMethodBuildItem extensionMethod = findTemplateExtensionMethod(root, null, extensionMethods, + expression, index, templateIdToPathFun, results); + if (extensionMethod != null) { + MethodInfo method = extensionMethod.getMethod(); + ClassInfo returnType = index.getClassByName(method.returnType().name()); + if (returnType != null) { + match.setValues(returnType, method.returnType()); + } else { + // Return type not available + return putResult(match, results, expression); + } + } else { + // No namespace extension method found - incorrect expression + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + String.format("no matching namespace [%s] extension method found", namespace), expression.getOrigin())); + match.clearValues(); + return putResult(match, results, expression); + } + + } else if (rootClazz == null) { + // No namespace is used or no declarative resolver (extension methods, @TemplateData, etc.) if (root.isTypeInfo()) { // E.g. |org.acme.Item| match.setValues(root.asTypeInfo().rawClass, root.asTypeInfo().resolvedType); @@ -750,7 +788,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI if (member == null) { // Then try to find an etension method - extensionMethod = findTemplateExtensionMethod(info, match.type(), templateExtensionMethods, expression, + extensionMethod = findTemplateExtensionMethod(info, match.type(), regularExtensionMethods, expression, index, templateIdToPathFun, results); if (extensionMethod != null) { @@ -1086,15 +1124,10 @@ public Function apply(ClassInfo clazz) { .entrySet()) { Map> namespaceToMethods = classEntry.getValue(); for (Entry> nsEntry : namespaceToMethods.entrySet()) { - Map> priorityToMethods = new HashMap<>(); - for (TemplateExtensionMethodBuildItem method : nsEntry.getValue()) { - List methods = priorityToMethods.get(method.getPriority()); - if (methods == null) { - methods = new ArrayList<>(); - priorityToMethods.put(method.getPriority(), methods); - } - methods.add(method); - } + + Map> priorityToMethods = nsEntry.getValue().stream() + .collect(Collectors.groupingBy(TemplateExtensionMethodBuildItem::getPriority)); + for (Entry> priorityEntry : priorityToMethods.entrySet()) { try (NamespaceResolverCreator namespaceResolverCreator = extensionMethodGenerator .createNamespaceResolver(priorityEntry.getValue().get(0).getMethod().declaringClass(), @@ -1597,11 +1630,7 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info } String name = info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name; for (TemplateExtensionMethodBuildItem extensionMethod : templateExtensionMethods) { - if (extensionMethod.hasNamespace()) { - // Skip namespace extensions - continue; - } - if (!Types.isAssignableFrom(extensionMethod.getMatchType(), matchType, index)) { + if (matchType != null && !Types.isAssignableFrom(extensionMethod.getMatchType(), matchType, index)) { // If "Bar extends Foo" then Bar should be matched for the extension method "int get(Foo)" continue; } @@ -1610,7 +1639,13 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info continue; } List parameters = extensionMethod.getMethod().parameters(); - int realParamSize = parameters.size() - (TemplateExtension.ANY.equals(extensionMethod.getMatchName()) ? 2 : 1); + int realParamSize = parameters.size(); + if (!extensionMethod.hasNamespace()) { + realParamSize -= 1; + } + if (TemplateExtension.ANY.equals(extensionMethod.getMatchName())) { + realParamSize -= 1; + } if (realParamSize > 0 && !info.isVirtualMethod()) { // If method accepts additional params the info must be a virtual method continue; @@ -1636,7 +1671,7 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info // Check parameter types if available boolean matches = true; // Skip base and name param if needed - int idx = TemplateExtension.ANY.equals(extensionMethod.getMatchName()) ? 2 : 1; + int idx = parameters.size() - realParamSize; for (Expression param : virtualMethod.getParameters()) { 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 8ccccccff0dc1..f9c1f0ee45a65 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 @@ -35,7 +35,7 @@ static List create(Expression expression, IndexView index, Function infos = new ArrayList<>(); boolean splitParts = true; for (Expression.Part part : expression.getParts()) { - if (splitParts) { + if (splitParts && part.getTypeInfo().contains(TYPE_INFO_SEPARATOR)) { List infoParts = Expressions.splitTypeInfoParts(part.getTypeInfo()); for (String infoPart : infoParts) { infos.add(create(infoPart, part, index, templateIdToPathFun, expression.getOrigin())); 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 index 6ec6fce4efe8d..bd8e811324853 100644 --- 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 @@ -1,14 +1,24 @@ package io.quarkus.qute.deployment; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; import org.junit.jupiter.api.Test; +import io.quarkus.qute.Engine; +import io.quarkus.qute.Expression; +import io.quarkus.qute.deployment.TypeInfos.Info; + public class TypeInfosTest { @Test @@ -19,6 +29,27 @@ public void testHintPattern() { assertHints("loop-element>", ""); } + @Test + public void testCreate() throws IOException { + List expressions = Engine.builder().build() + .parse("{@io.quarkus.qute.deployment.TypeInfosTest$Foo foo}{config:['foo.bar.baz']}{foo.name}") + .getExpressions(); + ; + IndexView index = index(Foo.class); + + List infos = TypeInfos.create(expressions.get(0), index, id -> "dummy"); + assertEquals(1, infos.size()); + assertTrue(infos.get(0).isProperty()); + assertEquals("foo.bar.baz", infos.get(0).value); + + infos = TypeInfos.create(expressions.get(1), index, id -> "dummy"); + assertEquals(2, infos.size()); + assertTrue(infos.get(0).isTypeInfo()); + assertEquals("io.quarkus.qute.deployment.TypeInfosTest$Foo", infos.get(0).asTypeInfo().rawClass.name().toString()); + assertTrue(infos.get(1).isProperty()); + assertEquals("name", infos.get(1).value); + } + private void assertHints(String hintStr, String... expectedHints) { Matcher m = TypeInfos.HintInfo.HINT_PATTERN.matcher(hintStr); List hints = new ArrayList<>(); @@ -28,4 +59,21 @@ private void assertHints(String hintStr, String... expectedHints) { assertEquals(Arrays.asList(expectedHints), hints); } + private static Index index(Class... classes) throws IOException { + Indexer indexer = new Indexer(); + for (Class clazz : classes) { + try (InputStream stream = TypeInfosTest.class.getClassLoader() + .getResourceAsStream(clazz.getName().replace('.', '/') + ".class")) { + indexer.index(stream); + } + } + return indexer.complete(); + } + + public static class Foo { + + public String name; + + } + } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/NamespaceTemplateExtensionValidationFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/NamespaceTemplateExtensionValidationFailureTest.java new file mode 100644 index 0000000000000..09da240df6850 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/NamespaceTemplateExtensionValidationFailureTest.java @@ -0,0 +1,58 @@ +package io.quarkus.qute.deployment.extensions; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +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.TemplateException; +import io.quarkus.qute.TemplateExtension; +import io.quarkus.test.QuarkusUnitTest; + +public class NamespaceTemplateExtensionValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "{bro:surname}\n" + + "{bro:name.bubu}"), + "templates/foo.html") + .addClasses(SomeExtensions.class)) + .assertException(t -> { + Throwable e = t; + TemplateException te = null; + while (e != null) { + if (e instanceof TemplateException) { + te = (TemplateException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + assertTrue(te.getMessage().contains("Found template problems (2)"), te.getMessage()); + assertTrue(te.getMessage().contains("no matching namespace [bro] extension method found"), te.getMessage()); + assertTrue(te.getMessage().contains("property/method [bubu] not found on class [java.lang.String]"), + te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + @TemplateExtension(namespace = "bro") + public static class SomeExtensions { + + static String name() { + return "bubu"; + } + + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java index fe24816a3a781..cc0ac867e2f68 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java @@ -54,9 +54,10 @@ public CompletionStage evaluate(Expression expression, ResolutionContext parts = expression.getParts().iterator(); List matching = namespaceResolvers.get(expression.getNamespace()); if (matching == null) { - String msg = "No namespace resolver found for: " + expression.getNamespace(); - LOGGER.errorf(msg); - return CompletedStage.failure(new TemplateException(msg)); + return CompletedStage.failure(new TemplateException(expression.getOrigin(), + String.format("No namespace resolver found for [%s] in expression {%s} in template %s on line %s", + expression.getNamespace(), expression.toOriginalString(), + expression.getOrigin().getTemplateId(), expression.getOrigin().getLine()))); } EvalContext context = new EvalContextImpl(false, null, parts.next(), resolutionContext); if (matching.size() == 1) { @@ -203,7 +204,7 @@ private static TemplateException propertyNotFound(Object result, Expression expr } else { propertyMessage = "Property not found"; } - return new TemplateException( + return new TemplateException(expression.getOrigin(), String.format("%s in expression {%s} in template %s on line %s", propertyMessage, expression.toOriginalString(), expression.getOrigin().getTemplateId(), expression.getOrigin().getLine())); } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/NamespaceResolversTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/NamespaceResolversTest.java index 0992caf38c4d5..7b67134239353 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/NamespaceResolversTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/NamespaceResolversTest.java @@ -76,4 +76,16 @@ public String getNamespace() { } } + @Test + public void testNoNamespaceFound() { + try { + Engine.builder().addDefaults().build().parse("{charlie:name}", null, "alpha.html").render(); + fail(); + } catch (TemplateException expected) { + assertEquals( + "No namespace resolver found for [charlie] in expression {charlie:name} in template alpha.html on line 1", + expected.getMessage()); + } + } + }