Renarde Web Framework - Main Concepts
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 jakarta.persistence.Entity;
import jakarta.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 jakarta.validation.constraints.NotBlank;
import jakarta.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 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
---|---|
Iterate over collections |
|
Conditional statement |
|
Switch statement |
|
Adds value members to the local scope |
|
Declare local variables |
|
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 |
---|---|
|
Generate a hidden HTML form element containing a CSRF token to be matched in the next request. |
|
Inserts the error message for the given field name |
|
Generates an HTML form for the given |
|
Inserts a gravatar image for the given |
|
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 |
---|---|---|
|
|
Formats the date to the |
|
|
Formats the date to the |
|
|
Returns |
|
|
Formats the date in terms of |
|
|
Returns an MD5 hash of the given string |
|
|
Returns true if the given object is exactly of the specified class name |
Global Variables
If you need to pass variables to every template, instead of passing them manually to every view, you can define them as
methods in a class annotated with @TemplateGlobal
:
package util;
import io.quarkus.qute.TemplateGlobal;
@TemplateGlobal
public class Globals {
public static String lineSeparator(){
return System.lineSeparator();
}
}
This one declares a lineSeparator
global variable that you can use in the views:
This system uses this line separator: {lineSeparator}
Renarde Predefined Global Variables
Type | Name | Description |
---|---|---|
|
|
The absolute request url, including scheme, host, port, path |
|
|
The request method ( |
|
|
The request HTTP scheme ( |
|
|
The request authority part (ex: |
|
|
The request host name (ex: |
|
|
The request port (ex: |
|
|
The request path (ex: |
|
|
The controller endpoint class and method (ex: |
|
|
True if the request is served over SSL/HTTPS |
|
|
The remote client IP address |
|
|
The remote client Host name, if available |
|
|
The remote client port |
External CSS, JavaScript libraries
You can use jars created by mvnpm.org to provide third-party JavaScript or CSS hosted on the NPM Registry.
For example, here is how you can import Bootstrap and Bootstrap-icons in your pom.xml
:
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>bootstrap-icons</artifactId>
<version>1.11.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-web-dependency-locator</artifactId>
</dependency>
After that, you can include them in your Qute templates with:
<head>
<link rel="stylesheet" media="screen" href="/_static/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" media="screen" href="/_static/bootstrap-icons/font/bootstrap-icons.css">
<script src="/_static/bootstrap/js/bootstrap.min.js" type="text/javascript" charset="UTF-8"></script>
</head>
Check the web-dependency-locator extension guide for more information.