• Keine Ergebnisse gefunden

3.5 Externe Klassendiagramme

4.3.6 Realisierung des neuen Dateiformats

Das in Abschnitt 4.3.5 beschriebene Dateiformat f¨ur .TG-Dateien wird in einer ei-genst¨andigen Klasse GraphIO implementiert.

Das Klassendiagramm hierzu wird in Abbildung 4.12 gezeigt. Es lassen sich wahlwei-se nur das Schema oder Schema und Graphen abspeichern oder laden. Als Eingabe bzw. Ausgabe sind entweder ein Dateiname oder ein DataInputStream bzw. DataOut-putStream erlaubt. Alle Methoden sind statisch ausgelegt.

+loadSchemaFromTG( filename : string) : Schema +loadGraphsFromTG( filename : string) : List<Graph>

+loadGraphsFromTG( filename : string, pf : ProgressFunction) : List<Graph>

+loadSchemaFromStream( dis : DataInputStream) : Schema +loadGraphsFromStream( dis : DataInputStream) : List<Graph>

+loadGraphsFromStream( dis : DataInputStream, pf : ProgressFunction) : List<Graph>

+saveSchemaToTG( filename : string, schema : Schema)

+saveGraphsToTG( filename : string, schema : Schema, graphs : List<Graph>)

+saveGraphsToTG( filename : string, schema : Schema, graphs : List<Graph>, pf : ProgressFunction) +saveSchemaToStream( dos : DataOutputStream, schema : Schema)

+saveGraphsToStream( dos : DataOutputStream, schema : Schema, graphs : List<Graph>)

+saveGraphsToStream( dos : DataOutputStream, schema : Schema, graphs : List<Graph>, pf : ProgressFunction) GraphIO

Abbildung 4.12: Klassendiagramm f¨ur die GraphIO-Klasse

Optional ist die ¨Ubergabe einer ProgressFunction als Parameter m¨oglich. Methoden dieser Schnittstelle werden von GraphIO aufgerufen, aufgeteilt in init f¨ur die Initia-lisierung der Funktion, progress f¨ur den Interval-abh¨angigen Aufruf und finished f¨ur

4.3. Eingabe/Ausgabe das Ende der Funktion. Der Anwender muss diese Schnittstelle implementieren, falls er R¨uckmeldungen ¨uber den Verlauf des Ladens oder Speicherns von GraphIO erhalten will11. Ein Klassendiagramm dieses Interfaces ist in Abbildung 4.13 zu sehen.

Abbildung 4.13: Klassendiagramm des Interfaces ProgressFunction

Die Ausgabe des Schemas und der Graphen in eine .TG-Datei stellt keinerlei Proble-me dar, nacheinander werden hier alle Eigenschaften in die Datei geschrieben. Es gibt lediglich einige Besonderheiten zu beachten, diese werden in diesem Abschnitt unter

”Besonderheiten“ beschrieben.

Der Ladevorgang gestaltet sich etwas schwieriger als der Speichervorgang, da hier ex-plizit mit einem Parser gearbeitet werden muss.

Dies liegt insbesondere daran, dass beim Einlesen der Werte von Listen, Sets und Re-cords Kaskadierungen auftreten k¨onnen, welche erst erkannt werden m¨ussen. Ein zus¨atz-licher Grund ist die ¨Ubersichtlichkeit und damit die Erweiterbarkeit eines Parsers.

In den folgenden Abschnitten wird erst die Funktionsweise eines Parsers selbst erl¨autert.

Anschließend wird die geschwindigkeitsm¨aßige Optimierung des Lade- und Speicher-vorgangs angesprochen.

Beschreibung eines Recursive Descent Parsers

Parsen ist der Vorgang des Entscheidens, ob eine Folge von Tokens (Eingabesymbole oder W¨orter, welche der Parser versteht) von einer Grammatik generiert werden kann.

In unserem Fall reicht ein so genannter recursive descent (rekursiv absteigender) Top-Down-Parser aus. Die n¨achsten Abschnitte wurden an [AhSeUl86], S.40ff angelehnt.

Top-Down bedeutet, dass ein Parser als erstes mit der Wurzel beginnt, d.h. mit dem startenden Non-Terminal. Daraufhin stellt der Parser eine Hypothese f¨ur eine Zerlegung

11Anmerkung: Ein Beispiel einer Implementation der Schnittstelle ProgressFunction existiert im Repo-sitory unterjgralab.impl.ProgressFunctionImpl.java.

dieses Non-Terminals auf und versucht diese anhand der Grammatikregeln zu verifizie-ren. Jedes Non-Terminal l¨asst sich nach der zugrunde liegenden Grammatik wieder in 0-n Non-Terminale und/oder Terminale aufl¨osen. Dieser Vorgang geschieht solange, bis nur noch Terminale ¨ubrig bleiben.

