Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qute: add build item to exclude discovered templates #46273

Merged
merged 1 commit into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading