Renarde renarde head Web Framework

Renarde

Renarde renarde head is a server-side Web Framework based on Quarkus, Qute, Hibernate and RESTEasy Reactive.

<dependency>
  <groupId>io.quarkiverse.renarde</groupId>
  <artifactId>quarkus-renarde</artifactId>
  <version>1.0.5</version>
</dependency>

First: an example

Let’s see how you can quickly build a Web Application with Renarde. Let’s start with a Controller:

package rest;

import javax.ws.rs.Path;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import io.quarkiverse.renarde.Controller;

public class Application extends Controller {

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

    @Path("/")
    public TemplateInstance index() {
        return Templates.index();
    }
}

A Controller is the logic class that binds URIs to actions and views. They are almost like regular JAX-RS endpoints, but you opt-in to special magic by extending the Controller class, which gives you nice methods, but also super friendly behaviour.

In this Controller we declare a Qute template, and map the / to it.

We can then define the main page in src/main/resources/templates/Application/index.html:

<!DOCTYPE html>
<html lang="en">
  <body>
    Hello, World!
  </body>
</html>

Now if you navigate to your application at http://localhost:8080 you will see Hello, World! rendered.

Models

By convention, you can place your model classes in the model package, but anywhere else works just as well. We recommend using Hibernate ORM with Panache. Here’s an example entity for our sample Todo application:

package model;

import java.util.Date;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.ManyToOne;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Todo extends PanacheEntity {

    @ManyToOne
    public User owner;

    public String task;

    public boolean done;

    public Date doneDate;

    public static List<Todo> findByOwner(User user) {
        return find("owner = ?1 ORDER BY id", user).list();
    }
}

Controllers

By convention, you can place your controllers in the rest package, but anywhere else works just as well. You have to extend the Controller class in order to benefit from extra easy endpoint declarations and reverse-routing, but that superclass also gives you useful methods. We usually have one controller per model class, so we tend to use the plural entity name for the corresponding controller:

package rest;

import java.util.Date;
import java.util.List;

import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;

import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;

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

public class Todos extends Controller {

    @CheckedTemplate
    static class Templates {
        public static native TemplateInstance index(List<Todo> todos);
    }

    public TemplateInstance index() {
        // list every todo
        List<Todo> todos = Todo.listAll();
        // render the index template
        return Templates.index(todos);
    }

    @POST
    public void delete(@RestPath Long id) {
        // find the Todo
        Todo todo = Todo.findById(id);
        notFoundIfNull(todo);
        // delete it
        todo.delete();
        // send loving message
        flash("message", "Task deleted");
        // redirect to index page
        index();
    }

    @POST
    public void done(@RestPath Long id) {
        // find the Todo
        Todo todo = Todo.findById(id);
        notFoundIfNull(todo);
        // switch its done state
        todo.done = !todo.done;
        if(todo.done)
            todo.doneDate = new Date();
        // send loving message
        flash("message", "Task updated");
        // redirect to index page
        index();
    }

    @POST
    public void add(@NotBlank @RestForm String task) {
        // check if there are validation issues
        if(validationFailed()) {
            // go back to the index page
            index();
        }
        // create a new Todo
        Todo todo = new Todo();
        todo.task = task;
        todo.persist();
        // send loving message
        flash("message", "Task added");
        // redirect to index page
        index();
    }
}

Methods

Every public method is a valid endpoint. If it has no HTTP method annotation (@GET, @HEAD, @POST, @PUT, @DELETE) then it is assumed to be a @GET method.

Most @GET methods will typically return a TemplateInstance for rendering an HTML server-side template, and should not modify application state.

Controller methods annotated with @POST, @PUT and @DELETE will typically return void and trigger a redirect to a @GET method after they do their action. This is not mandatory, you can also return a TemplateInstance if you want, but it is good form to use a redirect to avoid involuntary actions when browsers reload the page. Those methods also get an implicit @Transactional annotation so you don’t need to add it.

If your controller is not annotated with @Path it will default to a path using the class name. If your controller method is not annotated with @Path it will default to a path using the method name. The exception is if you have a @Path annotation on the method with an absolute path, in which case the class path part will be ignored. Here’s a list of example annotations and how they result:

Class declaration Method declaration URI

class Foo

public TemplateInstance bar()

Foo/bar

@Path("f") class Foo

public TemplateInstance bar()

f/bar

class Foo

@Path("b") public TemplateInstance bar()

Foo/b

@Path("f") class Foo

@Path("b") public TemplateInstance bar()

f/b

class Foo

@Path("/bar") public TemplateInstance bar()

bar

@Path("f") class Foo

@Path("/bar") public TemplateInstance bar()

f/bar

Furthermore, if you specify path parameters that are not present in your path annotations, they will be automatically appended to your path:

public class Orders extends Controller {

    // The URI will be Orders/get/{owner}/{id}
    public TemplateInstance get(@RestPath String owner, @RestPath Long id) {
    }

    // The URI will be /orders/{owner}/{id}
    @Path("/orders")
    public TemplateInstance otherGet(@RestPath String owner, @RestPath Long id) {
    }
}

Views

You can place your Qute views in the src/main/resources/templates folder, using the {className}/{methodName}.html naming convention.

Every controller that has views should declare them with a nested static class annotated with @CheckedTemplate:

public class Todos extends Controller {

    @CheckedTemplate
    static class Templates {
        public static native TemplateInstance index(List<Todo> todos);
    }

    public TemplateInstance index() {
        // list every todo
        List<Todo> todos = Todo.listAll();
        // render the index template
        return Templates.index(todos);
    }
}

Here we’re declaring the Todos/index.html template, specifying that it takes a todos parameter of type List<Todo> which allows us to validate the template at build-time.

Templates are written in Qute, and you can also declare imported templates in order to validate them using a toplevel class, such as the main.html template:

package rest;

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

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

Template composition

