GEDOPLAN
QuarkusWebprogrammierung

Typensicheres Quarkus Web Templating mit Qute

QuarkusWebprogrammierung
qute

Im Java Ökosystem haben wir bereits die Wahl zwischen unterschiedlichen Templating Engines von beispielsweise JSP über Thymeleaf bis zu FreeMarker. Wieso also eine weitere Option dazuholen? Wir schauen uns die Vorteile von Qute in einem kleinen Beispielprojekt an um genau das herauszufinden.

Die Bibliothek Qute ist spezifisch für die Anforderungen von Quarkus entwickelt worden und kann dort neben dem Templating für Webseiten auch z.B. in der Mailer-Erweiterung oder andere Templating Aufgaben verwendet werden. Dabei führt Qute möglichst viele Schritte bereits zur Build Zeit durch um zur Laufzeit Ladezeiten zu verringern und Fehler frühzeitig zu erkennen. Damit eignet sich Qute auch gut für nativ kompilierte Anwendungen. Die Validierung und Verarbeitung der Templates zur Build Zeit deckt Syntax- und Typfehler zuverlässig und frühzeitig auf und verringert die Zeit für manuelles Testen und Debuggen. Die Bibliothek unterstützt wahlweise ein reaktives Paradigma und bietet eine sehr gute Integration in den Quarkus Dev-Modus. Über die Quarkus IDE Tools werden Probleme auch bereits beim Entwickeln markiert und Vorschläge werden für bekannte Typen unterstützt.

Für unsere Beispielanwendung erstellen wir eine kleine Verwaltung der Planeten unseres Sonnensystems. Die Web-Schnittstelle realisieren wir mit Jakarta REST, dafür können wir folgende Maven-Dependency verwenden.

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-qute</artifactId>
</dependency>

Qute Templates erstellen