Ein rekursiv absteigender Parser ist eine spezielle Form eines Top-Down-Parsers. Er besteht aus einer Reihe von rekursiven Methoden, welche die Eingabe lesen. Jede Me-thode ist mit einem Non-Terminal der Grammatik assoziiert. An dieser Stelle benutzen wir eine besondere Form des rekursiv absteigenden Top-Down-Parsers: Einen predicti-ve Parser. Bei diesem Parser entscheidet das eingelesene Token eindeutig die Methode, welche f¨ur ein Non-Terminal aufgerufen wird.

F¨ur diesen Zweck muss die zu erstellende Grammatik f¨ur das TG-Dateiformat als so-genannte LL(k)-Grammatik ausgelegt werden. Diese Grammatik ist kontextfrei, und es wird jeder Ableitungsschritt eindeutig durch k Tokens bestimmt. Das bedeutet, dass die Frage, welches Non-Terminal als n¨achstes expandiert werden soll, eindeutig durch diese k Tokens beantwortet werden kann. Die EBNF, welche TG in Abschnitt 4.3.5 be-schreibt, erf¨ullt diese Voraussetzung bereits. Der Parameter k ist hier gleich 1, da immer nur ein Token gelesen wird (Achtung, ein Token kann auch mehr als nur einen Buchsta-ben bezeichnen!).

Die Klasse GraphIO, welche in JGraLab f¨ur die Ein- und Ausgabe zust¨andig ist, besitzt zwei besondere Methoden, welche intern verwendet werden:lookAhead():String undmatch(String token).

LookAhead ließt ein Token von der Eingabe, welches anschließend mit Terminalen der Grammatik verglichen werden kann. Daraufhin wird die entsprechende Methode aufge-rufen, welche f¨ur das zugeh¨orige Non-Terminal steht.

Die Methode match sorgt daf¨ur, dass der Eingabezeiger auf das n¨achste Token nach dem als Parameter ¨ubergebenen Token zeigt. Das ¨ubergebene Token muss dasselbe zu lesende Token sein, welches mit lookAhead ausgelesen werden kann.

Lade- und Speichervorgang unter Vermeidung von Reflection

In diesem Abschnitt geht es um Geschwindigkeitsoptimierung beim Laden und Spei-chern von Graphen aus dem TG-Dateiformat. Aufgrund der objektorientierten Zugriffs-schicht gestaltet sich der Lade- und Speichervorgang als ¨außerst langsam im Vergleich zum alten C++-GraLab. Daher haben Optimierungen in diesem Bereich eine recht große Auswirkung.

4.3. Eingabe/Ausgabe Eine M¨oglichkeit, persistente JGraLab-Graphen wieder einzulesen ist die folgende:

1. Parsen der .TG-Datei und Erkennen von Kanten und Knoten: Diesen Vorgang

¨ubernimmt der Parser.

2. Erzeugung des jeweiligen Graphelements mittels Reflection: Hier wird die create-Methode des Graphen aufgerufen, der die Graphelemente beinhaltet.

3. F¨ullen der Attribute des Graphelements mittels Reflection: Jeder einzelne Setter des Elements wird mittels Reflection gesucht und mit dem aus der .TG-Datei ausgelesenen Wert gef¨ullt.

Eine bessere M¨oglichkeit ist, die gesamten Werte des Graphelementes direkt seinem Konstruktor zu ¨ubergeben, so dass damit die zweite Reflection weg f¨allt. Dies hat al-lerdings den Nachteil, dass auch der entsprechende Konstruktor zun¨achst noch mittels Reflection gesucht werden muss.

Eine weitere geschwindigkeitsoptimierte Version benutzt nur den Standard-Konstruktor des Objekts, dieser kann ohne Reflection einfach mittels der newInstance-Methode in-itialisiert werden. Das F¨ullen des Graphelements mit seinen Werten geschieht in diesem Fall mittels einer Fill-Methode, welche ein Array aus Objects ¨ubergeben bekommt. Da-mit fallen beide Reflection-Vorg¨ange weg: Sowohl die Suche des Konstruktors als auch die Suche der Setter des Elements.

Die Fill-Methode wird in einem Interface vorher in die M1-Generalisierungshierarchie mit eingef¨ugt (siehe dunkel hinterlegte Interfaces in Abbildung 4.14), so dass sie von Graph/Edge/Vertex erbt, die Interfaces der M1-Klassen aber selbst wiederum von ihr erben. Damit muss jede Impl-M1-Klasse eine automatisch generierte Fill-Methode im-plementieren, welche ihre eigenen Werte mittels des ¨ubergebenen Arrays f¨ullen kann.

Diese Maßnahme f¨uhrt dazu, dass die Reflection f¨ur die Setter oder den ¨uberladenen Konstruktor weg f¨allt, da die Fill-Methode bereits zur Compile-Zeit feststeht.

In der Abbildung 4.14 erkennt man, dass die M1-Impl-Klassen sowohl die Getter/Setter und Factory-Methoden ihrer M1-Interfaces als auch die Fill-Methode implementieren, welche sie durch die Helper-Interfaces vererbt bekommen.

Das Speichern l¨asst sich ¨ahnlich beschleunigen. Normalerweise m¨ussten die Werte der M1-Elemente alle mit Hilfe ihrer Getter ausgelesen werden. Da der Name des Setters zur Compilezeit noch nicht feststeht, muss dazu Reflection benutzt werden.

