ZBNF invoked in Java

ZBNF invoked in Java

Inhalt


Topic:.ZbnfJava.

Das Parsen über ZBNF ist in Java programmiert. Die Javadoc findet sich unter javadoc:_org/vishia/zbnf/ZbnfParser bzw. mit private und src-Angaben unter javadoc-src:_org/vishia/zbnf/ZbnfParser.

Im folgenden Text sind einige TODO notiert als Hinweis für mögliche, empfehlenswerte oder bevorstehende Weiterentwicklungen. Diese sollen abwärtskompatibel ausgeführt werden, um bereits jetzt umfangreich bestehende Anwendungen nicht überarbeiten zu müssen.

Die Programmbeispiele sind der javadoc-src:_org/vishia/zbnf/Zbnf2Xml-Anwendung entnommen und für diese Darstellung etwas vereinfacht worden. Die genannte Quelle kann als Muster benutzt werden.


Topic:.ZbnfJava.


1 Instanziierung des ZBNF-Parser in Java

Topic:.ZbnfJava.JavaParserInvoke.

 import org.vishia.zbnf.ZbnfParser;
 ZbnfParser parser = new ZbnfParser(report);

Der Konstruktor übernimmt eine Instanz des javadoc-src:_org/vishia/mainCmd/Report-Interfaces. Eine Instanziierung dieses Interface ist beispielsweise mit javadoc-src:_org/vishia/mainCmd/MainCmd gegeben, oder mit javadoc-src:_org/vishia/mainCmd/MainCmdWin. Das Report-Interface regelt die Ausgabe von Zwischenergebnissen, die gegebenenfalls für eine Fehlersuche interessant sein könnten. Immerhin kann die zu parsende Syntax und Eingabetexte recht komplex sein. Der Report ist aber nicht unbedingt notwendig, soll nur ergänzend helfen. Derzeit kann kein null-Zeiger übergeben werden (TODO: Auch ohne Report-Angabe).

 String sFileSyntax = "syntax.zbnf";
 try
 { spSyntax = new StringPartFromFileLines(new File(sFileSyntax), 20000, "encoding=", null);
 }
 catch(Exception exception)
 { writeError("file read error:" + sFileSyntax);
   bOk = false;
 }
 if(bOk)
 { try
   { parser.setSyntax(spSyntax);
     parser.setSkippingComment("/*", "*/", true);
     parser.reportSyntax(this);
   }
   catch (ParseException exception)
   { writeError("Parser Syntax reading error: " + exception.getMessage());
     bOk = false;
   }
 }

Die Syntax muss dem ZbnfParser in Form eines javadoc-src:_org/vishia/util/StringPart übergeben werden. Ein StringPart ist eine Erweiterung des String-Konzeptes von Java und enthält selbst Methoden für einfache Parsing-Aktionen. Das Einlesen und Konvertieren der Syntax wird ausschließlich mit den Möglichkeiten des StringPart erledigt. Im Gegensatz zum ZbnfParser arbeitet das Parsen unter Nutzung der StringPart-Klasse nicht interpretativ sondern ist ausprogrammiert und damit schneller.

Die Klasse javadoc-src:_org/vishia/util/StringPartFromFileLines ist von StringPart abgeleitet. Sie ist dafür gedacht, einen StringPart je nach Verarbeitungsfortschritt aus einem textuellen File zu speisen. (TODO: Bisher hat die Puffergröße immer ausgereicht, einen gesamten File auf einmal zu speichern. Die Funktion des Nachschiebens von Inhalt ist hier noch nicht realisiert. Sie wurde von mir schonmal unter C++ implementiert, zu DOS-Zeiten als Nachschiebe-Puffer wirklich notwendig waren. Beim Nachschieben muss man Positionen korrigieren, das muss sorgfältig getestet werden.)

Eine wichtige gegebenenfalls notwendige Funktionalität der Klasse StringPartFromFileLines ist aber die Feststellung der Zeichencodierung (encoding). In Files werden meist Zeichen mit 8 Bits gespeichert, daher muss man verschiedene Encodings berücksichtigen. Die Erkennung geschieht jedoch nicht automatisch, das ist grundsätzlich unsicher. Vorgesehen ist, dass in der ersten Zeile, die selbst entweder Standard-ASCII (US-ASCII) oder UTF-16 ist, die Zeichencodierung im Klartext steht. Wie im Beispiel angegeben leitet "encoding=" diese Angabe ein. Ohne Encoding-Angabe wird der Fileinhalt in der System-Zeichencodierung gelesen.

Falls die Syntaxvorschrift selbst Syntaxfehler enthält, werden diese wie im Beispiel vorgegeben angezeigt. Dabei kann die message in der ParseException mehrere Zeilen umfassen.


2 Zusätzliche Angaben zum Parsen

Topic:.ZbnfJava..

Über Aufruf einiger set-Methoden kann der Parser konfiguriert werden. Teilweise sind dieselben Konfigurationsmöglichkeiten auch in den Steuerbefehlen innerhalb einer Syntaxvorschrift formulierbar. Das sind also zwei Möglichkeiten, um das Selbe zu erreichen. Die Steuerbefehle wirken innerhalb des Aufrufes von setSyntax(). Erfolgen die Aufrufe der set-Methoden in Java danach, dann werden die Einstellungen in der Syntaxvorschrift überschrieben.


