Skip to content

Commit

Permalink
Merge pull request quarkusio#37005 from mkouba/qute-additional-base-path
Browse files Browse the repository at this point in the history
Qute: allow extensions to register additional template roots
  • Loading branch information
mkouba authored Nov 14, 2023
2 parents b2798b8 + 30173e8 commit 830ef6e
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public class QuteProcessor {
private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath";
private static final String CHECKED_TEMPLATE_DEFAULT_NAME = "defaultName";
private static final String IGNORE_FRAGMENTS = "ignoreFragments";
private static final String BASE_PATH = "templates";
private static final String DEFAULT_ROOT_PATH = "templates";

private static final Set<String> ITERATION_METADATA_KEYS = Set.of("count", "index", "indexParity", "hasNext", "odd",
"isOdd", "even", "isEven", "isLast", "isFirst");
Expand All @@ -184,6 +184,20 @@ FeatureBuildItem feature() {
return new FeatureBuildItem(Feature.QUTE);
}

@BuildStep
TemplateRootBuildItem defaultTemplateRoot() {
return new TemplateRootBuildItem(DEFAULT_ROOT_PATH);
}

@BuildStep
TemplateRootsBuildItem collectTemplateRoots(List<TemplateRootBuildItem> templateRoots) {
Set<String> roots = new HashSet<>();
for (TemplateRootBuildItem root : templateRoots) {
roots.add(root.getPath());
}
return new TemplateRootsBuildItem(Set.copyOf(roots));
}

@BuildStep
List<BeanDefiningAnnotationBuildItem> beanDefiningAnnotations() {
return List.of(
Expand Down Expand Up @@ -2045,9 +2059,9 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedPaths,
BuildProducer<TemplatePathBuildItem> templatePaths,
BuildProducer<NativeImageResourceBuildItem> nativeImageResources,
QuteConfig config)
QuteConfig config,
TemplateRootsBuildItem templateRoots)
throws IOException {
Set<Path> basePaths = new HashSet<>();
Set<ApplicationArchive> allApplicationArchives = applicationArchives.getAllApplicationArchives();
List<ResolvedDependency> extensionArtifacts = curateOutcome.getApplicationModel().getDependencies().stream()
.filter(Dependency::isRuntimeExtensionArtifact).collect(Collectors.toList());
Expand All @@ -2056,7 +2070,12 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives,
watchedPaths.produce(HotDeploymentWatchedFileBuildItem.builder().setLocationPredicate(new Predicate<String>() {
@Override
public boolean test(String path) {
return path.startsWith(BASE_PATH);
for (String rootPath : templateRoots) {
if (path.startsWith(rootPath)) {
return true;
}
}
return false;
}
}).build());

Expand All @@ -2065,54 +2084,67 @@ public boolean test(String path) {
// Skip extension archives that are also application archives
continue;
}
for (Path path : artifact.getResolvedPaths()) {
if (Files.isDirectory(path)) {
// Try to find the templates in the root dir
try (Stream<Path> paths = Files.list(path)) {
Path basePath = paths.filter(QuteProcessor::isBasePath).findFirst().orElse(null);
if (basePath != null) {
LOGGER.debugf("Found extension templates dir: %s", path);
scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources,
config);
break;
}
}
for (Path resolvedPath : artifact.getResolvedPaths()) {
if (Files.isDirectory(resolvedPath)) {
scanPath(resolvedPath, resolvedPath, config, templateRoots, watchedPaths, templatePaths,
nativeImageResources);
} else {
try (FileSystem artifactFs = ZipUtils.newFileSystem(path)) {
Path basePath = artifactFs.getPath(BASE_PATH);
if (Files.exists(basePath)) {
LOGGER.debugf("Found extension templates in: %s", path);
scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources,
config);
try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedPath)) {
for (String templateRoot : templateRoots) {
Path artifactBasePath = artifactFs.getPath(templateRoot);
if (Files.exists(artifactBasePath)) {
LOGGER.debugf("Found extension templates in: %s", resolvedPath);
scan(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, templatePaths,
nativeImageResources,
config);
}
}
} catch (IOException e) {
LOGGER.warnf(e, "Unable to create the file system from the path: %s", path);
LOGGER.warnf(e, "Unable to create the file system from the path: %s", resolvedPath);
}
}
}
}
for (ApplicationArchive archive : allApplicationArchives) {
archive.accept(tree -> {
for (Path rootDir : tree.getRoots()) {
for (Path root : tree.getRoots()) {
// Note that we cannot use ApplicationArchive.getChildPath(String) here because we would not be able to detect
// a wrong directory name on case-insensitive file systems
try (Stream<Path> rootDirPaths = Files.list(rootDir)) {
Path basePath = rootDirPaths.filter(QuteProcessor::isBasePath).findFirst().orElse(null);
if (basePath != null) {
LOGGER.debugf("Found templates dir: %s", basePath);
basePaths.add(basePath);
scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources,
config);
break;
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
scanPath(root, root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources);
}
});
}
}

private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedPaths,
BuildProducer<TemplatePathBuildItem> templatePaths,
BuildProducer<NativeImageResourceBuildItem> nativeImageResources) {
if (!Files.isDirectory(path)) {
return;
}
try (Stream<Path> paths = Files.list(path)) {
for (Path file : paths.collect(Collectors.toList())) {
if (Files.isDirectory(file)) {
// Iterate over the directories in the root
// "/io", "/META-INF", "/templates", "/web", etc.
Path relativePath = rootPath.relativize(file);
if (templateRoots.isRoot(relativePath)) {
LOGGER.debugf("Found templates dir: %s", file);
scan(file, file, file.getFileName() + "/", watchedPaths, templatePaths,
nativeImageResources,
config);
} else if (templateRoots.maybeRoot(relativePath)) {
// Scan the path recursively because the template root may be nested, for example "/web/public"
scanPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources);
}
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@BuildStep
TemplateFilePathsBuildItem collectTemplateFilePaths(QuteConfig config, List<TemplatePathBuildItem> templatePaths) {
Set<String> filePaths = new HashSet<String>();
Expand Down Expand Up @@ -2367,7 +2399,8 @@ public boolean test(TypeCheck check) {
void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecorder recorder,
List<GeneratedValueResolverBuildItem> generatedValueResolvers, List<TemplatePathBuildItem> templatePaths,
Optional<TemplateVariantsBuildItem> templateVariants,
List<GeneratedTemplateInitializerBuildItem> templateInitializers) {
List<GeneratedTemplateInitializerBuildItem> templateInitializers,
TemplateRootsBuildItem templateRoots) {

List<String> templates = new ArrayList<>();
List<String> tags = new ArrayList<>();
Expand All @@ -2391,7 +2424,8 @@ void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecord
.supplier(recorder.createContext(generatedValueResolvers.stream()
.map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates,
tags, variants, templateInitializers.stream()
.map(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList())))
.map(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList()),
templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet())))
.done());
}

Expand Down Expand Up @@ -3353,10 +3387,6 @@ private static boolean isExcluded(TypeCheck check, Iterable<Predicate<TypeCheck>
return false;
}

private static boolean isBasePath(Path path) {
return path.getFileName().toString().equals(BASE_PATH);
}

private void checkDuplicatePaths(List<TemplatePathBuildItem> templatePaths) {
Map<String, List<TemplatePathBuildItem>> duplicates = templatePaths.stream()
.collect(Collectors.groupingBy(TemplatePathBuildItem::getPath));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.quarkus.qute.deployment;

import io.quarkus.builder.item.MultiBuildItem;

/**
* This build item represents a source of template files.
* <p>
* By default, the templates are found in the {@code templates} directory. However, an extension can produce this build item to
* register an additional root path.
* <p>
* The path is relative to the artifact/project root and OS-agnostic, i.e. {@code /} is used as a path separator.
*/
public final class TemplateRootBuildItem extends MultiBuildItem {

private final String path;

public TemplateRootBuildItem(String path) {
this.path = normalize(path);
}

public String getPath() {
return path;
}

static String normalize(String path) {
path = path.strip();
if (path.startsWith("/")) {
path = path.substring(1);
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.quarkus.qute.deployment;

import java.io.File;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Set;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* The set of template root paths.
*/
public final class TemplateRootsBuildItem extends SimpleBuildItem implements Iterable<String> {

private Set<String> rootPaths;

public TemplateRootsBuildItem(Set<String> paths) {
this.rootPaths = paths;
}

public Set<String> getPaths() {
return rootPaths;
}

@Override
public Iterator<String> iterator() {
return rootPaths.iterator();
}

/**
* The path must be relative to the resource root.
*
* @param path
* @return {@code true} is the given path represents a template root, {@code false} otherwise
*/
public boolean isRoot(Path path) {
String pathStr = normalize(path);
for (String rootPath : rootPaths) {
if (pathStr.equals(rootPath)) {
return true;
}
}
return false;
}

/**
* The path must be relative to the resource root.
*
* @param path
* @return {@code true} is the given path may represent a template root, {@code false} otherwise
*/
public boolean maybeRoot(Path path) {
String pathStr = normalize(path);
for (String rootPath : rootPaths) {
if ((rootPath.contains("/") && rootPath.startsWith(pathStr))
|| rootPath.equals(pathStr)) {
return true;
}
}
return false;
}

private static String normalize(Path path) {
String pathStr = path.toString();
if (File.separatorChar != '/') {
// \foo\bar\templates -> /foo/bar/templates
pathStr = pathStr.replace(File.separatorChar, '/');
}
return TemplateRootBuildItem.normalize(pathStr);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.quarkus.qute.deployment.templateroot;

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.Template;
import io.quarkus.qute.deployment.TemplateRootBuildItem;
import io.quarkus.test.QuarkusUnitTest;

public class AdditionalTemplateRootTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt")
.addAsResource(new StringAsset("Hello {name}!"), "web/public/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 TemplateRootBuildItem("web/public"));
}
}).produces(TemplateRootBuildItem.class)
.build();
}
};
}

@Inject
Template hello;

@Inject
Engine engine;

@Test
public void testTemplate() {
assertEquals("Hi M!", engine.getTemplate("hi").data("name", "M").render());
assertEquals("Hello M!", hello.data("name", "M").render());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.qute.deployment.templateroot;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

import io.quarkus.qute.deployment.TemplateRootBuildItem;

public class TemplateRootBuildItemTest {

@Test
public void testNormalizedName() {
assertEquals("foo", new TemplateRootBuildItem("/foo/ ").getPath());
assertEquals("foo/bar", new TemplateRootBuildItem("/foo/bar").getPath());
assertEquals("baz", new TemplateRootBuildItem(" baz/").getPath());
}

}
Loading

0 comments on commit 830ef6e

Please sign in to comment.