A logo showing the text blog.marcnuri.com
Español
Home»Java»Quarkus + JKube: Qute template with markdown processing from different sources

Recent Posts

  • Fabric8 Kubernetes Client 7.2 is now available!
  • Connecting to an MCP Server from JavaScript using AI SDK
  • Connecting to an MCP Server from JavaScript using LangChain.js
  • The Future of Developer Tools: Adapting to Machine-Based Developers
  • Connecting to a Model Context Protocol (MCP) Server from Java using LangChain4j

Categories

  • Artificial Intelligence
  • Front-end
  • Go
  • Industry and business
  • Java
  • JavaScript
  • Legacy
  • Operations
  • Personal
  • Pet projects
  • Tools

Archives

  • May 2025
  • April 2025
  • March 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • August 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • July 2022
  • June 2022
  • May 2022
  • March 2022
  • February 2022
  • January 2022
  • December 2021
  • November 2021
  • October 2021
  • September 2021
  • August 2021
  • July 2021
  • January 2021
  • December 2020
  • November 2020
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • February 2020
  • January 2020
  • December 2019
  • October 2019
  • September 2019
  • July 2019
  • March 2019
  • November 2018
  • July 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • December 2017
  • July 2017
  • January 2017
  • December 2015
  • November 2015
  • December 2014
  • March 2014
  • February 2011
  • November 2008
  • June 2008
  • May 2008
  • April 2008
  • January 2008
  • November 2007
  • September 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007

Quarkus + JKube: Qute template with markdown processing from different sources

2020-08-19 in Java tagged Eclipse / Java / Eclipse JKube / Quarkus / Web by Marc Nuri | Last updated: 2021-05-16

Introduction

Quarkus provides its very own templating engine, Qute. In this post, I will show you how to use it to render Markdown from different sources using flexmark-java.

Besides showcasing Qute, another reason for this post is to show complex assembly configurations to build container images using Eclipse JKube. You can learn more about JKube and how to get started here.

I designed the project to be run in Kubernetes, so some of the features won’t be available if the application is run locally. The application renders a template with markdown fragments loaded from different locations, highlighting how these fragments can be assembled into the container image using different techniques and ultimately packaged with Eclipse JKube.

Quarkus application

This is a really simple application with a single Template and resource.

Main template

The main template file is located in src/main/resources/templates/main.html, Qute loads templates located in this directory.

The file contains a few placeholders/values that will be replaced with markdown content parsed using flexmark-java.

1<!-- ... -->
2<div>{markdownInResources.raw}</div>
3<div>{markdownInJKubeAssembly.raw}</div>
4<div class="debug-info">{markdownFiltered.raw}</div>
5<!-- ... -->

Qute will replace each of the placeholders with the content provided in the data method for the loaded template. The .raw suffix prevents Qute from escaping the HTML provided by flexmark-java.

Main resource class

1@Path("/")
2public class MainResource {
3  /* ... */
4  @GET
5  @Produces(MediaType.TEXT_HTML)
6  public TemplateInstance get() throws IOException {
7    return main
8      .data("title", "Hello Quarkus, you're so qute!")
9      .data("markdownInResources", md.render(MainResource.class.getResourceAsStream("/main.md")))
10      // Following files will only be available if application was deployed using Eclipse JKube
11      .data("markdownInJKubeAssembly", md.render(new File(OPT_STATIC,"main.md")))
12      .data("markdownFiltered", md.render(new File(OPT_STATIC, "filtered-fragment.md")));
13  }
14
15  @GET
16  @Path("static/{fileName:.+}")
17  @Produces(MediaType.APPLICATION_OCTET_STREAM)
18  public File getStaticFile(@PathParam("fileName") String fileName) {
19    // File only available if application was deployed using Eclipse JKube
20    return new File(OPT_STATIC, fileName);
21  }
22}

I implemented the MainResource class to expose two endpoints.

The first endpoint delivers the processed template file and is accessible in the root path of the web application. I use the data method to replace 4 placeholders. The markdownInResources is located in a packaged resource, so it can always be loaded. The rest of the markdown resources depend on the assembly I describe later on. So if you don’t deploy the application using JKube, an error will be displayed instead of the markdown fragment.

The second endpoint delivers files located in the /opt/static directory. The regex pattern in the @Path annotation allows us to retrieve the file name of the requested file from the URL path (e.g. static/javaee-api-8.0.jar). Again, this only makes sense in the JKube deployed version of the application.

Project pom.xml

The Maven project pom.xml file is divided into several sections, let’s analyze the most important ones.

Properties

