Thymeleaf Template Engine

Ich hasse es, dass ich jetzt (mitten in der Spring-Umstellung) über **noch **eine Änderung nachdenke.

Andererseits ist es nicht eilig: Ich würde erst mal versuchen, auf die Funktionalität vom letzten Ninja-Stand zu kommen, bevor ich irgend etwas in dieser Richtung unternehme.

Ich habe mir gerade die Template-Engine Thymeleaf näher angeschaut. Im Prinzip sind die Template Engines ziemlich austauschbar, die Spring-Unterstützung ist bei allen üblichen Kandidaten ähnlich gut. Thymeleaf hat einen großen Vorteil und einen dummen Nachteil.

Der Vorteil ist, dass es das HTML-Layout nicht wie Freemarker oder Velocity “kaputtmacht”, sondern einerseits seine Konstrukte XML-konform einschleust und andererseits erlaubt, Default-Werte einzugeben. Mit anderen Worten: Man kann eine Seite ändern und im Browser anschauen, ohne dass der Webserver laufen muss - gerade bei Bootstrap-Gefrickel bestimmt sehr hilfreich. Das nennt sich “natural templating”.

Hier ein Beispiel: <h1 th:text="#{home.welcome}">Welcome!</h1>. Damit wird statisch ganz normal die Überschrift “Welcome!” angezeigt, aber in der Applikation dynamisch auf den Wert der Variable home.welcome gesetzt.

Der Nachteil ist, dass Thymeleaf **deutlich **langsamer als andere Template-Engines ist (siehe z.B. Shootout! Template engines for the JVM S.41).

Thymeleaf nutze ich auch :wink: Von der Sache her ist der Thymeleaf schon ganz gut, aber das Projekt ist noch verhältnismäßig jung.

Zur Performance bleibt abzuwarten, was mit der nächsten Majorversion kommt, dort ist ein vollständig neuer, vielfach schnellerer Parser integriert. Die neue Version soll glaube ich Mitte des Jahres kommen.

