Type-safe Templates

Type-safe Templates

There’s an alternate way to declare your templates in your Java itself, which relies on the following convention:
 

  • Organize your template files in the /src/main/resources/templates directory by grouping them into one directory per resource class. For example, if your ItemResource class references two templates hello and goodbye, place them at /src/main/resources/templates/ItemResource/hello.txt and /src/main/resources/templates/ItemResource/goodbye.txt respectively. Grouping templates per resource class makes it easier to navigate to them.
  • In each of your resource classes, declare a @CheckedTemplate static class Template {} class within your resource class.
  • Declare one public static native TemplateInstance method per template file for your resource.
  • Use those static methods to build your template instances.

Creating a Simple Template

Create a directory to hold templates for our HelloResource class (run this command from the root directory of the project):

mkdir -p src/main/resources/templates/HelloResource

Create a new file at src/main/resources/templates/HelloResource/hello.txt and set the contents to be:

Hello {name} from HelloResource!
Warning: Note that the hello.txt file in the previous step is located directly in the templates directory. Since this example is using the alternate convention, you'll be creating another file with the same name in the HelloResource subdirectory.

For the goodbye template, create a new file at src/main/resources/templates/HelloResource/goodbye.txt and enter the contents:

Goodbye {name} from HelloResource!

Even though you created new template files for this new convention, the code will still exist in the HelloResource.java file. Open that file (src/main/java/org/acme/HelloResource.java) and replace the existing contents with the following:
 

package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.CheckedTemplate;

@Path("hello")
public class HelloResource {

    @CheckedTemplate(requireTypeSafeExpressions = false)
    public static class Templates {
        public static native TemplateInstance hello(String name);
        public static native TemplateInstance goodbye(String name);
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return HelloResource.Templates.hello(name);
    }
}

A few notes on the updated code:

  • This code expects to use a template at the path templates/HelloResource/hello.txt, since the @CheckedTemplate static class is declared inside the HelloResource class. The name of the method, hello, is used to match files in the directory with common extensions like .txt or .html. You can specify an exact name and path using @ResourcePath.
  • The Templates.hello() method returns a new template instance that can be customized before the actual rendering is triggered. In this case, you put the name value under the key name. The data map is accessible during rendering.
  • Again, notice that we don’t explicitly trigger the rendering - this is done automatically.
  • Once you have declared a @CheckedTemplate class, Quarkus will check that all of its methods point to existing templates. As a result, if you try to use a template from your Java code and you forgot to add it, Quarkus will let you know at build time.

Keep in mind this style of declaration allows you to reference templates declared in other resources too.

Creating the Goodbye Resource

To fulfill the goodbye endpoint, you need to create a new resource.
 

Create a file at src/main/java/org/acme/GoodbyeResource.java and enter the following contents:
package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;

@Path("goodbye")
public class GoodbyeResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return HelloResource.Templates.goodbye(name);
    }
}

Testing the Endpoints

Let's start by testing the new implementation of the hello endpoint:

curl http://localhost:8080/hello?name=Daniel

You should see:

Hello Daniel from HelloResource!
Test the goodbye endpoint through a similar URL:
curl http://localhost:8080/goodbye?name=Daniel

This time, you should see the goodbye template in use:

Goodbye Daniel from HelloResource!

As stated earlier, since the TemplateInstance is declared as an inner class inside of HelloResource, Qute will attempt to locate the template in the HelloResource/ subdirectory. If instead, you want to create a top-level declaration, you can do this inside a separate class (do not copy this code for this exercise):

@CheckedTemplate
public class Templates {
    public static native TemplateInstance hello();
    public static native TemplateInstance goodbye();
}

This will cause Qute to look for the associated hello.txt or goodbye.txt in the src/main/resources/templates directory, instead of src/main/resources/templates/HelloResource. It is up to you how you wish to organize your templates.

Daniel Oh
Daniel Oh
Senior Principal Developer Advocate
Daniel Oh is a Senior Principal Developer Advocate at Red Hat. He works to evangelize building cloud-native microservices and serverless functions with cloud-native runtimes to developers. He also continues to contribute to various open-source cloud projects and ecosystems as a Cloud Native Computing Foundation (CNCF) ambassador for accelerating DevOps adoption in enterprises. Daniel also speaks at technical seminars, workshops, and meetups to elaborate on new emerging technologies for enterprise developers, SREs, platform engineers, and DevOps teams.