<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><atom:link href="https://www.lambdaschmiede.com" rel="self" type="application/rss+xml"></atom:link><title>lambdaschmiede Blog</title><link>https://www.lambdaschmiede.com</link><description>Ein Blog von lambdaschmiede GmbH</description><managingEditor>tim.zoeller@lambdaschmiede.com</managingEditor><category>Technologie</category><docs>https://cyber.harvard.edu/rss/rss.html</docs><generator>clj-rss</generator><item><title>Leichtgewichtige Prozesse mit Camunda und Zeebe</title><author>Tim Zöller (tim.zoeller@lambdaschmiede.com)</author><description>Durch den Übergang von Camunda 7 zu 8 wandelt sich die Prozessengine von einer Library, welche in Anwendungen integrierbar ist, zu einer umfangreichen Plattform mit vielen Komponenten. Beim Erkunden des empfohlenen Setups begegnen wir Helm Charts und Deployments mit acht und mehr Komponenten, wobei einige im Produktivbetrieb nur für Enterprise-Kunden verfügbar sind. Dieser Artikel zeigt, wie der Open-Source-Kernkomponente Zeebe Prozessapplikationen mit kleinem Fußabdruck erstellt werden können.</description><guid isPermaLink="false">leichtgewichtige-prozesse-mit-camunda-und-zeebe</guid><link>https://www.lambdaschmiede.com/de/blog/2024-02-19/leichtgewichtige-prozesse-mit-camunda-und-zeebe</link><pubDate>Mon, 19 Feb 2024 19:46:30 +0000</pubDate><content:encoded><![CDATA[<h2>Eine neue Architektur</h2><p>Es ist selten, dass eine neue Major-Version einer Software einen derart starken Paradigmenwechsel mit sich bringt: War die Camunda Platform in Version 7 noch komplett in Java Applikationen integrierbar, basiert Camunda 8 nun auf <a target="_blank" rel="noopener noreferrer" href="https://camunda.com/platform/zeebe/">Zeebe</a>, einer selbstständigen Komponente, mit welcher unser Code per gRPC kommuniziert. Anstatt den Zustand der Geschäftsprozesse in einer Datenbank abzubilden, werden die Operationen der Prozesse in einem Event Log gespeichert. Das Ziel der Camunda Entwickler war eine Prozessengine zu schaffen, welche Horizontal unbegrenzt skalierbar ist. Damit kann Camunda 8 einen weitaus höheren Durchsatz verarbeiten als Camunda 7, welches nur so viele Requests verarbeiten konnte wie seine zentrale, relationale Datenbank.&nbsp;</p><h2>Die Komplexität der Camunda Plattform 8</h2><p>Ein oft diskutierter Punkt an der neuen Plattform ist, dass die Komplexität einer durchschnittlichen Camunda Installation höher ist, als es für die alte Engine der Fall war. Ein produktives Deployment für Camunda 7 umfasste die Engine an sich, eine relationale Datenbank, optional die Taskliste um User Tasks abzuarbeiten und ein Cockpit, um in laufende Prozesse hineinzuschauen. Bis auf die Datenbank konnten wir all diese Komponenten in einer einzelnen Java Applikation bündlen, welche wir mit gewohnten Mitteln deployen konnten.</p><p><img src="https://strapi.lambdaschmiede.com/uploads/Camunda_7_arch_1c0a81183e.png" alt="Das Architekturdiagramm von Camunda 7, welches im voirherigen Text bereits beschrieben wurde" srcset="https://strapi.lambdaschmiede.com/uploads/thumbnail_Camunda_7_arch_1c0a81183e.png 245w,https://strapi.lambdaschmiede.com/uploads/small_Camunda_7_arch_1c0a81183e.png 500w,https://strapi.lambdaschmiede.com/uploads/medium_Camunda_7_arch_1c0a81183e.png 750w," sizes="100vw" width="750px"></p><p>Da die "neue" Zeebe Engine den Zustand der Prozessinstanzen nicht in einer Datenbank ablegt, sondern nur dafür verantwortlich ist die zugehörigen Ereignisse zu orchestrieren und an Job Worker zu delegieren, welche die eigentliche Verarbeitung mit Code vornehmen, gibt es keine API mit welcher wir den Zustand der Prozessinstanzen abfragen können. Es liegt an uns, den Zustand aus den Event-Logs herzustellen und abfragbar zu machen. Hilfsmittel hierfür sind die sogenannten Exporter, Plug-Ins in der Zeebe Engine, welche die Events an Kafka, Redis, Elasticsearch oder andere Speichertechnologien übermitteln, wo sie aggregiert werden können. Dieses Vorgehen nutzt die Camunda Plattform 8 selbst: Die Daten, welche von der Taskliste und dem "Cockpit" Nachfolger "Operate" benutzt werden, liegen in einem Elasticsearch Speicher, welcher Teil des Standard-Deployments ist. Diese Komponenten möchten natürlich alle gewartet und betreut werden, ganz abgesehen vom Ressourcenverbrauch.&nbsp;</p><p>Ein weiterer Wermutstropfen: Die grafischen Oberflächen "Tasklist" und "Operate" stehen nicht mehr unter einer Open Source Lizenz, und dürfen unlizensiert (und damit kostenlos) nicht in Produktion betrieben werden (auch wenn wir generell nicht empfehlen würden, unternehmenskritische Open Source Komponenten ohne Wartungsvertrag produktiv zu betreiben).</p><p><img src="https://strapi.lambdaschmiede.com/uploads/Camunda_8_arch_8cbf9cec50.png" alt="Ein Architekturdiagramm für Camunda Platform 8, welches im vorhergehenden Text bereits beschrieben wurde" srcset="https://strapi.lambdaschmiede.com/uploads/thumbnail_Camunda_8_arch_8cbf9cec50.png 240w,https://strapi.lambdaschmiede.com/uploads/small_Camunda_8_arch_8cbf9cec50.png 500w,https://strapi.lambdaschmiede.com/uploads/medium_Camunda_8_arch_8cbf9cec50.png 750w," sizes="100vw" width="750px"></p><h2>Eine Prozessengine für Großkonzerne?</h2><p>Gerade bei den Nutzer:innen der Camunda 7 Community Edition sorgen diese Änderungen für Verunsicherung. Nicht alle haben fachliche Anforderungen an eine Prozessengine, welche die Verwaltung dermaßen vieler Komponenten und die Betreuung eines Kubernetes Clusters rechtfertigen. Die alternative Nutzung des hauseigenen SaaS Produkts Camunda Cloud könnte auch für kleinere Unternehmen sinnvoll sein, hier skalieren die Kosten aber mit den ausgeführten Prozessinstanzen, ein Anwendungsfall sollte also sorgfältig berechnet werden. Der Break-Even-Point, ab welchem Self-Hosting sich lohnt dürfte erst bei einer beachtlichen Anzahl an Prozessinstanzen pro Stunde überschritten werden.</p><p>Aber was ist mit kleineren Projekten, welche von einer Prozessorchestrierung und dem Einsatz von BPMN profitieren können, mit der Nutzung des SaaS-Angebots oder dem Betrieb komplexerer Infrastruktur nicht mehr rentabel wären? Im folgenden Beispiel möchten wir dieser Frage nachgehen, indem wir einen leichtgewichtigen Prozess bauen und betreiben.</p><h2>Beispiel: Ein Erinnerungs-Bot für soziale Netzwerke</h2><p>Ein "Reminder-Bot" ist ein schönes Beispiel für einen leichtgewichtigen Prozess. Hierbei handelt es sich um einen Account, welcher in einer Nachricht mit dem Zusatz "in x Tagen" erwähnt wird, und nach Ablauf dieser Zeit per Post als Erinnerung antwortet. Als BPMN Prozess könnte dieser beispielsweise so aussehen:</p><p><img src="https://strapi.lambdaschmiede.com/uploads/reminder_process_e490ec3d06.png" alt="Ein BPMN 2.0 Diagramm, welches im nachfolgenden Text weiter beschrieben wird." srcset="https://strapi.lambdaschmiede.com/uploads/thumbnail_reminder_process_e490ec3d06.png 245w,https://strapi.lambdaschmiede.com/uploads/small_reminder_process_e490ec3d06.png 500w,https://strapi.lambdaschmiede.com/uploads/medium_reminder_process_e490ec3d06.png 750w," sizes="100vw" width="750px"></p><p>Aus dem Prozessmodell lässt sich der erwünschte Prozessflus transparent ablesen: Wir warten darauf, dass ein:e Nutzer:in eine Erinnerung anfordert, und starten dann den Prozess. Zunächst versuchen wir die Nachricht zu interpretieren. Können wir ein Zeitraum aus dem Text verstehen, oder nicht? Ist dies nicht der Fall, antworten wir mit einer Nachricht auf den Ausgangs-Beitrag und geben zu erkennen, dass wir keine Anweisung verstehen konnten. War uns dies jedoch möglich, bestätigen wir kurz den gesetzten Reminder und warten dann auf das berechnete Datum.</p><p>Nun kann einer von zwei Fällen eintreten: Entweder tritt das gewünschte Datum ein und wir schreiben den Reminder, so wie gewünscht. Oder uns wird mit dem Befehl "abbrechen" zu verstehen gegeben, dass der Reminder nicht mehr benötigt wird. In diesem Fall bestätigen wir dies ebenso und beenden den Prozess in einem anderen Zielstatus.</p><h2>Betrieb der Prozessengine</h2><p>Um die Parameter und benötigten Dateipfade oder Volumes darzustellen, haben wir ein Docker-Compose File aus der Camunda Dokumentation abgeleitet, welches außer Zeebe nichts enthält:</p><pre><code class="language-yaml">services:
  zeebe: # https://docs.camunda.io/docs/self-managed/platform-deployment/docker/#zeebe
    image: camunda/zeebe:8.4.1
    container_name: zeebe
    ports:
      - "26500:26500"
      - "9600:9600"
    environment:
      - ZEEBE_BROKER_DATA_DISKUSAGECOMMANDWATERMARK=0.998
      - ZEEBE_BROKER_DATA_DISKUSAGEREPLICATIONWATERMARK=0.999
      - "JAVA_TOOL_OPTIONS=-Xms512m -Xmx512m"
    restart: always
    healthcheck:
      test: [ "CMD-SHELL", "timeout 10s bash -c ':&gt; /dev/tcp/127.0.0.1/9600' || exit 1" ]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s
    volumes:
      - ./zeebe-data:/usr/local/zeebe/data</code></pre><p>Für den Container werden zwei Ports gemappt: 26500 für die gRPC Kommunikation und 9600 für den Startup- und Ready-Check von außerhalb. Ein Volume mappt die Daten aus <code>/usr/local/zeebe/data</code> auf den Host-Ordner <code>zeebe-data</code>, in diesem wird das Eventlog des Brokers gespeichert. An der Speicherkonfiguration <code>-Xmx512m</code>, welche einen maximalen Heap von 512MB definiert, sehen wir, dass die Engine keine großen Speicheranforderungen hat. Für Anwendungsfälle mit höherem Volumen dürften diese jedoch nicht ausreichend sein. Mit einem <code>docker-compose up</code> wird der Broker gestartet.</p><h2>Projektsetup mit Spring Boot</h2><p>Wir nutzen Spring Boot für unsere serverseitige Java Applikation, welche mit dem Zeebe Broker interagiert. Als Maven-Dependency fügen wir den Camunda Spring Boot Starter hinzu, welcher die Jobworker für uns konfiguriert und deren Lebenszyklus steuert:</p><pre><code class="language-java">&lt;dependency&gt;
    &lt;groupId&gt;io.camunda.spring&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-camunda&lt;/artifactId&gt;
    &lt;version&gt;8.4.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre><p>In der <code>application.properties</code> Datei definieren wir die URL unseres Zeebe Brokers. Da wir in einer Entwicklungs-Umgebung sind, müssen wir mit dem <code>plaintext</code> Property SSL deaktivieren - produktiv würden wir die Einrichtung einer Verbindung über TLS empfehlen:</p><pre><code class="language-plain">zeebe.client.broker.gateway-address=localhost:26500