Eine bessere M¨oglichkeit ist, auch hier in den Helper-Interfaces eine weitere Methode f¨ur das Auslesen einzuf¨uhren. Diese Methode wirdgetValues()genannt (siehe Abb.

4.14). Das Prinzip ist dasselbe: Da diese Methode zur Compilezeit bereits bekannt ist, f¨allt die Reflection weg. Implementiert wird die getValues-Methode genau wie die Fill-Methode von den Impl-M1-Klassen.

+getAlpha()

+fill( values: Object[] ) +getValues() : Object[]

GraphHelper {interface}

+fill( values: Object[] ) +getValues() : Object[]

EdgeHelper {interface}

+fill( values: Object[] ) +getValues() : Object[]

+fill( values: Object[] ) +getValues() : Object[]

+getAttribute1() +setAttribute1() +...()

M1Class...Impl

+fill( values: Object[] ) +getValues() : Object[]

+fill( values: Object[] ) +getValues() : Object[]

+getAttribute3() +setAttribute3() +...()

M1Class...Impl

Abbildung 4.14: Hilfs-Interfaces zur Umgehung von Reflection beim Laden und Spei-chern

Besonderheiten

Bei der Speicherung von Objects gibt es eine Besonderheit zu beachten. Java selbst bietet bereits mit der Serialisierung eine M¨oglichkeit an, Java-Objekte mittels eines Da-tenstroms zu repr¨asentieren. Dieser Datenstrom kann entsprechend in eine Datei ge-schrieben werden und anschließend auch einfach wieder geladen werden.

Da das .TG-Dateiformat allerdings lesbar bleiben soll, existiert hier ein Problem: Ein

4.3. Eingabe/Ausgabe

ObjectStream, welcher Java-Objekte serialisiert aufnimmt, l¨asst sich nur als Bin¨ardaten schreiben. Dies hat zur Folge, dass die eigentlich Text-orientierte Speicherung inner-halb des .TG-Formats nicht mehr so einfach lesbar bleibt, da in einem Texteditor einige Bin¨ardaten als Text interpretiert werden w¨urden, z.B. als Zeilenumbr¨uche.

Als Abhilfe wird die von Sun undokumentierte Funktion12des Base64En/Decoders ge-nutzt. Dieser kann einen Datenstrom in das Base64-Format transformieren.

Dieses Format ver¨andert jede 8 Bit-Zeichenfolge dahingehend, dass lediglich einige we-nige Zeichen des Zeichensatzes verwendet werden. Je 24 Bit des Bytestroms werden in vier 6 Bit-Bl¨ocke aufgeteilt, wobei jeder 6 Bit-Block zu einem Base64-Zeichen kodiert wird. Damit wird aus dem bin¨aren Datenstrom eine Zeichenfolge, in dem lediglich die Ziffern 0-9, die großen und kleinen Buchstaben des Alphabets sowie die Zeichen +, / und =13vorkommen.

Damit bleibt die Repr¨asentation des Java Objects unabh¨angig von der Codepage und zerst¨ort nicht die lesbare Formatierung der TG-Datei. Aufgrund der Kodierung wird da-durch der Datenstrom allerdings um ca. 33% gr¨oßer [Gali05].

Auch bei der Speicherung von Strings gibt es eine Besonderheit: In dieser JGraLab-Version soll die Unterst¨utzung von Unicode hinzukommen. Java selbst benutzt eine leicht modifizierte Version von UTF-16. Um Unicode-Zeichen in der TG-Datei nat¨urlich lesbar ablegen zu k¨onnen, werden alle Zeichen, welche einen Wert gr¨oßer als 127 dezimal bzw. 7F hexadezimal besitzen (also z.B. Umlaute oder Sonderzeichen), als Unicode-String abgelegt. Dies geschieht, indem das Zeichen als hexadezimaler Wert mit vorangestelltem \ukodiert wird. Der Backslash selbst (\) sowie die Anf¨uhrungs-zeichen (") werden durch einen weiteren vorangestellten Backslash gekennzeichnet.

Zeilenumbr¨uche werden durch\nund gegebenenfalls auch durch\rrepr¨asentiert.

Der Ladevorgang von EnumDomains und RecordDomains gestaltet sich auch etwas schwieriger als der Ladevorgang der anderen Dom¨anen. Hier m¨ussen mittels Reflection erst die Klassen, welche f¨ur die Record- bzw. Enumdom¨ane stehen, gesucht werden und anschließend die Attribute eingeladen werden.

12Der Base64-En/Decoder ist im Paket sun.misc zu finden und erf¨ahrt keinen offiziellen Support von Sun.

13Das Gleich-Zeichen (=) steht null bis zwei mal am Ende einer Base64-kodierten Zeichenfolge, um an-zugeben, wie viele F¨ullbytes an den String angef¨ugt wurden, falls die Gesamtanzahl der Eingabebytes nicht durch drei (bzw. die Gesamtzahl der Eingabebits nicht durch 24) teilbar ist.