2.1 Zeilenmodus

Topic:.ZbnfJava...

Wie beim Steuerbefehl in der Syntaxvorschrift

 $setLinemode.

kann dem Zeichen \n (Hexa 0a) seine Rolle als Whitespace zu- oder aberkannt werden. Damit wird ein Zeilenumbruch nicht als Whitespace erkannt. Die

 setLinemode(boolean);

-Methode kann den Zeilenmodus einschalten und ausschalten. Defaultmäßig ist er ausgeschaltet (Zeilenumbruch wirkt als Whitespace).

Zum Problem des carrige return: Bei Windows-Texten wird traditionell eine Zeile mit \r\d (hexa 0d0a) abgeschlossen. Damit, dass \r als Whitespace erkannt wird, wird es also am Zeilenende ignoriert, stört also nicht. Auch nicht im Linemode, da das \n maßgebend ist und vorhanden ist. Bei Macintosh-Texten, die nur \r als Zeilentrennung verwenden, funktioniert das aber nicht mehr. Dann muss man mit setWhiteSpaces(" \t\f\n") nachhelfen und so das \r aus der Menge der Whitespaces entfernen.


2.2 Whitespace-Zeichen

Topic:.ZbnfJava...

Mit

 setWhiteSpaces(String);

wird festgelegt, welche Zeichen als Whitespaces gelten. Damit können beispielsweise Tabulatoren ausgeschaltet werden. Die Methode überschreibt vorige Festlegungen von setLinemode(), da hier unabhängig von vorigen Einstellungen festgelegt wird, ob \n dabei ist. Die Defaulteinstellung ist " \t\r\f\n", also das Leerzeichen, Tabulator, form feed (Hexa 0c), carrige return (Hexa 0d) und line feed (hexa 0a).


2.3 Festlegen der Kommentar-Zeichen

Topic:.ZbnfJava...

Mit

 setSkippingComment(String commentStart, String commentEnd, boolean bStoreComment);

wird festgelegt, welche Zeichenkettenpaare als Kommentierung verstanden werden und ob Kommentierungen eigenständig im ParserStrore gespeichert werden sollen. Das entspricht der Angabe von

 $comment=/*...*/,

in der Syntaxvorschrift (/* und */ stehen hier als Beispiel, man kann eben Kommentare auch anderes kennzeichnen).

Mit

 setSkippingEndlineComment(String commentStart, boolean bStoreComment);

wird selbiges für die Kommentierung bis zum Zeileneende festgelegt, wie bei der Angabe von

 $endlineComment=//,

Defaultmäßig sind diese Zeichen wie im Beispiel anggeben. Mit Übergabe einer leeren Zeichenkette kann man die Erkennung von Kommentar ausschalten.

TODO: Bisher wurden die Kommentare nicht im ParserStore gespeichert, es bestand keine Notwendigkeit. Beim Java2C-Translator und beim Parsen von Headerfiles aus C Cheader.zbnf werden javadoc-Kommentare in der Syntax spezifisch berücksichtigt. Das Feature des automatischen Speicherns an sich überlesener Kommentare ist also ggf. nicht notwendig und bisher noch nicht implementiert, ist aber relativ einfach nachzurüsten.


2.4 Übergabe von Zusatzinformationen für den ParserStore

Topic:.ZbnfJava...

Es gibt Fälle, bei denen Informationen weder in der Syntaxvorschrift formuliert werden können noch im zu parsenden Eingangstext stehen, sondern anderweitig beim Aufruf bekannt sind. Beispielsweise ist dass der Filename und Path des zu parsenden Textes. Diese Informationen können beim Aufruf des Parsens als Zusatzargument übergeben werden und erscheinen damit für die Auswertung, zum Beispiel im erzeugten XML-tree, wie ein geparster Bestandteil.

 boolean parse(StringPart spToParse, List<String> additionalInfo);

Dabei ist in der List jeweils abwechselnd der erste String ein Semantik-Bezeichner, der folgende String ist der zugehörige Inhalt. Diese Informationen werden als erste Child-Items des Toplevel-Items gespeichert.


3 Aufruf des Parsens

Topic:.ZbnfJava..

Eine Instanz des ZbnfParser mit einer Syntax versehen ist in der Lage, beliebig oft verwendet zu werden. Als Input muss dem ZbnfParser wiederum ein StringPart gegenben werden:

 StringPart spToParse = null;
 if(bOk)
 { try
   { spToParse = new StringPartFromFile(new File(sFileIn));
   }
   catch(Exception exception)
   { writeError("file read error:" + sFileIn);
     bOk = false;
   }
 }