zeebe.client.security.plaintext=true</code></pre><p>Mit Hilfe der Spring Boot Integration können wir darüber hinaus definieren, welche Prozessinstanzen beim Start unserer Applikation aus dem Klassenpfad heraus deployed werden sollen. Dies erfolgt mit der Annotation <code>@Deployment</code>, welche wir an beliebigen Konfigurationsklassen ergänzen können.</p><pre><code class="language-java">@Configuration
@Deployment(resources = "reminder-process.bpmn")
public class ZeebeConfig {
}</code></pre><p>In unserem Beispiel geben wir den Dateinamen unserer BPMN-Definition direkt an. Es ist aber auch möglich mit Wildcards zu arbeiten, um etwa sämtliche BPMN Dateien, oder bestimmte Dateien mit einem bestimmten Namensschema zu berücksichtigen.</p><h2>Start des Reminder-Prozesses</h2><p>Wir möchten eine neue Prozessinstanz starten, sobald unser Account in einem Posting erwähnt wird. Um mit der Engine zu interagieren, benötigen wir eine Instanz des <code>ZeebeClient</code>, welche wir uns dank der Spring Boot Integration per Dependency Injection übergeben lassen können. Im Beispiel definieren wir den Java Record <code>ProcessInput</code>, welcher die benötigten Eingangsvariablen für den Start einer Prozessinstanz beinhaltet.&nbsp;</p><pre><code class="language-java">@Autowired
private final ZeebeClient zeebeClient;

record ProcessInput(String content,
                    String statusId,
                    String visibility,
                    String account) {};

