From d1d280326ee021bc290915517b517a3341a74797 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 24 Jun 2024 16:59:07 +0200 Subject: [PATCH] Qute: make it possible to supply a template backed by a build item - resolves #41386 --- .../qute/deployment/QuteProcessor.java | 12 +++- .../deployment/TemplatePathBuildItem.java | 58 +++++++++++++++---- .../AdditionalTemplatePathDuplicatesTest.java | 54 +++++++++++++++++ .../AdditionalTemplatePathTest.java | 56 ++++++++++++++++++ .../quarkus/qute/runtime/EngineProducer.java | 49 +++++++++++++++- .../io/quarkus/qute/runtime/QuteRecorder.java | 9 ++- 6 files changed, 222 insertions(+), 16 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathDuplicatesTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathTest.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 724c4384efffe..f591dcfb3cf27 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 @@ -2433,6 +2433,7 @@ void initialize(BuildProducer syntheticBeans, QuteRecord List templates = new ArrayList<>(); List tags = new ArrayList<>(); + Map templateContents = new HashMap<>(); for (TemplatePathBuildItem templatePath : templatePaths) { if (templatePath.isTag()) { // tags/myTag.html -> myTag.html @@ -2441,6 +2442,9 @@ void initialize(BuildProducer syntheticBeans, QuteRecord } else { templates.add(templatePath.getPath()); } + if (!templatePath.isFileBased()) { + templateContents.put(templatePath.getPath(), templatePath.getContent()); + } } Map> variants; if (templateVariants.isPresent()) { @@ -2454,7 +2458,7 @@ void initialize(BuildProducer syntheticBeans, QuteRecord .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates, tags, variants, templateInitializers.stream() .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList()), - templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()))) + templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()), templateContents)) .done()); } @@ -3423,8 +3427,10 @@ private void checkDuplicatePaths(List templatePaths) { if (!duplicates.isEmpty()) { StringBuilder builder = new StringBuilder("Duplicate templates found:"); for (Entry> e : duplicates.entrySet()) { - builder.append("\n\t- ").append(e.getKey()).append(": ") - .append(e.getValue().stream().map(TemplatePathBuildItem::getFullPath).collect(Collectors.toList())); + builder.append("\n\t- ") + .append(e.getKey()) + .append(": ") + .append(e.getValue().stream().map(TemplatePathBuildItem::getSourceInfo).collect(Collectors.toList())); } throw new IllegalStateException(builder.toString()); } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java index 85a4a9fdeeeae..f6aff326f15a3 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java @@ -1,28 +1,46 @@ package io.quarkus.qute.deployment; import java.nio.file.Path; +import java.util.Objects; import io.quarkus.builder.item.MultiBuildItem; /** - * Represents a template path. + * Discovered template. + *

