Java ist 21: String Templates (Teil 2/2)

Nachdem wir im Teil 1 unserer Blogserie die Welt der String Templates und die mit dem JDK-gelieferten Prozessors STR, FMT und RAW erkundet haben,  öffnen sich im Teil 2 neue Optionen. Eines der bemerkenswertesten Merkmale des String Template Features in Java 21 ist die Möglichkeit, massgeschneiderte Template-Prozessors zu erstellen. 

Custom TemplateProcessor

Das Spezielle am String Template Feature ist, dass man eigene Template Processors implementieren kann. Und das Resultat muss nicht unbedingt ein String sein. Es kann ein beliebiges Objekt sein.

public interface StringTemplate {

    @FunctionalInterface
    public interface Processor<R, E extends Throwable> {
         R process(StringTemplate stringTemplate) throws E;
    }    
}

Im Package java.lang gibt es neu das Interface StringTemplate mit einem Sub-Interface Processor und der zu implementierenden Methode process.

  • process nimmt als Input ein StringTemplate
  • process gibt als Return-Wert einen generischer Typ R zurück. Dies kann wie in den vorherigen Beispielen ein String sein, muss aber nicht
  • process kann bei der Bearbeitung eine Exception E werfen

Zum besseren Verständnis starten wir mit einem HELLO TemplateProcessor. Dieser gibt Debug Information aus und delegiert dann weiter an StringTemplate::interpolate.

public static StringTemplate.Processor<String, RuntimeException> HELLO = (StringTemplate template) -> {
    String fragmentsString = template.fragments()
            .stream()
            .collect(Collectors.joining(", "));
    
    String valuesTypesString = template.values()
            .stream()
            .map(obj -> obj.getClass().getSimpleName())
            .collect(Collectors.joining(", "));
    
    String valuesString = template.values()
            .stream()
            .map(obj -> obj.toString())
            .collect(Collectors.joining(", "));
    
    
    System.out.println("HELLO:");
    System.out.println(STR. "   fragments  : \{ fragmentsString }" );
    System.out.println(STR. "   values     : \{ valuesString }" );
    System.out.println(STR. "   value types: \{ valuesTypesString }" );
    
    return StringTemplate.interpolate(template.fragments(), template.values());
};

Der HELLO Processor gibt die ihm zur Verfügung stehenden Daten (fragments und values) aus und delegiert via StringTemplate::interpolate weiter an die interne Implementation für die String Interpolation.

String language = "Java";
int age = 21;

System.out.println("INPUT:");
System.out.println("   { language }_ist_{ age }_geworden!");

String infoJava21 = HELLO. "\{ language }_ist_\{ age }_geworden!" ;
System.out.println("RESULT:");
System.out.println("   " + infoJava21);

Nun kann man sehen, was beim Anfangsbeispiel genau passiert (zur Verdeutlichung sind beim Input Spaces durch „_“ ersetzt):
Die Inputs sind:

  • 2 Variablen: language vom Typ String und age vom Typ int
  • Das StringTemplate mit Embedded Expressions { language }ist{ age }_geworden!.

Aus dem Input werden die Strings zwischen den Embedded Expressions extrahiert und damit die fragments Liste befüllt. Die values Liste enthält die ausgewerteten Emedded Expression. Schliesslich mischt der Aufruf von StringTemplate::interpolate die fragements und values zu einem neuen String.

INPUT:
    { language }_ist_{ age }_geworden!
HELLO:
    fragments  : , _ist_, _geworden!
    values     : Java, 21
    value types: String, Integer
RESULT:
    Java_ist_21_geworden!

Mit diesem Vorwissen kann man relativ einfach das Verhalten von STR durch folgende Schritte nachbauen:

  • extrahieren der values() in eine mutable Liste values
  • einen StringBuilder für das Resultat defnieren
  • über die fragments iterieren
    • wenn das fragment nicht leer ist, es zum Output hinzufügen
    • wenn die values Liste nicht leer ist, das erste Element daraus nehmen (und in der Liste löschen) und zum Output hinzufügen
  • und dann den StringBuilder als String zurückgeben

PS: Dem aufmerksamen Leser ist eventuell aufgefallen, dass es im Code in List::removeFirst gibt. Ja, das ist auch neu in Java 21 und gehört zum Feature Sequenced Collections (JEP 431). Dies werden wir in einer der nächsten Folgen näher anschauen.

public static StringTemplate.Processor<String, RuntimeException> MY_STR = (StringTemplate template) -> {
    // convert values to a mutable list
    List<Object> values = template.values().stream().collect(Collectors.toList());

    StringBuilder sb = new StringBuilder();
    for (String fragment : template.fragments()) {
        if (!fragment.isEmpty()) {
            sb.append(fragment);
        }
        if (!values.isEmpty()) {
            sb.append(values.removeFirst());
        }
    }

    return sb.toString();
};

Die Implementation dient nur zur Demonstration des Grundverhaltens von STR. Für einen einfachen Test Input hat es auf jeden Fall funktioniert. Wir erhalten den gewünschten Output «Java hat aktuell Version 21 und ist im Ranking auf Position 3».