public Long startReminderProcess(Status status) {
   var processInput = new ProcessInput(status.content(), 
                                       status.id(), 
                                       status.visibility(),
                                       status.account().acct());

   var result = zeebeClient
           .newCreateInstanceCommand()
           .bpmnProcessId("Process_Reminder")
           .latestVersion()
           .variables(processInput)
           .send()
           .join();

   return result.getProcessInstanceKey();
}</code></pre><p>Geführt durch einen mehrstufigen Builder erstellen wir nun eine Prozessinstanz, welche aus der Prozessdefinition mit der ID <code>Process_Reminder</code> erstellt wird. Wir möchten diese in der neuesten deployeten Version starten, und die gemappten Inputvariablen übergeben. Der Aufruf von <code>send()</code> sendet den Befehl per gRPC asynchron an die Zeebe Engine; mit <code>join()</code> warten wir auf die erfolgreiche Ausführung, um die ID der neu erstellen Prozessinstanz als Rückgabewert der Methode nutzen zu können.</p><h2>Ausführen der Programmlogik mit Jobworkern</h2><p>Damit unser Geschäftsprozess nicht nur ein ansehnliches Bild ist, sondern unsere Applikation orchestriert, müssen wir an Service Tasks einen Typen definieren. Dieser wird von sogenannten Job Workern verwendet, um den Typ der zu verarbeitenden Aufgabe zu identifizieren.&nbsp;</p><figure class="image"><img src="https://strapi.lambdaschmiede.com/uploads/grafik_9500ddf1aa.png" alt="Ein Service Task, welcher im Camunda Modeler ausgewählt wurde. Im Properties Panel nebenan wurde der Typ der Task Definition auf &quot;parseDate&quot; festgelegt" srcset="https://strapi.lambdaschmiede.com/uploads/thumbnail_grafik_9500ddf1aa.png 245w, https://strapi.lambdaschmiede.com/uploads/small_grafik_9500ddf1aa.png 500w, https://strapi.lambdaschmiede.com/uploads/medium_grafik_9500ddf1aa.png 750w, https://strapi.lambdaschmiede.com/uploads/large_grafik_9500ddf1aa.png 1000w" sizes="100vw" width="1000"></figure><p>Beim folgenden Beispiel eines Java Jobworkers nutzen wir die Annotation <code>@JobWorker</code> aus dem oben erwähnten Spring Boot Starter um die Verbindung zum Service Task herzustellen. Im Hintergrund pollt unsere Anwendung die Engine per gRPC um zu ermitteln, ob zu verarbeitende Jobs existieren.&nbsp;</p><pre><code class="language-java">public record ParseRequestResponse(boolean dateUnderstood, 
                                   ZonedDateTime reminderDate) {};

@JobWorker(type = "parseDate")
public ParseRequestResponse parseRequest(
	@Variable String content) {
	
    var matcher = PATTERN.matcher(content);
    if(matcher.find()) {
        var result = matcher.group(1);
        return new ParseRequestResponse(true, 
                      				    ZonedDateTime.now()
                                          .plusDays(Long.parseLong(result)));
    } else {
        return new ParseRequestResponse(false, null);
    }
}</code></pre><p>Ist dies der Fall, wird unsere Methode aufgerufen. Der in der Signatur aufgeführte Parameter <code>String content</code> wird hierbei aus den Prozessvariablen abgebildet und an die Methode übergeben. Aus dem <code>content</code> versuchen wir mit Hilfe von regulären Ausdrücken eine Erwähnung der Tage zu extrahieren und addieren diese Tage zum aktuellen Datum um das Datum für den Reminder zu ermitteln. Die Antwort an die Prozessengine geben wir als Rückgabewert der Methode zurück. In unserem Fall haben wir lokal einen Java Record definiert, welcher zwei Variablen enthält: Einen Wahrheitswert, welcher abbildet ob die Anfrage überhaupt verstanden wurde, und ein <code>ZonedDateTime</code>, welches das berechnete Datum der Erinnerung enthält. Die Camunda Spring Integration mappt nach dem Aufruf der Methode jedes Feld des Response Objekts auf eine Prozessvariable und mappt die Feldnamen dabei auf Variablennamen. Wir führen also in diesem Beispiel zwei neue Prozessvariablen ein: <code>dateUnderstood</code> und <code>reminderDate</code>.&nbsp;</p><p>Bei der direkten Interaktion mit Posts des sozialen Netzwerks ist das Prinzip sehr ähnlich. Auch hierzu registrieren wir einen JobWorker an der Engine, welche auf den ihm zugeordneten Task lauscht. Wird dieser von der Engine innerhalb einer Prozessinstanz erreicht, wird die mit <code>@JobWorker</code> annotierte Methode aufgerufen, und die Variablen in die Parameter der Methode gemappt.&nbsp;</p><pre><code class="language-java">public record SuccessOutput(String reminderStatusId) {};

@JobWorker(type = "confirmCancellation")
public SuccessOutput confirmCancellation(
						@Variable String statusId,
                        @Variable String visibility,
                        @Variable String account) {
                        
    var message = MESSAGE_TEMPLATE.formatted(account, 
                                             "Na gut, der Timer ist abgebrochen");
    var result = mastodonService.replyToStatus(statusId, 
                                               message, 
                                               visibility);
    return new SuccessOutput(result.id());
}</code></pre><p>Dieser Worker erhält die Information auf welchen Status er antworten soll per <code>statusId</code>, in welcher Sichtbarkeit der Status geschrieben wurde mittels <code>visibility</code> und welchem Account geantwortet wird mit <code>account</code>. Die Status ID benötigen wir, um den Post an den korrekten Vorgänger anzuhängen, den Account um den Nutzer darin zu erwähnen und die Sichtbarkeit des Ausgangsposts, um in der selben Sichtbarkeit zu antworten - wir möchten schließlich nicht öffentlich sichtbar auf eine private Erwähnung antworten. Mit diesen Informationen setzen wir uns die Nachricht aus dem (nicht aufgeführten) Message Template zusammen und senden Sie an einen Server des dezentralen sozialen Netzwerks.&nbsp;</p><p>Ähnlich verhält es sich mit den JobWorkern für die anderen Tasks, welche hier nicht mehr im Detail beschrieben werden, aber der Vollständigkeit halber aufgelistet sind.</p><pre><code class="language-java">@JobWorker(type = "confirmReminder")
public SuccessOutput confirmReminder(
						@Variable String statusId,
                        @Variable ZonedDateTime reminderDate,
                        @Variable String visibility,
                        @Variable String account) {
    var message = MESSAGE_TEMPLATE.formatted(account, 
                                             "Hey, alles klar! Ich erinnere dich am %s".formatted(reminderDate));
    var result = mastodonService.
    				replyToStatus(statusId, message, visibility);
    return new SuccessOutput(result.id());
}