+ * Templates backed by files located in a template root are discovered automatically. Furthermore, extensions can produce this + * build item in order to provide a template that is not backed by a file. + * + * @see TemplateRootBuildItem */ public final class TemplatePathBuildItem extends MultiBuildItem { static final String TAGS = "tags/"; private final String path; - private final Path fullPath; private final String content; + private final Path fullPath; + private final String extensionInfo; + + public TemplatePathBuildItem(String path, String extensionInfo, String content) { + this(path, content, null, Objects.requireNonNull(extensionInfo)); + } + + TemplatePathBuildItem(String path, Path fullPath, String content) { + this(path, content, Objects.requireNonNull(fullPath), null); + } - public TemplatePathBuildItem(String path, Path fullPath, String content) { - this.path = path; + private TemplatePathBuildItem(String path, String content, Path fullPath, String extensionInfo) { + this.path = Objects.requireNonNull(path); + this.content = Objects.requireNonNull(content); this.fullPath = fullPath; - this.content = content; + this.extensionInfo = extensionInfo; } /** - * Uses the {@code /} path separator. + * The path relative to the template root. The {@code /} is used as a path separator. + *

+ * If there are multiple templates with the same path then the template analysis fails during build. * * @return the path relative to the template root */ @@ -31,14 +49,30 @@ public String getPath() { } /** - * Uses the system-dependent path separator. + * The full path of the template which uses the system-dependent path separator. * - * @return the full path of the template + * @return the full path, or {@code null} for templates that are not backed by a file */ public Path getFullPath() { return fullPath; } + /** + * + * @return the content of the template + */ + public String getContent() { + return content; + } + + /** + * + * @return the extension info + */ + public String getExtensionInfo() { + return extensionInfo; + } + /** * * @return {@code true} if it represents a user tag, {@code false} otherwise @@ -51,8 +85,12 @@ public boolean isRegular() { return !isTag(); } - public String getContent() { - return content; + public boolean isFileBased() { + return fullPath != null; + } + + public String getSourceInfo() { + return isFileBased() ? getFullPath().toString() : extensionInfo; } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathDuplicatesTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathDuplicatesTest.java new file mode 100644 index 0000000000000..515c9dd19fb5b --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathDuplicatesTest.java @@ -0,0 +1,54 @@ +package io.quarkus.qute.deployment.builditemtemplate; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.function.Consumer; + +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.deployment.TemplatePathBuildItem; +import io.quarkus.test.QuarkusUnitTest; + +public class AdditionalTemplatePathDuplicatesTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt")) + .addBuildChainCustomizer(buildCustomizer()) + .setExpectedException(IllegalStateException.class, true); + + 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 TemplatePathBuildItem("hi.txt", "test-ext", "Hello {name}!")); + } + }).produces(TemplatePathBuildItem.class) + .build(); + + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(new TemplatePathBuildItem("hi.txt", "test-ext", "Hello {name}!")); + } + }).produces(TemplatePathBuildItem.class) + .build(); + } + }; + } + + @Test + public void test() { + fail(); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathTest.java new file mode 100644 index 0000000000000..35ef23f9a6112 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathTest.java @@ -0,0 +1,56 @@ +package io.quarkus.qute.deployment.builditemtemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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.TemplatePathBuildItem; +import io.quarkus.test.QuarkusUnitTest; + +public class AdditionalTemplatePathTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt") + .addAsResource(new StringAsset("And... {#include foo/hello /}"), "templates/include.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 TemplatePathBuildItem("foo/hello.txt", "test-ext", "Hello {name}!")); + } + }).produces(TemplatePathBuildItem.class) + .build(); + + } + }; + } + + @Inject + Engine engine; + + @Test + public void testTemplate() { + assertEquals("Hi M!", engine.getTemplate("hi").data("name", "M").render()); + assertEquals("Hello M!", engine.getTemplate("foo/hello.txt").data("name", "M").render()); + assertEquals("Hello M!", engine.getTemplate("foo/hello").data("name", "M").render()); + assertEquals("And... Hello M!", engine.getTemplate("include").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 48a5360c3e92e..5e513a98d04c5 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 @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.nio.charset.Charset; @@ -78,6 +79,7 @@ public class EngineProducer { private final List tags; private final List suffixes; private final Set templateRoots; + private final Map templateContents; private final Pattern templatePathExclude; private final Locale defaultLocale; private final Charset defaultCharset; @@ -91,6 +93,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig this.contentTypes = contentTypes; this.suffixes = config.suffixes; this.templateRoots = context.getTemplateRoots(); + this.templateContents = Map.copyOf(context.getTemplateContents()); this.tags = context.getTags(); this.templatePathExclude = config.templatePathExclude; this.defaultLocale = locales.defaultLocale; @@ -334,10 +337,11 @@ private Optional locate(String path) { if (templatePathExclude.matcher(path).matches()) { return Optional.empty(); } + // First try to locate file-based templates for (String templateRoot : templateRoots) { URL resource = null; String templatePath = templateRoot + path; - LOGGER.debugf("Locate template for %s", templatePath); + LOGGER.debugf("Locate template file for %s", templatePath); resource = locatePath(templatePath); if (resource == null) { // Try path with suffixes @@ -357,6 +361,25 @@ private Optional locate(String path) { return Optional.of(new ResourceTemplateLocation(resource, createVariant(templatePath))); } } + // Then try the template contents + LOGGER.debugf("Locate template contents for %s", path); + String content = templateContents.get(path); + if (path == null) { + // Try path with suffixes + for (String suffix : suffixes) { + String pathWithSuffix = path + "." + suffix; + if (templatePathExclude.matcher(pathWithSuffix).matches()) { + continue; + } + content = templateContents.get(pathWithSuffix); + if (content != null) { + break; + } + } + } + if (content != null) { + return Optional.of(new ContentTemplateLocation(content, createVariant(path))); + } return Optional.empty(); } @@ -439,7 +462,7 @@ static class ResourceTemplateLocation implements TemplateLocation { private final URL resource; private final Optional variant; - public ResourceTemplateLocation(URL resource, Variant variant) { + ResourceTemplateLocation(URL resource, Variant variant) { this.resource = resource; this.variant = Optional.ofNullable(variant); } @@ -467,4 +490,26 @@ public Optional getVariant() { } + static class ContentTemplateLocation implements TemplateLocation { + + private final String content; + private final Optional variant; + + ContentTemplateLocation(String content, Variant variant) { + this.content = content; + this.variant = Optional.ofNullable(variant); + } + + @Override + public Reader read() { + return new StringReader(content); + } + + @Override + public Optional getVariant() { + return variant; + } + + } + } 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 0fff3270c5735..2e53fe166580d 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 resolverClasses, List templatePaths, List tags, Map> variants, - List templateGlobalProviderClasses, Set templateRoots) { + List templateGlobalProviderClasses, Set templateRoots, Map templateContents) { return new Supplier() { @Override @@ -48,6 +48,11 @@ public List getTemplateGlobalProviderClasses() { public Set getTemplateRoots() { return templateRoots; } + + @Override + public Map getTemplateContents() { + return templateContents; + } }; } }; @@ -67,6 +72,8 @@ public interface QuteContext { Set getTemplateRoots(); + Map getTemplateContents(); + } }