1<properties>
2  <maven.compiler.source>11</maven.compiler.source>
3  <maven.compiler.target>11</maven.compiler.target>
4  <buildTimestamp>${maven.build.timestamp}</buildTimestamp>
5  <version.quarkus>1.7.0.Final</version.quarkus>
6  <version.flexmark>0.62.2</version.flexmark>
7  <version.jkube>1.0.0</version.jkube>
8  <jkube.enricher.jkube-service.type>NodePort</jkube.enricher.jkube-service.type>
9</properties>

In the properties element, I’ve included the versions of the project dependencies that will be used later on and the Java target and sources version.

Besides, there is a buildTimestamp property that will be used in one of the filtered resources to replace a property placeholder in a Markdown file.

Note the JKube specific property too, this one is used to define the Kubernetes Service port type so that the service can be exposed later on using minikube service example.

Dependencies

1<dependencies>
2  <dependency>
3    <groupId>io.quarkus</groupId>
4    <artifactId>quarkus-resteasy-qute</artifactId>
5    <version>${version.quarkus}</version>
6  </dependency>
7  <dependency>
8    <groupId>com.vladsch.flexmark</groupId>
9    <artifactId>flexmark-all</artifactId>
10    <version>${version.flexmark}</version>
11  </dependency>
12  <dependency>
13    <!-- Random dependency to include in the image with preassemble.xml, Maven Assembly Plugin and JKube -->
14    <groupId>javax</groupId>
15    <artifactId>javaee-api</artifactId>
16    <version>8.0</version>
17    <scope>provided</scope>
18  </dependency>
19</dependencies>

The dependencies section, as usual, contains the project dependencies required to build and run the application.

However, I’ve included an additional random dependency with scope provided. As I’ll explain later on, I configured the Maven Assembly Plugin to process it.

Build: Resources

1<build>
2  <resources>
3    <resource>
4      <directory>src/main/resources</directory>
5    </resource>
6    <resource>
7      <directory>${project.basedir}/src/main/filtered-resources</directory>
8      <targetPath>${project.basedir}/target/filtered-resources</targetPath>
9      <filtering>true</filtering>
10    </resource>
11  </resources>
12  <!-- ... -->
13</build>

In addition to the standard convention src/main/resources directory for resources, I’m going to add an additional src/main/filtered-resources directory. This directory is configured to be filtered, which means that any property placeholder (e.g. ${project.artifactId }) will be replaced with the applicable Maven property when processed.

This directory contains a single file filtered-fragment.md with several properties including the buildTimestamp described earlier.

Build: Maven Assembly Plugin

1<build>
2  <!-- ... -->
3  <plugins>
4    <plugin>
5      <artifactId>maven-assembly-plugin</artifactId>
6      <configuration>
7        <outputDirectory>target</outputDirectory>
8        <archiveBaseDirectory>/</archiveBaseDirectory>
9        <appendAssemblyId>false</appendAssemblyId>
10        <finalName>preassembled</finalName>
11        <descriptors>
12          <descriptor>preassemble.xml</descriptor>
13        </descriptors>
14      </configuration>
15      <executions>
16        <execution>
17          <id>preassemble-files</id>
18          <phase>package</phase>
19          <goals>
20            <goal>single</goal>
21          </goals>
22        </execution>
23      </executions>
24    </plugin>
25    <!-- ... -->
26  </plugins>
27  <!-- ... -->
28</build>

I’m using the Maven Assembly Plugin to show how to include a dependencySet in the container image.

The plugin configuration references an assembly descriptor preassemble.xml, that will be processed with its output placed in the target directory. The finalName and appendAssemblyId configurations force the assembly to be placed in the resulting directory target/preassembled.

I’ve also bound the execution of the plugin to the package phase. The plugin execution will be triggered whenever the Maven package lifecycle phase runs.

Following is the content of the preassemble.xml file:

1<assembly>
2  <id>preassembled</id>
3  <formats>
4    <format>dir</format>
5  </formats>
6  <includeBaseDirectory>false</includeBaseDirectory>
7  <dependencySets>
8    <dependencySet>
9      <useProjectArtifact>false</useProjectArtifact>
10      <outputDirectory>/</outputDirectory>
11      <scope>provided</scope>
12    </dependencySet>
13  </dependencySets>
14</assembly>

I’ve configured the assembly to define a single dependencySet that will copy all dependencies with scope provided and their transitive dependencies to the assembly directory root (target/preassembled). Given the current combination of dependencies and assembly configuration, this directory should contain the following files when you package the project:

  • activation-1.1.jar
  • javaee-api-8.0.jar
  • javax.mail-1.6.0.jar

Build: Eclipse JKube