Zunächst brauchen wir dann ein Template für eine Übersichtsseite. Templates werden für die quarkus-rest-qute Integration überlicherweise im Verzeichnis src/main/resources/templates und möglichen Unterverzeichnissen angelegt. Für die typensichere Integration ist standardmäßig vorgesehen, dass Templates die in der Klasse PlanetResource verwendet werden in einem Unterordner mit demselben Namen liegen. Qute-Ausdrücke werden durch geschweifte Klammern eingeleitet und geschlossen. Eine Dokumentation der verschiedenen Möglichkeiten gibt es hier. In unserem Beispiel verwenden wir diese Arten von Ausdrücken.

  • Einfache Ausdrücke: {planet.name}
  • Ternäre Ausdrücke: {planet.hasRings ? 'Ja' : 'Nein'}
  • Schleifen: {#for planet in planets} und {/for}
  • Bedingte Bereiche: {#if planets.isEmpty()} und {/if}
  • Direkter Zugriff auf benannte CDI-Beans: {cdi:vertxRequest.getParam('search', '')}
  • Automatisches Einbinden von CSS/JS Resourcen mit quarkus-web-bundler: {#bundle /}

Hier sind die entsprechenden Zeilen auch noch einmal im Kontext markiert.

<!DOCTYPE html>
<html lang="de">
<head>
  [...]
  {#bundle /}
  [...]
</head>
<body>
  [...]
  <form action="/planets/ui" method="get">
    <div class="input-group">
      <input type="text" name="search" class="form-control" placeholder="Suche Planeten..."
             value="{cdi:vertxRequest.getParam('search', '')}">
      <button class="btn btn-outline-secondary" type="submit">Suche</button>
    </div>
  </form>
  [...]
  <table class="table table-striped table-hover">
    <thead class="table-dark">
    <tr>
      <th>Name</th>
      <th>Durchmesser (km)</th>
      <th>Abstand zur Sonne (Mio. km)</th>
      <th>Hat Ringe</th>
      <th>Anzahl der Monde</th>
      <th>Aktionen</th>
    </tr>
    </thead>
    <tbody>
    {#for planet in planets}
    <tr class="planet-row">
      <td><a href="/planets/ui/{planet.name}">{planet.name}</a></td>
      <td>{planet.diameterKm}</td>
      <td>{planet.distanceFromSunMillionKm}</td>
      <td>{planet.hasRings ? 'Ja' : 'Nein'}</td>
      <td>{planet.numberOfMoons}</td>
      <td>
        <div class="btn-group btn-group-sm">
          <a href="/planets/ui/{planet.name}" class="btn btn-info">Anzeigen</a>
          <a href="/planets/ui/{planet.name}/edit" class="btn btn-warning">Bearbeiten</a>
          <form action="/planets/ui/{planet.name}/delete" method="post">
            <button type="submit" class="btn btn-danger btn-sm" 
                    style="border-top-left-radius: 0; border-bottom-left-radius: 0;"
                    onclick="return confirm('Möchten Sie {planet.name} wirklich löschen?')">
              Löschen
            </button>
          </form>
        </div>
      </td>
    </tr>
    {/for}
    {#if planets.isEmpty()}
    <tr>
      <td colspan="6" class="text-center">
        Keine Planeten gefunden. 
        <a href="/planets/ui/new">Fügen Sie einen Planeten hinzu</a>.
      </td>
    </tr>
    {/if}
    </tbody>
  </table>
  [...]
</body>
</html>

src/main/resources/templates/PlanetResource/planets.html

Qute Templates verwenden

Sobald wir das Template fertiggestellt haben können wir uns dem Rendern der Seite zuwenden. Die Jakarta REST Methoden müssen dafür ein Objekt vom Typ TemplateInstance zurückgeben. Um Templates typensicher zu definieren gibt es mit Qute zwei Möglichkeiten. Die erste Möglichkeit ist es die Templates über public static native Methoden innerhalb einer statischen Klasse mit der Annotation @CheckedTemplate zu referenzieren. Die Methode planets referenziert standardmäßig dann die Template Datei src/main/resources/templates/PlanetResource/planets.html. In den Methoden Parametern können die für das Rendern notwendigen Daten mit dem Parameternamen als Key definiert werden. Über diesen Namen können wir diese Daten im Template verwenden. Aufgrund des native Keywords müssen wir keine Implementierung vornehmen und können die Methode einfach verwenden um unsere TemplateInstance zu erstellen. Die Implementierung übernimmt Qute für uns.

@Path("/planets/ui")
public class PlanetResource {
  @Inject
  PlanetService planetService;

  @CheckedTemplate
  static class Templates {
    public static native TemplateInstance planets(List<Planet> planets);
    public static native TemplateInstance planet(Planet planet);
    public static native TemplateInstance error(Integer code, String title);
  }

  @GET
  @Produces(MediaType.TEXT_HTML)
  public TemplateInstance getPlanetsPage(@QueryParam("search") String search) {
      List<Planet> planetList = search != null && !search.trim().isEmpty()
          ? planetService.searchPlanets(search)
          : planetService.getAllPlanets();
      return Templates.planets(planetList);
  }
  [...]
}

src/main/java/de/gedoplan/showcase/web/PlanetResource.java

Probleme mit dem Schlüsselnamen oder einem Methodennamen fallen bereits zur Compilezeit auf und der Vorgang wird abgebrochen. Läuft die Anwendung zu diesem Zeitpunkt bereits im Dev-Modus wird nach einem Reload im Dev-Modus direkt auf die Stelle im Template verwiesen, die für den Fehler verantwortlich ist.

Eine andere Möglichkeit Templates zu referenzieren funktioniert über Records. Für das Template PlanetForm.html definieren wir einen Record PlanetForm um neue Planeten anzulegen oder bestehende zu bearbeiten. Der Record muss das Interface TemplateInstance implementieren um Qute zu signalisieren, dass es sich hierbei um eine Templatereferenz handelt, und um einen validen Rückgabewert in den Methoden darzustellen.

@Path("/planets/ui")
public class PlanetResource {
  [...]
  record PlanetForm(Planet planet, Action action) implements TemplateInstance {}

  @GET
  @Path("/new")
  @Produces(MediaType.TEXT_HTML)
  public TemplateInstance getNewPlanetForm() {
    return new PlanetForm(new Planet(), Action.NEW);
  }

  @GET
  @Path("/{name}/edit")
  @Produces(MediaType.TEXT_HTML)
  public TemplateInstance getEditPlanetForm(@PathParam("name") String name) {
    Optional<Planet> planetOpt = planetService.getPlanetByName(name);
    if (planetOpt.isPresent()) {
      return new PlanetForm(planetOpt.get(), Action.EDIT);
    } else {
      return Templates.error(404, "Planet '" + name + "' nicht gefunden");
    }
  }
  [...]
}

src/main/java/de/gedoplan/showcase/web/PlanetResource.java

Hier kommen wir ohne das Keyword native aus. Wichtig ist es hier auf Groß- und Kleinschreibung zu achten und wenn wir z.B. das Template Planet.html verwenden möchten würde die Record Definition sich standardmäßig mit dem Namen der Model-Klasse Planet überschneiden. Hier müssten wir entweder einen voll qualifizierten Namen verwenden, den Klassen- oder Dateinamen anpassen oder die Standardeinstellungen mit der Annotation @CheckedTemplate anpassen.

Mit der Annotation @TemplateData an der beim Rendern verwendeten Klasse Planet können wir Qute anweisen einen sogenannten Resolver zu generieren um einen Zugriff über Reflection zur Laufzeit zu vermeiden. Für Enumerations gibt es die Annotation @TemplateEnum, die es uns auch erlaubt im Template auf die Enum-Konstanten zuzugreifen.

@TemplateData
public record Planet(String name, double diameterKm, double distanceFromSunMillionKm,
                     boolean hasRings, int numberOfMoons, String description) { [...] }

src/main/java/de/gedoplan/showcase/model/Planet.java

// Zugriff auf Enum Konstanten im Template über Action:NEW
@TemplateEnum
public enum Action {NEW, EDIT}

src/main/java/de/gedoplan/showcase/web/PlanetResource.java

Methodenerweiterungen für Datenobjekte

Manchmal wäre es hilfreich, wenn wir bestehende Java-Klassen um neue Methoden erweitern könnten, wie man es z.B. aus Kotlin kennt. Zumindest für die Datenobjekte, die wir in den Templates verwenden, können wir mit Qute solche Methoden definieren. In diesem Fall möchten wir auf der Planetenseite gerne auch den Abstand zur Sonne in astronomischen Einheiten angeben. Wir könnten hier natürlich auch einfach unsere Klasse erweitern, aber insbesondere wenn die betreffende Klasse nicht unter unserer Kontrolle liegt können wir diesen Ansatz verwenden.

Wir definieren eine statische Methode mit der Annotation @TemplateExtension die ein Planet Objekt entgegen nimmt und einen double Wert zurückgibt. In der Methode wird dann die entsprechende Berechnung vorgenommen. Bei der Ausgabe können wir auf eine weitere, bereits vordefinierte, Erweiterung zurückgreifen und über str:fmt() die Ausgabe entsprechend formatieren.

@TemplateExtension
static double distanceFromSunAU(Planet planet) {
  return planet.distanceFromSunMillionKm() / 149.597_870_700;
}

src/main/java/de/gedoplan/showcase/web/PlanetResource.java

{str:fmt("%.3f", planet.distanceFromSunAU)}

src/main/resources/templates/PlanetResource/planet.html

Das ganze Projekt findet ihr wie immer auch auf GitHub.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

IT-Training - GEDOPLAN
Jakarta EE (Java EE)

JEE Klassen > TypeScript

Ein JEE Backend ist mit wenig Aufwand über eine JSON basierte REST-Schnittstelle zur Verfügung gestellt. An diesem Punkt kann sich…
IT-Training - GEDOPLAN
Spring

Spring Boot + DATA JPA + HATEOAS

Spring Boot bietet die Möglichkeit auf sehr einfachem Wege standalone Anwendungen zu schreiben, die keinerlei Applicationserver als Laufzeitumgebung benötigen. Als…

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!