@JobWorker(type = "respondDateNotUnderstood")
public void replyNotUnderstood(
				@Variable String statusId,
                @Variable String visibility,
                @Variable String account) {
                
    var message = MESSAGE_TEMPLATE.formatted(account, 
    										 "Hi, das habe ich leider nicht verstanden. Schreibe mir 'in x Tagen' oder einfach 'x Tage'");
    mastodonService.replyToStatus(statusId, message, visibility);
}

@JobWorker(type = "remindUser")
public void remind(@Variable String statusId,
                   @Variable String visibility,
                   @Variable String account) {
    var message = MESSAGE_TEMPLATE.formatted(account, "Hi, hier ist deine Erinnerung!");
    mastodonService.replyToStatus(statusId, message, visibility);
}</code></pre><h2>Einschränkungen&nbsp;</h2><p>Am Beispiel können wir sehen, dass sich auch mit der "neuen" Camunda Engine kleinere Applikationen umsetzen lassen, ohne einen Kubernetes Cluster für die Infrastruktur zu betreiben. Neben unserer Programmlogik in einer Spring Boot Applikation benötigen wir vorerst nur den Zeebe Broker, welcher dafür auch mehrere Applikationen gleichzeitig bedienen könnte. Ein wichtiges Element von Prozessengines fehlt in unseren Beispiel jedoch: Der Einblick in die laufende Prozessengine. Wie können wir sehen, welche Prozessinstanzen sich in welchem Zustand befinden, oder Incidents betrachten und auflösen? Mit unserem Setup: Gar nicht. Wie eingangs erwähnt, ist es nicht möglich aktuelle Zustände aus dem Zeebe Broker per gRPC abzufragen, wir können nur auf Ereignisse der Engine reagieren. In der vollwertigen Camunda 8 Plattform werden diese Ereignisse mit den Elasticsearch-Exporter in eine Elasticsearch Instanz &nbsp;übermittelt, welche einen abfragbaren Zustand verwaltet. Auf diesem Zustand basieren die Taskliste und die Operate Applikation (vormals: Cockpit), welche nur mit einer kostenpflichtigen Enterprise Lizenz produktiv genutzt werden dürfen. Aus der Community heraus wurde als offene Lösung der <a target="_blank" rel="noopener noreferrer" href="https://github.com/camunda-community-hub/zeebe-simple-monitor">Simple Zeebe Monitor</a> entwickelt, welcher die grundlegenden Funktionen für den Einblick und die Interaktion mit Prozessinstanzen bereitstellt. Der Zustand der Prozessinstanzen wird hierfür in Kafka, Hazelcast oder Redis gespeichert und muss ebenfalls mit einem passenden Zeebe Exporter übermittelt werden. Hierzu benötigen wir also erneut zusätzliche Infrastruktur.</p><h2>Fazit</h2><p>Auch für kleinere Softwareprojekte kann die neue Prozessengine hinter Camunda 8, Zeebe, interessant sein. Keinesfalls müssen wir die Komplexität des vollen Camunda 8 Deployments bedienen. Ebenso ist es für die Nutzung von Zeebe nicht erforderlich, die Camunda Cloud oder eine Camunda 8 Enterprise Lizenz zu erwerben. Sind wir bereit etwas zusätzliche Infrastruktur zu pflegen, können wir mit dem Simple Zeebe Monitor sogar eine Überwachung und Administration der Prozessinstanzen ermöglichen. Während dies für kleine Projekte, Hobby- oder Open Source Applikationen ein gangbarer Weg sein kann, kommt dies eher nicht für die Migration von Camunda 7 Applikationen infrage, welche in der Community Edition produktiv in Unternehmen im Einsatz sind oder User Tasks nutzen.&nbsp;</p>]]></content:encoded></item><item><title>Mobile Navigationsmenüs ohne JavaScript</title><author>Paul Hempel (paul.hempel@lambdaschmiede.com)</author><description>Um unsere Firmen Webseite möglichst barrierefrei zu gestalten, wollten wir zentrale Funktionen ohne JavaScript verfügbar machen. Aus diesem Grund beschäftigten wir uns mit der Frage ob ein mobiles Navigationsmenü durch reines HTML und CSS gelöst werden kann.</description><guid isPermaLink="false">mobile-navigationsmenues-ohne-java-script</guid><link>https://www.lambdaschmiede.com/de/blog/2023-10-25/mobile-navigationsmenues-ohne-java-script</link><pubDate>Wed, 25 Oct 2023 11:28:40 +0000</pubDate><content:encoded><![CDATA[<div class="raw-html-embed"><video width="240" autoplay="true" loop="true">
  <source src="https://strapi.lambdaschmiede.com/uploads/Responsive_Menu_without_JS_Example_981d4598b0.webm">
</video></div><h2>Die zentrale Umsetzung</h2><p>Der <a target="_blank" rel="noopener noreferrer" href="https://gist.github.com/lambdaschmied2/fe226a473bf938f5691031b34aea775d">vollständige Code</a> ist als Gist auf GitHub zu finden.</p><p>Auf HTML Seite halten wir die Navigation recht einfach. Im &lt;nav&gt; Bereich sind zwei Listen, eine davon ist für die mobile Ansicht mit einem ausklappbaren details-Block umschlossen:</p><pre><code class="language-html">&lt;nav&gt;
&lt;!-- desktop --&gt;
&lt;ul&gt;
 &nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;a href="/"&gt;About&lt;/a&gt;&lt;/li&gt;
 &nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;a href="/"&gt;Blog&lt;/a&gt;&lt;/li&gt;
 &nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;a href="/"&gt;Contact&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;!-- mobile --&gt;
&lt;details&gt;
 &nbsp;&nbsp;&nbsp;&lt;summary aria-label="open mobile menu"&gt;&lt;/summary&gt;
 &nbsp;&nbsp;&nbsp;&lt;ul&gt;
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;a href="/"&gt;About&lt;/a&gt;&lt;/li&gt;
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;a href="/"&gt;Blog&lt;/a&gt;&lt;/li&gt;
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;&lt;a href="/"&gt;Contact&lt;/a&gt;&lt;/li&gt;
 &nbsp;&nbsp;&nbsp;&lt;/ul&gt;
&lt;/details&gt;
&lt;/nav&gt;</code></pre><p><br>Auf dem Desktop wollen wir den <code>details</code>-Tag und dessen Inhalte nicht sehen, in der mobilen Ansicht wiederum das Desktop-Menü nicht:</p><pre><code class="language-css">nav&gt;details {
 &nbsp;&nbsp;&nbsp;display: none;
}