1<build>
2  <!-- ... -->
3  <plugins>
4    <!-- ... -->
5    <plugin>
6      <groupId>org.eclipse.jkube</groupId>
7      <artifactId>kubernetes-maven-plugin</artifactId>
8      <version>${version.jkube}</version>
9      <configuration>
10        <images>
11          <image>
12            <name>%g/%a:%l</name>
13            <build>
14              <from>adoptopenjdk/openjdk11:jre-11.0.8_10-ubi</from>
15              <ports>8080</ports>
16              <entryPoint>
17                <exec>
18                  <arg>java</arg>
19                  <arg>-jar</arg>
20                  <arg>-Dquarkus.http.host=0.0.0.0</arg>
21                  <arg>/deployments/${project.artifactId}-${project.version}-runner.jar</arg>
22                </exec>
23              </entryPoint>
24              <assembly>
25                <targetDir>/</targetDir>
26                <excludeFinalOutputArtifact>true</excludeFinalOutputArtifact>
27                <inline>
28                  <fileSets>
29                    <fileSet>
30                      <directory>src/main/static</directory>
31                      <outputDirectory>opt/static</outputDirectory>
32                    </fileSet>
33                    <fileSet>
34                      <directory>target/filtered-resources</directory>
35                      <outputDirectory>opt/static</outputDirectory>
36                    </fileSet>
37                    <fileSet>
38                      <directory>target/preassembled</directory>
39                      <outputDirectory>opt/static</outputDirectory>
40                    </fileSet>
41                    <fileSet>
42                      <directory>target</directory>
43                      <outputDirectory>deployments/</outputDirectory>
44                      <includes>
45                        <include>*-runner.jar</include>
46                      </includes>
47                    </fileSet>
48                    <fileSet>
49                      <directory>target/lib</directory>
50                      <outputDirectory>deployments/lib</outputDirectory>
51                      <includes>
52                        <include>*.jar</include>
53                      </includes>
54                    </fileSet>
55                  </fileSets>
56                </inline>
57              </assembly>
58            </build>
59          </image>
60        </images>
61      </configuration>
62    </plugin>
63  </plugins>
64</build>

For this project, I’ve configured Eclipse JKube’s Kubernetes Maven Plugin to build a customized image.

Please note that there are other opinionated configuration-less approaches to build a Quarkus image. But in this case, since I want to demonstrate the power of assemblies I chose this procedure instead.

In the build section of the image I’ve defined the base image (from), the exposed port and the image entry point with the standard values.

Next comes the most important part, assembly configuration. The targetDir defines the directory under which files will be copied within the container. In this case, since I want to copy files to several locations, I used the root directory /. Later on, I can define specific directories for each fileSet.

The first fileSet configures JKube to copy all files in src/main/static to the container’s /opt/static directory.

The second fileSet configures JKube to grab the files previously processed and filtered by the Maven Resources Plugin (build:resources) and copy them to the /opt/static directory too.

The third fileSet configures JKube to copy the JAR files processed by the Maven Assembly Plugin to the /opt/static directory.

You can probably see the pattern now. We use other Maven plugins to gather files in the package phase and then JKube will assemble those files in the container image during the k8s:build goal.

The fourth and fifth fileSet configure JKube to copy the Quarkus generated artifact and dependencies to the /deployments and /deployments/lib directory.

Running the Quarkus application

For simplicity purposes and to avoid pushing the generated image to a shared registry I will be using Minikube.

To avoid having to push the image to a shared registry accessible from the cluster, I will share Minikube’s Docker daemon with my local environment:

1eval $(minikube docker-env)

Next, I’ll instruct Maven to package the application and to run JKube’s Kubernetes Maven Plugin goals:

1mvn package k8s:build k8s:resource k8s:apply

We can now open the application in our browser by running:

1minikube service example
An image displaying a browser with the deployed application
An image displaying a browser with the deployed application

If everything goes well, we will see a browser window with content similar to the previous image.

As you can see, the three markdown fragments have been replaced and loaded correctly with their appropriate styles and markup. The fragment with the debug information shows up with the placeholders correctly replaced with Maven properties. If you click on the Go! link, the JAR preassembled from the provided dependency should be downloaded too.

Conclusion

In this article, I showed you how to use Quarkus’ Qute template engine to render Markdown from different sources using flexmark-java. I’ve also explained how you can assemble these sources using different Maven Plugins and Eclipse JKube.

You can find the full source code for this post at GitHub.

You can also check out our team’s Quarkus+JKube demos on Eclipse’s virtual booth at J4K 2020.

Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Post navigation
Kubernetes 1.19 Ingress API from Java using YAKCApache Camel used on a Kubernetes Cassandra cluster
© 2007 - 2025 Marc Nuri