Skip to content

Commit f3e77fe

Browse files
committed
Qute: introduce a new strategy to handle duplicate template paths
- resolves quarkusio#43906
1 parent 6956d75 commit f3e77fe

File tree

11 files changed

+329
-68
lines changed

11 files changed

+329
-68
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.quarkus.qute.deployment;
2+
3+
import java.util.List;
4+
5+
import io.quarkus.builder.item.SimpleBuildItem;
6+
import io.quarkus.qute.runtime.QuteConfig;
7+
8+
/**
9+
* This build item represents all template paths of an application.
10+
* <p>
11+
* If {@link QuteConfig.DuplicitTemplateStrategy#PRIORITIZE} is used then duplicit template paths with lower priority are not
12+
* included.
13+
*/
14+
public final class EffectiveTemplatePathsBuildItem extends SimpleBuildItem {
15+
16+
private final List<TemplatePathBuildItem> templatePaths;
17+
18+
EffectiveTemplatePathsBuildItem(List<TemplatePathBuildItem> templatePaths) {
19+
this.templatePaths = templatePaths;
20+
}
21+
22+
public List<TemplatePathBuildItem> getTemplatePaths() {
23+
return templatePaths;
24+
}
25+
26+
}

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.lang.reflect.Modifier;
44
import java.util.HashMap;
5-
import java.util.List;
65
import java.util.Map;
76
import java.util.function.Predicate;
87

