From cf5d0bcffae2fa7933467ded585678fcaf331718 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 14 Feb 2025 11:52:16 +0100 Subject: [PATCH] Qute: add build item to exclude discovered templates - TemplatePathExcludeBuildItem - this is an addition to the quarkus.qute.template-path-exclude config property - resolves #46270 --- .../qute/deployment/QuteProcessor.java | 44 ++++++++++----- .../TemplatePathExcludeBuildItem.java | 23 ++++++++ .../TemplatePathExcludeBuildItemTest.java | 55 +++++++++++++++++++ .../quarkus/qute/runtime/EngineProducer.java | 26 +++++++-- .../io/quarkus/qute/runtime/QuteConfig.java | 5 +- .../io/quarkus/qute/runtime/QuteRecorder.java | 9 ++- 6 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathExcludeBuildItem.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/exclude/TemplatePathExcludeBuildItemTest.java 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 378c88e85401b..92bc4efc4454d 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 @@ -427,7 +427,7 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte continue; } MethodInfo canonicalConstructor = recordClass.method(MethodDescriptor.INIT, - recordClass.unsortedRecordComponents().stream().map(RecordComponentInfo::type) + recordClass.recordComponentsInDeclarationOrder().stream().map(RecordComponentInfo::type) .toArray(Type[]::new)); AnnotationInstance checkedTemplateAnnotation = recordClass.declaredAnnotation(Names.CHECKED_TEMPLATE); @@ -2185,6 +2185,7 @@ private String toKey(MethodInfo extensionMethod) { @BuildStep void collectTemplates(ApplicationArchivesBuildItem applicationArchives, CurateOutcomeBuildItem curateOutcome, + List templatePathExcludes, BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, @@ -2205,6 +2206,12 @@ public boolean test(String path) { } }).build()); + List excludePatterns = new ArrayList<>(templatePathExcludes.size() + 1); + excludePatterns.add(config.templatePathExclude()); + for (TemplatePathExcludeBuildItem exclude : templatePathExcludes) { + excludePatterns.add(Pattern.compile(exclude.getRegexPattern())); + } + final Set allApplicationArchives = applicationArchives.getAllApplicationArchives(); final Set appArtifactKeys = new HashSet<>(allApplicationArchives.size()); for (var archive : allApplicationArchives) { @@ -2215,12 +2222,13 @@ public boolean test(String path) { // Skip extension archives that are also application archives if (!appArtifactKeys.contains(artifact.getKey())) { scanPathTree(artifact.getContentTree(), templateRoots, watchedPaths, templatePaths, nativeImageResources, - config); + config, excludePatterns); } } for (ApplicationArchive archive : allApplicationArchives) { archive.accept( - tree -> scanPathTree(tree, templateRoots, watchedPaths, templatePaths, nativeImageResources, config)); + tree -> scanPathTree(tree, templateRoots, watchedPaths, templatePaths, nativeImageResources, config, + excludePatterns)); } } @@ -2228,7 +2236,7 @@ private void scanPathTree(PathTree pathTree, TemplateRootsBuildItem templateRoot BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, - QuteConfig config) { + QuteConfig config, List excludePatterns) { for (String templateRoot : templateRoots) { if (PathTreeUtils.containsCaseSensitivePath(pathTree, templateRoot)) { pathTree.walkIfContains(templateRoot, visit -> { @@ -2241,9 +2249,11 @@ private void scanPathTree(PathTree pathTree, TemplateRootsBuildItem templateRoot // remove templateRoot + / final String relativePath = visit.getRelativePath(); String templatePath = relativePath.substring(templateRoot.length() + 1); - if (config.templatePathExclude().matcher(templatePath).matches()) { - LOGGER.debugf("Template file excluded: %s", visit.getPath()); - return; + for (Pattern p : excludePatterns) { + if (p.matcher(templatePath).matches()) { + LOGGER.debugf("Template file excluded: %s", visit.getPath()); + return; + } } produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, relativePath, templatePath, visit.getPath(), config); @@ -2568,7 +2578,7 @@ void collecTemplateContents(BeanArchiveIndexBuildItem index, List syntheticBeans, QuteRecorder recorder, List templatePaths, Optional templateVariants, - TemplateRootsBuildItem templateRoots) { + TemplateRootsBuildItem templateRoots, List templatePathExcludes) { List templates = new ArrayList<>(); List tags = new ArrayList<>(); @@ -2592,11 +2602,17 @@ void initialize(BuildProducer syntheticBeans, QuteRecord variants = Collections.emptyMap(); } + List excludePatterns = new ArrayList<>(templatePathExcludes.size()); + for (TemplatePathExcludeBuildItem exclude : templatePathExcludes) { + excludePatterns.add(exclude.getRegexPattern()); + } + syntheticBeans.produce(SyntheticBeanBuildItem.configure(QuteContext.class) .scope(BuiltinScope.SINGLETON.getInfo()) .supplier(recorder.createContext(templates, tags, variants, - templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()), templateContents)) + templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()), templateContents, + excludePatterns)) .done()); } @@ -3529,8 +3545,7 @@ public static String getName(InjectionPointInfo injectionPoint) { private static void produceTemplateBuildItems(BuildProducer templatePaths, BuildProducer watchedPaths, BuildProducer nativeImageResources, String resourcePath, - String templatePath, - Path originalPath, QuteConfig config) { + String templatePath, Path originalPath, QuteConfig config) { if (templatePath.isEmpty()) { return; } @@ -3544,9 +3559,10 @@ private static void produceTemplateBuildItems(BuildProducer> excludes) { diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathExcludeBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathExcludeBuildItem.java new file mode 100644 index 0000000000000..83b5e898d500e --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathExcludeBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.qute.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * This build item is used to exclude template files found in template roots. Excluded templates are + * neither parsed nor validated during build and are not available at runtime. + *

