Skip to content

Commit

Permalink
Merge pull request #46273 from mkouba/issue-46270
Browse files Browse the repository at this point in the history
Qute: add build item to exclude discovered templates
  • Loading branch information
ia3andy authored Feb 14, 2025
2 parents 4e5050f + cf5d0bc commit 055f883
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -2185,6 +2185,7 @@ private String toKey(MethodInfo extensionMethod) {
@BuildStep
void collectTemplates(ApplicationArchivesBuildItem applicationArchives,
CurateOutcomeBuildItem curateOutcome,
List<TemplatePathExcludeBuildItem> templatePathExcludes,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedPaths,
BuildProducer<TemplatePathBuildItem> templatePaths,
BuildProducer<NativeImageResourceBuildItem> nativeImageResources,
Expand All @@ -2205,6 +2206,12 @@ public boolean test(String path) {
}
}).build());

List<Pattern> excludePatterns = new ArrayList<>(templatePathExcludes.size() + 1);
excludePatterns.add(config.templatePathExclude());
for (TemplatePathExcludeBuildItem exclude : templatePathExcludes) {
excludePatterns.add(Pattern.compile(exclude.getRegexPattern()));
}

final Set<ApplicationArchive> allApplicationArchives = applicationArchives.getAllApplicationArchives();
final Set<ArtifactKey> appArtifactKeys = new HashSet<>(allApplicationArchives.size());
for (var archive : allApplicationArchives) {
Expand All @@ -2215,20 +2222,21 @@ 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));
}
}

private void scanPathTree(PathTree pathTree, TemplateRootsBuildItem templateRoots,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedPaths,
BuildProducer<TemplatePathBuildItem> templatePaths,
BuildProducer<NativeImageResourceBuildItem> nativeImageResources,
QuteConfig config) {
QuteConfig config, List<Pattern> excludePatterns) {
for (String templateRoot : templateRoots) {
if (PathTreeUtils.containsCaseSensitivePath(pathTree, templateRoot)) {
pathTree.walkIfContains(templateRoot, visit -> {
Expand All @@ -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);
Expand Down Expand Up @@ -2568,7 +2578,7 @@ void collecTemplateContents(BeanArchiveIndexBuildItem index, List<CheckedTemplat
@Record(value = STATIC_INIT)
void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecorder recorder,
List<TemplatePathBuildItem> templatePaths, Optional<TemplateVariantsBuildItem> templateVariants,
TemplateRootsBuildItem templateRoots) {
TemplateRootsBuildItem templateRoots, List<TemplatePathExcludeBuildItem> templatePathExcludes) {

List<String> templates = new ArrayList<>();
List<String> tags = new ArrayList<>();
Expand All @@ -2592,11 +2602,17 @@ void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecord
variants = Collections.emptyMap();
}

List<String> 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());
}

Expand Down Expand Up @@ -3529,8 +3545,7 @@ public static String getName(InjectionPointInfo injectionPoint) {
private static void produceTemplateBuildItems(BuildProducer<TemplatePathBuildItem> templatePaths,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedPaths,
BuildProducer<NativeImageResourceBuildItem> nativeImageResources, String resourcePath,
String templatePath,
Path originalPath, QuteConfig config) {
String templatePath, Path originalPath, QuteConfig config) {
if (templatePath.isEmpty()) {
return;
}
Expand All @@ -3544,9 +3559,10 @@ private static void produceTemplateBuildItems(BuildProducer<TemplatePathBuildIte
}
watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(resourcePath, restartNeeded));
nativeImageResources.produce(new NativeImageResourceBuildItem(resourcePath));
templatePaths.produce(
new TemplatePathBuildItem(templatePath, originalPath,
readTemplateContent(originalPath, config.defaultCharset())));
templatePaths.produce(TemplatePathBuildItem.builder()
.path(templatePath)
.fullPath(originalPath)
.content(readTemplateContent(originalPath, config.defaultCharset())).build());
}

private static boolean isExcluded(TypeCheck check, Iterable<Predicate<TypeCheck>> excludes) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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;
}

}
Original file line number Diff line number Diff line change
@@ -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<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {
@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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,7 +83,7 @@ public class EngineProducer {
private final List<String> suffixes;
private final Set<String> templateRoots;
private final Map<String, String> templateContents;
private final Pattern templatePathExclude;
private final List<Pattern> templatePathExcludes;
private final Locale defaultLocale;
private final Charset defaultCharset;
private final ArcContainer container;
Expand All @@ -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<Pattern> excludesBuilder = ImmutableList.<Pattern> 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());

Expand Down Expand Up @@ -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<TemplateLocation> locate(String path) {
if (templatePathExclude.matcher(path).matches()) {
if (isExcluded(path)) {
return Optional.empty();
}
// First try to locate file-based templates
Expand All @@ -356,7 +372,7 @@ private Optional<TemplateLocation> 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;
Expand All @@ -377,7 +393,7 @@ private Optional<TemplateLocation> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@ public interface QuteConfig {
Optional<List<String>> 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.
* <p>
* 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.
* <p>
* By default, the hidden files are excluded. The name of a hidden file starts with a dot.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
public class QuteRecorder {

public Supplier<Object> createContext(List<String> templatePaths, List<String> tags, Map<String, List<String>> variants,
Set<String> templateRoots, Map<String, String> templateContents) {
Set<String> templateRoots, Map<String, String> templateContents, List<String> excludePatterns) {
return new Supplier<Object>() {

@Override
Expand Down Expand Up @@ -63,6 +63,11 @@ public Map<String, String> getTemplateContents() {
return templateContents;
}

@Override
public List<String> getExcludePatterns() {
return excludePatterns;
}

@Override
public void setGeneratedClasses(List<String> resolverClasses, List<String> templateGlobalProviderClasses) {
this.resolverClasses = resolverClasses;
Expand Down Expand Up @@ -99,6 +104,8 @@ public interface QuteContext {

Map<String, String> getTemplateContents();

List<String> 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.
Expand Down

0 comments on commit 055f883

Please sign in to comment.