@@ -27,10 +26,10 @@
2726
public class QuteDevModeProcessor {
2827

2928
@BuildStep
30-
void collectGeneratedContents(List<TemplatePathBuildItem> templatePaths,
29+
void collectGeneratedContents(EffectiveTemplatePathsBuildItem effectiveTemplatePaths,
3130
BuildProducer<ValidationErrorBuildItem> errors) {
3231
Map<String, String> contents = new HashMap<>();
33-
for (TemplatePathBuildItem template : templatePaths) {
32+
for (TemplatePathBuildItem template : effectiveTemplatePaths.getTemplatePaths()) {
3433
if (!template.isFileBased()) {
3534
contents.put(template.getPath(), template.getContent());
3635
}

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java

+105-34
Large diffs are not rendered by default.

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java

+52-5
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,38 @@
44
import java.util.Objects;
55

66
import io.quarkus.builder.item.MultiBuildItem;
7+
import io.quarkus.qute.runtime.QuteConfig;
78

89
/**
910
* Discovered template.
1011
* <p>
1112
* Templates backed by files located in a template root are discovered automatically. Furthermore, extensions can produce this
1213
* build item in order to provide a template that is not backed by a file.
1314
*
15+
* <h2>Warning</h2>
16+
*
17+
* Extensions should never <i>consume</i> this build item directly. However, they may consume the
18+
* {@link EffectiveTemplatePathsBuildItem} instead.
19+
*
1420
* @see TemplateRootBuildItem
1521
*/
1622
public final class TemplatePathBuildItem extends MultiBuildItem {
1723

24+
/**
25+
* The priority used for templates from the root application archive.
26+
*/
27+
public static final int ROOT_ARCHIVE_PRIORITY = 30;
28+
29+
/**
30+
* The default priority used for templates that are not backed by a file.
31+
*/
32+
public static final int BUILD_ITEM_PRIORITY = 20;
33+
34+
/**
35+
* The priority used for templates from non-root application archives.
36+
*/
37+
public static final int APP_ARCHIVE_PRIORITY = 10;
38+
1839
/**
1940
*
2041
* @return a new builder instance
@@ -30,23 +51,27 @@ public static Builder builder() {
3051
private final Path fullPath;
3152
private final String extensionInfo;
3253

54+
private final int priority;
55+
3356
/**
3457
*
3558
* @param path
3659
* @param fullPath
3760
* @param content
3861
* @deprecated Use the {@link #builder()} instead
3962
*/
40-
@Deprecated
63+
@Deprecated(forRemoval = true, since = "3.13")
4164
public TemplatePathBuildItem(String path, Path fullPath, String content) {
42-
this(Objects.requireNonNull(path), Objects.requireNonNull(content), Objects.requireNonNull(fullPath), null);
65+
this(Objects.requireNonNull(path), Objects.requireNonNull(content), Objects.requireNonNull(fullPath), null,
66+
BUILD_ITEM_PRIORITY);
4367
}
4468

45-
private TemplatePathBuildItem(String path, String content, Path fullPath, String extensionInfo) {
69+
private TemplatePathBuildItem(String path, String content, Path fullPath, String extensionInfo, int priority) {
4670
this.path = path;
4771
this.content = content;
4872
this.fullPath = fullPath;
4973
this.extensionInfo = extensionInfo;
74+
this.priority = priority;
5075
}
5176

5277
/**
@@ -86,6 +111,15 @@ public String getExtensionInfo() {
86111
return extensionInfo;
87112
}
88113

114+
/**
115+
* Templates with higher priority take precedence when duplicates are found.
116+
*
117+
* @return the priority
118+
*/
119+
public int getPriority() {
120+
return priority;
121+
}
122+
89123
/**
90124
*
91125
* @return {@code true} if it represents a user tag, {@code false} otherwise
@@ -111,7 +145,7 @@ public boolean isFileBased() {
111145
}
112146

113147
public String getSourceInfo() {
114-
return isFileBased() ? getFullPath().toString() : extensionInfo;
148+
return (isFileBased() ? getFullPath().toString() : extensionInfo) + " [" + getPriority() + "]";
115149
}
116150

117151
public static class Builder {
@@ -120,6 +154,7 @@ public static class Builder {
120154
private String content;
121155
private Path fullPath;
122156
private String extensionInfo;
157+
private int priority = BUILD_ITEM_PRIORITY;
123158

124159
/**
125160
* Set the path relative to the template root. The {@code /} is used as a path separator.
@@ -168,11 +203,23 @@ public Builder extensionInfo(String info) {
168203
return this;
169204
}
170205

206+
/**
207+
* Set the priority of the template.
208+
*
209+
* @param priority
210+
* @return self
211+
* @see QuteConfig#duplicitTemplateStrategy()
212+
*/
213+
public Builder priority(int priority) {
214+
this.priority = priority;
215+
return this;
216+
}
217+
171218
public TemplatePathBuildItem build() {
172219
if (fullPath == null && extensionInfo == null) {
173220
throw new IllegalStateException("Templates that are not backed by a file must provide extension info");
174221
}
175-
return new TemplatePathBuildItem(path, content, fullPath, extensionInfo);
222+
return new TemplatePathBuildItem(path, content, fullPath, extensionInfo, priority);
176223
}
177224

178225
}

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/devui/QuteDevUIProcessor.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.quarkus.devui.spi.page.Page;
2121
import io.quarkus.qute.ParameterDeclaration;
2222
import io.quarkus.qute.deployment.CheckedTemplateBuildItem;
23+
import io.quarkus.qute.deployment.EffectiveTemplatePathsBuildItem;
2324
import io.quarkus.qute.deployment.ImplicitValueResolverBuildItem;
2425
import io.quarkus.qute.deployment.TemplateDataBuildItem;
2526
import io.quarkus.qute.deployment.TemplateExtensionMethodBuildItem;
@@ -33,7 +34,7 @@ public class QuteDevUIProcessor {
3334

3435
@BuildStep(onlyIf = IsDevelopment.class)
3536
public void pages(
36-
List<TemplatePathBuildItem> templatePaths,
37+
EffectiveTemplatePathsBuildItem effectiveTemplatePaths,
3738
List<CheckedTemplateBuildItem> checkedTemplates,
3839
TemplateVariantsBuildItem variants,
3940
TemplatesAnalysisBuildItem templatesAnalysis,
@@ -45,7 +46,7 @@ public void pages(
4546

4647
CardPageBuildItem pageBuildItem = new CardPageBuildItem();
4748

48-
List<TemplatePathBuildItem> sortedTemplatePaths = templatePaths.stream()
49+
List<TemplatePathBuildItem> sortedTemplatePaths = effectiveTemplatePaths.getTemplatePaths().stream()
4950
.sorted(Comparator.comparing(tp -> tp.getPath().toLowerCase())).collect(Collectors.toList());
5051
pageBuildItem.addBuildTimeData("templates",
5152
createTemplatesJson(sortedTemplatePaths, checkedTemplates, templatesAnalysis, variants));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.quarkus.qute.deployment.builditemtemplate;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.function.Consumer;
6+
7+
import jakarta.inject.Inject;
8+
9+
import org.jboss.shrinkwrap.api.asset.StringAsset;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.builder.BuildChainBuilder;
14+
import io.quarkus.builder.BuildContext;
15+
import io.quarkus.builder.BuildStep;
16+
import io.quarkus.qute.Template;
17+
import io.quarkus.qute.deployment.TemplatePathBuildItem;
18+
import io.quarkus.test.QuarkusUnitTest;
19+
20+
public class AdditionalTemplatePathDuplicatesResolvedTest {
21+
22+
@RegisterExtension
23+
static final QuarkusUnitTest config = new QuarkusUnitTest()
24+
.withApplicationRoot(root -> root
25+
.addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt"))
26+
.addBuildChainCustomizer(buildCustomizer());
27+
28+
static Consumer<BuildChainBuilder> buildCustomizer() {
29+
return new Consumer<BuildChainBuilder>() {
30+
@Override
31+
public void accept(BuildChainBuilder builder) {
32+
builder.addBuildStep(new BuildStep() {
33+
@Override
34+
public void execute(BuildContext context) {
35+
context.produce(TemplatePathBuildItem.builder()
36+
.path("hi.txt")
37+
.extensionInfo("test-ext")
38+
.content("Hello {name}!")
39+
.priority(100)
40+
.build());
41+
}
42+
}).produces(TemplatePathBuildItem.class)
43+
.build();
44+
45+
}
46+
};
47+
}
48+
49+
@Inject
50+
Template hi;
51+
52+
@Test
53+
public void testHi() {
54+
// Build item with higher priority takes precedence
55+
assertEquals("Hello Lu!", hi.data("name", "Lu").render());
56+
}
57+
58+
}

extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/builditemtemplate/AdditionalTemplatePathDuplicatesTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class AdditionalTemplatePathDuplicatesTest {
2020
static final QuarkusUnitTest config = new QuarkusUnitTest()
2121
.withApplicationRoot(root -> root
2222
.addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt"))
23+
.overrideConfigKey("quarkus.qute.duplicit-template-strategy", "fail")
2324
.addBuildChainCustomizer(buildCustomizer())
2425
.setExpectedException(IllegalStateException.class, true);
2526

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.quarkus.qute.deployment.scanning;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import jakarta.inject.Inject;
6+
7+
import org.jboss.shrinkwrap.api.asset.StringAsset;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkus.qute.Template;
12+
import io.quarkus.test.QuarkusUnitTest;
13+
14+
public class DuplicateTemplatesResolvedTest {
15+
16+
@RegisterExtension
17+
static final QuarkusUnitTest config = new QuarkusUnitTest()
18+
.withApplicationRoot(root -> root.addAsResource(new StringAsset("Hello!"), "templates/hello.html"))
19+
.withAdditionalDependency(
20+
d -> d.addAsResource(new StringAsset("Hi!"), "templates/hello.html"));
21+
22+
@Inject
23+
Template hello;
24+
25+
@Test
26+
public void testHello() {
27+
// Root archive takes precedence
28+
assertEquals("Hello!", hello.render());
29+
}
30+
31+
}

extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/scanning/MultipleTemplatesDirectoryDuplicateFoundTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class MultipleTemplatesDirectoryDuplicateFoundTest {
1717
.withApplicationRoot(root -> root.addAsResource(new StringAsset("Hello!"), "templates/hello.html"))
1818
.withAdditionalDependency(
1919
d -> d.addAsResource(new StringAsset("Hi!"), "templates/hello.html"))
20+
.overrideConfigKey("quarkus.qute.duplicit-template-strategy", "fail")
2021
.assertException(t -> {
2122
Throwable e = t;
2223
IllegalStateException ise = null;

extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java

+25-24
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ public class EngineProducer {
8282

8383
private final Engine engine;
8484
private final ContentTypes contentTypes;
85-
private final List<String> tags;
8685
private final List<String> suffixes;
8786
private final Set<String> templateRoots;
8887
private final Map<String, String> templateContents;
@@ -100,7 +99,6 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
10099
this.suffixes = config.suffixes();
101100
this.templateRoots = context.getTemplateRoots();
102101
this.templateContents = Map.copyOf(context.getTemplateContents());
103-
this.tags = context.getTags();
104102
this.defaultLocale = locales.defaultLocale().orElse(Locale.getDefault());
105103
this.defaultCharset = config.defaultCharset();
106104
this.container = Arc.container();
@@ -112,7 +110,8 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
112110
}
113111
this.templatePathExcludes = excludesBuilder.build();
114112

115-
LOGGER.debugf("Initializing Qute [templates: %s, tags: %s, resolvers: %s", context.getTemplatePaths(), tags,
113+
LOGGER.debugf("Initializing Qute [templates: %s, tags: %s, resolvers: %s", context.getTemplatePaths(),
114+
context.getTags(),
116115
context.getResolverClasses());
117116

118117
EngineBuilder builder = Engine.builder();
@@ -219,7 +218,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
219218
LOGGER.debugf("Added generated value resolver: %s", resolverClass);
220219
}
221220
// Add tags
222-
for (String tag : tags) {
221+
for (String tag : context.getTags()) {
223222
// Strip suffix, item.html -> item
224223
String tagName = tag.contains(".") ? tag.substring(0, tag.indexOf('.')) : tag;
225224
String tagTemplateId = TAGS + tagName;
@@ -375,7 +374,27 @@ private Optional<TemplateLocation> locate(String path) {
375374
if (isExcluded(path)) {
376375
return Optional.empty();
377376
}
378-
// First try to locate file-based templates
377+
// First try the template contents, i.e. templates not backed by files
378+
LOGGER.debugf("Locate template contents for %s", path);
379+
String content = templateContents.get(path);
380+
if (content == null) {
381+
// Try path with suffixes
382+
for (String suffix : suffixes) {
383+
String pathWithSuffix = path + "." + suffix;
384+
if (isExcluded(pathWithSuffix)) {
385+
continue;
386+
}
387+
content = templateContents.get(pathWithSuffix);
388+
if (content != null) {
389+
break;
390+
}
391+
}
392+
}
393+
if (content != null) {
394+
return Optional.of(new ContentTemplateLocation(content, createVariant(path)));
395+
}
396+
397+
// Then try to locate file-based templates
379398
for (String templateRoot : templateRoots) {
380399
URL resource = null;
381400
String templatePath = templateRoot + path;
@@ -399,25 +418,7 @@ private Optional<TemplateLocation> locate(String path) {
399418
return Optional.of(new ResourceTemplateLocation(resource, createVariant(templatePath)));
400419
}
401420
}
402-
// Then try the template contents
403-
LOGGER.debugf("Locate template contents for %s", path);
404-
String content = templateContents.get(path);
405-
if (content == null) {
406-
// Try path with suffixes
407-
for (String suffix : suffixes) {
408-
String pathWithSuffix = path + "." + suffix;
409-
if (isExcluded(pathWithSuffix)) {
410-
continue;
411-
}
412-
content = templateContents.get(pathWithSuffix);
413-
if (content != null) {
414-
break;
415-
}
416-
}
417-
}
418-
if (content != null) {
419-
return Optional.of(new ContentTemplateLocation(content, createVariant(path)));
420-
}
421+
421422
return Optional.empty();
422423
}
423424

0 commit comments

Comments
 (0)