Der Aufruf des Parsens selbst erzeugt keine Exception bei einem Parserfehler, sondern wird mit false-Rückkehr beantwortet. Die Exception im Beispiel ist also ein allgemeiner Auffänger. Im Beispiel ist auch zu sehen, dass das Exception-Handling-Konzept (try-catch) mit einem bOk-Konzept verwoben ist: Der Gesamtablauf wird gesteuert von einer boolean-Variable bOk. Solange diese true bleibt, geht es weiter. Damit sind zwei Ebenen voneinander getrennt und das Programm ist übersichtlicher: Gesamtablauf mit bOk, Details mit Exceptionhandling. Es wäre aber auch denkbar, entweder alles in ein großes try zu packen oder zwei try-Ebenen zu installieren und bei einem ein catch ein weiteres throw auszulösen.

 if(bOk)
 {
   try
   { bOk = parser.parse(spToParse, additionalSemantic);
     if(!bOk)
     { report.writeError(parser.getSyntaxErrorReport());
       parser.reportStore(report);
     }
   }
   catch(Exception exception)
   { writeError("any exception while parsing:" + exception.getMessage());
     report.report("any exception while parsing", exception);
     parser.reportStore(report);
     bOk = false;
   }
 }

Bei einem Syntaxfehler des zu parsenden Textes wird mit der oben gezeigten Methode getSyntaxErrorReport() ein englischer Klartext ausgegeben, der neben der Fehlerstelle im Eingangstext einige Hinweise zum Syntaxfehler enthält. Ein Syntaxfehler in der Syntaxvorschrift ist eher nicht zu erwartet, da diese einmalig erstellt und dann richtig sein sollte. Ein Syntaxfehler im Eingangstext ist dagegen ein Normalfall. Das sind die zu übersetzenden Daten und sicherlich zunächst mit Schreibfehlern, Verwechslungen oder vergessenen Strukturierungselementen versehen. Daher ist die Syntaxfehleranzeige für den Benutzer wesentlich.


4 Auswertung der Parserergebnisse in Java

Topic:.ZbnfJava.JavaParserEvaluate.

Parserergebnisse lassen sich in Java auf zweierlei Arten auswerten:


4.1 Steppen durch das Parser-Ergebnis

Topic:.ZbnfJava.JavaParserEvaluate.ZbnfParseResultItem.

Wenn der Parsing-Prozess erfolgreich durchlaufen wurde, dann ist in der ZbnfParser-Instanz das Parsing-Ergebnis gespeichert. Für den Zugriff ist insgesamt das interface:_org/vishia/zbnf/ZbnfParseResultItem vorgesehen. Dieses Interface ist jeweils mit einem Ergebnis-Eintrag (Item) verbunden. Mit dem Aufruf

 ZbnfParseResultItem firstItem = myParser.getFirstParseResult();

wird das Toplevel-ResultItem zurückgeliefert. Es gibt ähnlich wie bei XML genau 1 Toplevel-Item.

Die Items sind in einem Baum (tree) strukturiert. Der Baum entspricht der Syntaxvorschrift. Alle erkannten Elemente einer Einzel-Synaxvorschrift werden in einer Ebene. Passt die Syntax der Syntaxvorschrift zu einer Syntaxkomponente, dann sind die erkannten Elemente dieser Syntaxkomponente eine untergeordnete Ebene, ein child.

In der Beipielsyntaxvorschrift

 einkaufsliste::=Einkaufszettel \\n
 <![=]*?> \\n
 { <position> \\n }.
 position::=<#?@menge> [<?@einheit>Stck|x|] <$?text()>.

sind die Elemente, die innerhalb position erkannt werden, ein child von einkaufsliste. Diese Anordnung ist wie im Xml, wenn man Zbnf2Xml konvertiert.


4.1.1 Zugriff auf child-Elemente

Topic:.ZbnfJava.JavaParserEvaluate.ZbnfParseResultItem..

Hat man das toplevel-Element mit

 ZbnfParseResultItem firstItem = myParser.getFirstParseResult();

ermittelt, dann gibt es mehrere Möglichkeiten, in die Tiefe des Baums einzudringen. Als Methode von javadoc:org/vishia/zbnf/_ZbnfParseResultItem gibt es die Möglichkeit, mit

 ZbnfParseResultItem item = firstItem.firstChild();

das erste Element der nachfolgenden Ebene zu erreichen. Mit diesem Element kann man dann alle Elemente der selben Ebene durchwandern:

 item = item.next();

Dieser Aufruf iteriert durch die gegebene Ebene. Um wieder eine Ebene tiefer zu kommen, muss man wieder item.firstChild() aufrufen. Die next()-Methode hat aber die Eigenschaft, nicht zu testen, ob eine Child-Ebene verlassen wird. Sie liefert das Folgeelement der Elternebene am letzten child. Nur am Ende des gesamten Parser-Ergebnisses wird null zurückgegeben. Die next()-Methode ist dafür gedacht, das Folgeelement zu ermitteln, wenn in der Syntaxvorschrift geregelt ist, dass es ein solches gibt.

Es gibt noch eine weiter Herangehensweisen, die in einem allgemein gehaltenen Interface javadoc:_org/vishia/util/SortedTree, von dem javadoc:org/vishia/zbnf/_ZbnfParseResultItem abgeleitet ist, definiert sind:

 ZbnfParseResultItem item = itemParent.getChild("name")