@media (max-width: 991px) {
 &nbsp;&nbsp;&nbsp;/* hide desktop menu */
 &nbsp;&nbsp;&nbsp;nav&gt;ul {
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;display: none !important;
 &nbsp;&nbsp;&nbsp;}
 &nbsp;&nbsp; 
 &nbsp;&nbsp;&nbsp;/* show mobile menu */
 &nbsp;&nbsp;&nbsp;nav&gt;details {
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;display: block !important;
 &nbsp;&nbsp;&nbsp;}
}</code></pre><p><strong>Fertig?!</strong></p><p>Unsere Lösung erfüllt nun alle technischen Anforderungen an ein responsives Menü. Visuell möchten wir im folgenden Teil das Ergebnis noch verbessern.</p><h2>Ein Hamburger Menü Icon hinzufügen</h2><p>Meistens sehen wir ein Hamburger Menü Icon und ein Schließen-Icon in mobilen Varianten von Webseiten. Das soll hier natürlich nicht fehlen – und nur mit CSS umgesetzt werden:</p><pre><code class="language-css">/* custom image for mobile menu */
nav &gt; details &gt; summary::after {
 &nbsp;&nbsp;&nbsp;content: '';
 &nbsp;&nbsp;&nbsp;background-image: url(img/menu.svg);
 &nbsp;&nbsp;&nbsp;background-size: 2rem 2rem;
 &nbsp;&nbsp; 
 &nbsp;&nbsp;&nbsp;display: inline-block;
 &nbsp;&nbsp;&nbsp;width: 2rem;
 &nbsp;&nbsp;&nbsp;height: 2rem;
}

/* alternative image for open mobile menu */
nav &gt; details[open] &gt; summary::after {
 &nbsp;&nbsp;&nbsp;background-image: url(img/close.svg);
}

/* icons used: https://ionic.io/ionicons */</code></pre><p>Das <code>details[open]</code>-Attribut lässt uns prüfen, ob <code>&lt;details&gt;</code> geöffnet oder geschlossen ist. Wenn es offen ist, ersetzen wir in unserem Fall das Hintergrundbild.<br>Da wir mit dem <code>::after</code> Pseudoelement arbeiten, wollen wir dafür sorgen, dass das Icon nicht die ganze Seite einnimmt. Mit <code>background-size</code>, <code>display: inline-block;</code>, <code>width: 2rem;</code> und <code>height: 2rem;</code> in Kombination können wir dafür sorgen, dass das Icon unsere gewünschte Größe hat.</p><h2>Animationen</h2><p>Da wir mit CSS nicht die Eltern-Knoten auswählen können, greifen wir hier zu einem kleinen Trick, der eine dezente Animation hinzufügt. Animationen sind nicht zwingend notwendig, da es sich hier, meiner Meinung nach, <i>nicht</i> um eine offensichtliche Lösung handelt, wollen wir uns kurz anschauen, was möglich ist:</p><pre><code class="language-css">nav&gt;details&gt;summary {
 &nbsp;&nbsp;&nbsp;[...]
 &nbsp;&nbsp;&nbsp;/* slight transition */
 &nbsp;&nbsp;&nbsp;transition: margin-bottom 150ms ease-out;
}