Thymeleaf hat derzeit noch keine vollständig funktionierende Unterstützung in IntelliJ IDEA (insbesondere in Kombination mit Spring Boot funktioniert das noch nicht so ganz: https://youtrack.jetbrains.com/issue/IDEA-132738 ), man kann es aber schon nutzen.
Ansonsten habe ich nicht viel an Thymeleaf auszusetzen. Auch die Spring (Security)-Integration ist brauchbar.
Ich habe in Spring für Version 4.2 eine Änderung angeregt, sodass insbesondere in Verbindung mit Thymeleaf URL-Patterns wie http://fjorum.org//thread möglich werden. Damit werden die google-Empfehlungen was Lokalisierung angeht eingehalten. Für Thymeleaf erstelle ich gerade einen Dialekt, sodass mit dem Utility #mvc die URLs direkt anhand der Controllermethoden erzeugt werden und dort die Lokalisierung transparent eingefügt wird. (Siehe * im letzten Edit)

Gerade mit bootstrap harmoniert Thymeleaf mit den natural templates sehr gut.

Ich nutze noch den layout-Dialekt, weil ich die Einbindung der Layoutvorlagen so intuitiver finde. Eine Unterscheidung der Möglichkeiten findet sich hier: Thymeleaf Page Layouts - Thymeleaf: java XML/XHTML/HTML5 template engine

*** Edit ***

Ohne belastbar zu sein, ein Vergleich der Performance der aktuellen Thymeleaf-Version mit einem Snapshot vom April diesen Jahres:

On my machine (I7-4750HQ) the improvement is around factor 10x from around 1000/s improved to 11000/s

Siehe dieser Thread hier: Thymeleaf News and Announcements - Thymeleaf 3 is coming…

*** Edit ***

Verglichen mit Folie 41 aus deinem Post würde sich Thymeleaf dann unter den schnelleren Templateengines einreihen oder sogar schneller als diese sein. Wenn es auch dort ein Faktor 10 wäre (wovon ich nicht ausgehe), dann wäre die Engine 3x so schnell wie z. B. Velocity.

*** Edit ***

  • Das kann dann im Template so aussehen:
<form th:action="${#mvc.url('RC#processRegister').build()}" method="post" th:object="${userDto}" class="form-horizontal"><!-- ... --></form>

Dort wird der Pfad vom RegisterController mit der Handlermethode processRegister aufgebaut.

Das klingt recht vielversprechend. Dann würde ich aber gleich auf die neue Version gehen wollen. Also mit dem Testen erst mal abwarten, bis die Beta draußen ist.

Ich hab mir Thymeleaf ein bisschen näher angeschaut. Alles schick soweit, aber eine Frage bleibt:

Statt etwas in meine Seiten hineinzuincluden arbeitet mein Freemarker “andersherum”. Der gesamte Seitenaufbau inklusive der ganzen js-Bibliotheken im Head und am Ende des Bodys erfolgt über ein zentrales Macro (defaultLayout.ftl.html), in dem dann irgendwo mittendrin wieder die Original-Seite eingefügt wird. Über Fragmente könnte ich mir in Thymeleaf zwar einen Header und Footer einbauen, aber ich kann nicht erkennen, ob etwas Ähnliches wie das Freemarker-Macro möglich ist, das also einen Standardseitenaufbau inclusive Head-Abschnitt vorgibt.

Update:

Der head-Abschnitt lässt sich auch ersetzen: Thymeleaf layout dialect and th:replace in head causes title to be blank - Stack Overflow

Das wäre wahrscheinlich komportabel genug.

Vielleicht kann mir jemand bei einem Thymeleaf-Problem weiterhelfen:


<tr th:each="user : ${users}">
...
    <tr th:each="r : ${roles}">
        <td><input type="checkbox" name="role" th:value="${r.name}" th:checked="${user.roles.contains(r)}?'checked':''"/></td>
        <td th:text="${r.name}">ROLE_USER</td>
    </tr>
...
</tr>

Die Checkbox ist immer für alle Rollen gesetzt (es stecht auch “checked” im Quelltext), egal was der Nutzer hat. Lasse ich mir dort ${user.roles.toString()} anzeigen, wird aber der korrekte Set-Inhalt geliefert. equals und hashCode von Role habe ich überschrieben, und zwar nur basierend auf der Role.id.

Hallo Landei,

Einfach mal so in eine HTML-Datei einfügen und im Browser öffnen.

<input type="checkbox">
<input type="checkbox" checked>
<input type="checkbox" checked="checked">
<input type="checkbox" checked="">
<input type="checkbox" checked=>

Welche Checkbox ist angehakt (Getestet mit Chromium)?

[SPOILER]Nur die erste.[/SPOILER]

Tutorial: Using Thymeleaf Punkt 5.5 geht auf checkboxen ein und dort wird folgendes empfohlen:

<td><input type="checkbox" name="role" th:value="${r.name}" th:checked="${user.roles.contains(r)}"/></td>

Also lediglich ein Boolean liefern, den Rest macht die Template-Engine.
Da ist also noch etwas mehr Magie dahinter und “” wird evtl. zu true evaluiert und dann mit einem checked ersetzt.

den Anteil mit

finde ich etwas bedenklich, bezweifelst bzw. hinterfragst du damit, ob das contains() mit Role richtig funktioniert?
an und für sich natürlich immer ein Punkt, aber dann hier in dieser komplizierten Umgebung schlecht zu machen,

-> Erinnerung, einfache Programme zu schreiben,
es sollte eine main-Methode geben (*) um mal eben einen User zu laden,
dazu alle Rollen falls die aus der DB kommen und nur im derart geladenen Session-Zustand sinnvoll zu testen sind,
und dann einfache System.out.println()-Ausgabe des User mit seinen Infos, Schleife für contains-Test aller Rollen schnell ergänzt,

(*) nicht in Github, lokal herumliegend, wichtig ist dass das einfach geht,
nicht auf irgendwelche gigantischen Kombinationen aus Spring- und Webserver-Initialisierung aufsetzt

In der nicht X(H)TML-konformen Welt reicht das Vorhanden sein des Wörtchens “checked” innerhalb des Input-Tags, um sie checked zu machen. Das ist ein Relikt aus altem HTML. Würde annehmen, dass Dein Browser nett sein will das Vorhandensein des leeren Attributs als solche Syntax interpretiert. Das leere checked-Attribut ist übrigens auch nicht XHTML-Konform. Nach Spez. muss da entweder checked=“ckecked” oder einfach nichts stehen. Wie man Thymeleaf das beibringt, weiß ich leider nicht.

Im Quelltext steht aber checked="checked", damit sieht es für mich so aus, als ob das contains nicht richtig funktioniert. Ich werde es aber mal als Text ausgeben lassen, damit ich ganz sicher bin.

Vorausgesetzt contains funktioniert korrekt.

th:checked ist ein “fixed-value boolean attribute”.

Damit macht die Template-Engine was besonderes.

Die Frage ist jetzt: Kommt das

checked=“checked”

von

th:checked="${user.roles.contains®}?‘checked’:’’"

oder werden

“” und “checked” zu true ausgewertet, wie in den if-Statements der Template-Engine (7.1 Conditionals Evaluation)* und damit intern der komplette Ausdruck durch

checked=“checked”

ersetzt?

th:checked="${user.roles.contains®}?‘foo’:‘bar’"

müsste laut Doku ebenfalls ein checked=“checked” ergeben, genauso wie

th:checked="${user.roles.contains®}?’’:’’"

[li] In der Doku ist allerdings nicht explizit wie ein leerer String ausgewertet wird. [/li]

true:
If value is a String and is not “false”, “off” or “no”

Ergänzend zu SlaterB’s Hinweis zum Umgang mit th:checked noch mein Hinweis auf 18 Appendix B. Dort sind Utility-Methoden zum Umgang mit speziellen Objekten beschrieben. U.a. mit #arrays, #lists, #sets auch welche, um contains aufzurufen. Da es diese Methoden so gibt, würde ich annehmen, dass man sie auch so verwenden muss, ein “direkter” Aufruf von contains per Expression also nicht möglich ist.

wieso ist das ergänzend zu meinem Hinweis, eher MrEuler?
ergänzend bei mir wäre Überschreiben/ Ersetzen des Sets und Mitloggen ob es dort zu Aufrufen kommt mit welchen Parametern und welchen Rückgabewerten :wink:

hier noch ein Thema von 2011, auch zu Rollen
General Usage - The checked attribute of the checkbox is not set in th:each.
dort u.a. auch mit th:checked="${#sets.contains(roles,role.id)}"

sowohl spannend als auch kompliziert klingt zudem:

…you do not need that “th:checked” attribute at all. The th:field="*{roles}" attribute should take care of checking each of the values in the “roles” property of your form-backing bean so that, when one of the elements equals ${role.id} (the value set in the “th:value” attribute), thymeleaf will automatically write a checked=“checked” attribute into your checkbox tag.

Erst mal hat MrEuler recht, das checked wird auch bei einem Leerstring gesetzt.

Jetzt sind alle Checkboxen leer, weil das contains noch nicht so richtig will (auch wenn ich die #sets.contains Utility-Methode verwende).

mein Posting kurz vor deinem gesehen?

und nochmalig dringender Hinweis, das doch in direkt in Java zu klären,

wenn dort contains auch schiefgeht dann schnell separat neues HashSet erstellen,
wer weiß schon welche Art von halb-initialisierten Proxy-Sets diese systemkontrollierten Entities haben
(ok, kann man nachschauen, wobei wiederum nicht unbedingt genau der Stand in der Anzeige wenn alles im Fluss ist…
-> eine Philosopohe: außerhalb interner Java-Klassen mit eindeutiger Lage lieber gar nicht mit DB-Entitys arbeiten, ob detachted oder open-session oder sonstwie verrückt,
stattdessen DTO-Kopien…)
usw.,
sind doch alles einfache Schritte, ohne Code dazu hier sowieso nicht groß zu besprechen,

in GitHub nicht ganz aktuell, Role ohne equals + hashCode, richtig?

[QUOTE=SlaterB]mein Posting kurz vor deinem gesehen?

und nochmalig dringender Hinweis, das doch in direkt in Java zu klären,

wenn dort contains auch schiefgeht dann schnell separat neues HashSet erstellen,
wer weiß schon welche Art von halb-initialisierten Proxy-Sets diese systemkontrollierten Entities haben
[/quote]

Habe ich im Controller debuggt, und dort funktioniert das contains.

(ok, kann man nachschauen, wobei wiederum nicht unbedingt genau der Stand in der Anzeige wenn alles im Fluss ist…
-> eine Philosopohe: außerhalb interner Java-Klassen mit eindeutiger Lage lieber gar nicht mit DB-Entitys arbeiten, ob detachted oder open-session oder sonstwie verrückt,
stattdessen DTO-Kopien…)
usw.,
sind doch alles einfache Schritte, ohne Code dazu hier sowieso nicht groß zu besprechen,

Na auf die Sonderbehandlung von checked wäre ich nicht so schnell gekommen.

in GitHub nicht ganz aktuell, Role ohne equals + hashCode, richtig?
https://github.com/fjorum/fjorum/tree/master/src/main/java/org/fjorum/model/entity

Richtig, will ja keinen halbfertiges Zeugs committen.

ein Problem nach dem anderen :wink:

Habe ich im Controller debuggt, und dort funktioniert das contains.

funktioniert es zufällig in der Anzeige, wenn auch beim Laden genau für diese Ausführung im Controller vorher Durchlauf und Ausgabe -> Set initialisiert und solche Späße?
werden diese Roles erst nachgeladen?

wie ist denn der Stand zu Session-In-View und ähnlichem, allgemein sicheres Gefühl oder alle möglichen Fehlerursachen einzuberechnen?

werden die Roles eigentlich für jeden User, bei jedem Request neu aus DB geladen?
könnte man cachen, dann vielleicht lieber Objektidentität, und nur Ids aus DB, an einem bestimmten Punkt initialisieren

na, soviel auf einmal, dafür nun ich gleich wahrscheinlich bis morgen Abend gar nicht mehr/ kaum mehr da :wink:

Hab’s jetzt doch mal committed. Wer es nachvollziehen möchte: Mit admin/admin anmelden, auf Administration gehen, dort sollte der Admin-Nutzer zu sehen sein. Wenn man in der Zeile auf “Rights” klickt, geht ein Dialog auf. In der zweiten Spalte zeige ich zu Debug-Zwecken neben dem Namen auch die Auswertung von contains und den Inhalt des Sets (sollte ROLE_OWNER enthalten, und in der Zeile mit dieser Rolle sollte eigentlich ein Checkbox-Häkchen und ein “true” zu sehen sein).

Es funktioniert, wenn ich im Controller eine entsprechende Expression auswerte.

wie ist denn der Stand zu Session-In-View und ähnlichem, allgemein sicheres Gefühl oder alle möglichen Fehlerursachen einzuberechnen?

Ich habe erst mal @Transactional(readOnly = true) benutzt. Vielleicht nicht die feine Englische, scheint aber bisher gut zu funktionieren.

werden die Roles eigentlich für jeden User, bei jedem Request neu aus DB geladen?
könnte man cachen, dann vielleicht lieber Objektidentität, und nur Ids aus DB, an einem bestimmten Punkt initialisieren

Wird immer wieder neu geladen. Caching würde ich gerne erst einmal außen vor lassen, das wäre nur eine mögliche Fehlerursache mehr.

Habe das ganze gebaut und nach 5 Minuten war dann auch Spring auf dem Rechner.

Eine Lösung auch wenn nicht wirklich die schönste kann ich dir in dieser Form anbieten:

<td><input type="checkbox" name="role" th:value="${r.name}"
  th:checked="${user.roles.toString().contains(r.name)}"/>
</td>
<td th:text="${r.name} + ' -- ' + ${user.roles.toString().contains(r.name)}+' -- ' + ${user.roles.toString()}">
  ROLE_USER
</td>
 

Vielleicht finde ich ja noch was passenderes. Aber die Daten sind zumindest schon einmal vorhanden.

*** Edit ***

Da der User erstmal nur eine Rolle hat, hole ich mir auch erstmal nur eine Rolle, bevor ich mich an contains mache.

${#lists.toList(user.roles).get(0).getId().equals(r.getId())} => true, an der entsprechenden Stelle

${#lists.toList(user.roles).get(0).equals(r)} => false, an der Stelle

Also mal equals debuggen

    public boolean equals(Object o) {
      System.out.println("Comparing: "+this+" with "+o);
        if (this == o) return true;
        if (o == null || !(o instanceof Role)) return false;

        Role role = (Role) o;

        return id != null && id.equals(role.id);
    }```

Mit dem Ergebnis:

Comparing: ROLE_OWNER with 1
Comparing: ROLE_OWNER with 2
Comparing: ROLE_OWNER with 3
Comparing: ROLE_OWNER with 4
Comparing: ROLE_OWNER with 5



Boom!!!

Aber wird noch krasser

Mal das equals umdrehen:

 ${r.equals(#lists.toList(user.roles).get(0))}


Comparing: ROLE_USER with 4
Comparing: ROLE_MODERATOR with 4
Comparing: ROLE_ADMINISTRATOR with 4
Comparing: ROLE_OWNER with 4
Comparing: ROLE_GUEST with 4



WTF!!!

Also mal Datentypen anschauen

```@Override
    public boolean equals(Object o) {
      System.out.println("Comparing: "+this+" with "+o);
      System.out.println(this.getClass()+" vs "+o.getClass());
        if (this == o) return true;
        if (o == null || !(o instanceof Role)) return false;

        Role role = (Role) o;

        return id != null && id.equals(role.id);
    }```


class org.fjorum.model.entity.Role vs class java.lang.Long



Also nochmal im Template die Klassen ausgeben lassen

`${r.getClass()} + ' -- ' + ${#lists.toList(user.roles).get(0).getClass()}`

Ergebnis auf der Seite:
`class org.fjorum.model.entity.Role -- class org.fjorum.model.entity.Role`

Im Template ist es noch eine Rolle, bei equals kommt allerdings nur ein Long an.

Und das meine Damen und Herren, war es dann jetzt erstmal vorerst.

*** Edit ***

So, das einzige was ich nach zahlreichen Versuchen noch finden konnte war eine eigene Helper Methode, die ich mal in Role gepackt habe.

```  public boolean contained(java.util.Collection c) {
    boolean result = c.contains(this);
    return result;
  }```

um dann damit den Haken zu setzen

`th:checked="${r.contained(user.roles)}"`



Habe auch einen Versuch mit einer "arrays" gemacht, der zwar zu gehen scheint, den ich allerdings nicht ganz verifizieren kann. Möglicherweise wird hier auf den index gegangen oder sonst etwas verglichen. :ka:

`th:checked="${#arrays.contains(#arrays.toArray(user.roles), r)}"`

Wow, wirklich ein ordentliches WTF. Ich dachte schon, ich bin völlig blöd. Danke für die ganze Arbeit!

Ich bin gerade außer Gefecht und gehe wahrscheinlich morgen zum Doc - mal sehen, wann mein Gehirn wieder anspringt.

@Landei : sry, dass ich nicht vorher geschrieben habe. Ich hatte diesen Thread aus Zeitgründen nicht verfolgt. Ich glaube aber, dass du mit der Lösung in #18 in die falsche Richtung läufst.
Der “richtige” Ansatz ist mit dem Spring-Security-Dialekt. Siehe dazu hier: Thymeleaf + Spring Security integration basics - Thymeleaf: java XML/XHTML/HTML5 template engine
Das sieht dann im Template in etwa so aus:

<div sec:authorize="hasRole('ROLE_ADMIN')">
  This content is only shown to administrators.
</div>
<div sec:authorize="hasRole('ROLE_USER')">
  This content is only shown to users.
</div>

*** Edit ***

Siehe auch hier: https://github.com/thymeleaf/thymeleaf-extras-springsecurity#features