“The function of a good software is to make the complex appear to be simple”
Grady Booch
Zum Ende des Jahres lässt man all die vielen unerledigten Dinge Revue passieren und versucht noch das eine oder andere abzuarbeiten. Als letzter Beitrag des Jahres 2023 wird deshalb noch CongoCC vorgestellt. Der Congo Parser Generator ist der Nachfolger von JavaCC 21 und dem noch älteren JavaCC.
Einige der hier vorgestellten Features existierten bereits in JavaCC 21, aber unter der Haube von CongoCC wurde in der Zwischenzeit eine Menge getan. Mittlerweile glänzt CongoCC mit Contextual Predicates, Context-sensitive Tokenization. Full 32-bit Unicode Support, Code Injection, Include Directive, Up-to-here Marker, Grammars for Java 19, Lua, Python and C#.
Eine erfreuliche Neuerung im CongoCC Projekt ist es außerdem, dass die aktuelle Version von CongoCC über Maven Central zur Verfügung gestellt wird.
<dependency> <groupId>org.congocc</groupId> <artifactId>org.congocc.parser.generator</artifactId> <version>2.0.0-RC5</version> </dependency>
Zusätzlich existiert auch ein Maven Plugin für CongoCC. Das automatische Erzeugen der Klassen im FreshMarker Projekt aus der FreeMarker Grammatik mit JavaCC 21 war mit einigen Stolpersteinen versehen. Ein Umstieg auf CongoCC wird an dieser Stelle sicherlich einiges einfacher gestalten.
<plugin> <groupId>org.congocc</groupId> <artifactId>org.congocc.maven.plugin</artifactId> <version>2.0.0-RC5</version> <executions> <execution> <goals> <goal>ccc-generate</goal> </goals> <configuration> <grammarFile>src/main/ccc/SOJA.ccc</grammarFile> <outputDir>target/generated-sources/ccc</outputDir> <jdk>17</jdk> </configuration> </execution> </executions> </plugin>
Diese Konfiguration des Maven Plugin liest eine Grammatik Datei aus dem src/main/ccc
Ordner und schreibt die generierten Dateien nach target/generated-sources/ccc
. Im Gegensatz zum altgewordenen JavaCC beherrscht CongoCC auch aktuelle JDK Versionen. Dieses Beispiel wird mit Java 17 erzeugt.
Die im Beispiel verwendete Datei SOJA.ccc
enthält keine Grammatik, sondern nur einige Code Injections für eine andere Grammatik. Die INCLUDE
und INJECT
Direktiven sind zwei der interessantes Verbesserungen für Nutzer von bestehenden Grammatiken.
PARSER_PACKAGE="de.schegge.soja.json"; NODE_PACKAGE="de.schegge.soja.json.ast"; INCLUDE "JSON.ccc" INJECT Node : import de.schegge.soja.JsonHandlerAdapter; extends JsonHandlerAdapter; { } INJECT Value : import de.schegge.soja.JsonHandlerAdapter; import de.schegge.soja.JsonHandler; implements JsonHandlerAdapter; { public void adapt(JsonHandler handler) { for (Node node : children()) { node.adapt(handler); } } } INJECT KeyValuePair : import de.schegge.soja.JsonHandlerAdapter; import de.schegge.soja.JsonHandler; implements JsonHandlerAdapter; { public void adapt(JsonHandler handler) { handler.startAttribute(get(0).getSource()); get(2).adapt(handler); handler.endAttribute(get(0).getSource()); } } INJECT JSONObject : import de.schegge.soja.JsonHandlerAdapter; import de.schegge.soja.JsonHandler; implements JsonHandlerAdapter; { public void adapt(JsonHandler handler) { handler.startObject(""); for (Node node : children()) { node.adapt(handler); } handler.endObject(""); } } INJECT Array : import de.schegge.soja.JsonHandlerAdapter; import de.schegge.soja.JsonHandler; implements JsonHandlerAdapter; { public void adapt(JsonHandler handler) { handler.startArray(""); for (Node node : children()) { node.adapt(handler); } handler.endArray(""); } } INJECT Literal : import de.schegge.soja.JsonHandlerAdapter; import de.schegge.soja.JsonHandler; implements JsonHandlerAdapter; { public void adapt(JsonHandler handler) { switch (getType()) { case NUMBER: handler.value(Long.parseLong(getSource())); break; case STRING_LITERAL: handler.value(getSource()); break; case TRUE: handler.value(true); break; case FALSE: handler.value(false); break; case NULL: handler.nullValue(); } } }
Mit der INCLUDE
Direktive wird die JSON.ccc
Beispiel-Grammatik aus dem CongoCC Projekt inkludiert und die folgenden INJECT
Direktiven fügen Implementierungen für die JsonHandlerAdapter#adapt(JsonHandler handler)
Methode in diverse generierte Klassen ein.
public interface JsonHandlerAdapter { default void adapt(JsonHandler handler) { } }
Das JsonHandlerAdapter
Interface enthält eine leere Default-Methode, die durch das Einfügen in das Interface Node
in allen generierten Knoten der Grammatik zur Verfügung stehen.
Die restlichen Code Einfügungen rufen zusätzlich die Methoden des JsonHandler
Interfaces auf. Damit kann einer JsonHandler
Implementierung Beginn und Ende eines Attributes, Arrays oder eines JSON Objects mitgeteilt werden.
Verwendet wird dies innerhalb des folgenden JsonReader
Klasse. Sie liest eine JSON Beschreibung aus einem String
mit einer ebenfalls generierten SOJAParser
Instanz.
public class JsonReader { private final SOJAParser parser; public JsonReader(String source) { parser = new SOJAParser(source); } public void read(JsonHandler handler) { handler.startDocument(); parser.Root(); Node rootNode = parser.rootNode(); rootNode.get(0).adapt(handler); handler.endDocument(); } }
Das Parsen der Eingabe wird mit der Methode Root ausgeführt. Danach findet sich das Ergebnis in der Variablen rootNode
, bzw. im einzigen Element des Knoten.
Der folgende Unit Tests zeigt die erfolgreiche Verarbeitung eines kleinen JSON Arrays, dass Informationen zu meinem Ahnen Wilhelm Wortmann enthält.
@Spy private JsonHandler handler = new PrintingJsonHandler(); @Test void withWilhelmWortmann() { new JsonReader(""" [{ "test": true, "name": "Wilhelm Wortmann", "birth": { "place": "Diepholz", "date": "1821-07-29" } }] """).read(handler); verify(handler).startDocument(); verify(handler).endDocument(); }
Der Unit Test prüft, ob bei der Verarbeitung die Methoden startDocument
und endDocument
aufgerufen wurden. Zusätzlich werden von dem PrintingJsonHandler
alle Methoden Aufrufe auf den STDOUT protokolliert.
startDocument startArray startObject startAttribute: "test" boolean: true endAttribute: "test" startAttribute: "name" string: "Wilhelm Wortmann" endAttribute: "name" startAttribute: "birth" startObject startAttribute: "place" string: "Diepholz" endAttribute: "place" startAttribute: "date" string: 1821-07-29" endAttribute: "date" endObject endAttribute: "birth" endObject endArray endDocument
Dieses Beispiel zeigt, wie einfach eigene Anwendungen durch CongoCC mit moderne Parsern ausgestattet werden können.