sucht das erste Element der untergeordnten Ebene mit der Semantik "name". Diese Methode ist hilfreich, wenn in der Syntaxvorschrift geregelt ist, dass es ein solches Element geben könnte. Aber es kann beispielsweise in einer Wiederholung mit mehreren Optionen oder optional an verschiedenen Stellen auftreten. Wird das Element nicht gefunden, dann wird null zurückgeliefert. Wird diese Methode aufgerufen, dann wird über alle children erst einmal ein Index erstellt. Das ist ein TreeMap, der die Semantik als key enthält, und eine Liste von Elementen, die dieser Semantik entsprechen. Es können auch mehrere sein. Das Erstellen dieser TreeMap wird also nur realisiert, wenn ein solcher Zugriff oder einer der weiteren nachfolgend genannten erfolgt. Das bedeutet zunächst einen kleinen Rechenzeitaufwand zur Bildung dieses TreeMap mit zugehörigen LinkedList, natürlich im Mikrosekundenbereich an einem Standard-PC. Aber dieser Rechenzeitaufwand macht sich bezahlt beim Folgezugriff, der dann auf die vorhandene TreeMap mit den LinkedList zugreifen kann. Das ist auch der Fall bei folgendem Befehl:

 Iterator<ZbnfParseResultItem> iter = getChildren("name");

Diese Methode liefert den java.utilIterator auf alle Items der angegebenen Semantik.

Der Aufruf von

 Iterator<ZbnfParseResultItem> iter = getChildren();

liefert dagegen einen Iterator durch alle untergeordneten Elemente, ohne zuvor ein TreeMap gebildet zu haben. Das ist nicht notwendig, weil hier keine semantische Sortierung verlangt ist.

Damit hat man einige Möglichkeiten der Evaluierung des Parser-Ergebnisses. Es muss nich dargestellt werden, dass in einem Baum Elemente wiederum children haben können, die selben Methoden also rekursiv angewendet werden können.

Lezlich sollen auch die alten, mittlerweile deprecated- Zugriffe dargestellt werden:

 ZbnfParseResultItem item = next(parent);

lieferte das nächste Item innerhalb des child und null am Ende des Childs (im Gegensatz zum parameterlosen next()), das ist aber durch die Möglichkeit des getChildren("name") verbessert.

 ZbnfParseResultItem item = nextSkipIntoComponent(parent);

war fast das gleiche wie firstChild(), nur das in firstChild() nicht mehr getestet wird, ob das parent überhaupt ein child enthält. Weis man das, ist die Anwendung einfacher. Das ist der häufigere Fall. Ist es unbekannt, sollte man getChildren() verwenden.


4.1.2 Auswertung der Inhalte eines Items

Topic:.ZbnfJava.JavaParserEvaluate.ZbnfParseResultItem..

Mit der Methode getSemantic() wird die Semantic des Items als String ausgegeben. Das ist die Semantik, wie sie in der Syntaxvorschrift zu dem gefundenen Abschnitt angegeben wurde. Der Aufruf von getSemantic() erübrigt sich natürlich, wenn man mit getChild("Semantic") Elemente einer bestimmten Semantik gesucht hat.

Im interface:org/vishia/zbnf/_ZbnfParseResultItem sind die Methoden getParsedFloat(), getParsedInteger() usw. definiert. Ist der Semantik syntaktisch ein Float, Integer usw. zugeordnet, dann liefern die Methoden den Inhalt des Input-Textes, der zu diesem Abschnitt gehört und im ParseResultItem gespeichert ist. Ein falscher Aufruf liefert den Wert 0. Man kann auch testen, ob das Item ein float, integer usw. beinhaltet, mittels Aufruf von isFloat() usw. das wird sich aber in den meisten Fällen erübrigen. Ein float ist allgemein eine Fließkommazahl und wird in Java mit einem double repräsentiert. Ein Integer wird als allgemeine Ganzzahl mit dem Java-long, also im Zahlenbereich bis +/-8000000000000000000 repräsentiert. Für die Weiterverarbeitung kann der Anwender ein entsprechendes cast nach float, int, short usw. ausführen, wenn die Größenordnung der Zahl das zulässt.

Der Aufruf von getParsedString() liefert den dem Item zugeordneten String, falls das Item einen geparsten String repräsentiert, ansonsten null. Dagegen liefert getParsedText() immer einen String, und zwar genau der Input-Text des Abschnittes, unabhängig von der Art des Items und einschließlich aller gegebenenfalls überlesenen Kommentare und Leerzeichen, die diesem Abschnitt zugeordnet sind.


5 Ablegen des Parser-Ergebnisses in eigene Java-Instanzen

Topic:.ZbnfJava.ZbnfJavaOutput.

Es ist möglich, mit Aufruf einer Methode das Ergebnis des Parse-Prozesses insgesamt in eine Java-Instanz und entsprechenden Sub-Instanzen abzulegen. Das ist dann zweckmäßig, wenn nicht erst eine Betrachtung einzelner Parse-Ergebnisse erfolgen soll sondern der gesamte Inhalt so wie geparst zur Weiterverarbeitung gleichzeitig anstehen soll. Das ist vergleichbar mit dem DOM-Modell in XML gegenüber dem SAX-Modell. Diese Arbeitsweise ist jedenfalls geeeignet, wenn es um überschaubare Daten, etwa aus einem ini-File geht.