/* add margin for slight transition */
nav&gt;details[open]&gt;summary {
 &nbsp;&nbsp;&nbsp;margin-bottom: 1rem;
}</code></pre><p>Da wir mit CSS Selektoren (noch?) nicht auf ein Eltern-Element zugreifen können um dieses im Ganzen zu animieren, fügen wir im offenen Zustand des <code>dialog</code> einen kleinen <code>margin-bottom</code> hinzu, der mit dem <a target="_blank" rel="noopener noreferrer" href="https://developer.mozilla.org/en-US/docs/Web/CSS/transition"><code>transistion</code>-Attribut</a> von <code>0rem</code> auf <code>1rem</code> in <code>150ms</code> wächst.</p><h2>Das Layout anpassen</h2><p>Das genaue Layout unterliegt individuellen Anforderungen. Daher soll in diesem Artikel nicht weiter darauf eingegangen werden. Nichtsdestotrotz haben wir im <a target="_blank" rel="noopener noreferrer" href="https://gist.github.com/lambdaschmied2/fe226a473bf938f5691031b34aea775d">Gist</a> den Code für eine ordentliche Layout Variante als Beispiel angereichert.</p><h2>Visuelle Variationen</h2><h3>Ein Fixierter Header</h3><p>Der bestehende Code lässt die Navigationsleiste mit der gesamten Webseite scrollen. Falls diese jedoch immer sichtbar sein soll, können wir sie wie folgt fixieren:</p><pre><code class="language-css">header {
 &nbsp;&nbsp;&nbsp;background-color: aliceblue;
 &nbsp;&nbsp;&nbsp;display: flex;
 &nbsp;&nbsp;&nbsp;justify-content: space-between;
 &nbsp;&nbsp; 
 &nbsp;&nbsp;&nbsp;/** toggle fixed header **/
 &nbsp;&nbsp;&nbsp;position: fixed;
 &nbsp;&nbsp;&nbsp;width: 100%;
}
main {
 &nbsp;&nbsp;&nbsp;/* don't forget to increase the top padding with fixed navbar */
 &nbsp;&nbsp;&nbsp;padding: 4rem 1rem;
}</code></pre><h3>Ein bildschirmfüllender, fixierter Header&nbsp;</h3><p>Bis jetzt ist der <code>header</code> im aufgeklappten Zustand nur so groß wie der Inhalt. Falls das nicht gewünscht ist und er stattdessen die ganze Seite ausfüllen soll, können wir das wie folgt beeinflussen:</p><pre><code class="language-css">nav&gt;details&gt;ul {
 &nbsp;&nbsp;&nbsp;flex-direction: column;
 &nbsp;&nbsp;&nbsp;align-items: end;
 &nbsp;&nbsp;&nbsp;margin: 0;
 &nbsp;&nbsp; 
 &nbsp;&nbsp;&nbsp;/** toggle full page menu **/
 &nbsp;&nbsp;&nbsp;height: 100vh;
}</code></pre><h3>Ein bekanntes Problem</h3><p>Durch fehlendes, sogenanntes "Focus-Trapping" kann die Seite im Hintergrund weiter gescrollt werden. Das kann ausschließlich durch JavaScript verhindert werden (Stand: 2023). Daher haben wir uns bei <a target="_blank" rel="noopener noreferrer" href="https://lambdaschmiede.com">unserer Webseite</a> für den fixierten Header entschieden, der nicht bildschirmfüllend ist.</p>]]></content:encoded></item><item><title>JavaScript mit GraalVM aus Java aufrufen</title><author>Tim Zöller (tim.zoeller@lambdaschmiede.com)</author><description>Bei der Entwicklung unserer Webseite und dieses Blogs war es uns wichtig, dass sie auch ohne JavaScript funktionsfähig sind. Eine Herausforderung stellte für uns dabei das Syntax-Highlighting für Quellcode Blöcke in Blog-Beiträgen dar – gängige Bibliotheken für diesen Anwendungsfall existieren hauptsächlich in JavaScript. In diesem Artikel zeigen wir, wie wir diese Bibliothek im Backend mit den polyglotten Features von GraalVM ausführen.</description><guid isPermaLink="false">java-script-mit-graal-vm-aus-java-aufrufen</guid><link>https://www.lambdaschmiede.com/de/blog/2023-09-10/java-script-mit-graal-vm-aus-java-aufrufen</link><pubDate>Sun, 10 Sep 2023 12:54:42 +0000</pubDate><content:encoded><![CDATA[<h2>Die Problemstellung</h2><p>Unsere Kernkompetenz liegt in Java und weiteren JVM Sprachen wie Clojure oder Kotlin. Daher ist uns die Entscheidung, unsere Webseite mit Clojure umzusetzen und auf einer JVM zu betreiben leicht gefallen. Ein vollwertiges Content Management System (CMS) einzusetzen hätte für uns einen höheren Wartungs- und Pflegeaufwand mit sich gebracht, da unsere Webseite lediglich Kerninformationen zu unserem Unternehmen darstellen soll. Die Entscheidung für serverseitiges Rendern der HTML Dateien fiel ebenfalls aus Gründen der Einfachheit: Die meist statischen Seiten erfordern keine dynamischen Inhalte im Browser, weshalb es uns nicht gerechtfertigt erscheint eine größere Menge an Javascript Dateien an den Browser der Betrachter:innen zu senden, welche die Geschwindigkeit des Seitenaufbaus reduzieren.&nbsp;</p><p>Aus diesen Gründen lässt sich aber noch nicht ableiten, wieso unsere Webseite komplett ohne JavaScript funktionieren sollte. Schließlich kann JavaScript auf der Webseite zu einer besseren Erfahrung für Nutzer:innen führen. Gute Gründe für diese Entscheidung liefert Nathaniel in seinem (englischen) Blog Post <a target="_blank" rel="noopener noreferrer" href="https://endtimes.dev/why-your-website-should-work-without-javascript/">"Why your website should work without JavaScript"</a>. Zahlen aus den letzten drei Jahren gehen davon aus, dass 1% aller Endgeräte welche eine Webseite besuchen kein JavaScript ausführen. Dies ist jedoch nur bei einem Fünftel dieser Endgeräte Absicht, etwa weil die Nutzer:innen JavaScript deaktiviert haben oder eine Browsererweiterung mit NoScript nutzen. Die anderen 0,8% aller Endgeräte führen JavaScript durch Fehler nicht aus: Veraltete Browser, fehlkonfigurierte Browser-Erweiterungen, Sicherheitseinstellungen auf beruflichen Geräten. Webseiten welche IT-Inhalte anbieten sollten davon ausgehen, dass die Zahl von Endgeräten mit absichtlich deaktiviertem JavaScript höher liegt als bei 0,2% aller Geräte.</p><p>Dieser Blog soll hauptsächlich ein technischer Blog sein. Wir werden Artikel über Softwareentwicklung und -architektur schreiben und diese mit ausführlichen Codebeispielen ergänzen. Syntax-Highlighting ist hierbei für eine gute Lesbarkeit dieses Codes hilfreich. Dies wird am nachfolgenden Codeblock ersichtlich, der Methode <code>read</code> aus der Klasse <code>java.io.CharArrayReader</code>:</p><pre><code class="language-java">@Override
public int read(CharBuffer target) throws IOException {
    synchronized (lock) {
        ensureOpen();

        if (pos &gt;= count) {
            return -1;
        }

        int avail = count - pos;
        int len = Math.min(avail, target.remaining());
        target.put(buf, pos, len);
        pos += len;
        return len;
    }
}</code></pre><p>Die Farbgebung hilft dem Auge sofort dabei die Struktur des Codebeispiels zu erfassen. Keywords der Sprache werden in einer gemeinsamen Farbe markiert, Klassen- und Methodennamen ebenfalls. Dies orientiert sich am Highlighting von Entwicklungsumgebungen, mit welchen Softwareentwickler den Großteil ihrer Arbeit verrichten. Ein solches Highlighting erfolgt üblicherweise im Frontend, etwa mit <a target="_blank" rel="noopener noreferrer" href="https://prismjs.com/">Prism.js</a> oder <a target="_blank" rel="noopener noreferrer" href="https://highlightjs.org/">highlight.js</a>. Diese sind leichtgewichtig und wären ohnen großen Aufwand in diese Webseite einzubinden, fügen das Syntax-Highlighting jedoch erst nach dem Laden der HTML-Ressource hinzu, indem sie nach <code>&lt;code&gt;</code> Tags suchen und auf deren Inhalt ihre Logik anwenden. Neben einer Verzögerung beim Rendering bedeutet dies aber auch, dass das Syntax-Highlighting bei Browsern mit deaktiviertem oder nicht funktionierendem JavaScript nicht angewandt würde und die Benutzbarkeit der Webseite auf betroffenen Endgeräten sich verschlechtert.</p><p>Aus diesen Gründen möchten wir also das Syntax-Highlighting bereits auf dem Server vornehmen. Die Recherche nach geeigneten Bibliotheken für Java war jedoch ernüchternd: Neben vielen toten Links zu dev.java.net und Google Code blieben noch ein paar kleinere Bibliotheken mit Unterstützung für wenige Sprachen und sehr geringer Verbreitung übrig. Da wir eine Lösung benötigten, welche auch in Zukunft noch mit Unterstützung für neue Programmiersprachen versorgt wird, entschieden wir uns dazu das JavaScript serverseitig mit Truffle auszuführen.</p><h2>Was ist GraalVM?</h2><p><a target="_blank" rel="noopener noreferrer" href="https://www.graalvm.org/">GraalVM</a> ist eine universelle virtuelle Maschine, die von Oracle entwickelt wurde. Sie wurde entwickelt, um verschiedene Programmiersprachen effizient auszuführen und miteinander zu integrieren. GraalVM unterstützt eine breite Palette von Sprachen, darunter Java, JavaScript, Python, Ruby, R, C, C++, und WebAssembly. Es ist darauf ausgerichtet, die Leistung von Sprachen auf einer gemeinsamen Plattform zu verbessern und Entwicklern die Möglichkeit zu bieten, verschiedene Sprachen nahtlos miteinander zu kombinieren. Diese Fähigkeit möchten wir nutzen, um prism.js aus Java (bzw. Clojure) heraus aufzurufen. Unter Java-Entwickler:innen ist GraalVM jedoch nicht in erster Linie für ihre polyglotten Eigenschaften bekannt, sondern für ihre Fähigkeit Java Applikationen mit Native Image zu nativen Binaries zu kompilieren.&nbsp;</p><h2>Einrichten von VM und Umgebung</h2><p>Das folgende Beispiel setzt die GraalVM Community Edition in Version 20.0.2 ein, sie sollte als JVM im Kontext (IDE oder $PATH) eingerichtet sein. Da die Laufzeitumgebung für JavaScript nicht standardmäßig installiert ist, muss diese zunächst mit dem GraalVM Updater installiert werden:</p><pre><code class="language-plain">&gt; gu install js