Typical web applications will have a main template for their layout and use composition in every method. For example, we can declare the following main template in main.html:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>{#insert title /}</title>
        <meta charset="UTF-8">
        <link rel="stylesheet" media="screen" href="/stylesheets/main.css">
        {#insert moreStyles /}
        <script src="/javascripts/main.js" type="text/javascript" charset="UTF-8"></script>
        {#insert moreScripts /}
    </head>
    <body>
        {#insert /}
    </body>
</html>

And then use it in our Todos/index.html template to list the todo items:

{#include main.html }
{#title}Todos{/title}

<table class="table">
  <thead>
    <tr>
      <th>#</th>
      <th>Task</th>
    </tr>
  </thead>
  <tbody>
    {#for todo in todos}
    <tr>
      <th>{todo.id}</th>
      <td>{todo.task}</td>
    </tr>
    {/for}
  </tbody>
</table>

{/include}

Standard tags

Tag Description

for/each

Iterate over collections

if/else

Conditional statement

switch/case

Switch statement

with

Adds value members to the local scope

let

Declare local variables

include/insert

Template composition

User tags

If you want to declare additional tags in order to be able to repeat them in your templates, simply place them in the templates/tags folder. For example, here is our user.html tag:

<span class="user-link" title="{it.userName}">
{#if img??}
{#gravatar it.email size=size.or(20) default='mm' /}
{/if}
{it.userName}</span>

Which allows us to use it in every template:

{#if inject:user}
    {#if inject:user.isAdmin}<span class="bi-star-fill" title="You are an administrator"></span>{/if}
    {#user inject:user img=true size=20/}
{/if}

You can pass parameters to your template with name=value pairs, and the first unnamed parameter value becomes available as the it parameter.

See the Qute documentation for more information.

Renarde tags

Renarde comes with a few extra tags to make your life easier:

Tag Description

{#authenticityToken/}

Generate a hidden HTML form element containing a CRSF token to be matched in the next request.

{#error 'field'/}

Inserts the error message for the given field name

{#form uri method='POST' class='css' id='id'}…​{/form}

Generates an HTML form for the given URI, method (defaults to POST) and optional CSS classes and IDs. Includes a CRSF token.

{#gravatar email size='mm'/}

Inserts a gravatar image for the given email, with optional size (defaults to mm)

{#ifError 'field'}…​{/ifError}

Conditional statement executed if there is an error for the given field

Extension methods

If you need additional methods to be registered to be used on your template expressions, you can declare static methods in a class annotated with @TemplateExtension:

package util;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import io.quarkus.qute.TemplateExtension;

@TemplateExtension
public class JavaExtensions {

    public static boolean isRecent(Date date){
        Date now = new Date();
        Calendar cal = new GregorianCalendar();
        cal.add(Calendar.MONTH, -6);
        Date sixMonthsAgo = cal.getTime();
        return date.before(now) && date.after(sixMonthsAgo);
    }

}

This one declares an additional method on the Date type, allowing you to test whether a date is recent or not:

{#if todo.done && todo.doneDate.isRecent()}
    This was done recently!
{/if}

Renarde extension methods

Target type Method Description

Date

format()

Formats the date to the dd/MM/yyyy format

Date

internetFormat()

Formats the date to the yyyy-MM-dd format

Date

future()

Returns true if the date is in the future

Date

since()

Formats the date in terms of X seconds/minutes/hours/days/months/years ago

String

md5()

Returns an MD5 hash of the given string

Object

instanceOf(className)

Returns true if the given object is exactly of the specified class name

External CSS, JavaScript libraries

You can use webjars to provide third-party JavaScript or CSS. For example, here is how you can import Bootstrap and Bootstrap-icons in your pom.xml:

<dependency>
  <groupId>org.webjars</groupId>
  <artifactId>bootstrap</artifactId>
  <version>5.1.3</version>
</dependency>
<dependency>
  <groupId>org.webjars.npm</groupId>
  <artifactId>bootstrap-icons</artifactId>
  <version>1.7.0</version>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-webjars-locator</artifactId>
</dependency>

After that, you can include them in your Qute templates with:

<head>
    <link rel="stylesheet" media="screen" href="/webjars/bootstrap/css/bootstrap.min.css">
    <link rel="stylesheet" media="screen" href="/webjars/bootstrap-icons/font/bootstrap-icons.css">
    <script src="/webjars/bootstrap/js/bootstrap.min.js" type="text/javascript" charset="UTF-8"></script>
</head>

Look at https://mvnrepository.com/artifact/org.webjars for the list of available options.

Forms

A lot of the time, you need to send data from the browser to your endpoints, which is often done with forms.

The HTML form

Creating forms in Renarde is easy: let’s see an example of how to do it in Qute:

{#form uri:Login.complete(newUser.confirmationCode)}

<fieldset>
    <legend>Complete registration for {newUser.email}</legend>
    {#formElement name="userName" label="User Name"}
        {#input name="userName"/}
    {/formElement}
    {#formElement name="password" label="Password"}
        {#input name="password" type="password"/}
    {/formElement}
    {#formElement name="password2" label="Password Confirmation"}
        {#input name="password2" type="password"/}
    {/formElement}
    {#formElement name="firstName" label="First Name"}
        {#input name="firstName"/}
    {/formElement}
    {#formElement name="lastName" label="Last Name"}
        {#input name="lastName"/}
    {/formElement}
    <button type="submit" class="btn btn-primary">Complete registration</button>
</fieldset>

{/form}

Here we’re defining a form whose action will go to Register.complete(newUser.confirmationCode) and which contains several form elements, which are just tags to make composition easier. For example formElement is a custom Qute tag for Bootstrap which defines layout for the form element and displays any associated error:

<div class="mb-3">
    <label class="form-label" for="{name}">{label}</label>
    {nested-content}
    {#ifError name}
        <span class="invalid-feedback">​{#error name/}</span>​
    {/ifError}
</div>

The input user tag is also designed for Bootstrap as an abstraction:

<input name="{name}"
 type="{type ?: 'text'}"
 placeholder="{placeholder ?: ''}"
 class="form-control {#ifError name}is-invalid{/ifError}"
 maxlength="{global:VARCHAR_SIZE}"
 value="{inject:flash.get(name)}"/>

As you can see, we have default values for certain attributes, a special error class if there is a validation error, and we default the value to the one preserved in the flash scope, which is filled whenever validation fails, so that the user can see the validation error without losing their form values.

As for the form Renarde tag, it is also fairly simple, and only includes an authenticity token for CRSF protection.

<form action="{it}" method="{method ?: 'POST'}" class="{klass ?: ''}">
 {#authenticityToken/}
 {nested-content}
</form>

The endpoint

Most forms will be a @POST endpoint, with each form element having a corresponding parameter annotated with @RestForm.

@POST
public void complete(@RestQuery String confirmationCode,
        @RestForm String userName,
        @RestForm String password,
        @RestForm String password2,
        @RestForm String firstName,
        @RestForm String lastName) {
    // do something with the form parameters
}

You can also group parameters in a POJO, but for now you have to add a special @Consumes(MediaType.MULTIPART_FORM_DATA) annotation:

@Consumes(MediaType.MULTIPART_FORM_DATA)
@POST
public void complete(@RestQuery String confirmationCode,
        FormData form) {
    // do something with the form parameters
}

public static class FormData {
    @RestForm String userName;
    @RestForm String password;
    @RestForm String password2;
    @RestForm String firstName;
    @RestForm String lastName;
}

Check out the RESTEasy Reactive documentation for more information about form parameters and multi-part.

Validation

You can place your usual Hibernate Validation annotations on the controller methods that receive user data, but keep in mind that you have to check for validation errors in your method before you do any action that modifies your state. This allows you to check more things than you can do with just annotations, with richer logic:

@POST
public Response complete(@RestQuery String confirmationCode,
        @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String userName,
        @RestForm @NotBlank @Length(min = 8, max = Util.VARCHAR_SIZE) String password,
        @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String password2,
        @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String firstName,
        @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String lastName) {
    // Find the user for this confirmation code
    User user = User.findForContirmation(confirmationCode);
    if(user == null){
        validation.addError("confirmationCode", "Invalid confirmation code");
    }

    // Make sure the passwords match
    validation.equals("password", password, password2);

    // Make sure the username is free
    if(User.findByUserName(userName) != null){
        validation.addError("userName", "User name already taken");
    }

    // If validation failed, redirect to the confirm page
    if(validationFailed()){
        confirm(confirmationCode);
    }

    // Now proceed to complete user registration
    ...
}

You can use the validation object to trigger additional validation logic and collect errors.

Those errors are then placed in the flash scope by a call to validationFailed() if there are any errors, and thus preserved when you redirect from your action method to the @GET method that holds the submitted form, which you can then access in your views using the {#ifError field}{/ifError} conditional tag, or the {#error field/} tag which accesses the error message for the given field.

Routing, URI mapping, redirects

We have seen how to declare endpoints and how URIs map to them, but very often we need to map from endpoints to URIs, which Renarde makes easy.

Redirects after POST

When handling a @POST, @PUT or @DELETE endpoint, it’s good form to redirect to a @GET endpoint after the action has been done, in order to allow the user to reload the page without triggering the action a second time, and such redirects are simply done by calling the corresponding @GET endpoint. In reality, the endpoint will not be called and will be replaced by a redirect that points to the endpoint in question.

package rest;

import javax.ws.rs.POST;
import javax.ws.rs.Path;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import io.quarkiverse.renarde.Controller;

public class Application extends Controller {

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

    @Path("/")
    public TemplateInstance index() {
        return Templates.index();
    }

    @POST
    public void someAction() {
        // do something
        ...
        // redirect to the index page
        index();
    }
}

If there are any parameters that form the URI, you must pass them along:

package rest;

import javax.ws.rs.POST;
import javax.ws.rs.Path;

import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import io.quarkiverse.renarde.Controller;

public class Application extends Controller {

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

    @Path("/")
    public TemplateInstance index() {
        return Templates.index();
    }

    public TemplateInstance somePage(@RestPath String id, @RestQuery String q) {
        // do something with the id and q
        return Templates.index();
    }

    @POST
    public void someAction() {
        // do something
        ...
        // redirect to the somePage page
        somePage("foo", "bar");
    }
}

If you want to redirect to another controller, you can use the redirect(Class) method:

package rest;

import javax.ws.rs.POST;

import io.quarkiverse.renarde.Controller;

public class Application extends Controller {

    @POST
    public void someAction() {
        // do something
        ...
        // redirect to the Todos.index() endpoint
        redirect(Todos.class).index();
    }
}

Obtaining a URI in endpoints

If you don’t want a redirect but need a URI to a given endpoint, you can use the Router.getURI or Router.getAbsoluteURI methods, by passing them a method reference to the endpoint you want and the required parameters:

package rest;

import java.net.URI;

import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import io.quarkiverse.renarde.Controller;
import io.quarkiverse.renarde.router.Router;

public class Application extends Controller {

    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance somePage();
        public static native TemplateInstance otherPage(URI uri);
    }

    public TemplateInstance somePage(@RestPath String foo, @RestQuery Long bar) {
        return Templates.somePage();
    }

    public TemplateInstance otherPage() {
        // Obtain a URI to somePage
        URI uri = Router.getURI(Login::somePage, "something", 23l);
        // pass it on to our view
        return Templates.otherPage(uri);
    }
}
If you plan on using Response.seeOther or Controller.seeOther, make sure to use the Router.getAbsoluteURI variant, especially if you use the quarkus.http.root-path configuration, otherwise your URIs contain that prefix twice.

Obtaining a URI in Qute views

If you want a URI to an endpoint in a Qute view, you can use the uri and uriabs namespace with a call to the endpoint you want to point to:

<a class="navbar-brand" href="{uri:Application.index()}">Todo</a>

Naturally, you can also pass any required parameters.

Emails

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-mailer</artifactId>
</dependency>

Often you will need your actions to send email notifications. You can use Qute for this too, by declaring your emails in an Emails class:

package email;

import io.quarkus.mailer.MailTemplate.MailTemplateInstance;
import io.quarkus.qute.CheckedTemplate;
import model.User;

public class Emails {

    private static final String FROM = "Todos <todos@example.com>";
    private static final String SUBJECT_PREFIX = "[Todos] ";

    @CheckedTemplate
    static class Templates {
        public static native MailTemplateInstance confirm(User user);
    }

    public static void confirm(User user) {
        Templates.confirm(user)
        .subject(SUBJECT_PREFIX + "Please confirm your email address")
        .to(user.email)
        .from(FROM)
        .send().await().indefinitely();
    }
}

You can then send the email from your endpoint by calling Emails.confirm(user).

You can use composition for emails too, by having a pair of base templates for HTML in src/main/resources/templates/email.html:

<!DOCTYPE html>

<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        {#insert /}
        <p>
            This is an automated email, you should not reply to it: your mail will be ignored.
        </p>
    </body>
</html>

And for text in src/main/resources/templates/email.txt:

{#insert /}

This is an automated email, you should not reply to it: your mail will be ignored.

You can then use those templates in your emails in src/main/resources/templates/Emails/confirm.html:

{#include email.html }

<p>
 Welcome to Todos.
</p>

<p>
 You received this email because someone (hopefully you) wants to register on Todos.
</p>

<p>
 If you don't want to register, you can safely ignore this email.
</p>

<p>
 If you want to register, <a href="{uriabs:Login.confirm(user.confirmationCode)}">complete your registration</a>.
</p>
{/include}

And for text in src/main/resources/templates/Emails/confirm.txt:

{#include email.txt}

Welcome to Todos.

You received this email because someone (hopefully you) wants to register on Todos.

If you don't want to register, you can safely ignore this email.

If you want to register, complete your registration by going to the following address:

{uriabs:Login.confirm(user.confirmationCode)}
{/include}

Note that in emails you will want to use the uriabs namespace for absolute URIs and not relative ones, otherwise the links won’t work for your email recipients.

You can find more information in the Quarkus mailer documentation.

Localisation / Internationalisation

You can declare your default language and supported languages in src/main/resources/application.properties:

# This is the default locale for your application
quarkus.default-locale=en
# These are the supported locales (should include the default locale, but order is not important)
quarkus.locales=en,fr

Next, you can declare your default language messages in the src/main/resources/message.properties file:

# A simple message
hello=Hello World
# A parameterised message for your view
views_Application_index_greet=Hello %s

Declare your other language translations in the src/main/resources/message_fr.properties file:

hello=Bonjour Monde
views_Application_index_greet=Salut %s

Now you can use these translated messages in your controller:

public static class Application extends Controller {

    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance index(String name);
    }

    public TemplateInstance index() {
        return Templates.index("Stef");
    }

    public String hello() {
		return i18n.formatMessage("hello");
    }
}

Or in your template:

With no parameter:
{m:hello}
With parameters:
{m:views_Application_index_greet(name)}

Selecting the language

The current language for a request will depend on the following (in order):

  1. The _renarde_language cookie, if set

  2. The Accept-Language HTTP header, if set, which defines an ordered list of languages by user preference. We will select a best matching language from the set of quarkus.locales.

  3. If nothing else, we default to the default language as set by quarkus.default-locale, which defaults to en_US.

You can override the user’s language with a cookie by calling i18n.set(language):

public static class Application extends Controller {

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

    public void index() {
        return Templates.index();
    }

    @POST
    public void french() {
        i18n.set("fr");
        index();
    }

    @POST
    public void english() {
        i18n.set("en");
        index();
    }
}

Flash scope

If you need to pass values from one endpoint to another request after a redirect, you can use the Flash scope. Usually this is done in a @POST endpoint, by filling the Flash scope with either errors or messages, before trigerring a redirect to the right @GET endpoint.

You can push values in the Flash scope in an endpoint using the flash(name, value) method, or using the Flash injectable component.

You can read values from the Flash scope in your Qute views using the {flash:name} namespace.

The Flash scope only lasts from one request to the next and is cleared at each request.

Extension Configuration Reference