From 5709bd67a6f46a2a6d534e766a022bc7116bedf1 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 11 Oct 2021 10:21:12 +0200 Subject: [PATCH] Qute type-safe validation improvements - validate expressions that start with a namespace coming from a TemplateData annotation - validate nested "inject:" namespace expressions, ie. used as method params - resolves #20621 - also introduce io.quarkus.qute.TemplateData.SIMPLENAME and fix TemplateData javadoc --- .../deployment/MessageBundleProcessor.java | 24 +- .../qute/deployment/QuteProcessor.java | 448 ++++++++++++------ .../deployment/TemplateDataBuildItem.java | 93 ++++ .../io/quarkus/qute/deployment/TypeInfos.java | 2 +- .../io/quarkus/qute/deployment/AsyncTest.java | 6 +- .../io/quarkus/qute/deployment/MultiTest.java | 4 +- .../qute/deployment/TemplateDataTest.java | 3 +- .../TemplateDataValidationTest.java | 53 +++ .../typesafe/ValidationFailuresTest.java | 7 +- .../java/io/quarkus/qute/TemplateData.java | 8 +- .../generator/ValueResolverGenerator.java | 14 +- 11 files changed, 513 insertions(+), 149 deletions(-) create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataValidationTest.java 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 ed28a9e4a097a..10fee619dbe1e 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 @@ -1,6 +1,7 @@ package io.quarkus.qute.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static java.util.stream.Collectors.toMap; import java.io.File; import java.io.IOException; @@ -43,10 +44,12 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; +import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; @@ -78,6 +81,7 @@ import io.quarkus.qute.Expression.Part; import io.quarkus.qute.Namespaces; import io.quarkus.qute.Resolver; +import io.quarkus.qute.deployment.QuteProcessor.LookupConfig; import io.quarkus.qute.deployment.QuteProcessor.Match; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; import io.quarkus.qute.generator.Descriptors; @@ -346,6 +350,8 @@ void validateMessageBundleMethodsInTemplates(TemplatesAnalysisBuildItem analysis BuildProducer incorrectExpressions, BuildProducer implicitClasses, List checkedTemplates, + BeanDiscoveryFinishedBuildItem beanDiscovery, + List templateData, QuteConfig config) { IndexView index = beanArchiveIndex.getIndex(); @@ -356,6 +362,18 @@ public String apply(String id) { } }; + // IMPLEMENTATION NOTE: + // We do not support injection of synthetic beans with names + // Dependency on the ValidationPhaseBuildItem would result in a cycle in the build chain + Map namedBeans = beanDiscovery.beanStream().withName() + .collect(toMap(BeanInfo::getName, Function.identity())); + + Map namespaceTemplateData = templateData.stream() + .filter(TemplateDataBuildItem::hasNamespace) + .collect(Collectors.toMap(TemplateDataBuildItem::getNamespace, Function.identity())); + + LookupConfig lookupConfig = new QuteProcessor.FixedLookupConfig(index, QuteProcessor.initDefaultMembersFilter(), false); + // bundle name -> (key -> method) Map> bundleMethodsMap = new HashMap<>(); for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { @@ -456,9 +474,9 @@ 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); + results, templateExtensionMethods, excludes, incorrectExpressions, expression, + index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, + checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData); 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 8810d5ded0148..3ace4467e18a1 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 @@ -1,6 +1,8 @@ package io.quarkus.qute.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static io.quarkus.qute.runtime.EngineProducer.INJECT_NAMESPACE; +import static java.util.function.Predicate.not; import static java.util.stream.Collectors.toMap; import java.io.File; @@ -82,7 +84,6 @@ import io.quarkus.qute.EngineBuilder; import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.VirtualMethodPart; -import io.quarkus.qute.Expressions; import io.quarkus.qute.LoopSectionHelper; import io.quarkus.qute.ParserHelper; import io.quarkus.qute.ParserHook; @@ -91,6 +92,7 @@ import io.quarkus.qute.SectionHelperFactory; import io.quarkus.qute.SetSectionHelper; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateData; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateExtension; import io.quarkus.qute.TemplateInstance; @@ -363,6 +365,7 @@ public CompletionStage resolve(SectionResolutionContext context) { }); builder.addLocator(new TemplateLocator() { + @Override public Optional locate(String id) { TemplatePathBuildItem found = templatePaths.stream().filter(p -> p.getPath().equals(id)).findAny().orElse(null); @@ -472,6 +475,7 @@ void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BuildProducer expressionMatches, BeanDiscoveryFinishedBuildItem beanDiscovery, List checkedTemplates, + List templateData, QuteConfig config) { IndexView index = beanArchiveIndex.getIndex(); @@ -491,51 +495,34 @@ public String apply(String id) { // Map implicit class -> set of used members Map> implicitClassToMembersUsed = new HashMap<>(); - for (TemplateAnalysis templateAnalysis : templatesAnalysis.getAnalysis()) { + Map namespaceTemplateData = templateData.stream() + .filter(TemplateDataBuildItem::hasNamespace) + .collect(Collectors.toMap(TemplateDataBuildItem::getNamespace, Function.identity())); - // Try to find the checked template - String path = templateAnalysis.path; - for (String suffix : config.suffixes) { - if (path.endsWith(suffix)) { - path = path.substring(0, path.length() - (suffix.length() + 1)); - break; - } - } - CheckedTemplateBuildItem checkedTemplate = null; - for (CheckedTemplateBuildItem item : checkedTemplates) { - if (item.templateId.equals(path)) { - checkedTemplate = item; - break; - } - } + LookupConfig lookupConfig = new FixedLookupConfig(index, initDefaultMembersFilter(), false); + for (TemplateAnalysis templateAnalysis : templatesAnalysis.getAnalysis()) { + // The relevant checked template, may be null + CheckedTemplateBuildItem checkedTemplate = findCheckedTemplate(config, templateAnalysis, checkedTemplates); // Maps an expression generated id to the last match of an expression (i.e. the type of the last part) Map generatedIdsToMatches = new HashMap<>(); + // Iterate over all top-level expressions found in the template for (Expression expression : templateAnalysis.expressions) { if (expression.isLiteral()) { continue; } - if (expression.hasNamespace()) { - if (expression.getNamespace().equals(EngineProducer.INJECT_NAMESPACE)) { - validateInjectExpression(templateAnalysis, expression, index, incorrectExpressions, - templateExtensionMethods, excludes, namedBeans, generatedIdsToMatches, - implicitClassToMembersUsed, templateIdToPathFun, checkedTemplate); - } else { - continue; - } - } else { - generatedIdsToMatches.put(expression.getGeneratedId(), - validateNestedExpressions(templateAnalysis, null, new HashMap<>(), templateExtensionMethods, - excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed, - templateIdToPathFun, generatedIdsToMatches, checkedTemplate)); - } + Match match = validateNestedExpressions(templateAnalysis, null, new HashMap<>(), templateExtensionMethods, + excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed, + templateIdToPathFun, generatedIdsToMatches, checkedTemplate, + lookupConfig, namedBeans, namespaceTemplateData); + generatedIdsToMatches.put(expression.getGeneratedId(), match); } - expressionMatches .produce(new TemplateExpressionMatchesBuildItem(templateAnalysis.generatedId, generatedIdsToMatches)); } + // Register an implicit value resolver for the classes collected during validation for (Entry> entry : implicitClassToMembersUsed.entrySet()) { ClassInfo clazz = index.getClassByName(entry.getKey()); if (clazz != null) { @@ -545,6 +532,32 @@ public String apply(String id) { } } + static Predicate initDefaultMembersFilter() { + // By default, synthetic, non-public and static members (excl. enum constants) are ignored + Predicate filter = QuteProcessor::defaultFilter; + Predicate enumConstantFilter = QuteProcessor::enumConstantFilter; + filter = filter.and(enumConstantFilter.or(not(QuteProcessor::staticsFilter))); + return filter; + } + + private CheckedTemplateBuildItem findCheckedTemplate(QuteConfig config, TemplateAnalysis analysis, + List checkedTemplates) { + // Try to find the checked template + String path = analysis.path; + for (String suffix : config.suffixes) { + if (path.endsWith(suffix)) { + path = path.substring(0, path.length() - (suffix.length() + 1)); + break; + } + } + for (CheckedTemplateBuildItem item : checkedTemplates) { + if (item.templateId.equals(path)) { + return item; + } + } + return null; + } + static String buildIgnorePattern(Iterable names) { // ^(?!\\Qbar\\P|\\Qfoo\\P).*$ StringBuilder ignorePattern = new StringBuilder("^(?!"); @@ -560,27 +573,57 @@ static String buildIgnorePattern(Iterable names) { } static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassInfo rootClazz, Map results, - List templateExtensionMethods, - List excludes, + List templateExtensionMethods, List excludes, BuildProducer incorrectExpressions, Expression expression, IndexView index, Map> implicitClassToMembersUsed, Function templateIdToPathFun, - Map generatedIdsToMatches, CheckedTemplateBuildItem checkedTemplate) { + Map generatedIdsToMatches, CheckedTemplateBuildItem checkedTemplate, + LookupConfig lookupConfig, Map namedBeans, + Map namespaceTemplateData) { - // First validate nested virtual methods + // 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, incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun, - generatedIdsToMatches, checkedTemplate); + generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData); } } } } - // Then validate the expression itself + Match match = new Match(index); + String namespace = expression.getNamespace(); + TemplateDataBuildItem templateData = null; + + if (namespace != null) { + if (namespace.equals(INJECT_NAMESPACE)) { + BeanInfo bean = findBean(expression, index, incorrectExpressions, namedBeans); + if (bean != null) { + rootClazz = bean.getImplClazz(); + } else { + // Bean not found + return putResult(match, results, expression); + } + } else { + templateData = namespaceTemplateData.get(namespace); + if (templateData != null) { + // @TemplateData with namespace defined + rootClazz = templateData.getTargetClass(); + // Only include the static members that are not ignored + Predicate filter = QuteProcessor::defaultFilter; + filter = filter.and(QuteProcessor::staticsFilter); + filter = filter.and(templateData::filter); + lookupConfig = new FirstPassLookupConfig(lookupConfig, filter, true); + } else { + // All other namespaces are ignored + return putResult(match, results, expression); + } + } + } + if (checkedTemplate != null && checkedTemplate.requireTypeSafeExpressions && !expression.hasTypeInfo()) { incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), "Only type-safe expressions are allowed in the checked template defined via: " @@ -590,14 +633,12 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI + checkedTemplate.bindings.keySet() + ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)", expression.getOrigin())); - results.put(expression.toOriginalString(), match); - return match; + return putResult(match, results, expression); } if (rootClazz == null && !expression.hasTypeInfo()) { - // No type info available or a namespace expression - results.put(expression.toOriginalString(), match); - return match; + // No type info available + return putResult(match, results, expression); } List parts = TypeInfos.create(expression, index, templateIdToPathFun); @@ -623,20 +664,26 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI } } else { // No type info available - results.put(expression.toOriginalString(), match); - return match; + return putResult(match, results, expression); } } } else { - // The first part is skipped, e.g. for {inject:foo.name} the first part is the name of the bean - match.setValues(rootClazz, Type.create(rootClazz.name(), org.jboss.jandex.Type.Kind.CLASS)); + if (INJECT_NAMESPACE.equals(namespace)) { + // Skip the first part - the name of the bean, e.g. for {inject:foo.name} we start validation with "name" + match.setValues(rootClazz, Type.create(rootClazz.name(), org.jboss.jandex.Type.Kind.CLASS)); + } else if (templateData != null) { + // Set the root type and reset the iterator + match.setValues(rootClazz, Type.create(rootClazz.name(), org.jboss.jandex.Type.Kind.CLASS)); + iterator = parts.iterator(); + } else { + return putResult(match, results, expression); + } } while (iterator.hasNext()) { // Now iterate over all parts of the expression and check each part against the current match type Info info = iterator.next(); if (!match.isEmpty()) { - // 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()) { @@ -687,13 +734,12 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI if (match.clazz() != null) { if (info.isVirtualMethod()) { member = findMethod(info.part.asVirtualMethod(), match.clazz(), expression, index, - templateIdToPathFun, - results); + templateIdToPathFun, results, lookupConfig); if (member != null) { membersUsed.add(member.asMethod().name()); } } else if (info.isProperty()) { - member = findProperty(info.asProperty().name, match.clazz(), index); + member = findProperty(info.asProperty().name, match.clazz(), lookupConfig); if (member != null) { membersUsed .add(member.kind() == Kind.FIELD ? member.asField().name() : member.asMethod().name()); @@ -755,7 +801,12 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI match.clearValues(); break; } + lookupConfig.nextPart(); } + return putResult(match, results, expression); + } + + private static Match putResult(Match match, Map results, Expression expression) { results.put(expression.toOriginalString(), match); return match; } @@ -843,45 +894,61 @@ private void produceExtensionMethod(IndexView index, BuildProducer incorrectExpressions, - List templateExtensionMethods, List excludes, - Map namedBeans, Map generatedIdsToMatches, - Map> implicitClassToMembersUsed, Function templateIdToPathFun, - CheckedTemplateBuildItem checkedTemplate) { + Map namedBeans) { Expression.Part firstPart = expression.getParts().get(0); if (firstPart.isVirtualMethod()) { incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), "The inject: namespace must be followed by a bean name", expression.getOrigin())); - return; - } - String beanName; - if (expression.hasNamespace()) { - beanName = firstPart.getName(); - } else { - // inject:foo.labels => foo - String firstInfoPart = Expressions.splitTypeInfoParts(firstPart.getTypeInfo()).get(0); - beanName = firstInfoPart.substring(EngineProducer.INJECT_NAMESPACE.length() + 1, - firstInfoPart.length()); + return null; } - + String beanName = firstPart.getName(); BeanInfo bean = namedBeans.get(beanName); if (bean != null) { - if (expression.getParts().size() == 1) { - // Only the bean needs to be validated - return; - } - generatedIdsToMatches.put(expression.getGeneratedId(), - validateNestedExpressions(templateAnalysis, bean.getImplClazz(), new HashMap<>(), - templateExtensionMethods, excludes, incorrectExpressions, expression, index, - implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, checkedTemplate)); - + return bean; } else { // User is injecting a non-existing bean incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), beanName, null, expression.getOrigin())); + return null; + } + } + + static boolean defaultFilter(AnnotationTarget target) { + short flags; + switch (target.kind()) { + case METHOD: + flags = target.asMethod().flags(); + break; + case FIELD: + flags = target.asField().flags(); + break; + default: + throw new IllegalArgumentException(); } + // public and non-synthetic members + return Modifier.isPublic(flags) && !ValueResolverGenerator.isSynthetic(flags); + } + + static boolean staticsFilter(AnnotationTarget target) { + switch (target.kind()) { + case METHOD: + return Modifier.isStatic(target.asMethod().flags()); + case FIELD: + // Enum constants are handled specifically due to {#when} enum support + return Modifier.isStatic(target.asField().flags()); + default: + throw new IllegalArgumentException(); + } + } + + static boolean enumConstantFilter(AnnotationTarget target) { + if (target.kind() == Kind.FIELD) { + return target.asField().isEnumConstant(); + } + return false; } static String findTemplatePath(TemplatesAnalysisBuildItem analysis, String id) { @@ -1608,36 +1675,29 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info return null; } - /** - * Attempts to find a property with the specified name, ie. a public non-static non-synthetic field with the given name or a - * public non-static non-synthetic method with no params and the given name. - * - * @param name - * @param clazz - * @param index - * @return the property or null - */ - private static AnnotationTarget findProperty(String name, ClassInfo clazz, IndexView index) { - Set interfaceNames = new HashSet<>(); + private static AnnotationTarget findProperty(String name, ClassInfo clazz, LookupConfig config) { + // Attempts to find a property with the specified name + // i.e. a public non-static non-synthetic field with the given name or a public non-static non-synthetic method with no params and the given name + Set interfaceNames = config.declaredMembersOnly() ? null : new HashSet<>(); while (clazz != null) { - addInterfaces(clazz, index, interfaceNames); - interfaceNames.addAll(clazz.interfaceNames()); + if (interfaceNames != null) { + addInterfaces(clazz, config.index(), interfaceNames); + } // Fields for (FieldInfo field : clazz.fields()) { - if (!Modifier.isPublic(field.flags()) || ValueResolverGenerator.isSynthetic(field.flags())) { - // Skip non-public and synthetic fields + if (!config.filter().test(field)) { continue; } - if (field.name().equals(name) && (field.isEnumConstant() || !Modifier.isStatic(field.flags()))) { + if (field.name().equals(name)) { // Name matches and it's either an enum constant or a non-static field return field; } } // Methods for (MethodInfo method : clazz.methods()) { - if (method.returnType().kind() != org.jboss.jandex.Type.Kind.VOID && Modifier.isPublic(method.flags()) - && !Modifier.isStatic(method.flags()) - && !ValueResolverGenerator.isSynthetic(method.flags()) && (method.name().equals(name) + if (method.returnType().kind() != org.jboss.jandex.Type.Kind.VOID + && config.filter().test(method) + && (method.name().equals(name) || ValueResolverGenerator.getPropertyName(method.name()).equals(name))) { // Skip void, non-public, static and synthetic methods // Method name must match (exact or getter) @@ -1645,22 +1705,23 @@ private static AnnotationTarget findProperty(String name, ClassInfo clazz, Index } } DotName superName = clazz.superName(); - if (superName == null) { + if (config.declaredMembersOnly() || superName == null) { clazz = null; } else { - clazz = index.getClassByName(clazz.superName()); + clazz = config.index().getClassByName(clazz.superName()); } } // Try interface methods - for (DotName interfaceName : interfaceNames) { - ClassInfo interfaceClassInfo = index.getClassByName(interfaceName); - if (interfaceClassInfo != null) { - for (MethodInfo method : interfaceClassInfo.methods()) { - if (Modifier.isPublic(method.flags()) && !Modifier.isStatic(method.flags()) - && !ValueResolverGenerator.isSynthetic(method.flags()) - && (method.name().equals(name) - || ValueResolverGenerator.getPropertyName(method.name()).equals(name))) { - return method; + if (interfaceNames != null) { + for (DotName interfaceName : interfaceNames) { + ClassInfo interfaceClassInfo = config.index().getClassByName(interfaceName); + if (interfaceClassInfo != null) { + for (MethodInfo method : interfaceClassInfo.methods()) { + if (config.filter().test(method) + && (method.name().equals(name) + || ValueResolverGenerator.getPropertyName(method.name()).equals(name))) { + return method; + } } } } @@ -1682,46 +1743,38 @@ private static void addInterfaces(ClassInfo clazz, IndexView index, Set } } - /** - * Find a non-static non-synthetic method with the given name, matching number of params and assignable parameter types. - * - * @param virtualMethod - * @param clazz - * @param expression - * @param index - * @param templateIdToPathFun - * @param results - * @return the method or null - */ private static AnnotationTarget findMethod(VirtualMethodPart virtualMethod, ClassInfo clazz, Expression expression, - IndexView index, Function templateIdToPathFun, Map results) { - Set interfaceNames = new HashSet<>(); + IndexView index, Function templateIdToPathFun, Map results, + LookupConfig config) { + // Find a method with the given name, matching number of params and assignable parameter types + Set interfaceNames = config.declaredMembersOnly() ? null : new HashSet<>(); while (clazz != null) { - addInterfaces(clazz, index, interfaceNames); + if (interfaceNames != null) { + addInterfaces(clazz, index, interfaceNames); + } for (MethodInfo method : clazz.methods()) { - if (Modifier.isPublic(method.flags()) && !Modifier.isStatic(method.flags()) - && !ValueResolverGenerator.isSynthetic(method.flags()) + if (config.filter().test(method) && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, results)) { return method; } } DotName superName = clazz.superName(); - if (superName == null || DotNames.OBJECT.equals(superName)) { + if (config.declaredMembersOnly() || superName == null || DotNames.OBJECT.equals(superName)) { clazz = null; } else { clazz = index.getClassByName(clazz.superName()); } } // Try interface methods - for (DotName interfaceName : interfaceNames) { - ClassInfo interfaceClassInfo = index.getClassByName(interfaceName); - if (interfaceClassInfo != null) { - for (MethodInfo method : interfaceClassInfo.methods()) { - // A default method is a public non-abstract instance method - if (Modifier.isPublic(method.flags()) && !Modifier.isStatic(method.flags()) - && !ValueResolverGenerator.isSynthetic(method.flags()) - && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, results)) { - return method; + if (interfaceNames != null) { + for (DotName interfaceName : interfaceNames) { + ClassInfo interfaceClassInfo = index.getClassByName(interfaceName); + if (interfaceClassInfo != null) { + for (MethodInfo method : interfaceClassInfo.methods()) { + if (config.filter().test(method) + && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, results)) { + return method; + } } } } @@ -1789,7 +1842,7 @@ private static boolean methodMatches(MethodInfo method, VirtualMethodPart virtua private void processsTemplateData(IndexView index, AnnotationInstance templateData, AnnotationTarget annotationTarget, Set controlled, Map uncontrolled, ValueResolverGenerator.Builder builder) { - AnnotationValue targetValue = templateData.value("target"); + AnnotationValue targetValue = templateData.value(ValueResolverGenerator.TARGET); if (targetValue == null || targetValue.asClass().name().equals(ValueResolverGenerator.TEMPLATE_DATA)) { ClassInfo annotationTargetClass = annotationTarget.asClass(); controlled.add(annotationTargetClass.name()); @@ -1819,6 +1872,52 @@ private void processsTemplateData(IndexView index, AnnotationInstance templateDa } } + @BuildStep + void collectTemplateDataAnnotations(BeanArchiveIndexBuildItem beanArchiveIndex, + BuildProducer templateDataAnnotations) { + IndexView index = beanArchiveIndex.getIndex(); + Set annotationInstances = new HashSet<>(); + annotationInstances.addAll(index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA)); + for (AnnotationInstance containingInstance : index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA_CONTAINER)) { + for (AnnotationInstance nestedInstance : containingInstance.value().asNestedArray()) { + // We need to use the target of the containing instance + annotationInstances.add( + AnnotationInstance.create(nestedInstance.name(), containingInstance.target(), nestedInstance.values())); + } + } + + for (AnnotationInstance templateData : annotationInstances) { + AnnotationValue targetValue = templateData.value(ValueResolverGenerator.TARGET); + AnnotationValue ignoreValue = templateData.value(ValueResolverGenerator.IGNORE); + AnnotationValue propertiesValue = templateData.value(ValueResolverGenerator.PROPERTIES); + AnnotationValue namespaceValue = templateData.value(ValueResolverGenerator.NAMESPACE); + AnnotationValue ignoreSuperclassesValue = templateData.value(ValueResolverGenerator.IGNORE_SUPERCLASSES); + + ClassInfo targetClass = null; + if (targetValue == null || targetValue.asClass().name().equals(ValueResolverGenerator.TEMPLATE_DATA)) { + targetClass = templateData.target().asClass(); + } else { + targetClass = index.getClassByName(targetValue.asClass().name()); + } + + if (targetClass != null) { + String namespace = namespaceValue != null ? namespaceValue.asString() : TemplateData.UNDERSCORED_FQCN; + if (namespace.equals(TemplateData.UNDERSCORED_FQCN)) { + namespace = ValueResolverGenerator + .underscoredFullyQualifiedName(targetClass.name().toString()); + } else if (namespace.equals(TemplateData.SIMPLENAME)) { + namespace = ValueResolverGenerator.simpleName(targetClass); + } + templateDataAnnotations.produce(new TemplateDataBuildItem(targetClass, + namespace, + ignoreValue != null ? ignoreValue.asStringArray() : new String[] {}, + ignoreSuperclassesValue != null ? ignoreSuperclassesValue.asBoolean() : false, + propertiesValue != null ? propertiesValue.asBoolean() : false)); + } + } + + } + static Map> collectNamespaceExpressions(TemplatesAnalysisBuildItem analysis, String namespace) { Map> namespaceExpressions = new HashMap<>(); @@ -1930,4 +2029,87 @@ private static boolean isExcluded(TypeCheck check, List filter(); + + boolean declaredMembersOnly(); + + default void nextPart() { + } + + } + + static class FixedLookupConfig implements LookupConfig { + + private final IndexView index; + private final Predicate filter; + private final boolean declaredMembersOnly; + + FixedLookupConfig(IndexView index, Predicate filter, boolean declaredMembersOnly) { + this.index = index; + this.filter = filter; + this.declaredMembersOnly = declaredMembersOnly; + } + + @Override + public IndexView index() { + return index; + } + + @Override + public Predicate filter() { + return filter; + } + + @Override + public boolean declaredMembersOnly() { + return declaredMembersOnly; + } + } + + static class FirstPassLookupConfig implements LookupConfig { + + private final LookupConfig next; + // used for the firt part + private Predicate filter; + private Boolean declaredMembersOnly; + + FirstPassLookupConfig(LookupConfig next, Predicate filter, Boolean declaredMembersOnly) { + this.next = next; + this.filter = filter; + this.declaredMembersOnly = declaredMembersOnly; + } + + @Override + public IndexView index() { + return next.index(); + } + + @Override + public Predicate filter() { + return filter != null ? filter : next.filter(); + } + + @Override + public boolean declaredMembersOnly() { + return declaredMembersOnly != null ? declaredMembersOnly : next.declaredMembersOnly(); + } + + @Override + public void nextPart() { + filter = null; + declaredMembersOnly = null; + } + + } + } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java new file mode 100644 index 0000000000000..4d77f114a9794 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java @@ -0,0 +1,93 @@ +package io.quarkus.qute.deployment; + +import java.util.Arrays; +import java.util.regex.Pattern; + +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +final class TemplateDataBuildItem extends MultiBuildItem { + + private final ClassInfo targetClass; + private final String namespace; + private final String[] ignore; + private final Pattern[] ignorePatterns; + private final boolean ignoreSuperclasses; + private final boolean properties; + + public TemplateDataBuildItem(ClassInfo targetClass, String namespace, String[] ignore, boolean ignoreSuperclasses, + boolean properties) { + this.targetClass = targetClass; + this.namespace = namespace; + this.ignore = ignore; + this.ignoreSuperclasses = ignoreSuperclasses; + this.properties = properties; + if (ignore.length > 0) { + ignorePatterns = new Pattern[ignore.length]; + for (int i = 0; i < ignore.length; i++) { + ignorePatterns[i] = Pattern.compile(ignore[i]); + } + } else { + ignorePatterns = null; + } + } + + public ClassInfo getTargetClass() { + return targetClass; + } + + public boolean hasNamespace() { + return namespace != null; + } + + public String getNamespace() { + return namespace; + } + + public String[] getIgnore() { + return ignore; + } + + public boolean isIgnoreSuperclasses() { + return ignoreSuperclasses; + } + + public boolean isProperties() { + return properties; + } + + boolean filter(AnnotationTarget target) { + String name = null; + if (target.kind() == Kind.METHOD) { + MethodInfo method = target.asMethod(); + if (properties && !method.parameters().isEmpty()) { + return false; + } + name = method.name(); + } else if (target.kind() == Kind.FIELD) { + FieldInfo field = target.asField(); + name = field.name(); + } + if (ignorePatterns != null) { + for (int i = 0; i < ignorePatterns.length; i++) { + if (ignorePatterns[i].matcher(name).matches()) { + return false; + } + } + } + return true; + } + + @Override + public String toString() { + return "TemplateDataBuildItem [targetClass=" + targetClass + ", namespace=" + namespace + ", ignore=" + + Arrays.toString(ignore) + ", ignorePatterns=" + Arrays.toString(ignorePatterns) + ", ignoreSuperclasses=" + + ignoreSuperclasses + ", properties=" + properties + "]"; + } + +} 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 18cd138865a07..8ccccccff0dc1 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 @@ -88,7 +88,7 @@ static Info create(String typeInfo, Expression.Part part, IndexView index, Funct if (part.isVirtualMethod() || Expressions.isVirtualMethod(typeInfo)) { return new VirtualMethodInfo(typeInfo, part.asVirtualMethod(), hint); } - return new PropertyInfo(typeInfo, part, hint); + return new PropertyInfo(part.getName(), part, hint); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/AsyncTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/AsyncTest.java index a637957035660..d1803600c7612 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/AsyncTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/AsyncTest.java @@ -24,7 +24,7 @@ public class AsyncTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClass(TemplateDataTest.Foo.class) + .addClass(Foo.class) .addAsResource(new StringAsset("{foo.val} is not {foo.val.setScale(2,roundingMode)}"), "templates/foo.txt")); @@ -34,14 +34,14 @@ public class AsyncTest { @Test public void testAsyncRendering() { CompletionStage async = foo.data("roundingMode", RoundingMode.HALF_UP) - .data("foo", new TemplateDataTest.Foo(new BigDecimal("123.4563"))).renderAsync(); + .data("foo", new Foo(new BigDecimal("123.4563"))).renderAsync(); assertEquals("123.4563 is not 123.46", async.toCompletableFuture().join()); } @Test public void testAsyncRenderingAsUni() { Uni uni = Uni.createFrom().completionStage(() -> foo.data("roundingMode", RoundingMode.HALF_UP) - .data("foo", new TemplateDataTest.Foo(new BigDecimal("123.4563"))).renderAsync()); + .data("foo", new Foo(new BigDecimal("123.4563"))).renderAsync()); assertEquals("123.4563 is not 123.46", uni.await().indefinitely()); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MultiTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MultiTest.java index cccf020882df9..717452817683e 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MultiTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MultiTest.java @@ -23,7 +23,7 @@ public class MultiTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClass(TemplateDataTest.Foo.class) + .addClass(Foo.class) .addAsResource(new StringAsset("{foo.val} is not {foo.val.setScale(2,roundingMode)}"), "templates/foo.txt")); @@ -33,7 +33,7 @@ public class MultiTest { @Test public void testCreateMulti() { Multi multi = foo.data("roundingMode", RoundingMode.HALF_UP) - .data("foo", new TemplateDataTest.Foo(new BigDecimal("123.4563"))).createMulti(); + .data("foo", new Foo(new BigDecimal("123.4563"))).createMulti(); assertEquals("123.4563 is not 123.46", multi .collect().in(StringBuffer::new, StringBuffer::append) .onItem().transform(StringBuffer::toString) diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataTest.java index c65c6050d64e5..7a57ae7810527 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataTest.java @@ -63,7 +63,8 @@ public boolean isBaz() { } - @TemplateData(namespace = "TransactionType") + // namespace is TransactionType + @TemplateData(namespace = TemplateData.SIMPLENAME) public static enum TransactionType { FOO, diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataValidationTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataValidationTest.java new file mode 100644 index 0000000000000..a66a0639494b0 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataValidationTest.java @@ -0,0 +1,53 @@ +package io.quarkus.qute.deployment; + +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.TemplateData; +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateDataValidationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyClass.class) + .addAsResource(new StringAsset( + "{foo_My:BAZ}"), + "templates/foo.txt")) + .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 (1)"), te.getMessage()); + assertTrue(te.getMessage().contains("foo_My:BAZ"), te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + @TemplateData(namespace = "foo_My") + public static class MyClass { + + public static final String FOO = "foo"; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java index 801b5fc4bebd1..1f1dff97d1765 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java @@ -35,7 +35,9 @@ public class ValidationFailuresTest { + "{#each movie.mainCharacters}{it.boom(1)}{/}" // Template extension method must accept one param + "{movie.toNumber}" - + "{#each movie}{it}{/each}"), + + "{#each movie}{it}{/each}" + // Bean not found + + "{movie.findService(inject:ageBean)}"), "templates/movie.html")) .assertException(t -> { Throwable e = t; @@ -48,7 +50,7 @@ public class ValidationFailuresTest { e = e.getCause(); } assertNotNull(te); - assertTrue(te.getMessage().contains("Found template problems (9)"), te.getMessage()); + assertTrue(te.getMessage().contains("Found template problems (10)"), te.getMessage()); assertTrue(te.getMessage().contains("movie.foo"), te.getMessage()); assertTrue(te.getMessage().contains("movie.getName('foo')"), te.getMessage()); assertTrue(te.getMessage().contains("movie.findService(age)"), te.getMessage()); @@ -57,6 +59,7 @@ public class ValidationFailuresTest { assertTrue(te.getMessage().contains("movie.toNumber(age)"), te.getMessage()); assertTrue(te.getMessage().contains("it.boom(1)"), te.getMessage()); assertTrue(te.getMessage().contains("movie.toNumber"), te.getMessage()); + assertTrue(te.getMessage().contains("inject:ageBean"), te.getMessage()); assertTrue( te.getMessage().contains("Unsupported iterable type found: io.quarkus.qute.deployment.typesafe.Movie"), te.getMessage()); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java index 71ea34e571655..4995fe30c778d 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java @@ -24,10 +24,16 @@ public @interface TemplateData { /** - * Constant value for {@link #key()} indicating that the annotated element's name should be used as-is. + * Constant value for {@link #namespace()} indicating that the fully qualified class name of the target class should be + * used. Dots and dollar signs are replaced by underscores. */ String UNDERSCORED_FQCN = "<>"; + /** + * Constant value for {@link #namespace()} indicating that the simple name of the target class should be used. + */ + String SIMPLENAME = "<>"; + /** * The class a value resolver should be generated for. By default, the annotated type. */ diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java index 1493048eb6e5d..fd82ed2807bff 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java @@ -193,8 +193,12 @@ private void generate(DotName className, int priority) { if (namespace.isBlank()) { namespace = null; } - if (namespace != null && namespace.equals(TemplateData.UNDERSCORED_FQCN)) { - namespace = clazzName.replace(".", "_").replace("$", "_"); + if (namespace != null) { + if (namespace.equals(TemplateData.UNDERSCORED_FQCN)) { + namespace = underscoredFullyQualifiedName(clazzName); + } else if (namespace.equals(TemplateData.SIMPLENAME)) { + namespace = simpleName(clazz); + } } } @@ -1101,7 +1105,7 @@ public static String capitalize(String name) { * @param clazz * @return the simple name for the given top-level or nested class */ - static String simpleName(ClassInfo clazz) { + public static String simpleName(ClassInfo clazz) { switch (clazz.nestingType()) { case TOP_LEVEL: return simpleName(clazz.name()); @@ -1198,6 +1202,10 @@ public static boolean isVarArgs(MethodInfo method) { return (method.flags() & 0x00000080) != 0; } + public static String underscoredFullyQualifiedName(String name) { + return name.replace(".", "_").replace("$", "_"); + } + private static class Match { final String name;