Der Anwender muss hierzu eine class-Struktur bereitstellen, die den Semantik-Angaben in der Syntaxvorschrift des ZBNF-Parsers folgt. Eine Instanz dieser class und das Parser-Ergebnis werden dann der statischen methode ZbnfJavaOutput.setContent(java.lang.Class, java.lang.Object, org.vishia.zbnf.ZbnfParseResultItem) übergeben. Die Zuordnung des Parser-Ergebnisses zu den einzelnen Elementen, Sub-classes und deren Elementen erfolgt unter Nutzung der Reflection.


5.1 Strikte Zuordnung, Exception bei nicht möglicher Zuordnung?

Topic:.ZbnfJava.ZbnfJavaOutput..

Ist ein Parser-Ergebnis-Element mit einer bestimmten Semantik vorhanden, gibt es dafür aber weder ein Field an entsprechender Stelle noch wird eine Methode gefunden, dann wird im Standardfall eine IllegalArgumentException erzeugt mit einem entsprechend langen Hinweistext. Die Nichtübereinstimmung kann gegebenenfalls ein Versionskonflikt oder bei der Programmentwicklung ein Schreibfehler sein. Würde man diessen Fehler ignorieren, dann kann es unerkannte Datenverluste mit Folgefehlern geben, die erst im Test zu finden sind.

Dieses Standardverhalten kann man abschalten mit ZbnfJavaOutput.setWeakErrors(boolean). Das Abschalten macht dann Sinn, wenn während der Programmentwicklung noch nicht alles abgestimmt ist. Wird keine Exception erzeugt, dann wird nicht abgebrochen, weitere Werte werden dann gegebenenfalls zugewiesen, auf die man sich zunächst konzentrieren möchte. Die methode setContent(...) liefert als Ergebnis entweder null, wenn es keinen Fehler gab, oder einen gegebenfalls mehrzeiligen Fehlertext.


5.2 Zuordnung zu Fields

Topic:.ZbnfJava.ZbnfJavaOutput..

Grundsätzlich gibt es zwei Möglichkeiten, die Parser-Ergebnisse zuzuordnen:

Die zweite Möglichkeit entspricht eher der Idee der Kapselung in der ObjectOrientierten Programmierung und bietet weitgehende Möglichkeiten der Verarbeitung der Werte. Die Variante a) ist aber mit weniger Aufwand programmierbar und soll daher zunächst betrachtet werden.


5.2.1 Field-Name und Semantic-Bezeichner, XML-Schreibweise

Topic:.ZbnfJava.ZbnfJavaOutput...

Der Name der Fields soll der Bezeichnung der Semantic entsprechen. Dabei wird aber die in Java übliche Kleinschreibung des Namens berücksichtigt.

Aufgrund der Möglichkeit der Erzeugung eines XML-Baumes als Parser-Output und der daran angelegten Möglichkeit der Schreibweise der Semantik gibt es noch einige Besonderheiten:

Konflikte der Doppelnennung beispielsweise an-example und an_example sind im ZBNF-Syntaxscript zu vermeiden und könnten auch anderweitig weniger günstig sein.


5.2.2 public-Attribute

Topic:.ZbnfJava.ZbnfJavaOutput...

Die Fields müssen public sein. Sonst ist ein Zugriff darauf über Reflection nicht möglich. Der Reflection-Zugriff erfolgt von außen, auch hier greift eine private oder protected-Kapselung und würde diesen Zugriff verbieten. Es stellt sich die Frage: Öffnet nicht eine ..public-Deklaration alle Türen für eine unkalkulierbare Änderung der Werte außerhalb des eigenen Programmcodes.

Die eine Hälfte der Antwort ist: Nein. Denn: Die Instanz, die die Werte aus dem Parser-Ergebnis aufnimmt, kann und sollte selbst gekapselt sein. Eine Änderung der Werte einer beliebigen global bekannten Instanz wäre möglich aufgrund der public-Definition ihrer Attribute, nicht aber die Änderung der konkret gegebenfalls nur private bekannten Instanz, die der Methode ZbnfJavaOutput.setContent(java.lang.Class, java.lang.Object, org.vishia.zbnf.ZbnfParseResultItem) ganz privat und incognito mitgeteilt wird. Also, kein Aufbrechen einer gewünschten Kapselung.

Die zweite Hältfe der Antwort ist: Softwarepflege ist erschwert. Bei einer Umbenennung oder funktionalen Änderung von Attributen, die private sind, braucht nur der bekannte Kontext beachtet zu werden. Bei public-Attributen ist es grundsätzlich nicht vorhersagbar, wo diese Attribute in anderer Software noch verwendet werden. Sie werden verwendet, als Semantik-Bezeichner in ZBNF-Scripts. Das muss beachtet werden und spricht gegebenfalls gegen die Attribute-Variante.


5.2.3 Typ der Attribute

Topic:.ZbnfJava.ZbnfJavaOutput...

Für einfache Semantiken muss der Typ der Attribute mit der Syntax des zugehörigen Elementes in der ZBNF übereinstimmen. Wird zum Beispiel geparst: <#?value>, dann ist das Parser-Ergebnis an dieser Stelle mit der Semantik value bezeichnet ein Integer-Wert. Der Parser speichert einen long-Wert, aber die Zuweisung zu einer int- oder float-Variablen wird ebenfalls unterstüzt. Alle nicht numerischen Werte werden als java.lang.String gespeichert.