var language = new ProgrammingLanguage("Java", 21, 3);

String infoJava = MY_STR. """
    \{ language.name() } \
    hat aktuell Version \{ language.version() } \
    und ist im Ranking of Position \{ language.ranking() }
    """ ;

System.out.println(infoJava);

Einschub/Nachtrag RAW Template Processor

Der HELLO Processor dient dazu, die Internas (fragments, values, interpolate) des STR besser zu verstehen. Das kann man auch einfacher haben, indem man den RAW Processor verwendet. Er ist ein Processor vom Typ: StringTemplate -> StringTemplate, d.h. er gibt das Input StringTemplate unverändert zurück. Und er hat eine nützliche toString Methode, die uns die Internas zeigt. Wir nehmen den gleichen Input wie beim HELO Processor und geben das Resultat aus:

public static void main(String[] args) {
    String language = "Java";
    int age = 20;

    StringTemplate infoJava21Raw = RAW. "\{ language } ist \{ age + 1 } geworden!" ;
    System.out.println(infoJava21Raw);
}
StringTemplate{ fragments = [ "", " ist ", " geworden!" ], values = [Java, 21] }

Man kann sehr schön sehen, wie der Input in die StringTemplate Bestandteile fragments und values aufgeteilt wurde und dass die Embedded Expressions ausgewertet wurden (man beachte den Wert von age).

JSON Processor

Als abschliessendes Beispiel möchten wir einen Processor implementieren, der einen Input String mit JSON in ein JSON Object umwandelt.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class JsonTemplateProcessor {
    private static String QUOTE = "\"";

    public static class JsonProcessException extends IOException {
        public JsonProcessException(String info) {
            super(info);
        }

        public JsonProcessException(String info, Exception e) {
            super(info, e);
        }
    }

    public static StringTemplate.Processor<JsonNode, JsonProcessException> JSON = template -> {
        List<Object> newValues = new ArrayList<>();
        for (Object value : template.values()) {
            if (value instanceof String str) {
                // wrap Strings in ""
                str = str.replaceAll(QUOTE, "\\\\\"");
                newValues.add(QUOTE + str + QUOTE);
            } else if (value instanceof Number || value instanceof Boolean) {
                newValues.add(value);
            } else {
                throw new JsonProcessException("Not supported value type " + value);
            }
        }

        String jsonString = StringTemplate.interpolate(template.fragments(), newValues);
        return convert(jsonString);
    };

    private static JsonNode convert(String jsonString) throws JsonProcessException {
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode jsonObj = mapper.readTree(jsonString);
            return jsonObj;
        } catch (IOException e) {
            throw new JsonProcessException("Error while converting String to JsonNode", e);
        }
    }
}

Hier einige Erklärungen zur Implementation:

  • JsonProcessException ist die Exception, die im Code des Processors geworfen werden kann.
  • Der Processor geht in einer Schleife über alle Template values. Falls es sich um Number oder Boolean handelt, wird value nicht verändert. Beim Typ String wird value in ein „_“ gepackt. Bei anderen auftretenden Typen wird eine Exception geworfen (der Typ ist noch nicht unterstützt).
  • Die Original Template fragments und die konvertierten values werden der StringTemplate::interpolate übergeben, die dann die eigentliche String Interpolation macht.
  • Mittels einer selbst geschriebenen convert Methode wird der JSON String dann in ein wirkliches JSON Object vom Typ JsonNode konvertiert. Hierzu wurde die Library com.fasterxml.jackson verwendet.
  • Der Aufruf ist fast identisch mit dem Aufruf unseres Text Block Beispiels. Nur dass wir hier den JSON TemplateProcessor verwenden und dass die String Values im Input JSON String nicht in „_“ eingepackt werden. Denn dies macht ja nun der Processor für uns.
try {
    JsonNode myDailyLanguage = JsonTemplateProcessor.JSON. """
    {
        "languages": [
            {
                "name": \{ langs[2].name() },
                "version": \{ langs[2].version() },
                "ranking": \{ langs[2].ranking() }
            }
        ]
    }
    """ ;

    System.out.println(myDailyLanguage);
    System.out.println(JsonUtils.toString(myDailyLanguage));

} catch (JsonTemplateProcessor.JsonProcessException e) {
    System.out.println("Error while processing JSON String: " + e);
}

Fazit

Java unterstützt nun auch String Interpolation. Die Implemenation dieses Features in Java geht weiter als die meisten anderen Sprachen. Es ist ein API, mit dem man eigene Processors implementieren kann. Wir haben gesehen, dass Vieles möglich ist: Formatierung und Validierung der Embedded Expressions sind nur zwei Beispiele. Da der Output des Processors nicht ein String sein muss, kann der Input String auf irgendetwas gemappt werden. Mögliche Beispiele sind das Mappen von String -> JSON oder String -> SqlStatement. Die Zukunft wird zeigen, welche Ideen die Entwicklerinnen und Entwickler umsetzen werden.

Es hat lange gedauert … aber String Templates sind grossartig!!!

Puzzle Blogs über Records

Java Records
Java Records 2

String Templates Teil 1

Kommentare sind geschlossen.