Downloading: Component catalog from www.graalvm.org
Processing Component: Graal.js
Processing Component: ICU4J
Processing Component: TRegex
Additional Components are required:
    ICU4J (org.graalvm.icu4j, version 23.0.1), required by: Graal.js (org.graalvm.js)
    TRegex (org.graalvm.regex, version 23.0.1), required by: Graal.js (org.graalvm.js)
Downloading: Component js: Graal.js from github.com
Downloading: Component org.graalvm.icu4j: ICU4J from github.com
Downloading: Component org.graalvm.regex: TRegex from github.com
Installing new component: TRegex (org.graalvm.regex, version 23.0.1)
Installing new component: ICU4J (org.graalvm.icu4j, version 23.0.1)
Installing new component: Graal.js (org.graalvm.js, version 23.0.1)
Refreshed alternative links in /usr/bin/</code></pre><p>Da wir unsere Applikation containerisiert ausliefern, ist dieser Schritt auch im Dockerfile nötig:</p><pre><code class="language-docker">FROM ghcr.io/graalvm/graalvm-community:20.0.2
COPY target/uberjar/website.jar /website/app.jar
EXPOSE 3000
RUN ["gu", "install", "js"]
CMD ["java", "-jar", "/website/app.jar"]</code></pre><p>Erwähnt sei an dieser Stelle noch, dass es nicht zwangsläufig nötig ist die gesamte Applikation auf GraalVM laufen zu lassen. <a target="_blank" rel="noopener noreferrer" href="https://www.graalvm.org/latest/reference-manual/js/RunOnJDK/">Wie hier beschrieben</a> ist es auch möglich die JavaScript Runtime in einer "herkömmlichen" JVM auszuführen.</p><h2>Die Bibliothek aufrufen</h2><p>Nach der langen Einleitung stellt sich der eigentliche Aufruf des Codes recht kurz dar. Zunächst erzeugen wir einen neuen <code>Context</code>, mit Hilfe eines Builder Patterns. Dieser stellt uns eine JavaScript Laufzeitumgebung bereit:</p><pre><code class="language-java">import org.graalvm.polyglot.Context;
import org.graalvm.ployglot.Source;

private Context buildContext() {
  return Context.newBuilder("js").build();
}</code></pre><p>Es empfiehlt sich, den Kontext einmalig zu initialisieren und danach wiederzuverwenden. Da die <code>buildContext</code> Methode also unser Einstiegs- und Initialisierungspunkt ist, können wir an dieser Stelle auch prism.js aus einer Datei (oder für die produktive Applikation besser einer Ressource) laden:</p><pre><code class="language-java">import org.graalvm.polyglot.Context;
import org.graalvm.ployglot.Source;