+ * The matched input is the file path relative from the root directory and the {@code /} is used as a path separator. + */ +public final class TemplatePathExcludeBuildItem extends MultiBuildItem { + + private final String regexPattern; + + public TemplatePathExcludeBuildItem(String regexPattern) { + this.regexPattern = regexPattern; + } + + public String getRegexPattern() { + return regexPattern; + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/exclude/TemplatePathExcludeBuildItemTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/exclude/TemplatePathExcludeBuildItemTest.java new file mode 100644 index 0000000000000..09b85528e2468 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/exclude/TemplatePathExcludeBuildItemTest.java @@ -0,0 +1,55 @@ +package io.quarkus.qute.deployment.exclude; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.function.Consumer; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.qute.Engine; +import io.quarkus.qute.deployment.TemplatePathExcludeBuildItem; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplatePathExcludeBuildItemTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addAsResource(new StringAsset("{@String name} Hi {name.nonexistent}!"), "templates/hi.txt") + .addAsResource(new StringAsset("Hello {name}!"), "templates/hello.txt")) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(new TemplatePathExcludeBuildItem("hi.txt")); + } + }).produces(TemplatePathExcludeBuildItem.class) + .build(); + + } + }; + } + + @Inject + Engine engine; + + @Test + public void testTemplate() { + assertNull(engine.getTemplate("hi")); + assertEquals("Hello M!", engine.getTemplate("hello").data("name", "M").render()); + } + +} 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 4192873666210..a65aa33209bfd 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 @@ -40,6 +40,7 @@ import io.quarkus.qute.EvalContext; import io.quarkus.qute.Expression; import io.quarkus.qute.HtmlEscaper; +import io.quarkus.qute.ImmutableList; import io.quarkus.qute.JsonEscaper; import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.ParserHook; @@ -82,7 +83,7 @@ public class EngineProducer { private final List suffixes; private final Set templateRoots; private final Map templateContents; - private final Pattern templatePathExclude; + private final List templatePathExcludes; private final Locale defaultLocale; private final Charset defaultCharset; private final ArcContainer container; @@ -97,11 +98,17 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig this.templateRoots = context.getTemplateRoots(); this.templateContents = Map.copyOf(context.getTemplateContents()); this.tags = context.getTags(); - this.templatePathExclude = config.templatePathExclude(); this.defaultLocale = locales.defaultLocale().orElse(Locale.getDefault()); this.defaultCharset = config.defaultCharset(); this.container = Arc.container(); + ImmutableList.Builder excludesBuilder = ImmutableList. builder() + .add(config.templatePathExclude()); + for (String p : context.getExcludePatterns()) { + excludesBuilder.add(Pattern.compile(p)); + } + this.templatePathExcludes = excludesBuilder.build(); + LOGGER.debugf("Initializing Qute [templates: %s, tags: %s, resolvers: %s", context.getTemplatePaths(), tags, context.getResolverClasses()); @@ -342,8 +349,17 @@ private TemplateGlobalProvider createGlobalProvider(String initializerClassName) } } + private boolean isExcluded(String path) { + for (Pattern p : templatePathExcludes) { + if (p.matcher(path).matches()) { + return true; + } + } + return false; + } + private Optional locate(String path) { - if (templatePathExclude.matcher(path).matches()) { + if (isExcluded(path)) { return Optional.empty(); } // First try to locate file-based templates @@ -356,7 +372,7 @@ private Optional locate(String path) { // Try path with suffixes for (String suffix : suffixes) { String pathWithSuffix = path + "." + suffix; - if (templatePathExclude.matcher(pathWithSuffix).matches()) { + if (isExcluded(pathWithSuffix)) { continue; } templatePath = templateRoot + pathWithSuffix; @@ -377,7 +393,7 @@ private Optional locate(String path) { // Try path with suffixes for (String suffix : suffixes) { String pathWithSuffix = path + "." + suffix; - if (templatePathExclude.matcher(pathWithSuffix).matches()) { + if (isExcluded(pathWithSuffix)) { continue; } content = templateContents.get(pathWithSuffix); diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java index 52baa52b703d1..63049f991e827 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java @@ -49,11 +49,10 @@ public interface QuteConfig { Optional> typeCheckExcludes(); /** - * This regular expression is used to exclude template files from the {@code templates} directory. Excluded templates are + * This regular expression is used to exclude template files found in template roots. Excluded templates are * neither parsed nor validated during build and are not available at runtime. *

- * The matched input is the file path relative from the {@code templates} directory and the - * {@code /} is used as a path separator. + * The matched input is the file path relative from the root directory and the {@code /} is used as a path separator. *

* By default, the hidden files are excluded. The name of a hidden file starts with a dot. */ diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java index 60797098731d9..dfcf6c9a11a71 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java @@ -12,7 +12,7 @@ public class QuteRecorder { public Supplier createContext(List templatePaths, List tags, Map> variants, - Set templateRoots, Map templateContents) { + Set templateRoots, Map templateContents, List excludePatterns) { return new Supplier() { @Override @@ -63,6 +63,11 @@ public Map getTemplateContents() { return templateContents; } + @Override + public List getExcludePatterns() { + return excludePatterns; + } + @Override public void setGeneratedClasses(List resolverClasses, List templateGlobalProviderClasses) { this.resolverClasses = resolverClasses; @@ -99,6 +104,8 @@ public interface QuteContext { Map getTemplateContents(); + List getExcludePatterns(); + /** * The generated classes must be initialized after the template expressions are validated (later during the STATIC_INIT * bootstrap phase) in order to break the cycle in the build chain.