Ist die Semantik einer Syntaxkomponente zugeordnet, dann muss das Parser-Ergebnis der Syntaxkomponente in die Instanz mit dem Namen der Semantik abgelegt werden können. Ist beispielsweise im ZBNF-Script notiert:

 BillOfMaterial::= ...<position>...
 position::= <#?amount> <#?code> <16*?description>  <*\r\n?value> .

dann muss es ein Attribut position geben. Dessen Typ muss aber die in position geparsten Elemente aufnehmen können. Der Typ von position muss also speziell passend sein. Allerdings kann dieser Typ nicht nur über Attribute zuordenbar sein, sondern auch über Methoden, es kann sich auch um einen Interfacetyp handeln. Der Typ muss public definiert sein, an beliebiger Stelle im Java-Code.

Wenn ein entsprechendes Attribute vorhanden, aber mit null besetzt ist, dann wird der Typ des Attributes als Instanz erzeugt und zugeordnet, bevor die Weiterverarbeitung erfolgt. Das geht nicht bei Interface-Typen und ist nicht passend wenn aus anderen Gründen ein abgeleiteter Typ benötigt wird. Will man das, so muss man Methoden für die Parser-Ergebniszuordnung wählen. Die Zuordnung zu Attributen ist die einfache Variante.


5.2.4 Container-Attribute

Topic:.ZbnfJava.ZbnfJavaOutput...

Insbesondere für Wiederholungs-Konstrukte in der ZBNF {...} sollen einem Attribute mehrere Exemplare von Parser-Ergebnissen zuordenbar sein. Das ist möglich, wenn das Attribute eine java.util.List ist. Unterstützt wird hierbei eine java.util.LinkedList und java.util.ArrayList. Ist das Attribut vom Typ List aber mit null vorbesetzt, dann wird bei der ersten Benutzung eine LinkedList intialisiert. Braucht man eine ArrayList, dann muss man vorher initialisieren.

Bei jedem Parser-Ergebnis, das diesem Attribut zugehordent werden soll, wird dann ein Element der List erzeugt und an die List angehangen (List.add(Object)). Der Typ des Elementes richtet sich dabei nach dem generischem Typ der List. Hier gilt gleiches wie bei einfachen Attributen: Ist ein Interfacetyp angegeben oder ein abgeleiteter Typ notwendig, dann geht das nicht mit der einfacheren Attribut-Variante, sondern es muss eine Parser-Ergebniszuordnung über Methoden erfolgen. Allerdings, für viele Fälle ist der einfache Weg gangbar und schneller ausprogrammierbar.


5.2.5 Beispiel

Topic:.ZbnfJava.ZbnfJavaOutput...

Das Beispiel BillOfMaterial ist der Veröffentlichung bei Sourceforge http://www.sf.net/projects/ZBNF entnommen. Die Syntax dieser Stückliste ist definiert in examples:_billOfMaterial.zbnf. Die class class:_org/vishia/zbnf/example/billOfMaterial/BillOfMaterialZbnf_Fields ist diejenige, die als primäre Instanz zur Aufnahme der Daten bereitgestellt wird. Dort finden sich entsprechend dem Toplevel-Syntaxprescript die Variablen order, position und date. Alle drei Variable sind Referenzen zu classes, die die Variablen entsprechend der Sub-Syntax enthalten, beziehungsweise im Fall order die Variablen, die aufgrund der Semantic-Angabe notwendig sind.

Das Field position ist als List<BillOfMaterialData.Position> angelegt. Folglich wird bei jedem Auftreten einer Syntaxkomponente mit der Semantic position eine Instanz von BillOfMaterialData_Fields.Position erzeugt, in die Liste eingehängt und mit den Inhalten der Syntaxkomponente gefüllt.

Die class-Typen sind an anderer Stelle definiert, in class:_org/vishia/zbnf/example/billOfMaterial/BillOfMaterialData_Fields, enthalten aber dort die der Semantic entsprechenden weiteren Fields.


5.3 Zuordnung mit Methoden

Topic:.ZbnfJava.ZbnfJavaOutput..

Im Gegensatz der Zuordnung zu Fields ist die Zuordnung mit Methoden mehr programmieraufwändig, schafft aber Möglichkeiten der Verarbeitung und Strukturierung der Daten unmittelbar bei der Zuordnung aber unabhängig von der ZBNF-Syntax.


5.3.1 Schreibweise von Methoden

Topic:.ZbnfJava.ZbnfJavaOutput...

Der Argument-type der Methode muss dem Datentyp des Parser-Ergebniselementes entsprechen, wobei hier Varianten möglich sind:

Um Komponenten als Parser-Ergebnisse zuzuordnen, wird wie folgt vorgegangen:


5.3.2 Vorteile der Methoden-Nutzung

Topic:.ZbnfJava.ZbnfJavaOutput...

Die Methoden lassen sich über Interfaces auch aus der class ZbnfJavaOutput ansprechen. Damit ist eine größtmögliche Trennung (Dependency break) zwischen der Zbnf-Syntax und implementierenden Klassen für die Daten möglich, entsprechend den Regeln der ObjektOrientierten Programmierung.

Im Test lassen sich in den Methoden Breakpoints setzen, Zusatzanweisungen schreiben oder eine Überwachung mit Aspektorientierter Programmierung unterbringen. Damit ist leichter nachvollziehbar und testbar, welche Daten über das ZbnfJavaOutput erzeugt werden.

