Skip to content

Commit

Permalink
Qute: make it possible to supply a template backed by a build item
Browse files Browse the repository at this point in the history
- resolves quarkusio#41386
  • Loading branch information
mkouba committed Jun 26, 2024
1 parent 32d0c2d commit 74ed772
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2433,6 +2433,7 @@ void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecord

List<String> templates = new ArrayList<>();
List<String> tags = new ArrayList<>();
Map<String, String> templateContents = new HashMap<>();
for (TemplatePathBuildItem templatePath : templatePaths) {
if (templatePath.isTag()) {
// tags/myTag.html -> myTag.html
Expand All @@ -2441,6 +2442,9 @@ void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecord
} else {
templates.add(templatePath.getPath());
}
if (!templatePath.isFileBased()) {
templateContents.put(templatePath.getPath(), templatePath.getContent());
}
}
Map<String, List<String>> variants;
if (templateVariants.isPresent()) {
Expand All @@ -2454,7 +2458,7 @@ void initialize(BuildProducer<SyntheticBeanBuildItem> 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());
}

Expand Down Expand Up @@ -3423,8 +3427,10 @@ private void checkDuplicatePaths(List<TemplatePathBuildItem> templatePaths) {
if (!duplicates.isEmpty()) {
StringBuilder builder = new StringBuilder("Duplicate templates found:");
for (Entry<String, List<TemplatePathBuildItem>> 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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
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.
* <p>
* 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 {

/**
*
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}

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;

/**
*
* @param path
* @param fullPath
* @param content
* @deprecated Use the {@link #builder()} instead
*/
@Deprecated
public TemplatePathBuildItem(String path, Path fullPath, String content) {
this(Objects.requireNonNull(path), Objects.requireNonNull(content), Objects.requireNonNull(fullPath), null);
}

private TemplatePathBuildItem(String path, String content, Path fullPath, String extensionInfo) {
this.path = path;
this.fullPath = fullPath;
this.content = content;
this.fullPath = fullPath;
this.extensionInfo = extensionInfo;
}

/**
* Uses the {@code /} path separator.
* The path relative to the template root. The {@code /} is used as a path separator.
* <p>
* The path must be unique, i.e. if there are multiple templates with the same path then the template analysis fails during
* build.
*
* @return the path relative to the template root
*/
Expand All @@ -31,14 +62,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
Expand All @@ -47,12 +94,87 @@ public boolean isTag() {
return path.startsWith(TAGS);
}

/**
*
* @return {@code true} if it does not represent a tag, {@code false} otherwise
*/
public boolean isRegular() {
return !isTag();
}

public String getContent() {
return content;
/**
*
* @return {@code true} if it's backed by a file
*/
public boolean isFileBased() {
return fullPath != null;
}

public String getSourceInfo() {
return isFileBased() ? getFullPath().toString() : extensionInfo;
}

public static class Builder {

private String path;
private String content;
private Path fullPath;
private String extensionInfo;

/**
* Set the path relative to the template root. The {@code /} is used as a path separator.
* <p>
* The path must be unique, i.e. if there are multiple templates with the same path then the template analysis fails
* during build.
*
* @param path
* @return self
*/
public Builder path(String path) {
this.path = Objects.requireNonNull(path);
return this;
}

/**
* Set the content of the template.
*
* @param content
* @return self
*/
public Builder content(String content) {
this.content = Objects.requireNonNull(content);
return this;
}

/**
* Set the full path of the template for templates that are backed by a file.
*
* @param fullPath
* @return self
*/
public Builder fullPath(Path fullPath) {
this.fullPath = Objects.requireNonNull(fullPath);
return this;
}

/**
* Set the extension info for templates that are not backed by a file.
*
* @param info
* @return self
*/
public Builder extensionInfo(String info) {
this.extensionInfo = info;
return this;
}

public TemplatePathBuildItem build() {
if (fullPath == null && extensionInfo == null) {
throw new IllegalStateException("Templates that are not backed by a file must provide extension info");
}
return new TemplatePathBuildItem(path, content, fullPath, extensionInfo);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {
@Override
public void accept(BuildChainBuilder builder) {
builder.addBuildStep(new BuildStep() {
@Override
public void execute(BuildContext context) {
context.produce(TemplatePathBuildItem.builder()
.path("hi.txt")
.extensionInfo("test-ext")
.content("Hello {name}!").build());
}
}).produces(TemplatePathBuildItem.class)
.build();

builder.addBuildStep(new BuildStep() {
@Override
public void execute(BuildContext context) {
context.produce(TemplatePathBuildItem.builder()
.path("hi.txt")
.extensionInfo("test-ext")
.content("Hello {name}!").build());
}
}).produces(TemplatePathBuildItem.class)
.build();
}
};
}

@Test
public void test() {
fail();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {
@Override
public void accept(BuildChainBuilder builder) {
builder.addBuildStep(new BuildStep() {
@Override
public void execute(BuildContext context) {
context.produce(TemplatePathBuildItem.builder()
.path("foo/hello.txt")
.extensionInfo("test-ext")
.content("Hello {name}!").build());
}
}).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());
}

}
Loading

0 comments on commit 74ed772

Please sign in to comment.