private Context buildContext() {
  var context = Context.newBuilder("js").build();
  context.eval(Source.newBuilder("js", new File("/path/to/prism.js")).build());
  return context;
}</code></pre><p>Mit diesem initialisierten Kontext können wir nun Arbeiten um einen Text um Syntax-Highlighting für die Anzeige in HTML zu ergänzen:</p><pre><code class="language-java">private String highlightSyntax(Context context, String language, String content) {
  var command = "Prism.highlight('%s', Prism.languages.%s, '%s');".formatted(content, language, language);
  return context.eval("js", command).asString();
}</code></pre><p>Wir benutzen den erzeugten Kontext um den formatierten JavaScript Code auszuführen und geben das Ergebnis der Anweisung als Java String zurück. Um den Code hervorzuheben, nutzen wir die Funktion <code>highlight</code>, deren <a target="_blank" rel="noopener noreferrer" href="https://prismjs.com/docs/Prism.html#.highlight">Dokumentation sich hier finden lässt</a>. Hierbei sticht schnell ins Auge, dass wir den Script-Befehl mit String-Manipulation erzeugen und hier eine potenzielle Türe für Script-Injection öffnen. Die Eingaben sollten sich deshalb (wie in unserem Fall) aus Daten zusammensetzen welche wir selbst kontrollieren, oder extrem gut validiert werden.</p><p>Durch einen kurzen Test können wir überprüfen, dass unser Syntax-Highlighting wie gewünscht funktioniert:</p><pre><code class="language-java">String javaCode = "private static final Integer BEST_NUMBER = 42;";
var highlightedCode = highlightSyntax(context, "java", javaCode);
System.out.println(highlightedCode);</code></pre><pre><code class="language-html">&lt;span class="token keyword"&gt;private&lt;/span&gt; 
&lt;span class="token keyword"&gt;static&lt;/span&gt; 
&lt;span class="token keyword"&gt;final&lt;/span&gt; 
&lt;span class="token class-name"&gt;Integer&lt;/span&gt; 
&lt;span class="token constant"&gt;BEST_NUMBER&lt;/span&gt; 
&lt;span class="token operator"&gt;=&lt;/span&gt; 
&lt;span class="token number"&gt;42&lt;/span&gt;
&lt;span class="token punctuation"&gt;;&lt;/span&gt;</code></pre><p>Die Symbole sind mit CSS Klassen versehen worden und können nun mit CSS hervorgehoben werden</p><h2>Vermeiden paralleler Zugriffe</h2><p>Der Code sieht simpel aus, doch gerade im Backend einer Webseite, welche wünschenswerterweise viele gleichzeitige Anfragen ausliefert, birgt er Gefahren. Bei der Simulation mehrerer hundert paralleler Usersessions wurde in unserem Code folgende Exception ausgelöst:</p><pre><code class="language-plain">java.lang.IllegalStateException: Multi-threaded access requested by thread Thread[http-nio-8778-exec-1,5,main] but is not allowed for language(s) js.</code></pre><p>Die JavaScript Runtime von GraalVM unterstützt (wie so viele) kein Multithreading. Greift unsere Applikation aus zwei verschiedenen Threads auf den <code>context</code> zu, kommt es zum oben genannten Fehler. GraalVM <a target="_blank" rel="noopener noreferrer" href="https://github.com/oracle/graaljs/blob/master/graal-js/src/com.oracle.truffle.js.test.threading/src/com/oracle/truffle/js/test/threading/ConcurrentAccess.java">bietet hier ein umfangreiches Toolset</a> an, um diese Zugriffe zu synchronisieren.&nbsp;</p><h2>Den Code in der HTML-Quelle mit Clojure dekorieren</h2><p>Wir verfassen unsere Blog-Einträge in einem Headless CMS. An unser Frontend, welches Sie, liebe Leser:innen gerade betrachten, wird der Inhalt als HTML geliefert. Dies bedeutet, dass wir keinen einfachen Zugriff auf die reinen Quelltexte im Artikel haben, sondern Prism.js manuell auf diese anwenden müssen. Haben wir bisher die Nutzung von JavaScript auf GraalVM mit Java Code verdeutlicht, zeigen wir alle weiteren Schritte nun am Clojure Code, welchen wir für diesen Blog auch so (oder so ähnlich) einsetzen. Als erstes müssen wir wieder die Initialisierung der Script-Engine mit prism.js vornehmen. Diesen Kontext erzeugen wir im entsprechenden Namensraum mit <code>defonce</code>, und <code>delay</code>. Mit <code>delay</code> stellen wir sicher, dass die Engine erst erzeugt wird, wenn sie auch wirklich benutzt wird, also nach einem Applikationsneustart zum ersten Mal ein Syntax Highlighting erfolgt. Damit stellen wir sicher, dass die Initialisierung nicht während des Applikationsstarts erfolgt und hier blockiert und nehmen dabei inkauf, dass beim allerersten Aufruf eines Renderings 200-300ms länger verstreichen. &nbsp;</p><pre><code class="language-clojure">(defonce js-context
         (delay (doto
                  (.build (Context/newBuilder (into-array ["js"])))
                  (.eval
                    (.build
                      (Source/newBuilder "js"
                                         (clojure.java.io/resource "prism.js")))))))</code></pre><p>Der Code, leider gespickt mit Java Interop Syntax, um aus Clojure heraus mit Java Syntax zu arbeiten, funktioniert hierbei genau so wie im Java-Beispiel oben. &nbsp;</p><p>Ebenfalls keine großen Überraschungen erwarten uns beim Ausführen der eigentlichen JavaScript Syntax:</p><pre><code class="language-clojure">(defn highlight-code [language code]
  (let [context @js-context]
    (locking context
      (.asString
        (.eval context
               "js"
               (format "Prism.highlight('%s', Prism.languages.%s, '%s');" 
                       code 
                       language 
                       language))))))</code></pre><p>Auch hier kommt uns der Code sehr bekannt vor. Bemerkenswert sind drei Dinge: Das <code>@</code> vor <code>js-context</code> dereferenziert unseren lazy initialisierten Kontext. Ist dies bis hierhin noch nicht geschehen, wird er erst an dieser Stelle initialisiert. Weiterhin nutzen wir Clojures <code>locking</code> Funktion um parallelen Zugriff auf den <code>js-context</code>zu verhindern. Zum anderen möchten wir uns hier vielleicht doch ein bisschen am Kopf kratzen: Wir nutzen Java-Interop Code aus Clojure heraus um am Ende JavaScript auszuführen, das darf man gerne auch amüsant finden.</p><p>Um nun abschließend unser Syntax-Highlighting auf allen Codeblöcken in unseren Artikeln anzuwenden, gilt es diese im HTML zu finden, zu extrahieren und die Stellen mit dem neuen, annotierten Code zu ersetzen. Glücklicherweise müssen wir dies nicht von Hand erledigen, sondern können auf <a target="_blank" rel="noopener noreferrer" href="https://github.com/cgrand/enlive">enlive</a> zurückgreifen, eine Clojure Bibliothek welche auf <a target="_blank" rel="noopener noreferrer" href="https://jsoup.org/">JSoup</a> basiert:</p><pre><code class="language-clojure">(defn highlight-code-in-article [content]
  (apply str
         (-&gt; content
             (html/html-snippet)
             (html/transform [:code]
                             (fn [selected-node]
                               (update-in selected-node [:content]
                                          (fn [[content]]
                                            (if-let [language-class (get-in selected-node [:attrs :class])]
                                              (html/html-snippet (highlight-code (clojure.string/replace language-class #"language-" "") content))
                                              content)))))
             (html/emit*))))</code></pre><p>Der Funktion <code>highlight-code-in-article</code> wird der gesamte Artikeltext übergeben. Dieser Text wird mit <code>html/html-snippet</code> in enlive eingeladen und in eine Clojure Datenstruktur überführt. Die Funktion <code>html/transform</code> sucht mit dem Selektor <code>:code</code> nun alle <code>&lt;code&gt;</code> Elemente in dieser Datenstruktur und wendet auf jeden eine anonyme Funktion an, welche wir uns für eine bessere Lesbarkeit noch einmal extrahieren:&nbsp;</p><pre><code class="language-clojure">(fn [selected-node]
  (update-in selected-node [:content]
             (fn [[content]]
               (if-let [language-class (get-in selected-node [:attrs :class])]
                 (html/html-snippet (highlight-code (clojure.string/replace language-class #"language-" "") content))
                 content))))</code></pre><p>Die Teildatenstruktur welche dem <code>&lt;code&gt;</code> Element entspricht wird als Parameter <code>selected-node</code> übergeben. Enlive wird diese Datenstruktur mit dem Rückgabewert der Funktion ersetzen. Innerhalb dieser Funktion aktualisieren wir den Inhalt des Elements, welcher sich hinter dem <code>:content</code> Keyword befindet, indem wir unsere <code>highlight-code</code> Funktion darauf anwenden. Um welche Sprache es sich bei dem Snippet handelt extrahieren wir aus dem Klassennamen mit Regex. Wäre der Klassenname hier <code>language-java</code> &nbsp;würden wir <code>java</code> an prism.js übergeben. Dies geschieht aber nur, wenn überhaupt eine Sprachklasse am Element existiert.</p><p>Da prism.js uns einen String zurückliefert, müssen wir diesen mit enlive und <code>html/html-snippet</code> wiederum in eine Clojure Datenstruktur überführen, bevor wir sie ins HTML einhängen können. Das Ergebnis lässt sich exakt auf dieser Seite sehen: Der Code wird vom Server bereits fertig mit CSS Klassen dekoriert.&nbsp;</p><h2>Diskussion&nbsp;</h2><p>Ist das Ergebnis den Aufwand wert? Unserer Meinung nach schon. Wir haben unser Ziel erreicht, uns der Flexibilität einer ausgereiften Syntax-Highlighting Bibliothek im Backend bedienen zu können. Dabei nutzen wir Java-, beziehungsweise JVM-Bordmittel und die Bibliothek selbst. Zu erwähnen sei hier noch, dass die Ausführungsgeschwindigkeit dieser Lösung nicht unbedingt hoch ist. Bei 5 bis 6 Codeblöcken können bei vorher initialisiertem JavaScript Kontext schon einmal 200-300ms extra aufgeschlagen werden. Dies ist in unserem Anwendungsfall nicht weiter schlimm, da wir die Artikel ohnehin cachen und nicht jedes Mal erneut beim Ausliefern verarbeiten müssen. Würde sich dies anders verhalten, würden wir hier vermutlich nach einem performanteren Weg suchen.</p><p>Die Anwendungsfälle für die polyglotten Features von GraalVM sind auf jeden Fall zahlreich. Neben JavaScript sind vor allem Ruby, Python und R interessant. Die Möglichkeit, diese Sprachen mit Java auf der selben VM operieren zu lassen sind interessant und könnten vor allem im Bereich statistischer Auswertungen neue Möglichkeiten eröffnen.</p>]]></content:encoded></item></channel></rss>