In den Methoden können Parser-Ergebnisse entweder weiter verarbeitet oder anders strukturiert werden.

Beispiel: ZBNF-Komponenten sollen letzlich in einer Map abrufbereit sein, sortiert nach einem Schlüssel, der einem Element der Komponente entspricht. Das ist mit wenig Aufwand in der add_semantic(...)-Methode realisierbar, da die Instanz alle Informationen enthält und beliebige Elemente der Anwenderklasse angesprochen werden können.

Beispiel: Die Anordnung der Parser-Ergebnisse soll ganz anders erfolgen, in Anwenderklassen, die unabhängig vom Parser geschrieben worden sind und auch in anderen Kontexten benötigt werden. Hier können die Parser-Ergebnisse komponentenweise erst nur zwischensgepeichert werden. Dazu kann man auch die einfache Zuordnung zu Fields verwenden. Die add_semantic(...)-Methode holt dann diese zwischengespeicherten Ergebnisse aus der temporären Instanz und ordnet sie so an, wie sie letztlich benötigt werden.


5.3.3 Vorgehensweise bei Einsatz von Methoden-Interfaces

Topic:.ZbnfJava.ZbnfJavaOutput...

  • Es werden Interfaces für die jeweiligen einzelnen Syntaxvorschriften erstellt, die dort erzeugte Semantik muss entgegengenommen werden. Diese Interfaces müssen also direkt dem ZBNF-Script folgen. Inhaltlich, wie die Daten zu verarbeiten sind, wird dabei nichts festgelegt.

  • Es wird das Konzept der inhaltlichen Verarbeitung der Daten unabhängig von den Interfaces und dem ZBNF-Script erstellt bzw. betrachtet.

  • Die Interfaces werden formell der Implementierung zugeführt. Arbeitet man beispielsweise unter Eclipse, dann ist das unter Zuhilfenahme der IDE-Bedieneigenschaften sehr einfach: implements Interface an die implementierende class anhängen und über quick fix die Methoden zunächst leer erzeugen lassen.

  • Die implementierenden Methoden werden mit Inhalten gefüllt und getestet. Man hat dabei alle Gestaltungsfreiheiten unabhängig vom ZBNF-Script.


  • 5.3.4 Beispiel

    Topic:.ZbnfJava.ZbnfJavaOutput...

    Das Beispiel BillOfMaterial der Veröffentlichung bei Sourceforge http://www.sf.net/projects/ZBNF enthält auch eine Methoden-Variante, konsequenterweise mit Nutzung von Interfaces. Die Syntax dieser Stückliste ist definiert in examples:_billOfMaterial.zbnf. Das interface:_org/vishia/zbnf/example/billOfMaterial/BillOfMaterial_Zbnf_ifc enthält als Rahmen alle Interfaces zur Ablage von Informationen aus ZBNF-Komponenten. Die primäre Instanz implementiert interface:_org/vishia/zbnf/example/billOfMaterial/BillOfMaterial_Zbnf_ifc.ZbnfStore_BillOfMaterial und ist instanziiert als class:_org/vishia/zbnf/example/billOfMaterial/BillofMaterialData_Methods. Die Attribute sind hier private, sihe auch javadoc-src:_org/vishia/zbnf/example/billOfMaterial/BillofMaterialData_Methods.


    5.4 Speicherung von Positionen in der Zeile des Parser-Inputs als Parser-Ergebnis

    Topic:.ZbnfJava.ZbnfJavaOutput..

    In bestimmten Fällen sollen die Positionen des Parser-Inputs bekannt sein, weil in der Verarbeitung darauf bezug genommen werden soll. Beispielsweise wenn ein Formular geparst wird, und eine Anordnung in der Zeile wesentlich ist. Die Input-Position wird vom Parser gespeichert, wenn TODO

    Um diese Input-Position im Java-Output zu speichern, gibt es sowohl für die Speicherung in Fields als auch mit Methoden folgende Möglichkeiten:

    Die Regel ist: Wird ein solches Field oder eine solche Methode gefunden, wird die Position gespeichert, ansonsten nicht. Es erfolgt keine Fehlermeldung, wenn eine Position vorhanden ist aber nicht eines dieser Elemente gefunden wird.


    5.5 Speicherung von Texten aus einer Optionskomponente

    Topic:.ZbnfJava.ZbnfJavaOutput..

    Eine [<?semantic> option1| option2...] ist eine ZBNF-Komponente. Sie hat die Besonderheit, dass der geparste Text ihr zusätzlich zugewiesen wird, neben Details der optionalen Syntaxbestandteile. Dieser geparste Text wird in eine String-Variable geschrieben oder einer set_sematic(String) oder add_sematic(String)-Methode übergeben, wenn diese vorhanden ist. Das kann zusätzlich zur Übergabe des Inhalt der Komponente oder alternativ erfolgen. Die Übergabe als String ist dann wesentlich, insbesondere dann wenn feste Bezeichnungen als Option syntaktisch zulässig sind. Beispielsweise [<?unit> second | millisec | ms | µs | ms ].


    5.6 Aufrufmöglichkeiten in ZbnfJavaOutput

    Topic:.ZbnfJava.ZbnfJavaOutput..

    Um ein erfolgreich geparstes Ergebnis abzulegen, ist folgende Aufrufreihenfolge günstig:

     ZbnfJavaOutput javaOutput = new ZbnfJavaOutput(report);
     javaOutput.setMethodsOnly(true);
     javaOutput.setContent(Destination.class, destination, zbnfParser.getFirstParseResult());
    

    Es wird eine Instanz angelegt. Es können bestimmte Einstellungen vorgenommen werden. Dann wird die Kern-Routine aufgerufen. Hierbei ist Destination die class oder das interface, in der die Elemente zur Aufnahme des Toplevel-Parserergebnisses definiert sind. destination ist eine zugehörige Instanz. zbnfParser ist die class:_org/vishia/zbnf/ZbnfParser-Instanz. Alle Exceptions entstehen dann, wenn etwas mit den aufnehmenden classes oder Intanzen nicht stimmt und sind Programmierfehler. D.h. ein spezielles Abfangen der Exceptions ist nicht zweckmäßig und notwendig.

    Man kann ohne Anlegen einer eigenen Instanz arbeiten, in dem statische Methoden aufgerufen werden. Es stehen mehrere Varianten zur Verfügung. Hierbei wird das Anlegen einer Instanz intern ausgeführt.

    Die class:_org/vishia/zbnf/ZbnfJavaOutput enthält auch den Aufruf des gesamten Parsers einschließlich Ablage des Ergebnisses in eine Anwenderinstanz. Dann hat man alles mit einer Befehlszeile. Das kann zweckmäßig sein beispielsweise für Konfigurationsfiles:

     String sError = ZbnfJavaOutput.parseFileAndFillJavaObject(destination.getClass(), destination, fileCfg, fileSyntax, report, 0);
     if(sError != null)
     { throw new ParseException("Error reading config file" + sError, 0);
     }
    

    Diese Methode ist so gebaut, dass keine Exceptions erzeugt werden sondern ein Fehlertext zurückgeliefert wird. Dieser ist null, wenn kein Fehler auftrat. Ein Fehler kann sowohl in der Syntax des Parsers vorhanden sein, beim Parsen entstehen (das ist erwartbar, der Input-Text kann falsch sein) oder bei der Ablage der Daten. Alle drei Fälle werden hier in der Weiterverarbeitung nicht unterschieden. Zweckmäßig ist eine Fehleranzeige für den Anwender, für Eingabetextfehler. Das in der Syntax oder bei der Ablage Fehler entstehen können,ist vorher durch Test zu minimieren. Eine Restwahrscheinlichkeit wird dann ebenfalls angezeigt.

    Um ein erfolgreich geparstes Ergebnis abzulegen, ist folgende Aufrufreihenfolge günstig:

     ZbnfJavaOutput javaOutput = new ZbnfJavaOutput(report);
     javaOutput.setMethodsOnly(true);
     javaOutput.setContent(Destination.class, destination, zbnfParser.getFirstParseResult());
     
    

    Es wird eine Instanz angelegt. Es können bestimmte Einstellungen vorgenommen werden. Dann wird die Kern-Routine aufgerufen. Hierbei ist Destination die class oder das interface, in der die Elemente zur Aufnahme des Toplevel-Parserergebnisses definiert sind. destination ist eine zugehörige Instanz. zbnfParser ist die class:_org/vishia/zbnf/ZbnfParser-Instanz. Alle Exceptions entstehen dann, wenn etwas mit den aufnehmenden classes oder Intanzen nicht stimmt und sind Programmierfehler. D.h. ein spezielles Abfangen der Exceptions ist nicht zweckmäßig und notwendig.

        
    

    Man kann ohne Anlegen einer eigenen Instanz arbeiten, in dem statische Methoden aufgerufen werden. Es stehen mehrere Varianten zur Verfügung. Hierbei wird das Anlegen einer Instanz intern ausgeführt.

    Die class:_org/vishia/zbnf/ZbnfJavaOutput enthält auch den Aufruf des gesamten Parsers einschließlich Ablage des Ergebnisses in eine Anwenderinstanz. Dann hat man alles mit einer Befehlszeile. Das kann zweckmäßig sein beispielsweise für Konfigurationsfiles:

     String sError = ZbnfJavaOutput.parseFileAndFillJavaObject(destination.getClass(), destination, fileCfg, fileSyntax, report, 0);
     if(sError != null)
     { throw new ParseException("Error reading config file" + sError, 0);
     }
       
    

    Diese Methode ist so gebaut, dass keine Exceptions erzeugt werden sondern ein Fehlertext zurückgeliefert wird. Dieser ist null, wenn kein Fehler auftrat. Ein Fehler kann sowohl in der Syntax des Parsers vorhanden sein, beim Parsen entstehen (das ist erwartbar, der Input-Text kann falsch sein) oder bei der Ablage der Daten. Alle drei Fälle werden hier in der Weiterverarbeitung nicht unterschieden. Zweckmäßig ist eine Fehleranzeige für den Anwender, für Eingabetextfehler. Das in der Syntax oder bei der Ablage Fehler entstehen können,ist vorher durch Test zu minimieren. Eine Restwahrscheinlichkeit wird dann ebenfalls angezeigt.