Fakultät für Elektrotechnik und Informatik Institut für Praktische Informatik Fachgebiet Datenbanken und Informationssysteme Leibniz Universität Hannover Bachelorarbeit im Studiengang Informatik Entwicklung eines erweiterbaren SQL-Compilers Lutz Steinleger ( Matr.-Nr. 2240820 ) 28. August 2006 Erstprüfer: Prof. Dr. Udo Lipeck Zweitprüfer: Dr. Hans Hermann Brüggemann Betreuer: Dipl.–Math. Michael Tiedge Zusammenfassung Im Rahmen dieser Bachelorarbeit ist ein erweiterbarer SQL-Compiler erstellt worden. Mittels dieses Compilers ist es möglich, die Ausdrücke eines erweiterten SQL–Dialekts zu parsen und anschließend in semantisch äquivalente SQL-92–Ausdrücke zu transformieren. Als Grundlage für das Transformieren eines SQL–Ausdrucks ist die abstrakte Datenstruktur Syntaxbaum benutzt worden. Die Knoten eines solchen Syntaxbaums repräsentieren die unterschiedlichen Produktionen, die während des Parsens eines SQL–Ausdrucks hergeleitet worden sind. Die in einer Produktion enthaltenen Token sind mit diesen Knoten logisch verknüpft, weshalb eine Veränderung der Baumstruktur und damit implizit auch der verknüpften Token zu der gewünschten Transformation des geparsten SQL–Ausdrucks führt. Das Visitor Pattern, welches zur Abkapselung der Transformationslogik von den Komponenten der Datenstruktur Syntaxbaum dient, ist als zentrales softwaretechnisches Konzept in den erweiterbaren SQL-Compiler integriert worden. Mittels dieses Pattern können in einer separaten Klasse alle benötigten Transformationsanweisungen für die betroffenen Knotentypen implementiert werden. Dadurch ist es möglich, die Schnittstellen für die Eingabe von benutzerdefinierten Transformationsanweisungen zu bündeln und sie damit unabhängig von der Ausprägung der jeweiligen SQL– Erweiterung zu gestalten. Neben der Transformation eines SQL–Ausdrucks basieren auch die semantische Analyse, sowie die Ermittlung einzelner Teilklauseln eines SQL– Ausdrucks auf der Nutzung des Visitor Pattern. Als weitere Funktionalitäten bietet der erweiterbare SQL-Compiler die detailierte Beschreibung eines während des Parsens aufgetretenen Fehlers, sowie den Export des abstrakten Syntaxbaums in einem auf XML basierenden Format. Inhaltsverzeichnis 1 Einleitung 1.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Zielsetzung dieser Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Aufbau dieser Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Parsergeneratoren 2.1 Untersuchte Alternativen . . . 2.1.1 yacc . . . . . . . . . . 2.1.2 AnTLR . . . . . . . . 2.1.3 JavaCC . . . . . . . . 2.2 JavaCC . . . . . . . . . . . . 2.2.1 Grammatikdatei . . . 2.2.2 Tokenklassen . . . . . 2.2.3 Syntaktische Analyse . 2.2.4 Lookahead . . . . . . . 2.2.5 Generierte Dateien . . 2.3 JJTree . . . . . . . . . . . . . 2.3.1 Grammatikdatei . . . 2.3.2 Generierte Dateien . . 2.3.3 Erzeugter Syntaxbaum 5 5 6 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 8 8 9 9 10 11 12 13 13 15 18 19 21 24 3 SQL-92 Grammatik für JavaCC 3.1 Gesetzte Optionen . . . . . . . . . . . 3.2 Java Compilation Unit . . . . . . . . . 3.3 Definierte Tokenstruktur . . . . . . . . 3.4 Eingefügter Java-Code . . . . . . . . . 3.5 Abwandlungen der Grammatik–Vorlage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 29 29 30 31 . . . . 33 34 34 35 36 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Konzept der Erweiterbarkeit 4.1 Abstrakte Datenstruktur Syntaxbaum 4.1.1 Anbindung der Tokenliste . . . 4.1.2 Baumoperationen . . . . . . . . 4.2 Einheitliche Schnittstellen . . . . . . . 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 37 38 38 40 43 44 45 46 47 50 50 59 5 Export der abstrakten Baumstruktur 5.1 Definition des PAPL-Formats . . . . . . . . . . . . . . . . . . . . . . 62 63 6 Semantische Analyse 66 4.3 4.4 4.5 4.6 4.7 4.2.1 AdvancedNode . . . . . . . . . . . . . . . . 4.2.2 SimpleNode . . . . . . . . . . . . . . . . . . 4.2.3 SQLParser . . . . . . . . . . . . . . . . . . . Visitor Pattern . . . . . . . . . . . . . . . . . . . . 4.3.1 Anwendungsfall erweiterbarer SQL-Compiler Zugriff auf Teilklauseln . . . . . . . . . . . . . . . . Zugriff auf Datenbankinhalte . . . . . . . . . . . . . Erweitern von Parseplus . . . . . . . . . . . . . . . . 4.6.1 Parser einer erweiterten Grammatik . . . . . 4.6.2 Syntaxbaumklassen der Erweiterung . . . . Beispiele für Erweiterungen . . . . . . . . . . . . . 4.7.1 Natürlicher Innerer Verbund . . . . . . . . . 4.7.2 Aggregate–By . . . . . . . . . . . . . . . . . 7 Implementierung 7.1 Realisierte Schnittstellen . . . . . . . . . 7.2 Paketstruktur . . . . . . . . . . . . . . . 7.3 Paket parseplus . . . . . . . . . . . . . . 7.4 Paket parseplus.modifier . . . . . . . . . 7.5 Paket parseplus.sql92parser.parser . . . . 7.6 Paket parseplus.sql92parser.treebuilder . 7.7 Paket parseplus.dbcontent . . . . . . . . 7.8 Paket parseplus.semantics . . . . . . . . 7.9 Erweiterung des Pakets org.javacc.jjtree . 7.10 Erweiterung des Pakets org.javacc.parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 69 70 72 76 90 92 98 101 110 111 8 Ausblick 113 A SQL-92 Grammatik A.1 Grammatik . . . . . . . . . . . A.1.1 Nichtterminale Symbole A.1.2 Terminale Symbole . . . A.1.3 Startsymbol . . . . . . . A.1.4 Produktionen . . . . . . 114 114 114 115 115 116 . . . . . B Handbuch für Erweiterungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 4 Kapitel 1 Einleitung 1.1 Einführung Der Zugriff auf die Daten in temporalen oder förderierten Datenbanksystemen (DBS) wird mittels einzelner erweiterter Dialekte der strukturierten Abfragesprache SQL bewerkstelligt. Da die Anfragen in diesen Dialekten jedoch nicht von den DBS verarbeitet werden können, ist eine Transformation des SQL–Ausdrucks in den vom DBS verwendeten Standard zwingend notwendig. Das geeignete Mittel, um eine solche Transformation vorzunehmen, stellt ein individuell erweiterbarer SQL-Compiler dar. Ein solcher Compiler ermöglicht es, die SQL-Anfrage gemäß der erweiterten Syntax zu parsen und mittels zu definierender Regeln in den SQL-92 Standard umzuformen. Zusätzlich kann dieser Compiler dazu genutzt werden, vor dem Ausführen einer SQL-92 Anfrage auf dem DBS mögliche Fehler in deren Syntax oder den inhaltlichen Zusammenhängen zu ermitteln, um damit unnötigen Netzwerkverkehr und zusätzliche Bearbeitungszeit durch das DBS zu verhindern. In der Abbildung 1.1 wird die Struktur eines erweiterten SQL-Compilers verdeutlicht, der einen Ausdruck in erweitertem SQL nach den Regeln der Erweiterung parst und ihn in der Folge in einen SQL-92–Ausdruck umformt. Abbildung 1.1: Struktur eines erweiterten SQL-Compilers 5 KAPITEL 1. EINLEITUNG 1.2 Zielsetzung dieser Arbeit Im Rahmen dieser Bachelorarbeit wird ein erweiterbarer SQL-Compiler erzeugt, welcher in der objektorientierten Programmiersprache Java codiert wird. Der SQL-Compiler (Produktname Parseplus“) wird für den Zugriff auf Oracle–Datenbanksysteme aus” gerichtet und die folgenden funktionalen Bestandteile aufweisen: • Scannen und Parsen eines übergebenen SQL–Ausdrucks bzgl. der Einhaltung der vollständigen SQL-Syntax; hierfür ist ein reduzierter SQL-92 Standard (DMLAnweisungen und CREATE TABLE in einfacher Form) zzgl. objektrelationaler Erweiterungen (in Form von Methodenaufrufen) maßgebend. • Semantische Überprüfung des geparsten Ausdrucks mittels Zugriff auf den DBKatalog • Umwandlung des geparsten SQL–Ausdrucks gemäß des oben genannten Syntaxstandards und Ausgabe des umgewandelten SQL–Ausdrucks • Möglichkeit den ausgegebenen SQL–Ausdruck als Syntaxbaum zur Weiterverarbeitung im XML-Format auszugeben • Möglichkeit die ursprüngliche SQL-92 Grammatik mit Spracherweiterungen anzureichern. Das erweiterte SQL soll durch benutzerdefinierte Anweisungen in die SQL-92 Syntax umgewandelt werden können. • Ausgabe eines möglichen Fehlers im eingelesenen Ausdruck mit Positionsangabe und erwartetem Token. Es ist keine Error Recovery“ vorgesehen und somit ist ” ausschließlich der erste gefundene Fehler anzuzeigen. 1.3 Aufbau dieser Arbeit Im 2. Kapitel dieses Dokuments werden die Vor-/ Nachteile der untersuchten Parsergeneratoren und im Detail der ausgewählte Generator JavaCC beschrieben. Der Präprozessor JJTree, welcher vor JavaCC aufgerufen wird, um die Klassen zur Erzeugung des Syntaxbaums zu generieren, wird ebenfalls in diesem zweiten Kapitel vorgestellt. Das 3. Kapitel beschreibt die, für den ausgewählten Sprachumfang des Sprachstandards SQL-92 erzeugte, Grammatik für JavaCC. Es wird in diesem Kapitel außerdem die Auswahl der benutzten JavaCC–Optionen und Tokenklassen begründet. Das erarbeitete Konzept zur Realisierung der Erweiterbarkeit wird im Kapitel 4 beschrieben. Die anschließenden Kapitel 5 und 6 befassen sich mit dem Anwendungsfeature Export der Baumstruktur und der semantischen Analyse des geparsten SQL– Ausdrucks anhand des Visitor Pattern. 6 KAPITEL 1. EINLEITUNG Im 7. Kapitel wird die Implementierung der, in den vorhergehenden drei Kapiteln vorgestellten, Konzepte dokumentiert. Bevor das 8. Kapitel einen Ausblick auf die weiteren Entwicklungsziele bezüglich des erweiterbaren SQL-Compilers gibt. 7 Kapitel 2 Parsergeneratoren In diesem Kapitel werden die Ergebnisse der Analyse unterschiedlicher Parsergeneratoren anhand ihrer Vor- und Nachteile für diesen konkreten Anwendungsfall wiedergegeben. Die Untersuchung des Parsergenerators yacc“ basiert auf dem Skript zur Vor” lesung Programmiersprachen und Übersetzer [Par05]. Als Informationsquelle für die Arbeitsweise des Parsergenerators AnTLR“ diente die Website des Projekts [ANT]. ” Gleiches in Bezug auf die Art der Informationsquelle gilt für den Parsergenerator JavaCC“, dessen Projektseite in der unter [JCC] angegebene Version als Informati” onsquelle diente. 2.1 2.1.1 Untersuchte Alternativen yacc Der Name des hier beschriebenen Parsergenerators yacc entstand aus der englischen Bezeichnung yet another compiler compiler“. yacc gehört zu der Gruppe der Par” sergeneratoren, die deterministische bottom-up Parser erzeugen und somit LR(1)Grammatiken benötigen. Allerdings beschränkt man sich bei yacc auf LALR(1)Grammatiken, da die Grammatiken ansonsten zu umfangreich sein würden. Um diese Reduzierung des Umfangs zu realisieren, werden in den Grammatiken zusätzlich die Priorität und die Assoziativität einzelner Operatoren eingetragen. So werden auf kompaktere Weise mögliche Mehrdeutigkeiten in den Produktionen aufgelöst. Neben dem größeren Umfang der Grammatiken für den oben genannten Generator ist noch die wesentlich kompliziertere Parsingtabelle samt Steuerung, als Nachteil gegenüber top-down Parsern zu nennen, was insbesondere in Bezug auf die Lesbarkeit / Erweiterbarkeit von erheblichem Nachteil sein würde. Die durch yacc generierten Parser benötigen außerdem noch einen externen lexikalischen Scanner, der im Gegensatz zu JavaCC nicht automatisch mitgeneriert wird. Nachteilig für diese Arbeit ist außerdem der Umstand, dass yacc Parser auf Basis 8 KAPITEL 2. PARSERGENERATOREN der Programmiersprache C generiert und somit gegen die Zielvorgabe der Benutzung von Java verstößt. 2.1.2 AnTLR Die ursprüngliche Bezeichnung dieses Generators lautete PCCTS, bevor sich der Entwickler Terence Parr, Professor an der Universität von San Francisco, zur Umbenennung in AnTLR entschloss. AnTLR ist die Abkürzung für Another Tool for Language ” Recognition“. Dieser objektorientierte Parsergenerator ist in der Lage, Parser zu erzeugen, die auf drei verschiedenen Programmiersprachen (C++, Java und C#) basieren können und Ausdrücke einer LL(k)–Grammatik akzeptieren. Der Parsergenerator selbst ist in der Sprache Java codiert. In der großen Variabilität des Parsergenerators liegt allerdings auch der entscheidende Nachteil für diesen konkreten Anwendungsfall begründet. Hierdurch ist es nämlich nicht möglich, Java–Code schon in die Grammatik einzubinden, um so direkt auf aktuelle Daten während des Parsens zuzugreifen. Neben der geringeren Verbreitung im Vergleich zu JavaCC und der ausschließlich privat initiierten Entwicklung des Generators AnTLR, ist auch der verschachtelte Aufbau mit einzelnen Parserbibliotheken, die die generierten Dateien benötigen, als Nachteil zu nennen. 2.1.3 JavaCC Der Java Compiler Compiler(JavaCC) erzeugt Parser auf Basis der objektorientierten Programmiersprache Java. JavaCC selbst ist auch in Java geschrieben und verarbeitet Grammatiken, die in ihrer Syntax denen von yacc ähneln – im Abschnitt 2.2.1 sind weitere Details zum Aufbau einer Grammatikdatei zu finden. Der Parsergenerator JavaCC ist als OpenSource verfügbar und unterliegt der BSD–Lizenz(Berkeley Software Distribution), wonach individuelle Änderungen am Sourcecode nicht zwingend veröffentlicht, jedoch die ursprünglichen Copyright–Angaben beibehalten werden müssen. Laut den Angaben der Firma Sun Microsystems auf der JavaCC–Projekt– seite [JCC], handelt es sich bei JavaCC um den weit verbreitetsten Parsergenerator für Java Anwendungen. JavaCC ist auf allen JRE ab Version 1.1 lauffähig und wurde von Sun mit verschiedenen Prozessortypen getestet. Die von JavaCC akzeptierten Grammatiken sind vom Typ LL(k), der den Vorteil einer frei zu bestimmenden Anzahl an Lookahead Zeichen aufweist. Aus der großen Verbreitung und einer aktiven Entwickler–Community lässt sich außerdem eine hohe Stabilität und Fehlerresistenz von JavaCC ableiten. Trotzdem bieten die Grammatikdateien viele Schnittstellen für die individuelle Gestaltung des zu erzeugenden Compilers an. Ein weiterer Vorteil von JavaCC ist die Generierung der Klassen zur Exception–Erzeugung, welche insbesondere bei dieser Arbeit für die Ausgabe der Fehlerposition im SQL–Ausdruck benutzt werden. 9 KAPITEL 2. PARSERGENERATOREN 2.2 JavaCC Aufgrund der im Abschnitt 2.1 geschilderten Eigenschaften der unterschiedlichen Parsergeneratoren wurde für diese Arbeit der Java Compiler Compiler in der Version 4.0 ausgewählt. Nachfolgend sind nun einige der charakteristischen Merkmale der JavaCC – Programmgruppe, zu der auch der Präprozessor JJTree gehört, aufgelistet: • JavaCC erzeugt top-down Parser, welche eine kompaktere Grammatikschreibweise unterstützen. Ein weiterer Vorteil dieser Parser liegt darin, dass man Attribute im geparsten Syntaxbaum beliebig nach oben/unten weitergeben kann. • Insbesondere in Bezug auf die Erweiterungen der Syntax ist es von Vorteil, dass JavaCC die Schreibweise der Produktionen in EBNF akzeptiert und somit eine kompaktere Darstellung der erweiterten Produktionen möglich ist. • Der durch JavaCC generierte lexikale Scanner kann den gesamten Umfang des Unicode Zeichensatzes einlesen und ist somit für die Verarbeitung aller möglichen Benennungen in SQL-Ausdrücken gerüstet. • Eine Grammatik, die nicht vom Typ LL(1) ist, verursacht beim Erzeugen des Parsers aus JavaCC heraus eine Warnmeldung. Es wird in diesem Fall zwar der gewünschte Parser dieser Grammatik erzeugt, jedoch wird er, bedingt durch ein fehlendes Backtracking, nicht alle Ausdrücke der betroffenen Produktionen bei Bedarf herleiten können. Um diese Ausdrücke trotzdem während der Herleitung eines Ausdrucks der Grammatik erreichen zu können, kann die Anzahl der Lookahead Zeichen des generierten Parsers bei Beibehaltung der ursprünglichen Grammatik beliebig herauf gesetzt werden, weswegen die von JavaCC erzeugten Parser auch LL(k) Parser genannt werden. • Für die Syntax der Grammatikdatei kann man die folgenden bekannten erweiterten Schreibweisen benutzen: – ( exp1 | exp2 | . . . ) steht für alternative Wahlmöglichkeiten – ( exp )? steht für eine optionale Auswahl – ( exp )* steht für 0. . . n Wiederholungen von exp – ( exp )+ symbolisiert 1. . . n Wiederholungen • Es gibt vier verschiedene Tokenklassen in JavaCC, die, sollten sie geparst werden, unterschiedlich behandelt werden. Die Benennungen lauten SKIP, MORE, TOKEN, und SPECIAL TOKEN. Die Erläuterung dieser Klassen folgt im Abschnitt 2.2.2. 10 KAPITEL 2. PARSERGENERATOREN • JavaCC ist optional ein weiteres Programm namens JJTree voran gestellt. Es bietet die Möglichkeit für jede hergeleitete Produktion einen Knoten im Syntaxbaum hinzuzufügen. Diese erweiterte Funktionalität wird durch eine Grammatikdatei gesteuert, welche zusätzlich zur Grammatik der zu parsenden Sprache noch weitere die Erzeugung des Syntaxbaums betreffende Parameter enthalten kann. Mittels der durch JJTree erzeugten Java–Klassen lassen sich so die einzelnen Knoten eines Syntaxbaums des geparsten Ausdrucks erstellen. 2.2.1 Grammatikdatei Die Grammatiken, für die JavaCC die gewünschten Parser erzeugt, werden in Dateien mit der Endung “ .jj ” gespeichert. Sie beginnen wahlweise mit einem Optionenblock – das Schlüsselwort hierfür lautet options, in dem verschiedene Parameter für den zu erzeugenden Parser gesetzt werden können. Diese sind zum Beispiel die Anzahl der Lookahead–Zeichen und die Ignorierung von Groß–/ Kleinschreibung beim lexikalen Scannen bzw. dem anschließenden Parsen. Dem Block der Optionen schließt sich zwingend die Definition der Java compilation unit an. Dieser zweite Abschnitt der Grammatikdatei wird von den Ausdrücken PARSER BEGIN(Name des Parsers) und PARSER END(Name des Parsers) eingerahmt und muss eine Klassendeklaration mit dem festgelegten Namen des Parsers enthalten. Zusätzlich kann an dieser Stelle noch beliebiger Java-Code eingefügt werden, der in die zu generierende Datei der Parserklasse übernommen wird. Daran anschließend folgen die einzelnen Produktionen in der Form Java ähnlicher Methodendefinitionen. Jede dieser Methoden symbolisiert eine Produktion der Grammatik und kann sowohl Teile der Grammatik, als auch Java-Code für die zu erzeugende Anwendung enthalten. Nachfolgend ist als Beispiel die Definition eines nichtterminalen Symbols SQLStatement mit der Kombination aus grammatikalischen Anweisungen und Java–Code in Form einer return–Anweisung abgebildet: ASTSQLStatement SQLStatement( ) : {} { ( QueryStatement() | DeleteStatement() ) [”;”] < EOF > { return jjtThis; } } Eine weitere Möglichkeit, die gewünschte Funktionalität der zu erzeugenden JavaKlassen schon in der Grammatik zu definieren, bieten Blöcke die durch das reservierte 11 KAPITEL 2. PARSERGENERATOREN Wort JAVACODE eingeleitet werden. In diesen Blöcken können die Produktionen einzelner nichtterminaler Symbole in der üblichen Syntax des Java Quelltextes definiert werden. 2.2.2 Tokenklassen Der lexikale Scanner eines von JavaCC generierten Compilers ist vor dem Beginn des Einlesens im Status DEFAULT. Von diesem Status aus wird mittels der Analyse der gelesenen Zeichen das nächste Token bestimmt. Dazu wird das Token aus der Grammatik gesucht, welches die längste Übereinstimmung mit den gelesenen Zeichen aufweist. Sollten hierbei mehrere gleich lange Token gefunden werden, so gibt die Reihenfolge des Auftretens der Token in der Grammatik das auszuwählende Token vor. Alle Token einer Grammatik werden einer bestimmten Klasse zugewiesen. Es sind hierbei die Tokenklassen SKIP, MORE, TOKEN und SPECIAL TOKEN zu unterscheiden: • SKIP: Verwerfen des gefundenen Token und Fortsetzen des Scannens nach dessen letztem Zeichen. • MORE: Weiterscannen der Eingabe mit dem gefundenen Token als Präfix des nächsten gefundenen Token. • TOKEN: Aus dem passenden String der Eingabe wird das entsprechende Token erzeugt und an den Parser zwecks syntaktischer Analyse übergeben. • SPECIAL TOKEN: Wird ein String gelesen, der einem solchen Token entspricht, so wird ein spezielles Token erzeugt. Dieses Token wird nicht dem Parser übergeben, sondern an das nächste gefundene Token als Attribut gehängt. Jedem Tokenobjekt sind zwei Attribute gemein, die die lineare Verkettung der geparsten Token repräsentieren. Beide Attribute sind vom Typ Token und lauten next bzw. specialToken. Der Wert des Attributs next ist entweder null, falls auf das Tokenobjekt kein weiteres reguläres Token folgen sollte, oder das nachfolgende Token selbst. Diese Eigenschaft dient sowohl der Verkettung von regulären Token, die als Mitglied der Klasse TOKEN in der Grammatik definiert wurden, als auch von speziellen Token. Das Attribut specialToken wird für die Anbindung der SPECIAL TOKEN an die regulären Token benutzt und kann, neben dem vorhergehenden speziellen Token, alternativ auch den Wert null annehmen. Somit hat ein einzelnes spezielles Token, welches nach einem beliebigen regulären ermittelt wurde, und auf welches direkt wieder ein reguläres Token folgt, diese Attributwerte next == null“ und specialToken == null“. ” ” Weitere gemeinsame Integer–Attribute der Token sind kind, beginLine, beginColumn, endLine und endColumn. Die Funktion des ersten hier genannten Attributs ist die Zuweisung eines der in der Grammatikdatei definierten Token an das aktuell erstellte Tokenobjekt. Anhand des Integerwerts wird der entsprechende Eintrag vom 12 KAPITEL 2. PARSERGENERATOREN Typ String im erzeugten Java Interface <Parsername>Constants für den vergebenen Tokennamen ermittelt. Die vier weiteren Eigenschaften eines Tokenobjekts (beginLine, beginColumn, endLine und endColumn) beschreiben die geparste Position des repräsentierten Token im Eingabestrom. Auf sie wird zur Ausgabe der genauen Fehlerposition zurückgegriffen, falls eine zu parsende Eingabe nicht von der Grammatik akzeptiert worden ist. Ein weiteres gemeinsames Attribut aller Tokenobjekte ist vom Datentyp String und lautet image. Diese Zeichenkette besteht aus den einzelnen Zeichen des ermittelten Token und ist zur Reproduktion des geparsten Ausdrucks gedacht. Der detaillierte Zusammenhang der generierten Parserklassen und ihrer Attribute ist im Kapitel 2.2.5 beschrieben. 2.2.3 Syntaktische Analyse Die Parser, welche mittels JavaCC erzeugt werden können, realisieren die syntaktische Analyse allesamt ohne ein zeitaufwändiges Backtracking innerhalb der Produktionen. Stattdessen werden an Verzweigungen innerhalb einer Produktion die nachfolgenden Token betrachtet und anhand dieser die Wahl der Verzweigung getroffen. Diese Entscheidungen werden im Nachhinein nicht mehr abgeändert. Folgende Kurzschreibweisen für Verzweigungen innerhalb der Produktionen sind vorgesehen: • ( exp1 | exp2 | . . . ) Der Parser muss einen der drei Ausdrücke auswählen und danach das Parsen fortsetzen. • ( exp )? Hiermit wird ausgedrückt, dass der Ausdruck erzeugt werden, oder übersprungen werden kann. Alternative Schreibweise: [ exp ]. • ( exp )* Durch diese Schreibweise kann der Parser den Ausdruck mehrmals hintereinander oder auch gar nicht erzeugen. • ( exp )+ Dieser Ausdruck steht für eine ein- bis n–malige Wiederholung von exp. 2.2.4 Lookahead Als Standard für die Anzahl der Lookahead–Zeichen ist von JavaCC der Wert 1 vorgesehen. Dieser Lookahead–Wert kann jedoch in der Kommandozeile, oder im Optionenblock am Anfang der Grammatik beliebig gesetzt werden. Sollte JavaCC eine Mehrdeutigkeit in den Produktionen einer Grammatik erkennen, so wird beim Erzeugen des Parsers eine Warnung ausgegeben, dass bestimmte 13 KAPITEL 2. PARSERGENERATOREN Bereiche der Produktionen nicht erreicht werden können. In diesem Fall wird trotzdem ein funktionsfähiger Parser der bemängelten Grammatik erstellt. Um das Problem der Präfixgleichheit zu beheben, können die Produktionen in die Form einer LL(1)-Grammatik umgeschrieben werden. Es gibt aber auch die Option mittels syntaktischem/ semantischem Lookahead die gefundene Mehrdeutigkeit aufzulösen: • Beim syntaktischen Lookahead wird versucht eine Produktion herzuleiten und, sollte das funktionieren, wird, ausgehend von dem Status vor diesem Versuch, der in der Grammatik definierte Ausdruck (SelectStar()) hergeleitet: LOOKAHEAD( SelectStar() ) SelectStar() Eine Verfeinerung dieser Technik stellt die Angabe der maximal möglichen Token dar, um die der Parser bei einer Herleitung vorausschauen darf (in diesem Fall sind es lokal 3 Token): LOOKAHEAD( 3 ) “COUNT” “(” “*” “)” • Das semantische Lookahead basiert auf boolschen Ausdrücken deren Auswertung die Mehrdeutigkeit der Produktionen aufhebt: [LOOKAHEAD({getToken(1).kind == C && getToken(2).kind != C }) <C:“c”>] Hier werden die beiden folgenden Token daraufhin überprüft, ob das nächste Token ein “c” und das übernächste kein “c” ist. Sollte dies so sein, wird der optionale Teil in den eckigen Klammern ausgeführt, also ein “c”-Token erzeugt und der Scanner eine Position weiter gesetzt. Das Label “C” ist hier notwendig, um auf das Token im boolschen Ausdruck verweisen zu können. Zusätzlich gibt es noch die Möglichkeit, die beiden Lookahead–Varianten zu kombinieren: [ LOOKAHEAD( “c”, { getToken(2).kind != C } ) <C:“c” >] Dieser Ausdruck ist semantisch äquivalent zu dem vorherigen Beispiel. Allerdings wird nach der syntaktischen Überprüfung des nächsten Token, die Eingabe auf Ungleichheit mit dem Token C“ kontrolliert. ” 14 KAPITEL 2. PARSERGENERATOREN 2.2.5 Generierte Dateien Nachdem der Parsergenerator JavaCC die im Kapitel 2.2.1 beschriebene Grammatikdatei eingelesen und geparst hat, werden die Java Codedateien für den Parser dieser Grammatik erzeugt. JavaCC erzeugt sieben verschiedene Dateien, deren beinhalteten Klassen und Interfaces hier in ihrer Funktion und ihrem Zusammenspiel kurz vorgestellt werden sollen (der Ausdruck <Parsername> steht für den in der Grammatik vergebenen Namen des Parsers). 1. <Parsername>.java“ ” Die genannte Datei enthält die Hauptklasse <Parsername> des erzeugten Parsers. Diese Klasse implementiert das Interface <Parsername>Constants für den Zugriff auf die definierten Token der zugrunde liegenden Grammatik. Bei optionaler Nutzung des im Abschnitt 2.3 beschriebenen JavaCC-Präprozessors JJTree implementiert die Klasse <Parsername> auch das Interface <Parsername>TreeConstants, welches im Abschnitt 2.3.2 erläutert wird. Jede Produktion der Grammatik wird durch eine Methode in dieser Klasse repräsentiert. In diesen Methoden wird das lexikale Scannen der Zeichen des Eingabestroms initiiert und nachfolgend das Parsen der Token gemäß der Produktionen ausgeführt. Innerhalb der Methoden wird außerdem die Erzeugung der Knotenobjekte des Syntaxbaums ausgelöst, falls zuvor der Präprozessor JJTree ausgeführt worden ist. Der Vorgang des Parsens wird durch den Aufruf des Konstruktors dieser Klasse mit der zu parsenden Zeichenfolge vorbereitet. Dieser Aufruf zieht die Instanziierung der Klassen JavaCharStream oder SimpleCharStream nach sich, welche abhängig von der gewählten Eingabekodierung erzeugt werden. Im Anschluss an den Aufruf einer dieser beiden Konstrukturen wird ein Objekt der Klasse <Parsername>TokenManager, mit dem soeben erzeugten CharStream“-Objekt ” als Parameter, im Konstruktor der Klasse <Parsername> erzeugt. Ab diesem Zeitpunkt ist der Parser bereit, die übergebene Eingabequelle gemäß der Grammatik zu parsen. Das Parsen selbst wird dann durch den Aufruf der Methode des Startsymbols der Grammatik ausgeführt. 2. <Parsername>Constants.java“ ” Wie schon im Kapitel 2.2.2 beschrieben, enthält diese Datei das Interface <Parsername>Constants zur Zuordnung der Integerkonstanten im Attribut kind eines jeden Objekts der Klasse Token zu den definierten Token der Grammatik. Zu diesen Token zählen jene, die explizit im Tokenblock der Grammatik deklariert werden, aber auch solche Token, die in den Produktionen als Strings lokal definiert werden. Sämtliche auf die eine oder andere Art definierten Token werden nach dem Parsen der Grammatikdatei in das String-Array tokenImage mit ihrer Benen- 15 KAPITEL 2. PARSERGENERATOREN nung eingetragen. Die Position innerhalb dieses Feldes wird dem Integerwert des Attributs kind der Tokenobjekte zugewiesen. Für die explizit im Tokenblock der Grammatik deklarierten Token werden zusätzlich Integerkonstanten, die die Position im String-Array dieser Klasse widerspiegeln, angelegt. 3. <Parsername>TokenManager.java“ ” Die Klasse <Parsername>TokenManager stellt den lexikalen Scanner des generierten Parsers dar. Durch das Erzeugen eines Objekts dieser Klasse im Konstruktor des Parsers wird die jeweilige Eingabe als CharStream“-Objekt dem ” lexikalen Scanner bekannt gemacht. Der Scanner befindet sich dann im Zustand default“, welcher intern mit dem Integerwert 0 repräsentiert wird. ” Nachfolgend kann durch den Parser das jeweils nächste Token mit der Methode getNextToken() des lokalen Objekts der Klasse <Parsername>TokenManager angefordert werden. Somit stehen dem Parserobjekt zwei Schnittstellen zum lexikalen Scanner zur Verfügung, mit denen die Eingabequelle gesetzt bzw. das Ergebnis des lexikalen Scannens in Form des resultierenden Tokenobjekts geholt werden kann. 4. JavaCharStream.java“ oder SimpleCharStream.java“ ” ” Wie bereits erwähnt, bestimmt ein Parameter im Optionenblock der Grammatikdatei, welche dieser beiden Dateien von JavaCC erzeugt wird. Diese Option lautet JAVA UNICODE ESCAPE. Wird sie gesetzt, so können auch UnicodeEscape-Angaben des ASCII-Standards verarbeitet werden und folglich wird die Klasse JavaCharStream generiert. Sollen keine Unicode-Escapes gelesen werden, so kann diese Option in der Grammatik auf false gesetzt werden oder ganz entfallen, da false dem Defaultwert entspricht. Zur Laufzeit des generierten Parsers greift ausschließlich der lexikale Scanner auf die vom Objekt der jeweiligen CharStream“-Klasse gelieferten Informationen ” zu. Die dazu benutzten Schnittstellen der hier beschriebenen Klasse sind dabei die Methoden readChar(), getImage(), sowie die Methoden zur Bestimmung der Position eines Zeichens relativ zur bisher gelesenen Eingabe. Hierbei liefert die Methode readChar() das nächste Zeichen des Eingabestroms als einfaches char“-Zeichen und getImage() den String des Eingabepuffers von der letzten ” Endeposition des vorherigen Token bis zur aktuellen Position. Die Methode getImage() wird zusammen mit den Methoden zur Positionsbestimmung dazu eingesetzt, die vorhandenen Attribute des im lexikalen Scanner erzeugten Tokenobjekts zu füllen. 5. ParseException.java“ ” Diese Datei wird, wie auch die beiden nachfolgenden, unabhängig von möglichen Angaben in der Grammatikdatei erzeugt. Die hierin definierte Klasse ParseException erbt von der Klasse java.lang.Exception und ist somit throwable“. ” 16 KAPITEL 2. PARSERGENERATOREN Instanzen dieser Klasse können in den Methoden der nichtterminalen Symbole (NT) in der Parserklasse erzeugt werden und dienen der Ausgabe des während des Parsens aufgetretenen Fehlers. Zur genauen Lokalisierung des Fehlers werden die im TokenManager gesetzten Tokenattribute image, beginLine beginColumn, endLine und endColumn genutzt. 6. Token.java“ ” Die Objekte dieser Klasse Token repräsentieren die in der Eingabe gefundenen Token. Da bereits im Kapitel 2.2.2 die Attribute dieser Objekte vorgestellt worden sind, soll an dieser Stelle nur noch auf die Benutzung durch die anderen generierten Klassen eingegangen werden. Die jeweiligen Token werden in der Klasse <Parsername>TokenManager erzeugt, in deren Methode jjFillToken() die Position und die enthaltenen Zeichen jedes Token gesetzt werden. Des weiteren wird in der Methode getNextToken() des TokenManagers die Verknüpfung eines Token zu den optional definierten Spe” cialToken“ gesetzt, die dem jeweiligen regulären Token vorausgehen. Nachdem der Parser das nächste Token mittels der Methode getNextToken() angefordert hat, wird dessen Attribut image, welches die enthaltenen Zeichen zusammenfasst, auf Einhaltung der syntaktischen Regeln hin überprüft. Sollte dieses Token die Produktion nicht erfüllen können, so wird eine ParseException ausgelöst. Diese nutzt ihrerseits wieder die Attribute des fehlerverursachenden Token zur Lokalisierung des aufgetretenen Fehlers und seiner Darstellung gegenüber dem Benutzer. 7. TokenMgrError.java“ ” Die Klasse TokenMgrError erbt von der Klasse java.lang.Error und ist somit ebenfalls throwable“. Die erzeugten und geworfenen Objekte dieser Klasse sollten, ” im Gegensatz zu solchen der Klasse ParseException, nicht abgefangen werden, da sie durch eine fehlerhafte Programmlogik verursacht worden sind. Auftretende Fehler können sein: • Lexikale Fehler, die während des Einlesens der Zeichen aus dem Eingabestrom aufgetreten sind • Verletzung des Singleton Pattern in Bezug auf den TokenManager“ durch ” den Versuch, eine zweite Instanz zu erzeugen • Unbekannter Zustand des lexikalen Scanners, der durch externes Setzen dieses Zustands ausgelöst worden ist • Endlosschleife, die innerhalb des TokenManagers“ entstanden ist ” Die einzige Stelle im Programmfluss, an der Fehler in Form von Objekten dieser Klasse ausgelöst werden können, ist die Methode getNextToken() der Klasse <Parsername>TokenManager. 17 KAPITEL 2. PARSERGENERATOREN Klassendiagramm Das unterhalb abgebildete Klassendiagramm (Abbildung 2.1) verdeutlicht die beschriebenen logischen Abhängigkeiten der generierten Klassen anhand des SQL-92 Parsers: Abbildung 2.1: Durch JavaCC erzeugte Klassenhierarchie 2.3 JJTree JJTree ist ein Präprozessor für JavaCC, welcher verschiedene Optionen zur Erstellung der Knotenklassen des Syntaxbaums bereitstellt. Das Ausführen des Programms JJTree mit einer passenden Grammatik (Dateiendung .jjt“) erzeugt eine entspre” chende JavaCC Grammatik (Dateiendung .jj“) und die Klassen sowie Interfaces zur ” Syntaxbaumerzeugung. Es können wahlweise für alle oder nur einzelne Produktionen, oder auch nur für Teile von Produktionen Knoten erstellt werden. Bei der Wahl des Erstellungsmodus der Knotenklassen gibt es die Alternativen multi und simple. Den simple-Modus kennzeichnet, dass jeder Knoten des geparsten Syntaxbaums ein Objekt der generierten Klasse SimpleNode ist. Es wird somit nur eine Klasse für alle Knoten erzeugt, wodurch mögliche Änderungen des Knotenverhaltens alle Knoten gleichermaßen betreffen, was in dieser Arbeit nicht gewünscht ist. Als Alternative bietet sich der multi-Modus an, welcher von der Benennung des nichtterminalen Symbols auf der linken Seite der Produktion auf die jeweils zu generierende Klasse des zu erzeugenden Knotenobjekts schließt. Der gewünschte Klassentyp der Knotenobjekte kann im Optionenblock zu Beginn einer Grammatik mittels MULTI = true“ ausgewählt werden. ” 18 KAPITEL 2. PARSERGENERATOREN 2.3.1 Grammatikdatei Die Dateiendung der Grammatikdateien, die von JJTree akzeptiert werden, lautet .jjt“. Ebenso wie diese Dateiendung die Benennung der JavaCC-Grammatiken nur ” um einen Buchstaben erweitert, ist auch die Syntax der JJTree-Grammatiken lediglich eine Erweiterung der im Abschnitt 2.2.1 beschriebenen JavaCC–Syntax. Die Erweiterung der JavaCC-Grammatik beginnt mit zusätzlichen Parametern, die im Optionenblock zu setzen sind. Neben der bereits erläuterten Option MULTI“ ” gehören noch neun weitere Optionen zur JJTree-spezifischen Erweiterung. Nachfolgend nun die kurze Auflistung der zehn zusätzlichen Optionen und ihrer Bedeutung: BUILD NODE FILES: Diese Option ist standardmäßig gesetzt und bewirkt die Erzeugung der einzelnen Knotenklassen. MULTI: Wie schon beschrieben, ist false der Standardwert dieser Option, so dass jeder Knoten des Syntaxbaums ein Objekt der Klasse SimpleNode ist und nur anhand des Attributs int id einem NT zuzuordnen wäre. NODE DEFAULT VOID: Auch diese Option muss explizit gesetzt werden, falls nur die Knoten erzeugt werden sollen, deren NT–Produktion in der Grammatik explizit eine Benennung zugewiesen worden ist. NODE FACTORY: Ist diese Option gesetzt, so werden neue Knoten im Parser mittels einer statischen Methode erzeugt, die das Factory Pattern realisiert. NODE PACKAGE: Diesem Eintrag im Optionenblock kann ein String zugewiesen werden, der als Paketdeklaration zu Beginn jeder erzeugten Knotendatei eingefügt wird. Standardmäßig ist diesem Parameter der leere String zugeordnet. NODE PREFIX: Als Wert dieses Optionsparameters kann ebenfalls ein String eingetragen werden, der gefolgt von dem Namen des NT die Benennung der Knotenklasse darstellt. Als Standard ist hier AST“ gesetzt, was die Abkürzung des ” englischen Ausdrucks abstract syntax tree“ darstellt. ” NODE SCOPE HOOK: Durch das Setzen dieses Parameters werden die Aufrufe zweier fest definierter Methodensignaturen in die Erzeugung der Baumstruktur eingebunden, die beim Erzeugen bzw. Abspeichern eines Knotens aufgerufen werden. NODE USES PARSER: Wie bei dem vorherigen Parameter ist auch hier false der Standardwert. Erst durch das explizite Setzen dieser Option wird jedem Knotenobjekt bei der Erzeugung das aktive Objekt der generierten Parserklasse übergeben und in einem lokalen Attribut gespeichert. VISITOR: Wird dieser Parameter gleich true gesetzt, so wird in jede Knotenklasse die Methode jjtAccept(<Parsername>Visitor, Object) eingefügt und ein 19 KAPITEL 2. PARSERGENERATOREN Interface mit je einer Signatur der Methode visit(<Knotenklasse>, Object) für jede Knotenklasse angelegt. Der Standardwert ist auch bei diesem Parameter false. JJTREE OUTPUT DIRECTORY: Durch die Zuweisung der Pfadangabe zu einem bereits existierenden Ordner an diesen Parameter wird der Ausgabeordner der Java-Dateien festgelegt, die von JJTree erzeugt werden. JDK Version: Diesem Optionenparameter wird ein String mit der Versionsnummer des Java Development Kits(JDK) zugewiesen, unter dem der generierte Parser lauffähig sein soll. Diese Option muss nur explizit gesetzt werden, wenn die Features des JDK (ab Version 1.5) genutzt werden sollen. Eine zweite Erweiterung der JavaCC-Grammatik betrifft die Notation der Produktionen. Die Benennung der Knoten jeder einzelnen Produktion kann in der Grammatik mittels folgender Schreibweise bestimmt werden: void FromClause( ) #MyNode : {} { ... } Gemäß dieser Notation würde die Methode (entspricht inhaltlich der Produktion) dieses NT im Parser weiterhin FromClause()“ lauten, allerdings hätte sich die ” Benennung des Knotenobjekts in ASTMyNode“ geändert. ” Auch das Erstellen von Knotenobjekten einer bestimmten Produktion kann verhindert werden. Hierfür bietet die Grammatik des Präprozessors zwei verschiedene Möglichkeiten des Eingriffs. Zum einen kann als Benennung der Produktion das Schlüsselwort void benutzt werden: void FromClause( ) #void : {} { ... } Alternativ kann aber auch in den Optionen NODE DEFAULT VOID = true gesetzt und bei der Definition der NT-Symbole eine explizite Benennung weggelassen werden: void FromClause( ): {} { ... } Sollen bestimmte Teile eines geparsten Ausdrucks unter einem definierten Namen in den Syntaxbaum gehängt werden, so ist die folgende Syntax zu benutzen: void DeleteStatement( ): { } { "DELETE" ["FROM"] ( TableReference() )#TabRef ["WHERE" SQLExpression()] } 20 KAPITEL 2. PARSERGENERATOREN In diesem Beispiel würde also ein DeleteStatement“-Knoten als Kinder einen Tab” ” Ref“–Knoten und einen SQLExpression“–Knoten haben. Generell bezieht sich also ” der Ausdruck #Name“ nur auf den unmittelbar vorhergehenden Klammerausdruck. ” Eine erweiterte Schreibweise dieser lokalen Benennung einzelner Kindknoten beschreibt die generellen Regeln, nach denen einzelne Knotenobjekte erzeugt und in den Syntaxbaum gehängt werden sollen. Zu diesem Zweck gibt es zwei unterschiedliche Verfahren, nämlich die Verwendung von definite node oder conditional node: 1. definite node: Diese Knotenart hat eine festdefinierte Anzahl von Kindknoten, welche hinter dem Namen des Knotens als ganzzahliger Ausdruck steht: #ADefiniteNode(INTEGER EXPRESSION) 2. conditional node: Ein Knoten dieser Art bekommt alle Kindknoten zugewiesen, die im Bereich seines “node scopes” zu finden sind. Dies geschieht allerdings nur, wenn der Knoten die boolsche Bedingung erfüllt. Andernfalls verbleiben die Kindknoten auf dem Stack und es wird kein Elternknoten erzeugt. Als node scope“ wird ” der Bereich einer Produktion bezeichnet, der sich an den Deklarationsblock anschließt und während des Parsens hergeleitet wird. #ConditionalNode(BOOLEAN EXPRESSION) 2.3.2 Generierte Dateien Nachdem der für JavaCC entwickelte Präprozessor JJTree die im Abschnitt 2.3.1 beschriebene Grammatikdatei eingelesen und geparst hat, werden die Java Codedateien für die Erstellung des Syntaxbaums eines geparsten Ausdrucks generiert. JJTree erzeugt minimal vier verschiedene Dateien, deren beinhalteten Klassen und Interfaces hier in ihrer Funktion und ihrem Zusammenspiel vorgestellt werden sollen. Sinnvollerweise weist man JJTree durch die Parameter im Optionenblock an, noch weitere Klassen zu erzeugen, nämlich je eine Klasse für jeden möglichen Knotentyp im Syntaxbaum (der Ausdruck <Parsername> symbolisiert den in der Grammatik vergebenen Namen des generierten Parsers und <NT> die Benennung der enthaltenen NT-Produktionen). 1. Node.java“ ” Das Interface Node müssen alle Knotenklassen implementieren, um einheitliche Schnittstellen für die Baumerstellung unabhängig von der Grammatik zu gewährleisten. Die hierin deklarierten Methoden haben alle das Präfix jjt“ und ” dienen hauptsächlich dem Aufbau der Baumstruktur. Zusätzlich zu den Methoden zur Bestimmung der Elter–Kindknoten Beziehung werden die Methoden void jjtOpen() und void jjtClose() erstellt, die nach dem Erzeugen und vor dem Entfernen des jeweiligen Knotenobjekts vom zentralen Stack aufgerufen werden. 21 KAPITEL 2. PARSERGENERATOREN 2. SimpleNode.java“ ” Die Klasse SimpleNode implementiert das zuvor beschriebene Interface Node und ist die Elternklasse für alle zu erschaffenden Knotenklassen. Die Klasse beinhaltet Attribute für den Aufbau der Baumstruktur (Elternknoten und Kindknoten) und ein Integerattribut id, welches den Typ des jeweiligen Knotens repräsentiert. Optional kann auch noch eine lokale Referenz auf den erzeugten Parser gespeichert werden, was jedoch durch den im Abschnitt 2.3.1 beschriebenen Parameter NODE USES PARSER = true“ in der Grammatik deklariert werden muss. ” Neben den schon genannten Methoden zur Verknüpfung der einzelnen Knotenobjekte gemäß der logischen Baumstruktur (jjtSetParent(Node n), jjtAddChild(Node n, int i), . . . ), bietet die Klasse SimpleNode auch Methoden zur Ausgabe der erzeugten Baumstruktur in der Kommandozeile. Hierzu zählen die Methoden String toString() und dump(String prefix), die abhängig von den Einträgen im Interface <Parsername>TreeConstants die Knotenobjekte darstellen. 3. JJT<Parsername>State.java“ ” Diese Klasse JJT<Parsername>State beinhaltet den zentralen Stack vom Datentyp java.util.Stack auf den die Knotenobjekte nach ihrer Erzeugung gelegt, und von dem sie erst nach der Zuweisung zu einem Elternknoten gelöscht werden. Außerdem enthält die Klasse auch Methoden zur Bearbeitung dieses Stacks, die auf die speziellen Verknüpfungen der SimpleNode-Objekte abgestimmt sind. Hierbei sind insbesondere die Methoden openNodeScope(Node n), closeNodeScope(Node n, int num) und closeNodeScope(Node n, boolean condition) zu nennen, welche vor bzw. nach dem Hinzufügen möglicher Kindknoten aufgerufen werden. 4. <Parsername>TreeConstants.java“ ” Dieses Interface zu den Konstanten der Knotenbenennung ist das Pendant zu dem in Abschnitt 2.2.5 vorgestellten Interface <Parsername>Constants zu den definierten Token. Das Interface <Parsername>TreeConstants enthält ebenfalls ein String-Array, welches hier allerdings die in der Grammatik definierten Benennungen der Knoten – nicht die der NT-Produktion – speichert (Attribut jjtNodeName). Die Zuordnung der jeweiligen Strings zu den während des Parsens erzeugten Objekten der Knotenklassen wird wiederum durch hier definierte und initialisierte Integerkonstanten organisiert. Die Referenz auf eine der hier als public deklarierten Konstanten wird dem jeweiligen Konstruktor einer Knotenklasse als Parameter übergeben und in der Eigenschaft id gespeichert. Aus diesem Grund kann in späteren Baumoperationen dieses Attribut auf Gleichheit der Referenzen hin überprüft werden, was einem besser lesbaren und damit leichter wartbaren Code zuträglich ist. 5. AST<NT>.java“ ” Die Gruppe dieser mit dem Präfix AST“ versehenen Dateien beschreibt die ” 22 KAPITEL 2. PARSERGENERATOREN unterschiedlichen Klassen der zu erzeugenden Knotenobjekte. Jede dieser Dateien beinhaltet die Definition der Klasse eines abstrakten Syntaxbaumknotens mit der in der Grammatik festgelegten Benennung. Diese Klassen erben alle von der Elternklasse SimpleNode, welche zuvor schon beschrieben worden ist. Somit müssen lediglich die Methoden überladen werden, die eine individuelle Funktionalität für das jeweilige Knotenobjekt bereitstellen sollen. Standardmäßig werden individuelle Konstruktoren in die generierten Klassen eingefügt, sowie optional die Methoden jjtCreate und jjtAccept überladen, falls die Option dazu in der Grammatik ausgewählt wurde. Generell gilt, dass die Gruppe dieser Dateien nur erstellt wird, wenn die Option MULTI = true“ gesetzt worden ist, da ansonsten alle Knotenobjekte direkt ” von der Klasse SimpleNode abgeleitet werden. 23 KAPITEL 2. PARSERGENERATOREN Klassendiagramm Anhand des hier abgebildeten Klassendiagramms (Abbildung 2.2) werden die Abhängigkeiten der im Abschnitt 2.3.2 beschriebenen Klassen dargestellt. Abbildung 2.2: Durch JJTree erzeugte Klassenhierarchie 2.3.3 Erzeugter Syntaxbaum Ein geparster Ausdruck wird durch Objekte der Komponentenklassen der abstrakten Datenstruktur Syntaxbaum dargestellt. Die Verknüpfung dieser Knotenobjekte wird ausgehend von den Blattknoten, aufgebaut. Zu diesem Zweck wird jeder Knoten nach seiner Erzeugung auf den zenralen Stack gelegt und beim Auffinden eines Elternknotens an diesen angehängt, sowie dem Stack entnommen. Anschließend wird der Elternknoten selbst auf den Stack gelegt und die Prozedur beginnt bis zum Erreichen der Wurzel (entspricht normalerweise dem Startsymbol der Grammatik) von neuem. Jeder Knoten des auf Basis des Parsings erzeugten Syntaxbaumes verfügt zumindest über die Funktionalität der generierten Basisklasse SimpleNode, welche die Methoden zum Aufbau der Baumstruktur bereitstellt (siehe Abschnitt 2.3.2). Ist die entsprechende Option MULTI“ in der Grammatik gesetzt, so können diese Metho” den in den jeweiligen Knotenklassen überladen, bzw. durch weitere Methoden ergänzt werden, um eine individuelle Bearbeitung der Knoten zu ermöglichen. 24 KAPITEL 2. PARSERGENERATOREN Nachdem der gesamte übergebene Ausdruck fehlerfrei geparst worden ist, bleiben die jeweiligen Knotenobjekte weiterhin erreichbar, indem zum Beispiel die ParserMethode des Startsymbols das Wurzelobjekt des Baumes zurückgibt. Die Rückgabe eines Knotenobjekts muss durch den Anwender individuell, zum Beispiel schon in der Grammatikdatei, veranlasst werden. Andernfalls würde keine Referenz auf einen Knoten als Einstieg in den Baum vom Parser geliefert werden und ein späterer Baumdurchlauf unmöglich sein. An dieser Stelle soll nun anhand eines Beispielbaums (Abbildung 2.3) die JJTreeBaumstruktur gezeigt werden. Sie wird durch die Methoden der Komponentenklassen (s. Abschnitt 2.3.2) erzeugt und besteht hier konkret aus einem Knoten vom Typ ASTTableReference, sowie dessen Kindknoten ASTTableName und ASTCorrelationName. Alle drei Knoten zusammen repräsentieren einen geparsten Tabellenausdruck (TableReference) innerhalb einer From–Klausel inklusive des Tabellenbezeichners (TableName) sowie dessen Aliasbezeichnung (CorrelationName). Die Knotenattribute Node parent und Node[ ] children bilden die logische Verknüpfung der Baumknoten ab. Abbildung 2.3: JJTree Baumstruktur Lebenszyklus eines Knotenobjekts Nachdem nun die unterschiedlichen Möglichkeiten der Knotenerzeugung in den Abschnitten 2.3.2 und 2.3.3 vorgestellt worden sind, schildert dieser Abschnitt detailliert die von JavaCC und JJTree implementierte Abfolge der Methodenaufrufe der Knotenerzeugung: 1. Der Konstruktor des Knotens wird mit einem eindeutigen Integer Parameter aufgerufen. Anhand dieses Parameters wird im simple-Modus die Art des zu erzeugenden Knotens bestimmt. Dies geschieht in der automatisch erzeugten 25 KAPITEL 2. PARSERGENERATOREN Klasse <Parsername>TreeConstants, in welcher die Zuordnung zwischen Integerwert und Namensstring hinterlegt ist. 2. Sollte die Option NODE SCOPE HOOK = true in der Grammatik gesetzt sein, so wird die Methode openNodeScope mit dem erzeugten Knoten als Parameter aufgerufen. In dieser Methode können nun beispielsweise Attribute des Knotenobjekts verändert werden. 3. Die Methode jjtOpen des soeben erzeugten Knotens wird aufgerufen. Dadurch wird die Suche nach möglichen Kindknoten auf dem zentralen Stack eingeleitet. 4. Sollte während der Knotenerzeugung eine nicht abgefangene Exception geworfen werden, so wird der Knoten verworfen, ohne seine Methode jjtClose aufzurufen. Unter keinen Umständen wird die Methode closeNodeHook mit diesem fehlerhaften Knoten aufgerufen. 5. Ist die Bedingung eines conditional nodes nicht erfüllt, so wird auch dieser Knoten verworfen, ohne ihn zu schließen. In diesem Fall kann allerdings die Methode closeNodeHook mit dem abgelehnten Knoten aufgerufen werden. 6. Sollten keine Fehler bei der Knotenerzeugung auftreten, so werden je nach Typ des Knotens (defined / conditional) die entsprechenden Kindknoten angehängt. Die Reihenfolge der Kindkoten wird mittels eines Indexes aufsteigend von links nach rechts festgelegt. 7. Sollte die Option NODE SCOPE HOOK = true in der Grammatik gesetzt sein, so wird die Methode closeNodeScope mit dem Knoten als Parameter aufgerufen. Dadurch wird signalisiert, dass sich keine Kindknoten mehr auf dem Stack befinden und damit der Bereich (“Scope“) des aktuellen Knotens verlassen wird. 8. Falls der erzeugte Knoten nicht die Wurzel des zu erstellenden Syntaxbaums ist, wird er als Kindknoten an einen anderen Knoten angehängt und dieser Elternknoten als Parameter des Methodenaufrufs von jjtSetParent gesetzt. 9. Die Methode jjtClose des fehlerlos erzeugten Knotens wird aufgerufen. 10. Der soeben erzeugte und geschlossene Knoten wird auf den zentralen Stack gelegt. Die genaue Kenntnis der hier dargestellten Sequenz der Knotenerzeugung ist für den Programmierer von Anwendungen, die auf die von JavaCC und JJTree generierten Klassen zugreifen, von zentraler Bedeutung. Denn durch das Überladen der jeweiligen Methoden in den Knotenklassen können die Daten, die während des Parsens ermittelt worden sind, auf einfache Weise in die Baumstruktur einfließen. Als Beispiel sei hier das Speichern der jeweiligen Start– und Endtoken einer Produktion im zugehörigen Knotenobjekt genannt, welches in dieser Arbeit durch das Überladen der Methoden jjtOpen und jjtClose verwirklicht worden ist. 26 Kapitel 3 SQL-92 Grammatik für JavaCC Nachdem im vorangegangenen Kapitel 2 die für diese Arbeit benutzten Werkzeuge JavaCC und JJTree vorgestellt worden sind, folgt in diesem Kapitel eine Beschreibung der erstellten SQL-92 Grammatikdatei. Die erstellte Grammatik basiert auf den Angaben zum Oracle SQL-92 Sprachstandard in der Publikation [MS93]. Im Rahmen dieser Arbeit ist nicht der komplette Sprachumfang dieses Standards in der Grammatik abgebildet worden, sondern eine Teilmenge bestehend aus DML–Anweisungen und der DDL–Anweisung zur Tabellendefinition. Hinzu kommen noch objekt–relationale Erweiterungen in Form von Methodenaufrufen. Eine detaillierte Auflistung der Produktionen in erweiterter BNF, sowie der terminalen und nichtterminalen Symbole, ist im Anhang A zu finden. Das Hauptaugenmerk dieses Kapitels soll auf die benutzten JJTree- bzw. JavaCCspezifischen Optionen und Befehle gerichtet sein, die in den beiden folgenden Abschnitten erläutert werden. 3.1 Gesetzte Optionen Wie bereits erwähnt, ist JJTree ein Präprozessor des Java Compiler Compilers. Daher ist es möglich, die Grammatik, für die JavaCC den Parser generieren soll, bereits in der Eingabedatei von JJTree zu definieren (Dateiendung jjt“). Außerdem können ” in dieser Datei auch die Optionen für JavaCC ausgewählt werden, da JJTree alle Angaben in die generierte JavaCC-Grammatikdatei (Dateiendung jj“) weiterleitet. ” Im Rahmen dieser Arbeit sind fünf der in Abschnitt 2.3.1 vorgestellten Optionen in der Grammatikdatei dieses SQL-Compilers benutzt worden. Die erste Option ist für das Visitor Pattern und die vier übrigen für die im Compiler benötigte Funktionalität der Knotenerzeugung ausgewählt worden: • VISITOR = true;“ ” Hierdurch wird der Präprozessor JJTree angewiesen, ein Interface <Parsername>Visitor zu erzeugen, welches die Deklaration aller visit–Methoden enthalten 27 KAPITEL 3. SQL-92 GRAMMATIK FÜR JAVACC muss. Desweiteren wird die Methode jjtAccept in jeder Knotenklasse mit dem Aufruf der jeweiligen visit–Methode implementiert. • MULTI = true;“ ” Diese Option führt zur Erzeugung einer individuellen Knotenklasse für jede Produktion der definierten Grammatik. • NODE USES PARSER = true;“ ” Das Einfügen eines zusätzliches Konstruktors, mit dem jeweils erzeugenden Parser-Objekt und dem Typ des Knotens als Aktualparameter, in alle Knotenklassen wird durch diese Option veranlasst. So ist es möglich, die gewünschten Token durch den Parser geliefert zu bekommen. • NODE FACTORY = true;“ ” Durch diese Option werden die Knotenobjekte im Parser mittels der jjtCreate-Methode erzeugt. Durch Übergabe des Knotentyps als Integerkonstante des Interfaces <Parsername>TreeConstants wird der jeweils gewünschte Knoten erzeugt. Diese Konstruktion der Knoten, in Anlehnung an das Factory Pattern, vereinfacht die nachträgliche Erzeugung möglicher Syntaxbaumknoten. • JDK Version = ”1.5”;“ ” Durch die Übergabe der Versionsnummer des JDK 1.5, das für diese Arbeit genutzt worden ist, wird im zentralen Stack zur Verknüpfung der Knotenobjekte die generische Datentypendefinition eingesetzt, was zu Vorteilen in der Performance führt. Zusätzlich zu den JJTree-Optionen enthält die hier beschriebene jjt“-Datei auch ” bereits die Optionen, die die Arbeitsweise von JavaCC steuern. Es handelt sich dabei um die folgenden beiden Einträge im Optionenblock: • STATIC = false;“ ” Dieser Eintrag verhindert das Generieren von statischen Methoden, die die jeweilgen Produktionen simulieren. Da der erweiterbare Compiler so konzipiert ist, dass immer erst eine Instanz des generierten SQL-Parsers erzeugt werden muss, und diese die jeweils zu parsende Eingabe festlegt, ist eine statische Auslegung der Methoden nicht notwendig. • JAVA UNICODE ESCAPE = true;“ ” Durch das Setzen dieser Option wird es möglich, ASCII-Zeichen auch in der Unicode–Escape Schreibweise einzulesen. Die Umwandlung solcher Zeichenangaben in verarbeitbare char-Zeichen kann beim Einlesen einer Anfrage aus einer Datei notwendig werden. 28 KAPITEL 3. SQL-92 GRAMMATIK FÜR JAVACC 3.2 Java Compilation Unit Der Name der zu generierenden Parserklasse wird durch den logischen Block festgelegt, der auf die Optionen folgt. Dieser Teil der Grammatikdatei wird Java Com” pilation Unit“ genannt und ist durch die Schlüsselwörter PARSER BEGIN und PARSER END genau eingegrenzt. Hinter diesen Begriffen ist in Klammern der Name des zu erzeugenden Parsers (hier: SQL92) eingetragen. Innerhalb dieses Blocks muss eine öffentliche Klasse mit dem Namen des Parsers definiert werden, was im nachfolgenden Ausschnitt der Grammatik zu sehen ist: /********************************************************/ /* -----Start der Java compilation unit ----- */ /********************************************************/ PARSER_BEGIN(SQL92) import import import import parseplus.modifier.Token; parseplus.modifier.AdvancedNode; parseplus.modifier.ParseException; parseplus.modifier.SQLParser; public class SQL92 implements SQLParser { final public Object transformSQL(Object data) { return data; } } PARSER_END(SQL92) /********************************************************/ /* -----Ende der Java compilation unit ------ */ /********************************************************/ Der eingefügte Import der Klasse Token wird in die generierten Klassen SQL92 und SQL92TokenManager übernommen. Somit werden im erweiterbaren SQL-Compiler Tokenobjekte von der modifizierten Klasse parseplus.modifier.Token erzeugt. Der Import der Klasse ParseException und der Interfaces SQLParser und AdvancedNode ist für den Zugriff auf die zentralen Klassen des modifier–Pakets eingefügt worden. Die Methode transformSQL der Parserklasse SQL92 implementiert die in SQLParser deklarierte Schnittstelle jedes Parserobjekts, welche zur Umwandlung eines Ausdrucks in erweiterter SQL Syntax enthalten ist. Der inhaltliche Zusammenhang der Importanweisungen und Methodendefinition ist in den Kapiteln 4 und 7 dokumentiert. 3.3 Definierte Tokenstruktur In der für diese Arbeit erzeugten SQL-92 Grammatik sind zwei der verfügbaren JavaCC–Tokenklassen benutzt worden. Dies ist zum einen die Klasse SPECIAL TOKEN welche die Token umfasst, die zwar vom lexikalen Scanner erfasst, nicht aber an 29 KAPITEL 3. SQL-92 GRAMMATIK FÜR JAVACC den Parser SQL92 weitergegeben werden sollen. Der Nutzen dieses Vorgehens besteht darin, dass die Formatierung und eventuelle Kommentare durch das Parsen des SQL–Ausdrucks nicht verloren gehen, sondern zwischen den regulären Token wieder eingefügt werden. Von dieser Klasse wurden zwei getrennte Blöcke zwecks besserer Lesbarkeit des Codes erstellt. Im ersten Block sind die möglichen Formatierungszeichen definiert, welche die char-Zeichen für einen Blank “, einen Tabulator \t“, ein Return \r“ und den ” ” ” Zeilenumbruch \n“ sind: ” SPECIAL_TOKEN: { " " //Blank | "\t" //Tabulator | "\r" //Return | "\n" //Newline } Der zweite Block beinhaltet die möglichen Kommentarangaben eines SQL–Statements, die sich auch über mehrere Zeilen erstrecken können: SPECIAL_TOKEN: { <LINE_COMMENT: "--"(~["\r","\n"])*> | <MULTI_LINE_COMMENT: "/*" (~["*"])* "*" ("*" | (~["*","/"] (~["*"])* "*"))* "/"> } Die zweite JavaCC-Tokenklasse, die in der hier vorgestellten Grammatik Verwendung findet, ist die Klasse TOKEN. Mittels dieses Tokentyps werden in der Grammatikdatei sämtliche Schlüsselwörter definiert, um ihnen in dem im Nachhinein erzeugten Interface SQL92Constants je einen global erreichbaren Integerwert fest zuzuweisen. Durch das explizite Unterscheiden der Schlüsseltoken und der beliebig aneinander gereihten alphanumerischen Zeichen ist es beim Parsen des übergebenen Ausdrucks möglich, die falsche Benutzung von reservierten Schlüsseltoken zu erkennen. Die Schlüsselwörter sind im Anhang A unter den terminalen Symbolen zu finden. Zusätzlich werden als TOKEN die möglichen alphanumerischen Werte der SQL-92 Grammatik definiert. Auch hier ist zwecks Übersichtlichtkeit darauf verzichtet worden, die erweiterte BNF der Tokendefinition an dieser Stelle des Dokuments anzugeben. Die Angaben zu den akzeptierten aplhanumerischen Zeichen sind ebenfalls im Anhang A enthalten. 3.4 Eingefügter Java-Code Nachdem das im Kapitel 4 vorgestellte Konzept der Erweiterbarkeit erarbeitet worden war, konnten die im ersten Entwicklungsstadium eingefügten Java-Befehle zur Verknüpfung der Syntaxbaumstruktur mit der verketteten Tokenliste aus der Grammatik entnommen werden. Der Beweggrund dafür ist das Bestreben gewesen, etwaige 30 KAPITEL 3. SQL-92 GRAMMATIK FÜR JAVACC Angaben in Bezug auf die Erweiterung des SQL-92 Compilers an einer zentralen Stelle vornehmen zu können, welche gemäß des Erweiterungskonzepts jeweilige Visitorklasse ist. Nach dieser Optimierungsphase ist letztendlich die Rückgabe der erzeugten Knotenobjekte erhalten geblieben, wie es hier am Beispiel der Rückgabe des Wurzelknotens durch die Startproduktion SQLStatement gezeigt ist. AdvancedNode SQLStatement() : { } { ( CommitStatement() | DeleteStatement() | InsertStatement() | RollbackStatement() | SelectStatement() | SetTransactionStatement() | UpdateStatement() | CreateStatement() ) [ ";" ] <EOF> { return jjtThis; //Rückgabe des Wurzelknotens } } Durch das Einfügen dieser Java-Code Blöcke in jede Produktion existiert die Möglichkeit, auf die erzeugten Knotenobjekte mittels der realisierten Baumoperationen (s. Abschnitt 7.4) von der Wurzel ausgehend zuzugreifen. Dem Programmierer von Erweiterungen ist es somit möglich, auch Teilstatements parsen und hieraus Syntaxbäume erstellen zu können. Analog zum vorgestellten Verfahren der Rückgabe des Wurzelknotens weist jede Produktion der Grammatik die Rückgabe ihres repräsentierenden Knotens auf. Somit können die entsprechenden Methoden einer Parserinstanz für das Teilstatement aufgerufen werden und liefern als Ergebnis im Falle der erfolgreichen Herleitung den repräsentierenden Knoten der Produktion. 3.5 Abwandlungen der Grammatik–Vorlage Die SQL-92 Grammatik in erweiterter BNF aus der Publikation [MS93] ist als Vorlage für diese Arbeit genutzt worden. Gemäß der in Abschnitt 1.2 festgelegten Zielsetzung ist die erstellte Grammatik eine Teilmenge der in der genannten Vorlage angegebenen Produktionen. Ein Ziel bei der Erstellung dieser Teilmenge war es, die angegebenen Produktionen kompakter zu gestalten, um eine übersichtlichere Basis für Erweiterungen bereitzustellen. Zu diesem Zweck sing einzelne nichtterminale Symbole direkt durch ihre Produktionen ersetzt worden. Ein Beispiel hierfür ist die Startproduktion des Create 31 KAPITEL 3. SQL-92 GRAMMATIK FÜR JAVACC Table–Ausdrucks, die in der nachfolgenden Abbildung 3.1 in ihrer originalen Form im linken Teil und in der für diese Grammatik abgewandelten Form im rechten Teil der Abbildung dargestellt ist. Abbildung 3.1: Ersetzen des nichtterminalen Symbols <table element list> Außerdem ist es für eine von JavaCC zu verarbeitende Grammatik notwendig gewesen, die in einigen Produktionen enthaltenen Linksrekursionen aufzulösen. Zum Beispiel ist die linksrekursive Produktion des nichtterminalen Symbols search condition wie folgt aufgelöst worden: Abbildung 3.2: Auflösen einer Linksrekursion Im Hinblick auf die Klassenbezeichnung der Knoten des Syntaxbaums ist die Benennung sämtlicher nichtterminaler Symbole der Grammatik so verändert worden, dass sie den Java Code–Konventionen für Klassennamen entsprechen. Gemäß dieser Konventionen dürfen keine Leerzeichen im Klassennamen enthalten sein und der Klassenname muss mit einem Großbuchstaben beginnen. Zusätzlich sollten Teilbegriffe innerhalb des Klassennamens wiederum mit einem Großbuchstaben beginnen. In der oberen Abbildung 3.2 ist beispielsweise die Umbenennung der nichtterminalen Symbole search condition und boolean term in SearchCondition bzw. BooleanTerm zu sehen. Die vorgenommenen Abwandlungen der Grammatik–Vorlage resultierten schließlich in den, im Anhang A aufgeführten, terminalen und nichtterminalen Symbolen, sowie den Produktionen in erweiterter BNF. 32 Kapitel 4 Konzept der Erweiterbarkeit Ausgehend von dem Wunsch einer zentrale Schnittstelle für die Transformation der Ausdrücke in der erweiterten SQL–Syntax in solche in der Syntax des SQL-92 Standards ist das hier beschriebene Konzept entwickelt worden. Grundlage der Umwandlung ist die Abbildung des Ausdrucks auf die abstrakte Datenstruktur Syntaxbaum und die Anwendung der bereitgestellten Baumoperationen. Dieses Teilkonzept wird in den Abschnitten 4.1 und 4.2 erläutert. Die darauf aufbauende Stufe des Gesamtkonzepts ist das Visitor Pattern, welches zur Kategorie der Behavioral Pattern“ zählt. Das Pattern wird in diesem konzeptio” nellen Zusammenhang eingesetzt, um alle Knoten des Syntaxbaums zu durchlaufen und gegebenenfalls an einigen von ihnen Änderungen vorzunehmen. Somit bewirkt ein Durchlauf des Syntaxbaums im Ergebnis die Umwandlung des geparsten erweiterten SQL–Ausdrucks in ein semantisch entsprechendes Gegenstück des SQL-92 Standards. Im Detail wird dieses Pattern im Abschnitt 4.3 beschrieben. Zur Erweiterung des genannten Pattern sind zentrale Zugriffsmethoden für einzelne Bestandteile eines SQL–Ausdrucks erzeugt worden. Mit deren Hilfe ist es möglich von beliebigen Knoten eines Syntaxbaumes aus, auf die Knoten verschiedener Klauseln zuzugreifen und sie zu verändern. Beispielsweise können sämtlich From–Klauseln eines Ausdrucks ermittelt werden, oder auch nur die From–Klausel eines bestimmten Teilbaums. Auf diese Funktionalität wird im Abschnitt 4.4 näher eingegangen. Außerdem sind bereits Zugriffsmethoden für einzelne Datenbankinhalte wie etwa Spaltennamen einer Tabelle erzeugt worden, auf die im Abschnitt 4.5 näher eingegangen wird. Diese zentralen Schnittstellen für DBS-Zugriffe werden insbesondere als Unterstützung der Syntaxtransformation in den Methoden der Visitor Klassen eingesetzt. Der Abschnitt 4.6 enthält schließlich die Beschreibung der einzelnen Schritte zur Erweiterung von Parseplus , bevor dann im Abschnitt 4.7 zwei Beispielerweiterungen vorgestellt werden. 33 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT 4.1 Abstrakte Datenstruktur Syntaxbaum Zur Transformation beliebiger Ausdrücke vom erweiterten SQL in den SQL-92 Standard ist deren einheitliche Repräsentation in einer abstrakten Datenstruktur notwendig. Die in dieser Arbeit gewählte Datenstruktur ist ein abstrakter Syntaxbaum, wie er im Abschnitt 2.3.3 beschrieben worden ist. Diese Datenstruktur kennzeichnet, dass jede Produktion, die während des Parsens hergeleitet worden ist, durch einen Knoten im Syntaxbaum repräsentiert wird. Die Benennung des eingefügten Knotens entspricht dabei dem nichtterminalen Symbol auf der linken Seite der Produktion. Die bisher beschriebenen Eigenschaften der Baumstruktur werden durch die Klassen gewährleistet, die JJTree auf der Basis der übergebenen Grammatikdatei erstellt. Jedoch fehlt noch der Zugriff auf die geparsten terminalen Symbole, um die gewünschte Transformation in die SQL-92 Syntax vollziehen zu können. Dieser Aspekt wird im folgenden Abschnitt 4.1.1 behandelt. 4.1.1 Anbindung der Tokenliste Die geparsten terminalen Symbole werden zur Laufzeit als verkettete Liste aus Objekten der Klasse Token dargestellt. Näheres zur Verkettung der Tokenobjekt ist im Abschnitt 2.2.2 nachzulesen. Standardmäßig gibt es jedoch keine logische Verknüpfung zwischen den Knoten des Syntaxbaums und der während des lexikalen Scannens erstellten Tokenliste. In dieser Form böte der Syntaxbaum also keinen Zugriff auf die terminalen Symbole des geparsten Ausdrucks, der aber für eine Transformation unabdingbar ist. Nachdem jede hergeleitete Produktion bereits logisch anhand eines Knotens in der Baumstruktur verankert ist, fehlt noch die Anbindung der enthaltenen terminalen Symbole an den jeweiligen Produktionsknoten. Zu diesem Zweck ist auf die Methoden jjtOpen und jjtClose der Basisklasse der Baumstruktur zurückgegriffen worden. Wie im Abschnitt 2.3.3 schon geschildert worden ist, sind diese Methoden standardmäßig in den Lebenszyklus eines Knotenobjekts integriert. Daher kann innerhalb dieser Methoden mittels des Zugriffs auf den aktuellen Tokenstrom das erste bzw. letzte Token im Bereich einer jeden Produktion im entsprechenden Knoten gespeichert werden. Zur Rekonstruktion des geparsten Inhalts einer Produktion der Grammatik folgt man somit vom ersten bis zum letzten Token eines Knotens der linearen Verkettung der Token. Auf diese Weise wird die notwendige logische Verknüpfung zwischen Syntaxbaum und Tokenliste hergestellt. Aus dieser Verknüpfung der beiden Datenstrukturen Baum und linearer Liste folgt beispielsweise, dass der Wurzelknoten eines Syntaxbaums immer das erste und letzte Token des gesamten geparsten Ausdrucks speichert und somit den Anfang und das Ende des zu transfomierenden Ausdrucks markiert. In der folgenden Abbildung 4.1 ist ein Beispielstatement in der SQL-92 Syntax sowie die zugehörige Syntaxbaumdarstellung zu sehen. Es handelt sich um den Delete– 34 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Ausdruck auf der Tabelle angestellter: Delete From angestellter;. In den den einzelnen Knoten zugeordneten Kästen sind die Attributwerte der Knoten aufgelistet. Hierbei gilt es wieder zu beachten, dass im Wurzelknoten das erste (Delete) und letzte Token (;) des gesamten geparsten Ausdrucks gespeichert sind. Dieses Zuordnungsverfahren zwischen Token und Baumknoten wird natürlich auch für alle Teilausdrücke benutzt, so dass in diesem Beispiel die beiden Knoten ASTTableName und ASTActualIdentifier das selbe Tokenobjekt des Tabellennamens sowohl in ihrem ersten, als auch letztem Token speichern. Abbildung 4.1: Der abstrakte Syntaxbaum eines Delete–Ausdrucks 4.1.2 Baumoperationen Mit der Repräsentation eines geparsten SQL–Ausdrucks durch einen abstrakten Syntaxbaum ist es möglich geworden, die beabsichtigte Umwandlung des Ausdrucks in den SQL-92 Standard als Operationen auf der Baumstruktur zu implementieren. Die Baumoperationen können also unabhängig vom individuellen Einsatz innerhalb des Transformationsverfahrens implementiert werden. Zu diesem Zweck benötigen jedoch alle Komponentenklassen des abstrakten Datentyps Syntaxbaum gemein- 35 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT same Schnittstellen, um zum Beispiel auf die Kindknoten oder den Elternknoten eines beliebigen Knotens zugreifen zu können. Diese Schnittstellen sind in dem Interface AdvancedNode(s. Abschnitt 4.2.1) zusammengefasst. Die innerhalb dieser Arbeit benötigten Baumoperationen seien hier nun erläutert: • Zur Manipulation des Syntaxbaums ist es notwendig einzelne Knoten löschen zu können. Die Struktur der angebundenen Tokenliste wird hierdurch gleichfalls verändert, so dass alle Token vom ersten bis zum letzten Token des zu löschenden Knotens ebenfalls gelöscht werden und eine neue Verkettung des vorhergehenden und nachfolgenden Token hergestellt wird. • Auch das Einfügen einzelner Knoten ist eine notwendige Operation auf dem Syntaxbaum. Jeder einzufügende Knoten enthält bereits vorher die gewünschten Token, so dass die Einfügeoperation dessen Token in die ursprüngliche Tokenstruktur einbinden kann. 4.2 Einheitliche Schnittstellen Die Motivation für die Schaffung einheitlicher Schnittstellen der Knoten des Syntaxbaums ist bereits im Abschnitt 4.1.2 erläutert worden. Diese Schnittstellen sind im Interface AdvancedNode gebündelt und werden im Abschnitt 4.2.1 aufgelistet. Auch die Basisklasse der Datenstruktur abstrakter Syntaxbaum existiert in Form der Klasse SimpleNode innerhalb eines individuell erweiterten SQL-Compilers genau einmal. Die daraus entstehende Hierarchie der Knotenklassen der Erweiterung und der bereits im Compiler enthaltenen SQL-92 Grammatik wird im Abschnitt 4.2.2 beschrieben. Zusätzlich ist eine einheitliche Schnittstelle der verschiedenen Parser, zum einen der des SQL-92 Standards und andererseits der Parser des erweiterten SQL, notwendig. Der Grund hierfür liegt im einheitlichen Zugriff auf den erweiterbaren SQLCompiler, der unabhängig von der Art der Erweiterung über die Methoden einer zentralen Klasse erfolgen soll. Die gemeinsamen Schnittstellen der Parser sind im Interface SQLParser zusammengefasst worden (s. Abschnitt 4.2.3). 4.2.1 AdvancedNode Um alle Knoten eines Syntaxbaumes einheitlich verwalten zu können, ist es notwendig, dass alle Komponentenklassen über dieselben Schnittstellen verfügen. Diese Schnittstellen bietet das im erweiterbaren SQL-Compiler zentral gespeicherte Interface AdvancedNode, welches alle Knotenklassen implementieren. Die Einfüge– und Löschoperationen auf dem Syntaxbaum basieren ebenso auf dieser einheitlichen Schnittstelle, wie der im Abschnitt 4.4 beschriebene Zugriff auf die Teilklauseln eines SQL– Ausdrucks. Auch die logische Verknüpfung der Knoten untereinander wird durch die Speicherung von Referenzen des Typs AdvancedNode aufgebaut. 36 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Der Zugriff auf die nachfolgend aufgelisteten Attribute eines Knotens der beschriebenen Baumstruktur wird durch dieses Interface gewährleistet: • die Referenzen auf die Kindknoten, sowie deren Anzahl • die Referenz auf den Elternknoten • der Typ des Knotens • die Referenzen auf den rechten und linken Nachbarn • die Referenz auf den Wurzelknoten • die Referenz auf das erste und letzte Tokenobjekt eines Knotens Außerdem bietet dieses Interface noch zenrale Schnittstellen zur Ausgabe der Tokeninhalte eines Knotens und zum Auslösen von Pre- bzw. Postordnung–Durchläufen des Syntaxbaums mittels verschiedener accept–Methoden des Visitor Pattern. 4.2.2 SimpleNode Dies ist die Basisklasse des in Abschnitt 4.1 vorgestellten abstrakten Syntaxbaums. Sie implementiert das Interface AdvancedNode und bietet zusätzlich noch accept– Methoden zur Ermittlung im Baum enthaltener verschiedener Klausel- und Tabellenspaltenknoten (s. Abschnitt 4.4). Von dieser Klasse werden sämtliche Knotenklassen der unterschiedlichen Produktionen abgeleitet, wodurch die abgeleiteten Knotenklassen gleichzeitig auch das Interface AdvancedNode implementieren. Das folgende Diagramm 4.2 verdeutlicht die gleichartige Klassenhierarchie der Knoten einer Erweiterung, sowie der Knoten eines SQL-92 Syntaxbaums. Abbildung 4.2: Die Schnittstellen SimpleNode und AdvancedNode 37 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Stellvertretend für alle möglichen Knotenarten der Syntaxbäume sind in diesem Diagramm zwei Knotenklassen abgebildet. Der Knoten der Erweiterung wird durch die Klasse ASTExtension und der Knoten des SQL-92 Baums durch die Klasse ASTSQL92 repräsentiert. Beide sind von der zentralen Klasse SimpleNode abgeleitet, die ihrerseits das Interface AdvancedNode implementiert. 4.2.3 SQLParser Das Interface SQLParser beinhaltet sämtlich Schnittstellen, die sowohl der SQL-92 Parser, als auch der Parser des erweiterten SQL bereitstellen müssen. Diese Schnittstellen sind wie folgt spezifiziert worden: • Um gleichsam den Vorgang des Parsens eines Ausdrucks des erweiterten SQL, wie eines SQL-92–Ausdrucks starten zu können, ist die Definition eines einheitlichen nichtterminalen Startsymbols notwendig. Dieses Startsymbol wird durch die Methode SQLStatement innerhalb aller SQL-Parser repräsentiert. • Auch die Initiierung der Umformung des ursprünglichen SQL–Ausdrucks gemäß des SQL-92 Standards wird durch eine einheitliche Schnittstelle des Parsers gestartet. Diese Schnittstelle ist die Methode transformSQL. • Die Methode getToken ist die Schnittstelle der Parser zu den Knotenklassen, um auf ein beliebiges Objekt des aktuellen Tokenstroms zugreifen zu können. Mittels dieser Methode werden das erste und letzte Tokenobjekt einer jeden hergeleiteten Produktion im repräsentierenden Syntaxbaumknoten gespeichert (s. Abschnitt 4.1.1). 4.3 Visitor Pattern Dieses Pattern gehört zu der Gruppe der Verhaltensmuster (Behavorial Pattern), die sich mit der Kommunikation zwischen einzelnen Objekten beschäftigt (siehe Publikation [GHJV95]). Die Kernidee des Visitor Pattern ist die Abkapselung der Elemente einer Datenstruktur von den verschiedenen Operationen, die auf ihnen ausgeführt werden können. Der hierdurch realisierte Vorteil gegenüber direkt in den Objektelementen implementierten Algorithmen ist die Möglichkeit, an zentraler Stelle neue Operationen definieren zu können. Dadurch ist es zum Beispiel leicht möglich, verschiedene Umwandlungen einer Baumstruktur anhand der gleichen Schnittstelle zu den Komponentenklassen zu realisieren. Die Definition der Operationen auf Elementen der Datenstruktur wird in den Visitorklassen in speziellen visit–Methoden vorgenommen. Allerdings müssen konkrete Instanzen der Visitorklassen von den zu verändernden Elementen auch zu empfan” gen“ sein. Aus diesem Grund wird eine Schnittstelle benötigt, welche alle Elemente der 38 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Datenstrukur akzeptieren müssen. Die Schnittstelle wird in der Form eines abstrakten Visitors realisiert, der alle visit–Methoden deklariert, die für die unterschiedlichen Elemente der Datenstruktur ausgeführt werden können. Dadurch, dass die konkreten Visitorklassen diese Schnittstelle implementieren, werden ihre instanziierten Objekte von den Datenstrukturelementen als Visitor akzeptiert. Womit die Bedeutung der Methodengruppe accept“ erklärt ist, da diese Methoden in allen Elementen einer zu ” verändernden Datenstruktur enthalten sein müssen, um die jeweiligen Methoden des Visitors auszuführen. Es ist also festzuhalten, dass sich das Visitor Pattern aus mindestens drei Bestandteilen zusammensetzt: 1. das Interface Visitor zur Deklaration je einer visit–Methode pro Element der Datenstruktur 2. die Methode accept, die in jedem Element einer zu besuchenden Datenstruktur enthalten ist und die visit–Methode dieses Elements ausführt 3. eine konkrete Visitorklasse, die die Operationen auf den Elementen implementiert und so beispielsweise eine Transformation einer Baumstruktur auslöst Mit der hier gezeigten Abbildung 4.3 soll noch einmal der Zusammenhang der aufgelisteten Bestandteile verdeutlicht werden. Die Klassen ASTSQLStatement, ASTDeleteStatement und ASTTableName seien drei Beispiele für die Komponentenklassen einer Datenstruktur. Abbildung 4.3: Bestandteile des Visitor Pattern In diesem Diagramm ist zu erkennen, dass zu jeder der drei Komponentenklassen eine passende visit–Methode im Interface Visitor existiert. Die konkrete Visitorklasse KonkreterVisitor implementiert das genannte Interface und somit kann jede ihrer Instanzen als Parameter der accept–Methoden der Komponentenklassen übergeben werden. 39 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Ein weiterer Aspekt des Visitor Pattern ist das Hinzufügen von Funktionalität zu einer Visitorklasse durch eine weitere externe Klasse. Dies wird beispielsweise dazu genutzt, eine konkrete Visitorklasse zu schaffen, die den Durchlauf der Datenstruktur implementiert, und eine zweite Visitorklasse, die diesen Durchlauf durch das Speichern aller Elemente eines bestimmten Typs für den späteren direkten Zugriff erweitert, indem sie die Funktionalität der ersten Klasse erbt. 4.3.1 Anwendungsfall erweiterbarer SQL-Compiler In der vorliegenden Arbeit wird das Visitor Pattern zur Umwandlung des geparsten Syntaxbaums eingesetzt. Die Manipulation der Baumknoten und deren Inhalt führt letztendlich zur Umwandlung des erweiterten SQL–Ausdrucks in die Syntax des SQL92 Standards. Die Basisklassen BasicVisitor und RunThroughVisitor implementieren im Rahmen dieses Konzepts eine visit–Methode pro Knotenklasse, von der während des Parsens Knoten erzeugt werden können. Die Zuordnung der jeweiligen Methoden zu den Knotenobjekten wird dabei durch die Polymorphie der Definition dieser Methoden sichergestellt. Diese Methoden erlauben die Weitergabe von Attributen innerhalb des Baums, indem sie über einen entsprechenden Parameter verfügen und diesen standardmäßig ohne Veränderung an die aufrufende Instanz zurückgeben. Die zusätzliche Funktionalität schließlich wird durch eine Visitorklasse ergänzt, die von dieser Basisklasse erbt und nur die visit–Methoden überlädt, die gemäß der Syntaxerweiterung einem neuen oder veränderten Syntaxbaumknoten zuzuordnen sind. Innerhalb dieser überladenen Methoden wird nun zentral die Transformation des geparsten SQL–Ausdrucks je Knoten angegeben. Die visit–Methoden dienen zusätzlich der Weitergabe von Informationen innerhalb des Baums, indem vor der Weitergabe die übergebenen Attribute verändert bzw. neue Attribute erzeugt werden können. Weiterhin ist es möglich in dieser überladenen Methode deren Pendant der Elternklasse aufzurufen, um etwa den dort schon implementierten Baumdurchlauf weiterzuführen und so die Funktionalität für die konkrete Methode zu erweitern, jedoch nicht zu ersetzen. Der Startimpuls zum Aufruf der einzelnen Methoden der Visitorklassen wird durch die jeweilige accept–Methode der Knotenobjekte initiiert. Diesen Methoden wird die Information übergeben, welche der Visitorklassen zu benutzen ist. Gemäß des Visitor Pattern muss dies die Basisklasse selbst oder eine der Klassen sein, die die Basisklasse um ihre individuelle Funktionalität erweitert. So können unter der Annahme, dass sich die Art und Anzahl der NT-Symbole und damit auch der Knotenklassen nicht verändert haben, verschiedene Erweiterungen mit dem selben Mechanismus, aber individuell erstellten Visitorklassen bearbeitet werden. Durch das erschaffene Zusammenspiel der Methoden führt ein Durchlauf des Syntaxbaums zur gewünschten Modifikation seiner Struktur und damit im Ergebnis zu der Umwandlung des geparsten erweiterten SQL–Ausdrucks in einen semantisch entsprechenden Ausdruck in der Syntax des SQL-92 Standards. 40 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Der JavaCC-Präprozessor JJTree unterstützt dieses Pattern durch die Option VISITOR, welche in der Grammatik gesetzt werden kann. Ist zusätzlich auch die Erzeugung einzelner Knoten aus definierten Produktionen aktiviert, so wird durch JJTree neben dem Interface <ParserName>Visitor als Schnittstelle aller implementiereden Visitorklassen, auch die Methode jjtAccept(SQL92Visitor, Object) in jeder Knotenklasse angelegt. Das erzeugte Interface enthält die Deklarationen sämtlicher visit–Methoden, welche durch ihre Polymorphie für alle erzeugbaren Knotenobjekte des geparsten Syntaxbaums eine passende Variante bereitstellen. Den Aufruf dieser Methoden mittels der den Visitorklassen gemeinsamen Schnittstelle <ParserName>Visitor fügt JJTree in die jjtAccept-Methoden der Knotenklassen ein. In der Abbildung 4.4 ist die Struktur des Visitor Pattern dargestellt, welche durch das Ausführen von Parseplus mit einer Grammatikdatei erzeugt wird. Das Interface SQL92Visitor und die Klasse SimpleNode in dieser Abbildung werden nicht jedesmal generiert, sondern sind fest in den erweiterbaren SQL Compiler integriert. Abbildung 4.4: Klassendiagramm des generierten Visitor Pattern Als Erweiterung des von JJTree erzeugten Grundgerüsts des Visitor Pattern werden die Visitorklassen BasicVisitor und RunThroughVisitor erzeugt, die zu Beginn dieses Abschnitts beschrieben worden sind. Ebenfalls eine Erweiterung ist die Vererbung der im Interface SQL92Visitor gebündelten Schnittstellen, durch die es möglich ist, einen Syntaxbaum zu durchlaufen, der sowohl aus Knoten der Erweiterung, als auch 41 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT aus solchen des SQL-92 Standards besteht. Um diese beiden Knotentypen beispielsweise für die Ermittlung von Klauselmengen auf eine einheitliche und von der jeweiligen Erweiterung unabhängigen Art durchlaufen zu können, ist die Position der Klasse SimpleNode in der Klassenhierarchie verändert worden (s. Abschnitt 4.2.2). Dieser zentrale Aspekt der Erweiterung der durch JJTree erzeugten Struktur des Visitor Pattern ist in der Abbildung 4.5 noch einmal in Form eines Klassendiagramms dargestellt. Die Abbildung zeigt die Schnittpunkte der generierten Klassen mit den zentralen Bestandteilen des erweiterbaren SQL-Compilers SimpleNode, AdvancedNode und SQL92Visitor. Abbildung 4.5: Hierarchieeinordnung des generierten Visitor Interfaces 42 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT 4.4 Zugriff auf Teilklauseln Ausgehend von der Annahme, dass einige Klauseltypen auch in einem erweiterten SQL noch enthalten sind, ist die Option verwirklicht worden, auf Teilklauseln eines geparsten SQL–Ausdrucks in Form der repräsentierenden Syntaxbaumknoten zugreifen zu können. Zu diesem Zweck wird nach dem erfolgreichen Abschluss des Parsings, oder dem Einfügen eines neuen Knotens in den erstellten Syntaxbaum ein Durchlauf des Baumes mittels des Visitor Pattern durchgeführt. Während dieses Baumdurchlaufs in Präordnung werden die besuchten Klauselknoten der nachfolgend aufgelisteten Typen global gespeichert und stehen somit zum späteren Zugriff bereit. Die Knoten der folgenden Klauseltypen werden ermittelt: • QueryExpression beinhaltet den gesamten Inhalt einer SQL–Anfrage • SelectList enthält alles, was zwischen den Schlüsselwörtern select“ und from“ ” ” steht • FromClause entspricht den benutzten Tabellen und Unterabfragen • WhereClause wird durch das Schlüsselwort where“ eingeleitet ” • GroupByClause beinhaltet die Gruppierungkriterien • SetClause steht für die Verknüpfung der Ergebnismengen einzelner QueryExpressions • OrderByClause enthält die Kriterien, nach denen die Ergebnismenge geordnet wird Der Visitor–Durchlauf des Syntaxbaums wird durch die Methode acceptClauseVisitor der Klasse SimpleNode gestartet, die ein Object der Visitorklasse ClauseVisitor übergeben bekommt. Die Klasse ClauseVisitor erbt die Implementation der unterschiedlichen visit–Methoden von der Klasse RunThroughVisitor und überlädt lediglich die Methode runThrough, um Referenzen auf die besuchten Klauselknoten global zu speichern. Wie bereits geschildert, werden die globalen Klauseleinträge automatisch nach dem Einfügen eines neuen Knotens durch einen erneuten Baumdurchlauf aktualisiert. Im Falle des Entfernens einer der gespeicherten Klauselknoten aus dem Syntaxbaum ist ein Methodenaufruf integriert, der den betreffenden Knoten, sowie alle Klauselknoten, die Kindknoten dieses gelöschten Knotens sind, aus der global gespeicherten Menge der Klauselknoten entfernt. Als weiteres Merkmal der Klauselverwaltung ist die Suche nach dem ersten Vorgänger– und linkesten Nachfolgerknoten eines zu definierenden Typs realisiert worden. Als Ergebnis beider Suchvorgänge wird eine Referenz auf den betreffenden Knoten geliefert, anhand derer er verändert werden kann. Hierdurch ist es beispielsweise möglich, 43 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT die Knoten zweier Tabellenreferenzen abhängig davon zu vergleichen, ob sie innerhalb des selben Teilbaums einer From–Klausel positioniert sind. Zu der Funktionalität des Auffindens des ersten Vorgängerknotens ist außerdem noch die Überprüfung auf Gleichheit der Vorgängerknoten – eines oben aufgelisteten Typs – zweier Knoten hinzugefügt worden. Hierdurch kann beispielsweise die Zugehörigkeit zweier unterschiedlicher Tabellenreferenzen zur selben From–Klausel überprüft werden, was bei der Berechnung der verfügbaren Spaltenmenge einer Anfrage von Vorteil ist. Auch die Ermittlung der einzelnen Spaltennamen der Tabellen, die innerhalb einer Klausel geparst wurden, ist auch in Hinsicht auf die semantische Analyse von Interesse. Daher bietet der erweiterbare SQL-Compiler auch in diesem Fall die Möglichkeit, durch einen vorbereiteten Durchlauf des entsprechenden Teilbaums des kompletten Syntaxbaums anhand des Visitor Pattern, eine Auflistung aller enthaltenen Spaltennamen-Knoten zu ermitteln. In dieser Auflistung werden keine Spalten berücksichtigt, die innerhalb einer Unterabfrage in der betreffenden Klausel auftauchen. Der für die Bestimmung der Spaltenmenge angestoßene Teilbaumdurchlauf wird durch die Methode acceptColumnVisitor der zentralen Klasse SimpleNode initiiert. Die zu diesem Zweck erzeugte Visitorklasse lautet ColumnVisitor und überlädt analog zum zuvor vorgestellten ClauseVisitor lediglich die Methode runThrough der Klasse RunThroughVisitor, um die beschriebene Funktionalität zu realisieren. 4.5 Zugriff auf Datenbankinhalte Die Umwandlung eines Ausdrucks in erweitertem SQL macht einen Datenbankzugriff zur Bestimmung seiner semantischen Bedeutung unabdingbar. Hierbei hat sich als Schnittmenge vieler Erweiterungen der Zugriff auf die Spaltennamen einer Tabelle herausgestellt. Aus diesem Grund bietet Parseplus verschiedene Schnittstellen in der abstrakten Klasse DBContent zum Zugriff auf die Tabelle all tab columns des Oracle Data Dictionary. Neben der Methode fetchAllTabCol, die alle Zeilen aus der Tabelle all tab columns abfragt und global zur Laufzeit erreichbar abspeichert, existiert die Methode fetchOneTabCol, die alle Spalten einer ausgewählten Tabelle abfragt und speichert. Als Alternative zu der Berechnung der Spaltenmenge einer Anfrage, ist die Methode getColVector vorgesehen, die die übergebene Anfrage ausführt und alle Spaltennamen der Ergebnismenge liefert. Der Zugriff auf die gespeicherten Spaltennamen zur Laufzeit des SQL-Compilers ist durch die Methode getOneTabCol gewährleistet, die, sofern im Vorfeld die Spalten der gewünschten Tabelle aus dem Data Dictionary abgefragt worden sind, alle Spalten einer Tabelle liefert. Auch die Verarbeitung der ermittelten Benennung von Tabellenspalten steht bereits in zwei konkreten Anwendungsfällen zur Verfügung. Hier ist die Methode getEqualColVector zu nennen, die auf Basis zweier übergebener Tabellennamen die 44 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT jeweiligen Spaltenmengen bestimmt und anschließend die Menge der gleichlautenden Spalten zurückgibt. Auch die Methode getIntersectVector verarbeitet die gefundenen Tabellenspalten, allerdings in allgemeinerer Form. Dieser Methode können zwei Listen aus Strings übergeben werden, aus denen sie die Schnittmenge bestimmt und zurückgibt. Die Verwaltung der Verbindung zum gewünschten Datenbanksystem, welches den Zugriff auf die beteiligten Tabellen koordiniert, ist in der abstrakten Klasse DBGlobals realisiert worden. Mit den Methoden dieser Klasse können die Referenz auf ein existierendes Verbindungsobjekt gespeichert und zurückgegeben, sowie die Metadaten der Verbindung ermittelt werden. 4.6 Erweitern von Parseplus Der Vorgang des Erweiterns des in dieser Arbeit vorgestellten SQL-Compilers setzt sich aus drei grundsätzlichen Bausteinen zusammen: 1. Erzeugen der Parser- und Syntaxbaumklassen gemäß modifizierter Grammatikdatei (siehe Abschnitte 4.6.1 und 4.6.2) 2. Einbinden des Parseplus -Archivs in den Klassenpfad des erzeugten Parsers 3. Einfügen der Operationen zur Transformation der Ausdrücke in erweitertem SQL in solche, die die SQL-92 Syntax einhalten, in den visit–Methoden (behandelt im Abschnitt 4.6.2) Die drei grundsätzlichen Bausteine der Erweiterung lassen sich in fünf Stufen der auszuführenden Tätigkeiten unterteilen, die in der Abbildung 4.6 dargestellt sind. Der Vorgang des Erweiterns beginnt mit der Veränderung der für diese Arbeit erstellten Grammatikdatei gemäß des erweiterten SQL. Ist diese Tätigkeit abgeschlossen, wird die ausführbare Archivdatei des erweiterbaren SQL-Compilers gestartet. Nach der Auswahl der erstellten Grammatikdatei in einem GUI dieses Compilers werden dann die Parser- und Syntaxbaumklassen erzeugt. Hieran schließt sich das Einbinden der ausführbaren Archivdatei in den Klassenpfad des neu erstellten Parsers und Syntaxbaumgenerators an. Hierdurch kann auf die vorbereiteten Baumoperationen, die Exportfunktionalität und die Struktur des Visitor Pattern in vollem Umfang zugegriffen werden. Die beiden letzten Aktivitäten des Erweiterns von Parseplus legen die Regeln und den Ablauf der Umwandlung eines geparsten SQL–Ausdrucks in die Syntax des SQL92 Standards fest. Hierzu sind zunächst die visit–Methoden der betroffenen Syntaxbaumknoten mit den Transformationsanweisungen zu überladen und anschließend die Art des Baumdurchlaufs anhand einer der accept–Methoden auszuwählen. Als zentrale Schnittstelle für die Erweiterung des SQL-Compilers Parseplus ist die Klasse ParsePlus konzipiert. Die statischen Methoden dieser Klasse leiten den Vorgang des Parsens ein und lösen durch den Aufruf einer der implementierten accept– 45 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Abbildung 4.6: Tätigkeiten zur Erweiterung von Parseplus Methoden des Wurzelknotens auch die Umformung des geparsten SQL–Ausdrucks aus. Im Kapitel 7 sind genauere Angaben zum Quellcode inklusive der Beschreibung der einzelnen Methoden enthalten. 4.6.1 Parser einer erweiterten Grammatik Der Parser der individuell erweiterten SQL-Sprache wird mittels des modifizierten Parsergenerators JavaCC und seines Präprozessors JJTree erzeugt. Zu diesem Zweck ist der SQL-Compiler Parseplus als ausführbares Archiv konzipiert, welches nach dem Start ein Graphical User Interface (GUI) zur Auswahl einer Grammatikdatei einblendet. Mit der dort ausgewählten Grammatik wird der Präprozessor und der Parsergenerator nachfolgend aufgerufen, wodurch die benötigten Java–Quellcode–Dateien erzeugt werden. Diese enthalten dann bereits die Methoden und importieren Klassen und Interfaces, die speziell auf das Zusammenspiel mit dem SQL-Compiler abgestimmt sind. Im konkreten Fall des Parsers und seines zugehörigen Tokenmanagers beschränkt sich die Anpassung der automatisch generierten Dateien darauf, die erzeugten und verarbeiteten Tokenobjekte von der Tokenklasse Token innerhalb des SQL-Compilers zu erstellen. Nachfolgend sind die auf die beschriebene Weise generierten Parserda- 46 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT teien noch einmal aufgelistet, welche im Abschnitt 2.2.5 schon inhaltlich beschrieben worden sind: • <Parsername>.java • <Parsername>Constants.java • <Parsername>TokenManager.java • JavaCharStream.java • TokenMgrError.java Die Änderungen des generierten Quellcodes der Syntaxbaumklassen ist dagegen umfangreicher und wird im folgenden Abschnitt 4.6.2 beschrieben. 4.6.2 Syntaxbaumklassen der Erweiterung Nachdem die Grammatik im GUI ausgewählt worden ist, werden die folgenden Javadateien der Syntaxbaumklassen durch JJTree erstellt ( AST<NT>“ steht hierbei für ” die Gruppe der abstrakten Knotenklassen): • JJT<Parsername>State.java • <Parsername>TreeConstants.java • <Parsername>Visitor.java • RunThroughVisitor.java • BasicVisitor.java • AST<NT>.java Die erste Änderung gegenüber den standardmäßig von JJTree generierten Klassen betrifft die Ersetzung des Interfaces Node durch AdvancedNode, welches im Paket parseplus.modifier des erweiterbaren SQL-Compilers zu finden ist. Dieses Interface wird demzufolge nicht erneut generiert, sondern in den erzeugten Klassen referenziert. AdvancedNode deklariert zum einen die gleichen Methoden, die im ursprünglichen Interface Node schon enthalten waren, um die Aufrufe im generierten Parser nicht verändern zu müssen. Zusätzlich dazu werden noch die Methoden zur Anknüpfung des geparsten Tokenstroms an den Syntaxbaum, zur Baummanipulation und zur Ausgabe des Knoteninhalts in dem besagten Interface deklariert. Aufbauend auf dieser neu erschaffenen gemeinsamen Schnittstelle aller Knotenklassen können die Implementationen der hinzugefügten accept–Methoden und der Methoden zur Baummanipulation in die generierten Knotenklassen einfließen. Konkret sind dies die im Folgenden aufgelisteten Methoden, welche im Kapitel 7 im Zusammenhang mit den Klassen SimpleNode und AST<NT> näher beschrieben werden. 47 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT • getNodeType( ) : int • nextLeft( ) : AdvancedNode • nextRight( ) : AdvancedNode • root( ) : AdvancedNode • postOrderAccept(<Parsername>, Object) : Object • preOrderAccept(<Parsername>, Object) : Object Auch die Generierung der Klasse SimpleNode entfällt bei der Erzeugung der Syntaxbaumklassen mittels Parseplus , da sie ebenfalls zentral im Paket parseplus.modifier hinterlegt ist. Diese Basisklasse des abstrakten Syntaxbaums implementiert das Interface AdvancedNode, bietet jedoch zusätzlich noch accept–Methoden für die semantische Analyse, die nicht zu den gemeinsamen Schnittstellen aller Knotenklassen gehören. Eine konkrete Implementation des Interfaces <Parsername>Visitor stellt die generierte Klasse RunThroughVisitor dar. Mit ihrer Hilfe wird ein Präordnung–Durchlauf der Knoten, angestoßen durch die Methode jjtAccept, ausgeführt. Im Gegensatz zum Aufruf der ebenfalls verfügbaren Methoden (pre|post)OrderAccept, die ein Visitorobjekte der Klasse BasicVisitor akzeptiert, hat dieser Durchlauf den Vorteil, dass zwischen dem Abarbeiten der einzelnen visit–Methoden zusätzliche Anweisungen ausgeführt werden können. Die konkrete Visitorklasse RunThroughVisitor implementiert die unterschiedlichen visit–Methoden dergestalt, dass sie die ebenfalls enthaltene Methode runThrough aufrufen und deren Rückgabewert zurückgeben. Die besagte Methode runThrough realisiert einen Durchlauf in Präordnung der Kindknoten des übergebenen Knotenobjekts und bietet damit die Möglichkeit zwischen den Aufrufen der Methode jjtAccept zusätzliche Anweisungen einzufügen. Die zweite Visitorklasse BasicVisitor verfügt ebenso wie RunThroughVisitor über eine Implementation zu jeder im Interface <Parsername>Visitor deklarierten visit– Methode. Die erzeugten Methodenkörper der Klasse BasicVisitor enthalten allerdings nur die Rückgabe des übergebenen Objekts, ohne daran eine Änderung vorzunehmen. Der eigentliche Knotendurchlauf wird in diesem Fall bereits in den Methoden (pre|post)OrderAccept initialisiert. Gemäß des Visitor Pattern ist es außerdem möglich im Rahmen einer Erweiterung eine eigene Visitorklasse zu erzeugen, die von den bereitgestellten Klassen RunThroughVisitor oder BasicVisitor erbt und nur bestimmte visit–Methoden zwecks Manipulation des geparsten SQL–Ausdrucks überlädt. Im folgenden Klassendiagramm (Abbildung 4.7) einer Beispielerweiterung trägt diese zweite Visitorklasse die Bezeichnung NJoinTrafoVisitor, da sie auf die Umwandlung der Syntax eines natürlichen inneren Verbundes (Natural Join) in die Join-Syntax innerhalb der Where–Klausel abzielt. 48 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Abbildung 4.7: Visitorklassen zur Umwandlung eines Natural Joins“ ” In diesem Diagramm ist die Klassenhierarchie der erzeugten Visitorklassen dargestellt. Das Interface SQL92Visitor stellt dabei die Spitze der Hierarchie dar. Es handelt sich hierbei um das Visitorinterface der Syntaxbaumklassen des SQL-92 Baumgenerators, die fest im erweiterbaren SQL-Compiler integriert sind. Deren visit–Methodendeklarationen erbt das Interface SQL99Visitor, so dass die implementierende Klasse BasicVisitor sowohl die Methoden zum Besuch der Syntaxbaumklassen der Erweiterung, als auch der Klassen des SQL-92 Standards implementieren muss. Auf diese Weise können auch Knoten des SQL-92 Standards in den Syntaxbaum der Erweiterung eingehängt werden. Dies ist zum Beispiel dann notwendig, wenn einzelne Produktionen der SQL-92 Grammatik in der Erweiterungsgrammatik nicht mehr enthalten sind und somit auch keine Knotenklassen dieser Produktionen erzeugt werden. Müssen jedoch für die Transformation der erweiterten SQL–Syntax genau diese Knoten in den Syntaxbaum eingefügt werden, so ermöglicht die abgebildete Klassenhierarchie auch einen problemlosen Durchlauf dieses gemischten“ Syntaxbaums ” ( gemischt“ bezieht sich hierbei auf die Verwendung von Knotenklassen zweier ver” schiedener Grammatiken). Die Klasse NJoinTrafoVisitor schließlich erbt von der Klasse BasicVisitor sämtliche visit–Methodenimplementationen und überlädt ausschließlich die Methoden, die für Knoten aufgerufen werden, die von der Umwandlung der Natural Join“ Schreibweise ” 49 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT betroffen sind. 4.7 Beispiele für Erweiterungen In diesem Abschnitt werden zwei Beispiele für die Verwendung des erweiterbaren SQLCompilers geschildert. Die erste Erweiterung transformiert die Syntax des natürlichen inneren Verbunds (z. Bsp.: A Natural Join B) in die inhaltlich entsprechenden Joinangaben innerhalb der Where–Klausel. Das zweite Beispiel für die Nutzung des erweiterbaren SQL–Compilers stellt das Konzept zur Umformung einer SQLSF–Anfrage mit Aggregate–By–Klausel (erläutert in [War06]) in eine Anfrage in semantisch äquivalentem SQL-92 vor. 4.7.1 Natürlicher Innerer Verbund Die Aufgabe des erweiterten SQL-Compilers besteht in diesem Beispiel darin, einen Ausdruck in erweiterter Natural Join“ Schreibweise in einen inhaltlich äquivalenten ” Ausdruck umzuwandeln, der den natürlichen inneren Verbund durch Gleichsetzung der äquivalenten Attribute in der Where–Klausel gemäß des SQL-92 Standards darstellt. Zur Berechnung der äquivalenten Verbundattribute zu verbindender Tupelmengen und den daraus resultierenden Veränderungen einzelner Teiläste des Syntaxbaums wird das in Abschnitt 4.3 beschriebene Visitor Pattern eingesetzt. Dabei werden die visit–Methoden bestimmter Knotentypen mit den Anweisungen dieses konkreten Transformationsfalls in einer neuen Visitorklasse (NJoinTrafoVisitor) überladen und in der Reihenfolge des Postordnung–Durchlaufs des Syntaxbaums aufgerufen. Um diesen Syntaxbaum der SQL–Erweiterung aufbauen zu können, ist zunächst eine Veränderung der SQL–92 Produktionen notwendig. Zu diesem Zweck ist die Grammatikdatei des erweiterbaren SQL– Compilers modifiziert worden. Es sind zwei neue Produktionen(TablePrimary und JoinClause) hinzugefügt, sowie die schon existierende Produktion TableReference inhaltlich verändert worden. Diese Modifikationen sind in der folgenden Abbildung 4.8 durch Ausschnitte aus den Grammatikdateien illustriert. 50 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Abbildung 4.8: Erweiterung der SQL-92 Grammatikdatei Der linke Teil der Abbildung 4.8 gibt den Inhalt der SQL-92 Grammatik wider, die als Basis für Erweiterungen genutzt wird. Die dort dargestellten Produktionen lauten FromClause, TableReference, TableSubQuery und CorrelationName. Wie deren Name schon vermuten lassen, enthält die erste Produktion die erweiterte BNF der From–Klausel des SQL–Ausdrucks. Innerhalb einer From–Klausel können beliebig viele, durch Kommata getrennte Tabellenreferenzen hergeleitet werden. Diese Referenzen bestehen ihrerseits entweder aus dem Namen der referenzierten Tabelle oder einer Unterabfrage die in Form der Produktion TableSubQuery eindeutig nur an dieser Stelle der Grammatik auftreten kann. Die letzte Produktion (CorrelationName) dieses Ausschnitts beschreibt die Aliasbezeichnung, die einer Tabelle oder Unterabfrage in der From–Klausel zugewiesen werden kann. Sowohl die erste Produktion, als auch die beiden letzten, sind unverändert in die erweiterte Grammatik übernommen worden. Ausschnitte dieser Grammatik sind in der rechten Bildhälfte zu finden. Die Erweiterungen der Grammatik sind dort durch dick und kursiv gedruckte Zeichen hervorgehoben. Die erste von der Erweiterung betroffene Produktion ist TableReference. Ihre ursprünglichen Regeln sind in die Produktion TablePrimary eingefügt worden, was durch die beiden kleineren Rahmen samt 51 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Verbindungslinie verdeutlicht wird. Durch die Veränderung der erweiterten BNF der Tabellenreferenz, sowie durch die alternative Teilproduktion innerhalb der Produktion TablePrimary, kann somit eine Verschachtelung der Verbundausdrücke hergeleitet werden. Die Anordnung der Schlüsselwörter der erweiterten Syntax des Verbundausdrucks innerhalb der From–Klausel ist dann schließlich in der Produktion JoinClause festgelegt worden. Auf der Basis dieser Grammatik sind in der Folge die in Abschnitt 4.6 beschriebenen Klassen durch den Aufruf von Parseplus erzeugt worden. Dem in Abbildung 4.6 dargestellten Ablauf folgend, sind anschließend die Anweisungen zur Transformation der Natural Join–Syntax in die des SQL-92 Standards eingefügt worden. Zu diesem Zweck wurde die Visitorklasse NJoinTrafoVisitor erzeugt, die die Implementierung sämtlicher visit–Methoden von der automatisch generierten Klasse BasicVisitor erbt. In dem Klassendiagramm 4.9 wird die Hierarchiestufe der erzeugten Klasse NJoinTrafoVisitor veranschaulicht. Zusätzlich zu den überladenen visit–Methoden verfügt die Klasse noch über eine private Hilfsmethode, die den Zugriff auf das private Parserobjekt parser ermöglicht. Dieses Parserobjekt wird mit dem String des zu parsenden Ausdrucks initialisiert und liefert durch den Aufruf einer seiner Methoden den Wurzelknoten des Syntaxbaums dieses Ausdrucks. Dadurch können zu Umwandlungszwecken neu geschaffene Teilbäume erzeugt und mit Hilfe der Klasse Modifier in den bestehenden Baum eingehängt werden. Abbildung 4.9: Die Visitorklasse NJoinTrafoVisitor 52 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Ein neu erzeugtes Objekt der Klasse NJoinTrafoVisitor und eine leere Hashtabelle zur Weitergabe der ermittelten Spaltenmengen werden zu Beginn dem Aufruf der Methode postOrderAccept des Wurzelknotens übergeben. Dieser Aufruf ist in der Methode transform der Parserklasse SQL99 enthalten, wodurch der Visitor–Durchlauf des Syntaxbaums zur Umwandlung des geparsten Ausdrucks angestoßen wird. Wie bereits im Abschnitt 4.3 dargelegt wurde, soll während dieses Visitor–Durchlaufs die Transformation des Syntaxbaums vorgenommen und damit als Ergebnis ein SQL–Ausdruck erzeugt werden, den der SQL-92 Parser akzeptiert. In der Abbildung 4.9 sind die visit–Methoden der Knotenklassen aufgelistet, deren Objekte von der Transformation betroffen sind. Die visit–Methoden der Knoten vom Typ ASTTableName und ASTTableSubQuery werden während des Visitor–Durchlaufs dazu genutzt, fehlende Aliasbezeichnungen der durch die Knoten repräsentierten Tupelmengen in den Syntaxbaum einzufügen. In der visit–Methode der ASTJoinClause–Knoten werden die Attributmengen von enthaltenen Tabellen und Unterabfragen ermittelt und zur Auswertung an die nachfolgenden Knoten weitergegeben. Wird ein Knoten der Klasse ASTTableReference besucht, so werden wiederum auch Attributmengen bestimmt, jedoch wird von diesem Knoten aus auch bei Bedarf die Where–Klausel verändert bzw. erzeugt. Ein Besuch der From– Klausel, repräsentiert durch den Aufruf der visit–Methode für Knoten vom Typ ASTFromClause, bewirkt die Umformung des von diesem Knoten verästelnden Teilbaums gemäß der SQL-92 Syntax. Als letzter Knoten des Teilbaums einer SQL–Anfrage wird der Knoten des Typs ASTQueryExpression besucht. Dessen visit–Methode führt im Bedarfsfall die Umformung der Select–Klausel gemäß der Regeln der Transformation aus und speichert abschließend die verfügbaren Attribute der Ergebnismenge dieser Anfrage ab. Nachfolgend werden die Zusammenhänge und Auswirkungen der in diese Methoden eingefügten Anweisungen vorgestellt. Die Methode visit(ASTTableName node, Object data) überprüft zunächst den Elternknoten des besuchten Knotens, ob er die Produktion TablePrimary repräsentiert. Ist dies nicht der Fall, wird der Baumdurchlauf mit dem nächsten Knoten fortgesetzt. Der entgegengesetzte Fall führt zu der Überprüfung, ob bereits eine Aliasbezeichnung dieser Tabelle im geparsten SQL–Ausdruck enthalten ist. Sollte dies nicht zutreffen, so wird ein neuer Knoten der Klasse ASTCorrelationName erzeugt und mit einer Aliasbezeichnug ( papl“ mit angehängtem eindeutigen Integerwert aus der Klasse Alias des ” Pakets parseplus.semantics) als Inhalt versehen. Bei dieser Wahl der Aliasbezeichnung wird vorausgesetzt, dass keine derartigen Bezeichnung zuvor im Ausdruck enthalten gewesen sind. Durch das erste Prädikat ist also ermittelt worden, dass die Produktion TableName innerhalb der Produktion TablePrimary hergeleitet worden ist, wodurch gleichzeitig feststeht, dass dieser Tabellenname innerhalb einer From–Klausel positioniert ist. Somit wird das Einfügen eines Alias dieser Tabelle notwendig, um etwaige Join–Attribute eindeutig qualifizieren zu können. Abschließend wird dieser lokal erzeugte Knoten mittels der Methode insertNode der Klasse Modifier aus dem Paket parseplus.modifier als rechter Nachbar des besuchten Knotens vom Typ ASTTableName in den Syntaxbaum eingefügt. Die Baumdarstellung 53 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT in Abbildung 4.10 veranschaulicht diesen Vorgang anhand des geparsten Tabellennamens genre“ und des zugewiesen CorrelationName–Inhalts papl23“. ” ” Abbildung 4.10: Einfügen eines ASTCorrelationName Knotens Die Methode visit(ASTTableSubQuery node, Object data) weist eine Funktionalität analog zu der zuvor beschriebenen Methode auf. Durch die einmalige Referenzierung der Produktion TableSubQuery innerhalb der Grammatik ist jedoch an dieser Stelle keine Überprüfung des Elternknotens notwendig, da diese Unterabfrage nur als Kindknoten einer Instanz vom Typ ASTTablePrimary enthalten sein kann. Durch die Positionierung innerhalb der From–Klausel benötigt diese Unterabfrage daher einen Alias, um mögliche Verbundattribute eindeutig qualifizieren zu können. In der Methode visit(ASTJoinClause node, Object data) sind die beim Besuch eines Knotens vom Typ ASTJoinClause auszuführenden Operationen enthalten. Wie im Ausschnitt aus der erweiterten Grammatikdatei in Abbildung 4.8 zu sehen ist, kann dieser Knoten nur einen Kindknoten der Klasse ASTTablePrimary aufweisen. Der Kindknoten selbst kann wiederum drei verschiedene Knotentypen (zzgl. zum definitiv vorhandenen ASTCorrelationName Knoten) als Kinder haben. Die Alternativen lauten ASTTableReference, ASTTableName oder ASTTableSubQuery. Die Unterscheidung nach diesen Knoten legt dann auch die logische Grundstruktur des Methodenkörpers dieser visit–Methode fest. Da ein nachfolgender ASTTableReference–Knoten bereits durch die zugehörige visit–Methode besucht worden ist (es findet ein Durchlauf in Postordnung statt), müssen lediglich die beiden restlichen Fälle behandelt werden. Beim Auftreten eines Knotens vom Typ ASTTableName werden die Attributnamen dieser Tabelle anhand eines Zugriffs auf das Data Dictionary ermittelt und in einem Objekt vom abstrakten Java Datentyp Vector gespeichert. Sollte der Knoten vom Typ ASTTableSubQuery sein, so werden die global gespeicherten Ergebnisspalten dieser Unterabfrage ebenfalls in ein Vector–Objekt gespeichert. Die globale Speicherung der Attributmenge wurde in dem zuvor besuchten Knoten vom Typ ASTQueryExpression vorgenommen. Dieser Vorgang wird in der visit–Methode dieses Knotentyps im Anschluß beschrieben. 54 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Ungeachtet der Quelle der Attributmenge werden diese Vector–Objekte abschließend in der Hashtabelle, die als Parameter Object data übergeben worden ist, eingefügt. Als Schlüssel wird, wie bei jeder Einfügeoperation auf der Hashtabelle innerhalb des Visitor–Durchlaufs, ein Knotenobjekt der Klasse ASTTablePrimary benutzt, welches in dieser Methode der Kindknoten des aktuell besuchten Knotens ASTJoinClause node und gleichzeitig der Elternknoten des ASTTableName– bzw. ASTTableSubQuery– Knotens ist. Durch dieses Vorgehen wird neben der reinen Zuordnung zwischen Tabellenname und Spaltenmenge auch die Referenz auf den betroffenen Knoten für die spätere Umformung gespeichert. Um den beschriebenen Ablauf zu verdeutlichen, zeigt die Abbildung 4.11 die betroffenen Teile eines Beispielbaums mit der Besuchsreihenfolge. Der Wurzelknoten des abgebildeten Baums repräsentiert dabei den Knoten vom Typ ASTJoinClause an dem sich der Visitor–Durchlauf zum Zeitpunkt des Aufrufs dieser hier beschriebenen visit– Methode befindet. Der dargestellte Knoten ASTTablePrimary verfügt hier zusätzlich über eine Identifikationsnummer (ID), die die Eindeutigkeit eines erzeugten Objekts zur Laufzeit der Java VM symbolisieren soll. Unter dieser eindeutigen ID sind die beispielhaft gewählten Spaltennamen movie“ und genre“ der Beispieltabelle genre“ ” ” ” in der Hashtabelle abgelegt. Abbildung 4.11: Besuch eines ASTJoinClause Knotens Die Methode visit(ASTTableReference node, Object data) basiert auf den soeben beschriebenen Anweisungen innerhalb der Methode visit(ASTTableSubQuery node, Object data). Sie sind allerdings je einmal in zwei alternativen Blöcken enthalten, in die dieser Methodenkörper unterteilt ist. Im ersten Block stehen die Anweisungen, die ausgeführt werden sollen, falls der aktuelle Knoten ein Kind eines ASTFromClause– Knotens ist. Der im alternativen Fall abzuarbeitende zweite Block dieser Methode enthält die gleichen Operationen wie in der Methode visit(ASTTableSubQuery node, Object data). Innerhalb des ersten Blocks wird zu Beginn wieder die Attributmenge einer Tabelle oder Unterabfrage ermittelt und in die Hashtabelle unter dem entsprechenden eindeutigen Schlüsselknoten eingefügt. Dieser Schlüssel vom Typ ASTTablePrimary 55 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT entspricht an dieser Stelle immer dem linkesten Kindknoten des aktuell besuchten Knotens node. Daran schließt sich das Auslesen aller ermittelten Attributmengen innerhalb der aktuellen From–Klausel aus dem übergeben bekommenen Objekt vom Typ Hashtable an. Aus diesen Attributnamen wird die eventuelle Schnittmenge gemäß des Natural ” Join“–Operators gebildet und mit den eindeutigen Aliasbezeichnungen der Tabellen und Unterabfragen für den Eintrag in die Where–Klausel gespeichert. Außerdem werden an dieser Stelle auch sämtliche verfügbaren Spaltennamen unter dem Schlüssel des übergeordneten Knotens vom Typ ASTTableReference gespeichert, damit im Falle des select *“–Befehls alle Spaltennamen einer Unterabfrage für den Verbund–Operator ” bereit stehen. Es folgt die Veränderung der Spaltenreferenzen in allen Klauseln des Ausdrucks, die einem der Verbund–Attribute entsprechen. Sie werden mit der Aliasbezeichnung einer der Join–Partner qualifiziert, um die geforderte Eindeutigkeit der Spaltenreferenz herzustellen. Als vorletzter Teilaspekt dieses Blocks werden die, der Where–Klausel hinzuzufügenden, Gleichheitsausdrücke der Join–Attribute als ein String erstellt und an diesen der mögliche vorherige Inhalt der Where–Klausel angehängt. An dieser Stelle wird die Methode getLocalParser(String toParse) mit dem neuen Inhalt der Where– Klausel als Zeichenkette aufgerufen. Der Rückgabewert entspricht einem Parserobjekt, dessen Methode WhereClause() aufgerufen wird, um den benötigten Teilbaum dieser Klausel neu zu generieren. Der Rückgabewert dieser Methode ist schließlich der Wurzelknoten der Where–Klausel, welcher anschließend anstelle eines möglicherweise vorher schon existierenden Knotens innerhalb dieses QueryExpression–Zweiges in den Baum eingehängt wird. Der erste logische Block dieser Methode wird von einer Löschoperation der in dieser Methode verarbeiteten Attributmengen abgeschlossen. Durch die leere Attributmenge einer Tabelle in der, an den folgenden Knoten weiterzugebenden, Hashtabelle, werden in der folgenden From–Klausel alle einzufügenden Tabellen und Unterabfragen identifiziert. Dazu verbleiben die Schlüsseleinträge der ermittelten Tabellen weiterhin in der Hashtabelle. In der Abbildung 4.12 auf der folgenden Seite wird das beschriebene Verfahren veranschaulicht. Die umrandeten Zahlen eins bis zehn stellen wieder die Reihenfolge der besuchten Knoten dar. Der an der Positionsnummer acht hervorgehobene gemeinsame Attributnname movie“ der beiden Tabellen genre“ und remark“ ” ” ” stellt das Verbundattribut der Tabellen dar. Somit wird der Eintrag papl12.movie ” = papl13.movie“ in der entsprechenden Where–Klausel eingefügt. Weiterhin ist in der Abbildung unter den Positionen neun und zehn zu erkennen, wie alle gespeicherten Attributnamen der Tabellen aus dem übergebenen Parameter vom Typ Hashtable gelöscht worden sind und die, durch den natürlichen Verbund der beiden Tabellen verfügbaren, Attributnamen unter dem Schlüssel des ASTTableReference–Knotens in der globalen Variable ClauseManager.nodeToVector gespeichert werden. 56 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Abbildung 4.12: Ermitteln der Join–Attribute 57 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Die besagte From–Klausel wird mittels der Methode visit(ASTFromClause node, Object data) besucht und zwar, aufgrund des Postordnung–Durchlaufs, nach den Besuchen aller enthaltenen Knoten vom Typ ASTTableReference. Diese Methode erfüllt hierbei drei verschiedene Aufgaben: 1. Vereinigen der Attributmengen aller Kindknoten vom Typ ASTTableReference und Abspeichern dieser Gesamtmenge für den Knoten vom Typ ASTQueryExpression, in dessen Teilzweigen des Gesamtbaums der besuchte From–Klauselknoten enthalten ist 2. Erstellen und Einhängen des neuen Teilbaums der modifizierten From–Klausel 3. Löschen aller Einträge der in dieser From–Klausel enthaltenen Tabellen und Unterabfragen aus der weiterzugebenden Hashtabelle Die letzte Methode dieser Visitorklasse lautet visit(ASTQueryExpression node, Object data). In ihr wird die Menge der Attributnamen der Anfrage ermittelt, die durch den Teilbaum repräsentiert wird, dessen Wurzel der besuchte Knoten des Typs ASTQueryExpression ist. Zu diesem Zweck wird auf das Vector–Objekt der Spalten zugegriffen, welches in der vorher besuchten Methode der From–Klausel gespeichert worden ist. Der Schlüsselknoten, unter dem dieses Vector–Objekt anzusprechen ist, ist der Knoten node an dem sich der Baumdurchlauf aktuell befindet. Die Bestimmung der Ergebnisattribute dieser Anfrage setzt eine Auswertung des Kindknotens vom Typ ASTSelectList voraus, welcher durch die in Abschnitt 4.4 vorgestellte Methodik schnell ermittelt werden kann. Das Ergebnis der Operationen dieser visit–Methode kann zum einen die Umwandlung eines etwaigen select *“–Befehls in ” die Auswahl der durch den *“–Operator tatsächlich repräsentierten Attributnamen ” sein. Dies ist zwingend notwendig, da der Operator Natural Join“ alle Attribute, ” die zur Bildung des Verbunds benutzt worden sind, nur einmal in der Ergebnismenge beläßt. Wird diese Schreibweise des natürlichen Verbunds allerdings in die SQL-92– Syntax umgewandelt, so werden zwar auch die die Verbundbedingung erfüllenden Ergebnistupel aus dem karthesischen Produkt der beteiligten Tabellen geliefert. Jedoch sind weiterhin die Verbundattribute aller beteiligten Tabellen gemäß der Bildung des karthesischen Produkts enthalten. Bei jeder möglichen Konstellation einer geparsten Anfrage steht am Ende dieser visit–Methode zwingend die Speicherung der Ergebnisattribute dieser Anfrage in der globalen Variable ClauseManager.nodeToVector unter dem aktuellen Knoten node. Dies bedeutet gleichzeitig die Löschung der zuvor durch die visit–Methode der From– Klausel vorgenommene Speicherung der gesamten verfügbaren Attributmenge unter dem selben Schlüssel. Durch den Zugriff auf die hier abgespeicherte Ergebnismenge, wird gewährleistet, dass die visit–Methode für die Knoten vom Typ ASTTableSubquery, die bereits zuvor in ihrer Funktionalität beschrieben worden ist, stets die tatsächliche Attributmenge einer Unterabfrage in die weiterzugebende Hashtabelle einfügt. 58 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT 4.7.2 Aggregate–By Bei der Anfrage an ein föderiertes Datenbanksystem können Datenkonflikte auftreten, wenn einzelne Objekte aus enthaltenen Komponentendatenbanken zusammengefasst werden sollen. Diese Datenkonflikte sind dadurch begründet, dass sich die zusammenzufassenden Objekte in ihren Attributwerten unterscheiden. Die Klausel Aggregate–By der SQL–Erweiterung SQLSF ermöglicht es in diesem Fall, die Tupel der zusammenzufassenden Objekte zu Ergebnistupeln mit mehrwertigen Attributen zu aggregieren. Diese mehrwertigen Attribute werden in der Ergebnisrelation als Arrays dargestellt, die dem jeweiligen Wert des Aggregierungsattributs zugeordnet sind. Bei der Transformation einer SQLSF–Anfrage, die eine Aggregate–By–Klausel enthält, in ihr semantisch äquivalentes SQL-92 Pendant werden Aggregierungsfunktionen zur Bildung der Arrays aus mehrwertigen Attributen eingesetzt. Diese Aggregierungsfunktionen sind nach den möglichen Datentypen der Attribute gegliedert und werden mit den mehrwertigen Attributen als Parameter in der Select–Klausel aufgerufen. Diese Vorgehensweise gilt allerdings nur für solche Attribute der Select–Klausel, die nicht zur Aggregierung in der Aggregate–By–Klausel eingesetzt werden. Im Konzept zur Transformation eines Ausdrucks dieser SQL–Erweiterung soll ein Postordnung–Durchlauf des Syntaxbaums zum Ergebnis in Form eines semantisch äquivalenten SQL-92–Ausdrucks führen. Bei diesem Durchlauf sind zunächst die Attributmengen der beiden Klauseln Aggregate–By und Select zu bestimmen, bevor anschließend die Attribute der Select–Klausel ermittelt werden, die nicht in der Aggregate–By–Klausel enthalten sind. Anhand dieser dritten Attributmenge wird dann die Select–Klausel um die Aufrufe der datentypabhängigen Aggregierungsfunktionen erweitert. Im letzten Schritt wird die Aggregate–By–Klausel durch eine Group– By–Klausel ersetzt, die die zuvor für die Aggregate–By–Klausel ermittelte Attributliste beinhaltet. Neben einer konkreten Visitorklasse, die während des Durchlaufs des Syntaxbaums die Transformation vornimmt, ist es sinnvoll zwei Datenstrukturen als Hilfsmittel im Vorfeld zu erzeugen. Hier ist als erstes eine festdefinierte Zuordnung zwischen den möglichen Datentypen der Attribute und der anzuwendenden Aggregierungsfunktion in Form einer globalen Hashtabelle angedacht. Das zweite Hilfsmittel ist eine neu zu schaffende Klasse, die die ermittelten Tupelattribute mit den folgenden Eigenschaften abbildet: • ein Knotenobjekt des geparsten Syntaxbaums, dass das ermittelte Attribut darstellt • die Benennung des Attributs • die Benennung des optionalen Qualifizierers • der aus dem Data Dictionary ermittelte Datentyp des Attributs 59 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT • eine Methode die zwei Attribute auf Gleichheit des Datentyps überprüft Für den Start des Postordnung–Durchlaufs des Syntaxbaums wird im nächsten Schritt eine Instanz der nachfolgend beschriebenen Visitorklasse erzeugt und für die Weitergabe innerhalb des Syntaxbaums eine Hashtabelle aus Mengen von Objekten der Attributklasse. Diese Tabelle beinhaltet nach dem Besuch der entsprechenden Knoten die Menge der Attribute der Select–Klausel bzw. der Aggregate–By–Klausel und alle in den Tupelmengen der From–Klausel enthaltenen Attribute. In der Visitorklasse, die diese Beispielerweiterung gemäß der zu Beginn genannten Regeln verändert, sind für den Besuch vier verschiedener Knotentypen die entsprechenden Anweisungen zur Transformation zu implementieren. Gemäß des Postordnung– Durchlaufs ist es der Knoten der Select–Klausel, der von diesen vier Knotentypen zuerst besucht wird. Anschließend folgt der Besuch der From–Klausel und nachfolgend der der Aggregate–By–Klausel, bevor abschließend der Elternknoten dieser drei, der die Gesamtanfrage darstellt, besucht wird. Der betreffende Ausschnitt des Syntaxbaums ist in der folgenden Abbildung 4.13 zu sehen. Abbildung 4.13: Teilbaum einer SQLSF–Anfrage In der visit–Methode der Select–Klausel werden sämtliche enthaltenen Attribute ermittelt und als Objekte der zuvor beschriebenen Hilfsklasse abgebildet. Dazu werden zunächst die die Attribute repräsentierenden Knoten aus dem Teilbaum der Select–Klausel ermittelt. Die Referenzen auf diese Knoten werden in den jeweiligen Objekten der Attributklasse gespeichert, bevor die Benennungen des Attributs und seines Qualifizierers aus den verlinkten Tokenobjekten gelesen und in die entsprechenden Eigenschaften des Objekts geschrieben werden. Die den Datentyp des Attributs repräsentierende Eigenschaft wird zu diesem Zeitpunkt nicht gefüllt, da erst die Zuordnung zu der das Attribut enthaltenen Tupelmenge eine entsprechende Anfrage an das Data Dictionary ermöglichen würde. Nachdem alle Objekte der gefundenen Attribute erzeugt und mit Daten gefüllt worden sind, erfolgt die Speicherung dieser Objektmenge unter dem Schlüssel des aktuellen Select–Klauselknotens in der Hashtabelle, die zum nachfolgenden Knoten weitergegeben wird. In der Reihenfolge der betrachteten Klauselknoten steht als nächstes der Besuch der From–Klausel an. In deren visit–Methode wird für alle enthaltenen Tabellen oder 60 KAPITEL 4. KONZEPT DER ERWEITERBARKEIT Unterabfragen die Menge der Attribute ermittelt. Für jedes dieser Attribute wird wie zuvor in der Select–Klausel ein Objekt der Attributklasse erzeugt und mit der aus dem Data Dictionary ermittelten Benennung und dem Datentyp versehen. Die Referenz auf ein konkretes Knotenobjekt bleibt leer, jedoch wird die Benennung des Qualifizierers entweder mit dem Tabellennamen oder, falls vorhanden, dem Tupelalias gefüllt. Die Menge dieser Attribute wird wiederum in der Hashtabelle gespeichert, jedoch dieses mal unter dem Knoten der From–Klausel. Beim Besuch der Aggregate–By–Klausel schließlich wird auf die gleiche Weise, wie beim Besuch der Select–Klausel die Attributmenge bestimmt und unter dem Knoten der jetzt besuchten Klausel in der weiterzugebenden Hashtabelle abgespeichert. Zum Abschluss des Visitor–Durchlaufs einer Anfrage wird deren Wurzelknoten besucht. In der visit–Methode dieses Knotentyps werden die bisher erstellten Attributmengen verarbeitet, sowie auf der Basis der dadurch erzeugten Teilmenge der Attribute die Select–Klausel verändert. Außerdem wird an dieser Stelle die bisher enthaltene Aggregate–By–Klausel gelöscht und eine neue Group–By–Klausel eingefügt. Die in dieser visit–Methode durchzuführenden Operationen sollen nun detaillierter vorgestellt werden. Im ersten Schritt wird die Menge der Attribute der Select– und der Aggregate–By–Klausel auf Gleiheit der Benennung verglichen und die Attribute der Select–Klausel, welche kein gleichlautendes Gegenstück in der Aggregate–By– Klausel haben, in einer neuen Menge gespeichert. Diese neue Teilmenge aller Attribute der Select–Klausel wird mit der Attributmenge aus der From–Klausel ebenfalls bzgl. gleichlautender Benennungen verglichen. Bei einem Treffer wird die entsprechende Angabe des Datentyps im Attributobjekt der Select–Klausel gespeichert, um nach dem Abschluss aller Vergleiche die passende Aggregierungsfunktion jedes Attributs auswählen zu können. Im nächsten Schritt wird der ursprüngliche Knoten jedes Attributs aus der Select–Klausel entfernt und durch den Teilbaum des Aufrufs der Aggregierungsfunktion mit dem jeweiligen Attribut als Parameter ersetzt. Die anzuwendende Aggregierungsfunktion wird hierbei durch den Zugriff auf die zu Beginn beschriebene globale Hashtabelle mit möglichem Datentyp und zugehöriger Aggregierungsfunktion bestimmt. Der letzte Schritt dieser Transformation besteht schließlich darin, den Teilbaum der Aggregate–By–Klausel durch einen der Group–By–Klausel zu ersetzen, der alle ursprünglich in der Aggregate–By–Klausel enthaltenen Attributknoten übernimmt und somit die selbe Attributliste darstellt. 61 Kapitel 5 Export der abstrakten Baumstruktur Neben der im Kapitel 4 dargestellten Erweiterbarkeit ist auch der Export der Baumstruktur in einem XML-Format ein Ziel dieser Arbeit. Dieser Aspekt der Zielsetzung wird durch das hier beschriebene XML-Schema verwirklicht. Die allgemeine Definition der Extensible Markup Language (XML) erlaubt es, die geparste Herleitung des SQL–Ausdrucks in Form eines Syntaxbaums in einem eigenen Dokumentenformat darzustellen. Die Struktur dieser Dokumente wird durch die Darstellung aller Zeichen im Unicode-Format und die Kennzeichnung der inhaltlichen Angaben durch die jeweiligen Tags (Markierungen) charakterisiert. Der Export des geparsten Syntaxbaums wird im Rahmen von Parseplus im PAPLFormat (PAPL als Abkürzung für PArse P lus Language) ausgeführt. Dieses Format basiert auf der Benennungen der Produktionen als öffnende Tags des jeweiligen Knotens und bietet außerdem Information über die Token, die den jeweiligen Knoten des Baums zugeordnet sind. Das entwickelte Format der PAPL-Dateien wird im folgenden Abschnitt 5.1 beschrieben. Die im Abschnitt 7.4 beschriebene Implementierung des Exports ist in der Basisklasse SimpleNode der Baumstruktur realisiert worden. Der Export der Baumstruktur ist in seiner Methodik unabhängig von den abgeleiteten Knotenklassen, wodurch der Vorteil des Visitor Pattern (s. Abschnitt 4.3), eine einheitliche Schnittstelle für die unterschiedliche Verarbeitung der einzelnen Komponenten des Syntaxbaums anzubieten, hier nicht zum Tragen kommt. Da auch die Baumknoten jeder möglichen SQL–Erweiterung im PAPL-Format repräsentiert werden sollen, ist es von Vorteil, die Exportroutine zentral in der Klasse SimpleNode zu implementieren, da alle Knoten eine Instanz dieser Klasse darstellen. Die alternative Implementierung gemäß des Visitor Pattern hätte entweder die automatische Erzeugung einer neuen Visitorklasse für den Besuch aller Knotenklassen der Erweiterung und eines entsprechenden Startbefehls zum Baumdurchlauf in der Parserklasse oder allen generierten Knotenklassen zur Folge gehabt. Oder es hätte eine Visitorklasse erzeugt werden müssen, die in den visit–Methoden für die Knoten der Klasse SimpleNode (für alle Knotenklassen der Er- 62 KAPITEL 5. EXPORT DER ABSTRAKTEN BAUMSTRUKTUR weiterung) und allen Knoten eines SQL–92 Baums die Logik des Syntaxbaumexports aufruft. Dies hätte im Ergebnis wieder die Nutzung der zentralen Basisklasse notwendig gemacht, weshalb sich die umgesetzte Implementierungsform als effizientere Alternative erweist. 5.1 Definition des PAPL-Formats Die Darstellung des Syntaxbaums im PAPL-Format beschränkt sich auf die Knoten deren Anfangstoken nicht den Anfangstoken des linkesten Kindknotens, bzw. deren Endetoken nicht dem Endetoken des rechtesten Kindes entsprechen. Hierdurch werden in der Darstellung der Baumstruktur die Knoten der nichtterminalen Symbole ausgespart, die nur Token enthalten, welche nachfolgende Knoten ebenfalls referenzieren. Als Ergbnis entsteht eine übersichtlichere Baumstruktur des geparsten Ausdrucks. Die grundlegende Struktur besteht in einem öffnenden Tag des darzustellenden Knotens mit den folgenden Attributen: die Id der Knotenart (Schlüssel im Interface <Parsername>TreeConstants), die Anfangsposition des ersten und die Endeposition des letzten enthaltenen Token. Als Kindelemente eines solchen Knotentags können die Tags der enthaltenen Token und/oder der Kindknoten in beliebiger Reihenfolge geschrieben werden. Die Tokentags werden mit dem Schlüsselwort Token“ markiert und enthalten als ” Attribute den dargestellten String sowie die zugewiesene Id aus dem Interface <Parsername>Constants. Diese Tokentags verfügen immer über genau ein Kindelement vom Typ Position, welches in seinen vier Attributen die Position des Token in der Eingabe definiert. Die Bezeichnungen der benutzten Tags hängt somit auch von der Erweiterung der erstellten SQL-92 Grammatik ab, weswegen in diesem Abschnitt die Gruppe der unterschiedlichen Tags der Syntaxbaumknoten durch den Ausdruck <Node> dargestellt ist. Beschreibung des <Node>–Tags: • Attribute: 1. Id ist der zugewiesene globale Integerwert aus dem Interface <Parsername>TreeConstants (Datentyp: positiveInteger) 2. BeginLine gibt die Anfangszeile des ersten Token des Knotens an (Datentyp: positiveInteger) 3. BeginColumn gibt die Anfangsspalte des ersten Token des Knotens an (Datentyp: positiveInteger) 4. EndLine gibt die Endeszeile des letzten Token des Knotens an (Datentyp: positiveInteger) 63 KAPITEL 5. EXPORT DER ABSTRAKTEN BAUMSTRUKTUR 5. EndColumn gibt die Endesspalte des letzten Token des Knotens an (Datentyp: positiveInteger) • Kindelemente: – Token beschreibt die Token, die nur in diesem Knoten, aber keinem Kindknoten enthalten sind. Von diesen Tags können beliebig viele innerhalb einer <Node>-Markierung enthalten sein. – <Node> stellt die enthalten Kindknoten gemäß der Baumstrukur dar. Auch von diesen Elementen können beliebig viele als Kinder zugeordnet sein. Beschreibung des <Token>–Tags: • Attribute: 1. Id ist der zugewiesene globale Integerwert aus dem Interface <Parsername>Constants (Datentyp: positiveInteger) 2. String enthält den String des dargestellten Token (Datentyp: String) • Kindelemente: – Position stellt die Position des Token im Eingabezeichenstrom dar. Beschreibung des <Position>–Tags: • Attribute: 1. BeginLine gibt die Anfangszeile des Token an (Datentyp: positiveInteger) 2. BeginColumn gibt die Anfangsspalte des Token an (Datentyp: positiveInteger) 3. EndLine gibt die Endeszeile des Token an (Datentyp: positiveInteger) 4. EndColumn gibt die Endesspalte des Token an (Datentyp: positiveInteger) Zur Veranschaulichung des vorgestellten PAPL-Schemas ist in der folgenden Abbildung 5.1 ein SQL–Ausdruck und Auszüge aus der es repräsentierenden PAPL-Datei dargestellt. Die letzen beiden Spalten innerhalb der Select-Liste sind in dieser Abbildung aus Gründen der Übersichtlichkeit ausgespart worden, zumal ihre Darstellung sich nicht von der der ersten Spalte (persid) unterscheidet. Die hinzugefügten Pfeile ausgehend von den Schlüsselwörtern der Anfrage verweisen auf die entsprechenden Tags innerhalb der PAPL-Datei. 64 KAPITEL 5. EXPORT DER ABSTRAKTEN BAUMSTRUKTUR Abbildung 5.1: Ein SQL–Ausdruck und die entsprechende PAPL-Darstellung 65 Kapitel 6 Semantische Analyse Die semantische Analyse des zuvor auf syntaktische Korrektheit hin überprüften SQL92–Ausdrucks ist das Thema dieses Kapitels. Um diesen Teil des erweiterbaren SQLCompilers Parseplus zu verwirklichen, ist wiederum auf das Visitor Pattern zurückgegriffen worden, welches bereits im Abschnitt 4.3 erläutert worden ist. Das Ziel der semantischen Analyse ist es hierbei, die Referenzierung von Tabellenattributen zu ermitteln, welche in keiner der Tabellen der From–Klausel enthalten sind. Die konkrete Visitorklasse SemanticVisitor des Visitor Pattern führt zu diesem Zweck eine inhaltliche Überprüfung durch und bricht bei gegebenenfalls gefundenen Fehlern mit einer entsprechenden Meldung ab. Die genannte Visitorklasse besucht die Knoten des geparsten Syntaxbaums in Postordnung, um die Verschachtelung in mögliche Unterabfragen korrekt verarbeiten zu können. Dieser Durchlauf wird in der Methode semanticizeSQL92Statement der Klasse ParsePlus mit dem Aufruf der Methode acceptSemanticVisitor des Wurzelknotenobjekts angestoßen. Im nachfolgenden Baumdurchlauf werden lediglich beim Besuch der Knoten des Typs ASTFromClause oder ASTQueryExpression Operationen zur semantischen Überprüfung durchgeführt. Der Besuch einer From–Klausel führt zu der Anforderung der einzelnen Spaltennamen der enthaltenen Tabellen und Unterabfragen vom Data Dictionary, sowie zu deren Speicherung in logischer Verknüpfung zum Elternknoten dieser From–Klausel, welcher gemäß der Grammatik immer vom Typ ASTQueryExpression ist. Durch den Besuch der Knoten in Postordnung ist weiterhin sichergestellt, dass erst im Anschluß an die From–Klausel die visit–Methode der Anfrage (ASTQueryExpression) aufgerufen wird. Dort wird auf die, zuvor in der From–Klausel ermittelten, Menge der maximal verfügbaren Spaltennamen zugegriffen und jede enthaltene Teilklausel auf die korrekte Verwendung der Spaltenreferenzen hin überprüft. Zu diesem Zweck werden mittels sequentieller Anfragen an den Klauselmanager(Klasse ClauseManager, s. Abschnitt 4.4) die Wurzelknoten sämtlicher in dieser Anfrage enthaltenen Klauseln ermittelt. Für jede dieser Klauseln werden anschließend die darin enthaltenen Knoten vom Typ ASTColumnReference durch weitere Anfragen an den Klauselmanager für semantische Überprüfungen verfügbar gemacht. 66 KAPITEL 6. SEMANTISCHE ANALYSE Jeder dieser ASTColumnReference–Knoten wird dahingehend überprüft, ob er eine durch die From–Klausel zur Verfügung gestellte Spalte referenziert. Diese Prüfung kontrolliert sowohl die Bezeichnung des referenzierten Spaltennamens, als auch den möglicherweise eingesetzten Qualifizierer der Spaltenreferenz. Hierbei werden die Teile des Qualifizierers überpüft, die den Tabellennamen oder eine alternative Aliasbezeichnung darstellen, sowie im Falle eines Tabellennamens eine möglicherweise vorgestellte Schemabezeichnung. Die Abbildung 6.1 zeigt das Sequenzdiagramm des Visitor Baumdurchlaufs, welcher durch den Aufruf der Methode semanticizeSQL92Statement initiiert wird. Nachdem ein beliebiger Knoten des Syntaxbaums besucht worden ist, der weder eine Instanz der Klasse ASTFromClause noch ASTQueryExpression ist, wird in diesem Beispiel ein Knoten der Klasse ASTFromClause besucht. Daran schließt sich ein erneuter Besuch eines Knotenobjekts an, das wiederum keine From–Klausel oder Anfrage darstellt. Die nächste Station des Baumdurchlaufs ist dann ein ASTQueryExpression–Knoten, in dessen visit–Methode die fehlerhafte Benutzung einer Spaltenreferenz entdeckt und in der Folge eine SemanticException ausgelöst wird. Der textuelle Inhalt dieser Exception wird anschließend bis zum Wurzelknoten des Gesamtstatements weitergegeben und ist schließlich der Rückgabewert der Methode semanticizeSQL92Statement an das aufrufende Programm. Die Darstellungsform dieses Sequenzdiagramms folgt der allgemeinen UML–Notation, nach der die vertikale Achse des Diagramms die Zeitachse abbildet und die waagerechte Kopfzeile alle beteiligten Instanzen in Form ihrer Klassenbezeichnungen enthält. Die senkrechten Balken unterhalb der einzelnen Objekte stellen die Zeitspanne ihrer Existenz dar. Hierbei symbolisieren gestrichelte Abschnitte des Balkens die inaktive Phase und die durchgezogenen Balkenabschnitte die aktive Phase des Objekts. Die Kreuzsymbole am Ende der Existenzbalken der Objekte vom Typ SemanticVisitor und SemanticException bilden das Löschen der Objekte zur Laufzeit des Programms ab. Im Gegensatz dazu bleiben sämtliche Knotenobjekte auch nach der Beendigung der semantischen Überprüfung im Hauptspeicher verfügbar, was durch das Weiterführen ihrer Existenzbalkens dargestellt ist. 67 KAPITEL 6. SEMANTISCHE ANALYSE Abbildung 6.1: Sequenzen der semantischen Analyse im Fehlerfall 68 Kapitel 7 Implementierung Das 7. Kapitel Implementierung beschreibt die Umsetzung der Konzepte und Ideen der vorausgegangenen Kapitel 4 bis 6. Als Entwicklungsplattform der Implementierung ist das Java Development Kit (JDK) in der Version 1.5 genutzt worden. 7.1 Realisierte Schnittstellen Zur gewünschten Einbindung in zukünftige javabasierte Softwareprojekte des Instituts ist es von großer Bedeutung gewesen, klar definierte Schnittstellen des Parseplus zu entwickeln. Daher folgt nun eine Auflistung der realisierten Schnittstellen, welche ihre Entsprechung in den, in Klammern aufgeführten, Methoden der Klasse ParsePlus im Paket parseplus haben. Der Input des SQL-Compilers setzt sich zusammen aus: 1. den Verbindungsdaten zum DBS, an welches das (erweiterte) SQL–Ausdruck gerichtet werden soll (Methode setDBSConnection(Connection conn)) 2. dem zu parsenden SQL-92–Ausdruck - inkl. aller gewünschten Formatierungssymbole (Methode startParsing(String statement oder startParsing(File statement) 3. der Auswahl der Grammatik auf der der erweiterte Parser basieren soll (Methode main(String[] args)) 4. einem Objekt des erweiterten Parsers, welcher mit dem zu parsenden Ausdruck des erweiterten SQL instantiiert wurde (Methode startParsingExtension(SQLParser parserObj)) Für den unterschiedlichen Output des Programms Parseplus sind folgende Schnittstellen realisiert worden: 69 KAPITEL 7. IMPLEMENTIERUNG Die Ausgabe . . . 1. der Wurzelknoten des Syntaxbaums des geparsten SQL–Ausdrucks (SQL-92 oder erweitertes SQL) (Methode getRoodNode()) 2. der Meldung eines Parsingfehlers mit der Positionsangabe des Auftretens und den gemäß Grammatik erwarteten Token (für SQL-92 und erweitertes SQL) (jede der drei startParsing“-Methoden) ” 3. des Syntaxbaums (für SQL-92 und erweitertes SQL) samt zugeordneter Token im PAPL-Format (Methode exportXMLTree(String outFile, AdvancedNode root)) 4. des, in den SQL-92 Standard umgewandelten, erweiterten SQL–Ausdrucks (Methoden transformExtdSQL() und getSQLStatement()) 5. des geparsten SQL–Ausdrucks (SQL-92 oder erweitertes SQL) (Methode getSQLStatement()) 6. etwaiger semantischer Fehler im geparsten SQL-92–Ausdruck (Methode semanticizeSQL92Statement()) 7.2 Paketstruktur Die Anwendung Parseplus setzt sich aus zehn Teilpaketen zusammen. Nachfolgend werden die Pakete genannt und die in ihnen realisierte Funktionalität kurz erläutert, bevor sie dann in den gleichnamigen Abschnitten dieses Kapitels im Detail vorgestellt werden: parseplus ist das Hauptpaket der beschriebenen Anwendung. In ihm ist ausschließlich die Klasse ParsePlus enthalten, welche die, im Abschnitt 7.1 genannten, Schnittstellen des erweiterbaren SQL-Compilers nach außen bietet. parseplus.modifier enthält die gemeinsamen Klassen und Interfaces des zugrundeliegenden SQL-92 Compilers und der abgeleiteten Erweiterungen. Das beinhaltete Interface AdvancedNode ist die gemeinsame Schnittstelle aller erstellten Syntaxbaumknoten und die Klasse SimpleNode stellt die Basisklasse des abstrakten Datentyps Syntaxbaum dar. Das Interface SQLParser deklariert die einheitlichen Zugänge zu allen erzeugten Parsern. Von der Klasse Token dieses Pakets werden sämtliche geparsten Tokenobjekte abgeleitet und die Klasse ParseException stellt die lokalisierte Fehlermeldung zur Verfügung. Außerdem enthält das Paket parseplus.modifier noch die abstrakte Klasse Modifier, welche unterschiedliche Methoden zur Baummanipulation und Bearbeitung der verketteten Tokenliste beinhaltet. Das Interface ParsePlusConst schließlich bietet den Zugriff auf globale Konstanten zur Steuerung der Programmfunktionen. 70 KAPITEL 7. IMPLEMENTIERUNG parseplus.sql92parser.parser beinhaltet die Klasse des Parsers SQL92 der im Anhang A beschriebenen SQL-92 Grammatik. Zusätzlich dazu sind die Klassen des lexikalen Scanners (SQL92TokenManager und JavaCharStream) und der Darstellung von Tokenfehlern (TokenMgrError) enthalten. Das Interface SQL92Constants schließlich verwaltet die in der Grammatikdatei definierten Token als Stringobjekte. parseplus.sql92parser.treebuilder ist das Paket, das die Klassen und Interfaces zur Syntaxbaumerzeugung nach außen kapselt. Für jede in der Grammatik definierte Produktion ist in diesem Paket eine Klasse mit dem Namen des nichtterminalen Symbols auf der linken Seite und dem Präfix AST“ (Abstract ” Syntax Tree) hinterlegt. Nur Objekte, die von einer dieser Klassen abgeleitet worden sind, können als Knoten der Datenstruktur Syntaxbaum während des Parsens eingefügt werden. Alle konkreten Komponentenklassen sind von der Basisklasse der Baumstruktur (SimpleNode) abgeleitet und können durch die global ansprechbaren Integerwerte des Interfaces SQL92TreeConstants identifiziert werden. Der Stapel, der während des Vorgangs des Parsens zur Verwaltung der erzeugten Knotenobjekte dient, wird durch die Klasse JJTSQL92State dieses Paket repräsentiert. Auch das Interface SQL92Visitor ist in diesem Paket als Implementation des abstrakte Visitors enthalten, ebenso wie die beiden konkreten Visitorklassen BasicVisitor und RunThroughVisitor. parseplus.dbcontent kapselt die Klassen zur Verwaltung der Daten des Datenbankkatalogs. Die Klasse DBGlobals bietet die Schnittstellen zum Speichern und Abrufen eines Verbindungsobjekts. Dieses Verbindungsobjekt wird in der Klasse DBContent genutzt, um vorgefertigte Anfragen an das Data Dictionary zu richten. parseplus.semantics enthält die Klassen zur Überprüfung der korrekten semantischen Benutzung der Spaltenbenennungen. org.javacc.jjtree entspricht in der Benennung dem original Paket des benutzten JavaCC–Präprozessor JJTree. In diesem Paket sind die funktional erweiterten Klassen von JJTree enthalten. Die hinzugefügte Funktionalität beschränkt sich auf die Erzeugung der Java–Dateien für Parser von Erweiterungen, welche auf das Zusammenspiel mit den Klassen der Pakete parseplus und parseplus.modifier abgestimmt worden sind. org.javacc.parser entspricht in der Benennung dem Originalpaket des Java Compiler Compilers in der Version 4.0. Aus dem Originalpaket wird lediglich die Klasse OtherFilesGen in ihrer Funktionalität insofern verändert, als dass die Erzeugung der Java-Code Dateien ParseException.java und Token.java unterbunden wird. 71 KAPITEL 7. IMPLEMENTIERUNG com ist ein Teilpaket des Java Web Services Developer Pack 1.6(JWSDP 1.6) zur seriellen Erstellung der XML-Datei mittels des StAX-Verfahrens. javax ist ein Teilpaket des Java Web Services Developer Pack 1.6(JWSDP 1.6) zur seriellen Erstellung der XML-Datei mittels des StAX-Verfahrens. Im Anschluss an die obige Auflistung und Erläuterung der Inhalte der einzelnen Pakete der Anwendung bietet die Abbildung 7.1 eine graphische Übersicht der Paketstruktur. Abbildung 7.1: Paketdiagramm der Applikation Parseplus 7.3 Paket parseplus Dieses Paket kapselt die Hauptklasse der Anwendung nach außen hin ab. Diese Klasse ParsePlus stellt die in Abschnitt 7.1 beschriebenen Schnittstellen mittels der folgenden statischen Methoden bereit: + exportXMLTree(String outFile, AdvancedNode root) : void. Dies ist die Methode zur Ausgabe des erstellten Syntaxbaums in einer Datei vom Typ *.papl, welches ein auf XML basierendes Format ist. Der erste Parameter bestimmt den zu benutzenden Dateinamen, welcher schon die Endung .papl enthalten kann. Ansonsten wird diese Endung automatisch hinten an den String 72 KAPITEL 7. IMPLEMENTIERUNG outFile angefügt. Der zweite Parameter ist ein Objekt vom Typ AdvancedNode, welches den Wurzelknoten darstellt, mit dem die Ausgabe begonnen wird. Die Ausgabe der Baumstruktur wird mittels der Pull-API StAX realisiert. Durch Anwendung des Iterator-Pattern in der Methode SimpleNode.doXMLExport(XMLEventFactory, XMLEventWriter, String) werden dort sowohl ein XMLEventWriter, der auf die gewünschte Ausgabedatei gesetzt ist, als auch ein Objekt der Klasse XMLEventFactory benötigt. Diese zu übergebenen Objekte werden in der hier beschriebenen Methode erzeugt und eventuelle Fehler bei der Ausgabe abgefangen. Auch die Ausgabe der Kopfzeile der .paplDatei wird hier geschrieben und nach Ausgabe des letzten Knotens die Datei geschlossen. Parameter: outFile - ist der String des Dateinamens root - ist der Wurzelknoten für die Baumausgabe + getRootNode() : AdvancedNode. Diese Methode liefert das zuletzt hinterlegte AdvancedNode–Objekt, welches die Wurzel des vorher erzeugten Syntaxbaums referenziert. Sollte noch kein oder ein fehlerhaftes Parsing durchgeführt worden sein, so würde null zurückgegeben werden. Rückgabewert: das im statischen Attribut AdvancedNode root gespeicherte Objekt vom Typ AdvancedNode (kann auch gleich null sein) + getSQLStatement() : String. Diese Methode gibt, angefangen beim ersten Token des Knotens private static AdvancedNode root bis zum letzten Token dieses Knotens, das geparste und ggfls. danach semantisch überprüfte SQL–Ausdruck als String zurück. Hierzu wird die Methode public String getContent(boolean choice) der Wurzel aufgerufen und deren Rückgabewert zurückgegeben. Ist die Initialisierung mit einem durch den Parser erstellten Wurzelobjekt nicht geschehen, und ist die Wurzel somit gleich null, so wird auch der Parser gleich null gesetzt und die Methode mit der Rückgabe von null beendet. Rückgabewert: das geparste und optional veränderte SQL–Ausdruck als String, oder null, wenn root gleich null sein sollte + main(String[] args) : void. Diese Methode ist die Schnittstelle für das Erstellen der Klassen zur Baumerzeugung und zum Parsen auf Basis einer Grammatikdatei. Es werden nacheinander zwei GUIs zur Auswahl der Grammatikdateien eingeblendet, die erste mit dem HOME-Verzeichnis des Benutzers initialisiert und die zweite mit dem Verzeichnis in dem die Syntaxbaumklassen erstellt wurden. Die erste Auswahl betrifft die *.jjt-Datei aus welcher die Syntaxbaumklassen und die Grammatik im Dateiformat *.jj abgeleitet werden. Auch wenn dieser Dialog abgebrochen werden sollte, kann trotzdem anschließend ein Parser anhand einer auszuwählenden *.jj-Datei erzeugt werden. Parameter: args - String-Array der uebergebenen Parameter (werden nicht benutzt). 73 KAPITEL 7. IMPLEMENTIERUNG + semanticizeSQL92Statement() : String. Dieses ist die Methode zur Ausführung der semantischen Analyse des zuvor geparsten SQL-92–Ausdrucks. Die Methode benutzt dazu das Wurzel-Attribut private static AdvancedNode root, so dass es vorher initialisiert werden sollte! Die semantische Analyse wird dann durch einen Baumdurchlauf realisiert, der zwingend diese Wurzel als Einstieg benötigt. Ist die Initialisierung mit einem durch den Parser erstellten Wurzelobjekt nicht geschehen, und ist die Wurzel somit gleich null, so wird auch der Parser gleich null gesetzt und die Methode ohne eine semantische Überprüfung beendet. Rückgabewert: der String der Fehlermeldung der semantischen Analyse oder null, falls keine Fehler auftraten + setDBSConnection(Connection conn) : void. Mittels dieser Methode wird das übergebene Connection-Objekt als Verbindung für anstehende semantische Überprüfungen gesetzt. Um die Funktionsweise der Methode semanticizeSQL92Statement dieser Klasse nutzen zu können, muss die Connection vorher gesetzt werden. Parameter: conn - das existierende Connection-Objekt für die Verbindung zur Datenbank + startParsing(File statement) : String. Durch den Aufruf dieser Methode wird der Inhalt des übergebenen File-Objekts auf syntaktische Korrektheit gemäß des SQL-92 Standards hin überprüft. Nachdem zunächst das File-Objekt auf Existenz der repräsentierten Datei hin überprüft wurde, wird ein Objekt des SQL-92 Parsers erzeugt und dem statischen Attribut SQLParser parser zugewiesen. Im Anschluss daran wird der übergebene SQL–Ausdruck geparst und der Wurzelknoten des erzeugten Syntaxbaums in dem statischen Attribut AdvancedNode root hinterlegt. Sollten während der Überprüfung des File-Objekts oder während des Parsens Fehler auftreten, so wird die entsprechende Meldung als String von dieser Methode zurückgegeben. Außerdem würden sowohl das Parserobjekt private static SQLParser parser, als auch die Wurzel private static AdvancedNode root gleich null gesetzt werden. Parameter: statement - ein File-Objekt, welches die Eingabedatei repräsentiert Rückgabewert: ein String, der die Fehlermeldung enthält, oder null, wenn keine Fehler auftraten + startParsing(String statement) : String. Durch den Aufruf dieser Methode wird der Inhalt des übergebenen Strings auf syntaktische Korrektheit gemäß des SQL-92 Standards hin überprüft. Nachdem zunächst der JavaCharStream und der SQL92TokenManager im Konstruktor des Parsers initialisiert wurden, wird ein Objekt des SQL92-Parsers erzeugt und dem statischen Attribut private static SQLParser parser zugewiesen. Im Anschluss daran wird der übergebene SQL–Ausdruck geparst und der Wurzelknoten des erzeugten Syntaxbaums in 74 KAPITEL 7. IMPLEMENTIERUNG dem statischen Attribut private static AdvancedNode root hinterlegt. Sollten während des Parsens Fehler auftreten, so wird die entsprechende Meldung als String von dieser Methode zurückgegeben. Außerdem würden sowohl das Parserobjekt private static SQLParser parser, als auch die Wurzel private static AdvancedNode root gleich null gesetzt werden. Parameter: statement - ist ein String, welcher den SQL–Ausdruck enthält Rückgabewert: ein String, der die Fehlermeldung enthält, oder null, wenn keine Fehler auftraten + startParsingExtension(SQLParser parserObj) : String. Durch den Aufruf dieser Methode wird das Parsing mit dem Objekt des erweiterten Parsers gestartet. Im Anschluss daran wird der Wurzelknoten des erzeugten Syntaxbaums in dem statischen Attribut private static AdvancedNode root hinterlegt. Sollten während des Parsens Fehler auftreten, so wird die entsprechende Meldung als String von dieser Methode zurückgegeben. Außerdem würden sowohl das Parserobjekt private static SQLParser parser, als auch die Wurzel private static AdvancedNode root gleich null gesetzt werden. Parameter: parserObj - ist das Objekt des erweiterten Parsers Rückgabewert: ein String, der die Fehlermeldung enthält, oder null, wenn keine Fehler auftraten + transformExtdSQL(Object data) : Object. Durch diese Methode gibt es die Möglichkeit, die Transformation des erweiterten SQL in SQL-92 unabhängig von der Art der Erweiterung zu starten. Zu diesem Zweck wird eine der gemeinsamen Schnittstellen aller Parser, nämlich die Methode transformSQL, des statischen Parserobjekts SQLParser parser der Klasse ParsePlus aufgerufen. Diese Methode des Parserobjekts ihrerseits führt dann die erweiterungsabhängige Implementation des Visitor Pattern aus. Ist keine Referenz im angesprochenen statischen Parserobjekt hinterlegt, so ist der Rückgabewert dieser Methode gleich null. Ist SQLParser parser ungleich null, so wird das Objekt geliefert, das die jeweilige Parsermethode transformSQL zurückgibt (kann auch gleich null sein). Rückgabewert: das Rückgabeobjekt der aufgerufenen Parsermethode oder null, falls keine Referenz auf einen Parser gespeichert ist Die Referenzen des zuletzt benutzten Parsers und des Wurzelknotens des letzten geparsten SQL–Ausdrucks werden in diesen statischen Attributen gespeichert: - SQLParser parser. Statisches SQLParser Objekt zum Setzen des Eingabeformats(String oder File) und zur Initialisierung der jeweiligen Klasse JavaCharStream - AdvancedNode root. Statischer Wurzelknoten mit dessen Hilfe sämtliche Operationen auf der Baumstruktur ausgeführt werden; Kann nur durch einen neuen Parsingvorgang gesetzt werden. 75 KAPITEL 7. IMPLEMENTIERUNG 7.4 Paket parseplus.modifier In diesem Paket sind die Klassen zur Knoten-/ Tokenbearbeitung, sowie allgemeine Schnittstellen für die Knotenklassen, Token und Parser enthalten. Das Interface AdvancedNode stellt die Schnittstellen zu jedem Knoten des erzeugten Syntaxbaumes dar. Die Klasse SimpleNode implementiert dieses Interface AdvancedNode, welches zentral im Paket parseplus.modifier hinterlegt ist. Zusätzlich zu den in diesem Interface deklarierten Methoden bietet die Klasse SimpleNode noch Funktionalitäten, die in ihrer sich anschließenden Beschreibung enthalten sind. + jjtOpen() : void. Diese Methode wird aus dem Parser heraus aufgerufen, sobald Kindknoten hinzugefügt werden können. Innerhalb der in Parseplus enthaltenen Klasse SimpleNode wird diese Methode zum Speichern des ersten Token benutzt. + jjtClose() : void. Diese Methode wird aus dem Parser heraus aufgerufen, sobald alle Kindknoten hinzugefügt wurden. Innerhalb der in Parseplus enthaltenen Klasse SimpleNode wird diese Methode zum Speichern des letzten Token benutzt. + jjtSetParent(AdvancedNode n) : void. Die Methode zum Setzen des Elternknotens im Attribut AdvancedNode parent des aufrufenden Kindknotens. Die Methode jjtSetParent wird unmittelbar nach dem Entfernen des Knotens vom Stack, aber vor dem Hinzufügen als Kindknoten zum Array AdvancedNode[] children des Elternknotens aufgerufen. Parameter: n - ist das Elternknotenobjekt vom Typ AdvancedNode + jjtGetParent() : AdvancedNode. Die Methode zum Holen des Elternknotens aus dem Attribut AdvancedNode parent des aufrufenden Kindknotens. Die Methode jjtGetParent wird standardmäßig nicht während der Erstellung des Syntaxbaums oder des generellen Parsingvorgangs aufgerufen, sondern dient allein den nachträglichen Baumoperationen. Rückgabewert: das Elternknotenobjekt vom Typ AdvancedNode, oder null im Falle des Wurzelknotens + jjtAddChild(AdvancedNode n, int i) : void. Nachdem der Kindknoten vom Stack des Parsers geholt und seine Methode jjtSetParent aufgerufen wurde, wird dieser Knoten vom Typ AdvancedNode als erster Parameter des Aufrufs der Methode jjtAddChild des Elternknotens benutzt. Der zweite Parameter bestimmt die Position des übergebenen Kindknotens im Array AdvancedNode[] children des Elternknotens. Das erste Kind ist unter dem Index 0 zu finden, jedoch wird es nicht zwangsläufig auch in der zeitlichen Abfolge als erstes Kindobjekt eingefügt. 76 KAPITEL 7. IMPLEMENTIERUNG Parameter: n - ist der einzufügende Kindknoten i - entspricht dem Index des Kindes im Array AdvancedNode[] children dieses Knotens + jjtGetChild(int i) : AdvancedNode. Die Methode jjtGetChild liefert den Kindknoten gemäß des übergebenen Indexes i. Die Indizes der Kindknoten im Array AdvancedNode[] children des Elternknotens beginnen bei 0 für das linkeste Kind und erhöhen sich jeweils um eins für jeden rechten Nachbarn dieses Kindes. Nicht zu benutzende Indizes (zu groß oder kein Knotenobjekt hinterlegt) werden von der Methode abgefangen und in diesen Fällen null zurückgegeben. Parameter: i - ist der Index des gewünschten Kindknotens Rückgabewert: die Referenz auf den Kindknoten vom Typ AdvancedNode, oder null bei nicht unterstütztem Index. + jjtGetNumChildren() : int. Die Methode zur Bestimmung der Kindknotenanzahl des aufrufenden Knotens. Wird jjtGetNumChildren während der Baumerzeugung aufgerufen, etwa durch manuell hinzugefügte Befehle in den Parsermethoden, so kann der zurückgegebene Integerwert nicht mit der Anzahl der hinterlegten Kindknoten übereinstimmen. Aus diesem Grund bitte immer das mittels jjtGetChild geholte Knotenobjekt auf Verschiedenheit von null überprüfen, wie im anschließen Bsp. gezeigt: AdvancedNode parentNd = this.parent; for (int i = 0; i < parentNd.jjtGetNumChildren(); ++i) { SimpleNode n = (SimpleNode) parentNd.jjtGetChild(i); if (n != null) { n.dump(prefix + " "); } } Rückgabewert: die Größe des Arrays AdvancedNode[] children dieses Knotens + getChildren() : AdvancedNode[ ]. Diese Methode liefert den Inhalt des Attributs AdvancedNode[] children des aufrufenden Knotens vom Typ AdvancedNode. Rückgabewert: ein AdvancedNode-Array der Kindknoten, oder null, sollte dieser Knoten keine Kindelemente haben + getNodeType() : int. Diese Methode liefert den, bei der Erzeugung des Knotenobjekts gesetzten, Typ des Knotens als Integerwert. Der Typ eines Knotens sollte im Nachhinein nicht mehr verändert werden, weshalb hierfür keine set-Methode bereit gestellt wird. Die Integerkonstanten aller möglichen Knotentypen eines Syntaxbaumes werden nach dem Parsen der Grammatikdatei in 77 KAPITEL 7. IMPLEMENTIERUNG dem Interface ParsernameTreeConstants hinterlegt. Somit kann der Rückgabewert dieser hier beschriebenen Methode auf Gleichheit der Referenz hin mit den Konstanten des Interfaces verglichen werden. Rückgabewert: Integerkonstante des Interfaces ParsernameTreeConstants, die den Knotentyp bestimmt + setChildren(AdvancedNode [ ] children) : void. Diese Methode setzt das Attribut AdvancedNode[] children des aufrufenden Knotens gleich dem übergebenen Array children. Um konsistente Daten zu erhalten, sollte vorher das Attribut AdvancedNode parent der Knoten im übergebenen Array children entsprechend der Baumlogik gesetzt werden. Die Methode setChildren bietet diese zusätzlich Funktionalität nicht. Parameter: children - ist das Array aus Kindknotenobjekten vom Typ AdvancedNode + removeChildren() : void. Der Aufruf dieser Methode dient dem rekursiven Löschen des Kindknotenarrays AdvancedNode[] children sowohl des aufrufenden Knotens, als auch aller vorhandenen Kindknoten. Zusätzlich wird auch das Attribut AdvancedNode parent der betroffenen Kindknoten gleich null gesetzt, um konsistente Daten für möglicherweise lokal ex. Referenzen auf diese Kindknoten zu erhalten. + getFirstToken() : Token. Diese Methode liefert den Inhalt des Attributs Token begin des aufrufenden SimpleNode-Objekts. Der Rückgabewert kann auch gleich null sein, wenn die Methode vor dem ersten Aufruf der Methode jjtOpen benutzt wird. Rückgabewert: das erste Token dieses Knotens (Nichtterminalen Symbols) + setFirstToken(Token t) : void. Diese Methode setzt das Attribut Token begin des aufrufenden SimpleNode-Objekts gleich dem übergebenen Token t und führt diese Änderung falls notwendig (das erste Token des aufrufenden Knotens entspricht dem ersten Token des ersten Kindknotens) bei allen Kindknoten auch aus. Beachte: Änderungen beim Elternknoten werden von dieser Methode nicht ausgeführt. Zu diesem Zweck muss die Methode vom höchsten Elternknoten aufgerufen werden, der das gleiche erste Token hat. Parameter: t - soll das neue erste Token dieses Knotens werden + getLastToken() : Token. Diese Methode liefert den Inhalt des Attributs Token end des aufrufenden SimpleNode-Objekts. Der Rückgabewert kann auch gleich null sein, wenn die Methode vor dem ersten Aufruf der Methode jjtClose benutzt wird. Rückgabewert: das letzte Token dieses Knotens (Produktion) 78 KAPITEL 7. IMPLEMENTIERUNG + setLastToken(Token t) : void. Diese Methode setzt das letzte Token des aufrufenden Knotens und stoesst diese Aenderung bei Bedarf (das letzte Token des aufrufenden Knotens entspricht dem letzten Token des Elternknotens) am Elternknoten rekursiv an. Beachte: Änderungen beim letzten Kindknoten werden von dieser Methode nicht ausgeführt. Zu diesem Zweck muss die Methode vom letzten Kindknoten aufgerufen werden, falls dieser das gleiche letzte Token hat. Parameter: t - wird als neues letztes Token gesetzt + postOrderAccept(SQL92Visitor visitor, Object data) : Object. Ein Durchlauf des Syntaxbaums in Postordnung, mit dem aufrufenden Knoten als Wurzel, wird mit dem Aufruf dieser Methode ausgeführt. Zurückgegeben wird der Rückgabewert der jeweiligen visit–Methode des übergebenen Objekts visitor einer Visitorklasse. Parameter: visitor - ein Objekt einer Visitorklasse, welche das SQL92Visitor-Interface implementiert data - ein beliebiges Objekt, welches während des Durchlaufs von Knoten zu Knoten übergeben wird Rückgabewert: der Rückgabewert der jeweiligen Implementation der visit–Methode in der Visitorklasse + preOrderAccept(SQL92Visitor visitor, Object data) : Object. Ein Durchlauf des Syntaxbaums in Präordnung, mit dem aufrufenden Knoten als Wurzel, wird durch diese Methode ausgeführt. Zurückgegeben wird der Rückgabewert der jeweiligen visit–Methode des übergebenen Objekts visitor einer Visitorklasse. Parameter: visitor - ein Objekt einer Visitorklasse, welche das SQL92Visitor-Interface implementiert data - ein beliebiges Objekt, welches während des Durchlaufs von Knoten zu Knoten übergeben wird Rückgabewert: der Rückgabewert der jeweiligen Implementation der visit-Methode in der Visitorklasse + jjtAccept(SQL92Visitor visitor, Object data) : Object. Dies ist die dritte Methode des Visitor Pattern. Zusammen mit der automatische generierten Klasse RunThroughVisitor bietet die Methode die Möglichkeit, zwischen den Aufrufen der visit–Methoden der einzelnen Knoten, einzelne Kommandos einzubinden. Hierzu muss lediglich in die Methode runthrough(AdvancedNode node, Object data) eingegriffen werden. 79 KAPITEL 7. IMPLEMENTIERUNG Parameter: visitor - ist ein Objekt der Klasse, deren visit–Methoden zu benutzen sind data - ist ein beliebiges Objekt, welches während des Durchlaufs von Knoten zu Knoten übergeben wird Rückgabewert: der Rückgabewert der jeweiligen Implementation der visit–Methode + nextLeft() : AdvancedNode. Die Methode nextLeft() liefert den linken Nachbarknoten des aufrufenden Knotens. Wird diese Methode vom Wurzelknoten oder dem linkesten Kindknoten eines Elternknotens aufgerufen, so wird null zurückgegeben. Rückgabewert: ein Objekt vom Typ AdvancedNode, welches dem linken Nachbarknoten entspricht, oder null falls dieser nicht ex. + nextRight() : AdvancedNode. Die Methode nextRight() liefert den rechten Nachbarknoten des aufrufenden Knotens. Wird diese Methode vom Wurzelknoten oder dem rechtesten Kindknoten eines Elternknotens aufgerufen, so wird null zurückgegeben. Rückgabewert: ein Objekt vom Typ AdvancedNode, welches dem rechten Nachbarknoten entspricht, oder null falls dieser nicht ex. + root() : AdvancedNode. Die Methode root() liefert den Wurzelknoten des Gesamtbaums. Wird diese Methode vom Wurzelknoten selbst aufgerufen, so gibt sie auch hier die Wurzel zurück. Die Funktionsweise dieser Methode beruht auf der rekursiven Überprüfung des Attributs AdvancedNode parent, welches mit null initialisiert wird und welchem beim Erstellen des Syntaxbaums die Referenz auf den Elternknoten zugewiesen wird. Sollte ein Knoten gelöscht werden, so setzt die Methode parseplus.modifier.Modifier.deleteNode das Attribut AdvancedNode parent wieder gleich null. Rückgabewert: ein Objekt vom Typ AdvancedNode, welches der Wurzel entspricht + toString() : String. Diese Methode überlädt die ursprüngliche Methode jedes Java-Objects. Sie wird zum Bsp. automatisch bei der Ausgabe in System.out aufgerufen und benutzt als Rückgabewert die geparsten Strings eines jeden NTSymbols. Diese Strings sind global durch das Interface ParsernameTreeConstants zu erreichen. Rückgabewert: ein Stringobjekt, welches den Namen des Nichtterminalen– Symbols enthält + toString(String prefix) : String. Die Methode toString(String prefix) liefert das übergebene Präfix gefolgt von der geparsten Benennung des NT-Symbols. Hierzu wird die Methode toString() aufgerufen und das prefix vorweg gestellt. 80 KAPITEL 7. IMPLEMENTIERUNG Parameter: prefix - ist die vorweg zu stellende Silbe einer jeden NT-Benennung Rückgabewert: das Präfix gefolgt von dem Namen des NT als Inhalt eines Strings + dump(String prefix) : void Der Rückgabewert der Methode toString(String prefix) wird in dieser Methode genutzt, um die Baumstruktur ausgehend vom aufrufenden Knoten, in System.out darzustellen. Der Inhalt des Parameters String prefix hat dabei die gleiche Auswirkung wie in der Methode toString(String prefix). Parameter: prefix - ist die gewünschte Ausgabe zu Beginn jeder neuen Zeile der Baumdarstellung + dump(PrintWriter ostr,String prefix,String suffix,int choice) : void. Die Methode zur Ausgabe der Nichtterminalen als Baumknoten. Zur Ausgabe wird der PrintWriter ostr benutzt, der entsprechend vor dem Aufruf dieser Methode init. werden muss. Als Werte der Parameter prefix und suffix sind die Zeichenketten zu wählen, die später als Vor-/Nachsilbe des nichtterminalen Symbols ausgegeben werden sollen. Der Parameter choice steuert die Ausgabeart der darzustellenden NTerm. Wird die Konstante ParsePlusConst.dumpNotTerminal übergeben, so wird ausschließlich die Benennung gemäß Grammatik plus prefix und suffix für jedes NT ausgegeben. Wählt man hingegen die Konstante ParsePlusConst.dumpContent als Aktualparameter, so wird zusätzlich zur oben beschriebenen Ausgabe auch noch der Inhalt (die Token von begin bis end) des NT mittels des PrintWriters ostr geschrieben. Diese Methode ist insbesondere für die Fehlersuche bei neu erstellten Grammatiken gedacht, da sie auf die aufwendige XML Notation verzichtet, und somit die Baumstruktur vom Programmierer schneller zu erkennen ist. Parameter: ostr - ist der PrintWriter fuer die Ausgabedatei prefix - ist moegliches Praefix der Knotenausgabe suffix - ist moegliches Suffix der Knotenausgabe choice - steuert Ausgabeart der Knoten + doXMLExport(XMLEventFactory factory, XMLEventWriter writer, String column) : void. Diese Methode dient dem Schreiben des geparsten Syntaxbaums in einem XML-ähnlichem Format. Die Methode arbeitet nach dem StAX-Iterator-Prinzip welches für jeden zu schreibenden Tag ein Event erzeugt. Um diese Ereignisse erzeugen zu können, wird der erste Parameter (factory) benötigt. Das iterative Schreiben der erzeugten Ereignisse in die gewünschte Datei wird dann mittels des vorher zu initialisierenden XMLEventWriter– Parameters bewerkstelligt. Der Parameter String column schließlich dient den Einrückungen der Ausgabe, welche durch den rekursiven Aufruf dieser Methode für die Kindknoten automatisch angepasst wird. Die Angaben zum Ausgabeformat der mit dieser Methode erstellten Datei sind im Abschnitt 5.1 gemacht. 81 KAPITEL 7. IMPLEMENTIERUNG Parameter: factory - ist die XMLEventFactory zur Erstellung der jeweiligen Tags writer - wird für das iterative Anfügen der erstellten Events an die Ausgabedatei benutzt column - dieser String beinhaltet die Leerzeichen für die Einrückungen, gemäß logischer Baumstruktur + setContent(String cont) : void. Diese Methode dient dem Zuweisen des Strings cont als Inhalt (image) des ersten Token des aufrufenden Knotenobjekts. Ferner werden alle möglichen Kindknoten, sowie alle Token zwischen dem ersten und dem letzten Token dieses Knotens gelöscht. Beim aufrufenden SimpleNodeObjekt sind die Token begin und end nach dem Aufruf dieser Methode gleich, die specialToken werden übernommen und die alten Verkettungen zu den vorhergehenden und nachfolgenden Token bleiben erhalten. Nach dem Aufruf dieser Methode entsprechen die Positionsangaben in den Token allerdings nicht mehr den tatsächlichen Positionen der Token, wenn die verketteten Token der Reihe nach ausgegeben werden würden. Wenn sie diese Konsistenz beibehalten wollen, müssen sie mit den Methoden parseplus.modifier.Modifier.insertToken und parseplus.modifier.Modifier. deleteToken arbeiten. Parameter: cont - der, dem aufrufendem Knoten, zuzuweisende Inhalt als Zeichenkette + getContent(boolean choice) : String. Diese Methode liefert den Inhalt aller Token beginnend mit dem Token this.begin und endend mit this.end als String zurück. Dabei werden auch die Inhalte aller Kindknoten, sowie alle dazwischenliegenden specialToken berücksichtigt. Sollten der aufrufende Knoten selbst und seine Kindknoten nur leere Token enthalten, so wird ein leerer String mit der Länge 0 zurückgegeben. Parameter: choice - true => specialToken zwischen den Token dieses Knotens und denen seiner Kindknoten werden auch in Rückgabestring eingefügt; false => ein String aus regulären Token, durch ein Leerzeichen getrennt, wird ausgegeben Rückgabewert: der Inhalt der Token von this.begin bis this.end als ein Stringobjekt, oder eine leere Zeichenkette Die Basis der abstrakten Datenstruktur Syntaxbaum ist die Klasse SimpleNode, welche das Interface AdvancedNode implementiert. In ihr sind sowohl Methoden und Attribute zum Aufbau und zur Manipulation der Baumstruktur enthalten, als auch solche, die den geparsten Inhalt der jeweiligen Baumknoten betreffen. Die Methoden, die im Interface AdvancedNode spezifiziert sind, werden hier nicht noch einmal 82 KAPITEL 7. IMPLEMENTIERUNG erläutert, da dies schon zuvor bei der Beschreibung des Interfaces geschehen ist. Nachfolgend sind daher nur die Attribute und die zusätzlichen Methoden dieser Basisklasse erläutert: - AdvancedNode parent. Die Referenz auf den Elternknoten dieses Knotens wird hierin hinterlegt. - AdvancedNode[ children. Dies ist das Array aller Kindknoten des jeweiligen Knotens. - int id. Der Typus des Knotens gemäß der Grammatik ist hierin hinterlegt und kann mittels des Interfaces SQL92TreeConstants entschlüsselt werden. - SQLParser parser. Benötigtes lokales Parserobjekt, um Zugriff auf die gefundenen Token während des Parsens zu haben. - Token begin. Dies ist das erste Token des Knotens bzw. der hierdurch repräsentierten Produktion. - Token end. Dies ist das letzte Token des Knotens bzw. der hierdurch repräsentierten Produktion. + SimpleNode(int i). Dies ist einer der beiden Konstruktoren zur Erzeugung der Objekte der Klasse SimpleNode. Der übergebene Integerwert entspricht der Konstante des Interfaces SQL92TreeConstants. Der Wert des Parameters wird dem Attribut int id“ zugewiesen. ” Parameter: i - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt + SimpleNode(SQLParser p, int i). Dies ist der zweite Konstruktor der Klasse, der, zusätzlich zum Typ des Knotens(int i), auch noch das aktuelle Parserobjekt(SQLParser p) übergeben bekommt. Beide Parameter werden den Attributen ind id“ bzw. SQLParser parser“ der Elternklasse zugewiesen. ” ” Parameter: p - Referenz auf das Parserobjekt, welches ein Objekt dieser Klasse erzeugen will i - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt + jjtCreate(int id) : AdvancedNode. Auch mittels dieser statischen Methode können Objekte der Klasse SimpleNode erzeugt werden, die die Methode dann zurückgibt. Der übergebene Parameter wird wie bei den Konstruktoren zur Zuweisung der Knotenart genutzt. 83 KAPITEL 7. IMPLEMENTIERUNG Parameter: id - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt Rückgabewert: die Referenz auf das erzeugte Knotenobjekt vom Typ AdvancedNode – kann in den Typ SimpleNode gecastet werden. + jjtCreate(SQLParser p, int id) : AdvancedNode. Die zweite statische create–Methode mit der Knotenobjekte vom Typ SimpleNode generiert werden können. Die Parameter werden wie beim zuvor beschriebenen zweiten Konstruktor benutzt. Parameter: p - Referenz auf das Parserobjekt, welches ein Objekt dieser Klasse erzeugen will id - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt Rückgabewert: die Referenz auf das erzeugte Knotenobjekt vom Typ AdvancedNode – kann in den Typ SimpleNode gecastet werden. + acceptClauseVisitor(ClauseVisitor visitor, Object data) : Object. Diese accept–Methode des Visitor Pattern wird ausschließlich zur Aktualisierung der Klauseleinträge benötigt. Aus diesem Grund muss der erste Parameter vom Typ parseplus.semantics.ClauseVisitor sein, da diese Klasse die entsprechende Funktionalität implementiert. Parameter: visitor - ein Objekt der Klasse parseplus.semantics.ClauseVisitor data - ein Object, welches für diesen Visitor nicht benötigt wird (kann gleich null sein) Rückgabewert: das übergebene Object-Objekt + acceptColumnVisitor(ColumnVisitor visitor, Object data) : Object . Diese accept–Methode des Visitor Pattern wird ausschließlich für das Ermitteln von Spalten-Knoten innerhalb einer Klausel benötigt. Daher muss der erste Parameter vom Typ parseplus.semantics.ColumnVisitor sein, da diese Klasse die entsprechende Funktionalität implementiert. Parameter: visitor - ein Objekt der Klasse parseplus.semantics.ColumnVisitor data - ein Vector-Object, welches in der Visitorklasse um die Referenz auf die gefundenen ColumnReferences erweitert wird Rückgabewert: der erstellte Vector der ColumnReferences 84 KAPITEL 7. IMPLEMENTIERUNG + acceptSemanticVisitor(SemanticVisitor visitor, Object data) : Object . Diese accept–Methode des Visitor Pattern wird für die semantische Analyse benötigt. Daher muss der erste Parameter vom Typ SemanticVisitor sein, da diese Klasse die entsprechende Funktionalität implementiert. Parameter: visitor - ein Objekt der Klasse parseplus.semantics.SemanticVisitor data - ein Object, welches für diesen Visitor nicht benötigt wird(kann gleich null sein) Rückgabewert: das übergebene Object-Objekt Das Interface ParsePlusConst beinhaltet die Konstanten für die Nutzung verschiedener Funktionen der Anwendung ParsePlus. + int dumpNotTerminal. Knotendarstellung des Syntaxbaums mit Bezeichnung der NichtTerminalen auf der linken Seite der Produktion ausgeben. + int dumpContent. Knotendarstellung des Syntaxbaums mit Bezeichnung der NichtTerminalen auf der linken Seite der Produktion und den enthaltenen Token ausgeben. Das Interface SQLParser stellt die gemeinsame Schnittstelle aller generierten Parser dar. Es bietet eine zentrale Startmethode an, die alle erstellten Erweiterungsparser implementieren müssen. Hierdurch kann in der Klasse ParsePlus zentral der Parsingvorgang gestartet werden und der zurückgegebene Wurzelknoten gespeichert werden. Mittels des Wurzelknotens können anschließend alle Ausgabearten aufgerufen werden, weswegen die Schnittstelle SQLParser somit indirekt auch für diese Funktionalität genutzt wird. + SQLStatement() : AdvancedNode. Diese Methode entspricht dem Startsymbol in jeder, auch der erweiterten, SQL-Grammatik. Sie kann inhaltlich natürlich unterschiedlich implementiert werden, jedoch muss sie immer zwingend den Wurzelknoten des Syntaxbaums zurückgeben. Die im Fehlerfall zu werfenden Exception-Objekte werden von der zentralen Klasse ParseException abgeleitet, was bereits standardmäßig in der Grammatik vermerkt ist. Rückgabewert: ein Knotenobjekt einer Klasse, die das Interface AdvancedNode implementiert Throws: ParseException - liefert die konkrete Fehlerposition, wenn während des Parsens ein Fehler aufgetreten sein sollte + transformSQL(Object data) : Object. Die Implementation dieser Methode löst die Transformation der Erweiterung in das SQL des 1992-Standards aus. Diese Methode wird als gemeinsame Schnittstelle aller konkreten SQLParser– 85 KAPITEL 7. IMPLEMENTIERUNG Implementationen in der Methode transformExtdSQL der Klasse ParsePlus genutzt. Die Standardimplementation der Methode aus der Basisgrammatik für Erweiterungen gibt null zurück, wenn kein Wurzelknoten in ParsePlus hinterlegt ist. Andernfalls wird ein beliebiges Objekt geliefert. Parameter: data - ist das Attribut, welches beim Baumdurchlauf von Knoten zu Knoten weiter gegeben wird Rückgabewert: eine Instanz von Object, das im Falle einer fehlenden Referenz im statischen Attribut AdvancedNode root der Klasse ParsePlus gleich null ist + getToken(int index) : Token. Die einzige Schnittstelle, die von der Basisklasse SimpleNode der Knoten genutzt wird, um auf den Tokenstrom zuzugreifen. Der Rückgabewert dieser Methode wird in den Knoten zum Setzen des ersten und letzten Token der repräsentierten Produktion eingesetzt. Parameter: index - ist der Index des zu liefernden Token Rückgabewert: das Tokenobjekt, welches unter dem Index zu finden ist Die abstrakte Klasse Modifier ist als zentrale Schnittstelle für die vorbereiteten Einfüge- und Löschoperationen angelegt. Sie bietet sowohl die Möglichkeit Bäume mit Knoten vom Typ AdvancedNode zu manipulieren, als auch einzelne Token hinzuzufügen oder zu löschen. Um diese Methoden nutzen zu können, müssen ihre individuellen Klassen das Interface parseplus.modifier.AdvancedNode implementieren, bzw. von der Klasse parseplus.modifier.Token erben. Die Verschiebungen und Einfügungen arbeiten mit den Referenzen der übergebenen Objekte und wirken sich somit auch auf das Original aus. + insertNode(AdvancedNode child,AdvancedNode parent, int pos) : boolean. Die statische Methode zum Einhaengen von Knoten in die Baumstruktur. Positionsangaben in pos sollten bei 0 (1. Position) beginnen. Zu große oder zu kleine Posangaben werden von dieser Methode abgefangen, indem das Knotenobjekt child als letztes Kind eingefügt wird. Diese Methode arbeitet mit den übergebenen Referenzen auf die AdvancedNode Objekte. Parameter: child - der einzufuegende Kindknoten parent - ist der Elternknoten unter dem der neue Knoten einzufügen ist pos - bestimmt die Position des Kindknotens in Relation zu anderen Kindknoten des Knotenarrays im Elternknoten Rückgabewert: true bei erfolgreicher Einfügungsop., false sonst. + deleteNode(AdvancedNode node) : boolean. Diese statische Methode löscht den übergebenen Knoten node und alle anhängenden Zweige aus seinem Syntaxbaum. Sämtliche Verweise auf Elterknoten und Kindknoten werden aktualisiert. Der Index der verbleibenden Kindknoten im Array AdvancedNode[] children des 86 KAPITEL 7. IMPLEMENTIERUNG Elternknotens kann sich durch diese Löschung verändern. Der Aufruf der Methode deleteNode mit dem Wurzelknoten führt zum Rückgabewert false. Parameter: node - ist der zu löschende Knoten Rückgabewert: true bei erfolgreicher Entfernung des Knotens; false sonst. + insertToken(Token predec,Token toinsert,int beginCol, boolean newLn) : boolean. Diese Methode ermöglicht es ihnen, ein neues Token zwischen zwei bereits verkettete Token einzuhängen. Das Vorgängertoken predec verweist anschließend in seinem next Feld auf toinsert und dieses Token auf das ursprünglich folgende. Außerdem werden die Positionsangaben aller betroffenen Token aktualisiert und gegebenenfalls neue specialToken eingefügt. Wenn sie das neue Token einfach direkt hinter dem Vorgänger einfügen wollen (mit einem automatisch generierten Leerzeichen als Trennung), so setzen sie bitte newLn=false (der Wert in beginCol ist dann ohne Bedeutung). Möchte man das neu zu verkettende Token in eine neue Zeile setzen und Einrückungen vornehmen, so weisen sie newLn=true und beginCol den Wert der gewünschten Anfangsspalte (Nummerierung beginnend bei 1) zu. Vorher in toinsert angelegte specialToken werden durch diese Methode überschrieben! Parameter: predec - Vorgängertoken toinsert - einzufügendes Token beginCol - Wertebereich [1,n] newLn - true => beginCol wird benutzt false => neues Token wird hinten an predec angehängt Rückgabewert: true, falls Einfügen erfolgreich; false sonst. + addFirstToken(Token oldFst,Token newFst) : boolean. Eine Methode zum Vorschalten eines neuen ersten Token des geparsten Ausdrucks. Das übergebene Token newFst wird vor oldFst gesetzt und dessen Positionsangaben aktualisiert. Die neuen Positionsdaten von oldFst basieren auf den entsprechenden Daten von newFst, weshalb dessen Daten vor dem Aufruf dieser Methode gesetzt werden müssen. Diese Methode ist ausschließlich auf zwei reguläre Token anzuwenden, da ansonsten die Positionsangaben nicht mehr konsistent sind. Die Positionen der bestehenden speziellen Token, die an zu verschiebende reguläre Token gebunden sind, werden durch diese Methode automatisch aktualisiert. Parameter: oldFst - ehemals erstes Token des geparsten Ausdrucks (Aufruf mit regulären Token) bzw. der lokal verketteten specialToken (Aufruf mit zwei specialToken) newFst - neu zu setzendes erstes Token Rückgabewert: true bei erfolgreicher Vorschaltung; false sonst. + deleteToken(Token predec,Token todelete) : boolean. Dies ist die Methode zum Löschen eines Token durch Auflösung der Verkettung zum Vorgänger 87 KAPITEL 7. IMPLEMENTIERUNG und ggfls. zum Nachfolger. Die Verknüpfung vom Vorgänger- zum Nachfolgertoken wird mit dieser Methode ebenso hergestellt, wie die Aktualisierung der Positionsangaben. Parameters: predec - das Vorgängertoken todelete - das zu löschende Token Rückgabewert: true bei erfolgreicher Löschung; false sonst. + addUnicodeEscapes(String str) : String. Diese Methode wird bei der Ausgabe der Tokeninhalte aufgerufen, um, insbesondere für das Schreiben der Baumstruktur in eine .papl-Datei, die Sonderzeichen gemäß Unicode-Codierung umzuwandeln. Parameters: str - ist der umzuwandelnde String Rückgabewert: der gemäß Unicode umgewandelte Eingabestring Die Klasse Token bildet die Grundlage für die verkettete Tokenliste, die während des Parsens erzeugt wird. Sie unterscheidet sich in ihrem Funktionsumfang von der durch JavaCC automatisch generierten gleichnamigen Klasse erheblich. Die mittels Parse Plus aus Grammatikdateien erzeugten Parser verzichten daher auf die Generierung dieser ursprünglichen Tokenklasse und importieren stattdessen diese Klasse Token aus dem package parseplus.modifier. Die Tokenobjekte, die von der hier beschriebenen Klasse Token abgeleitet sind, kennzeichnen folgende Attribute: + int kind. Dieser Integerwert beschreibt die gemäß Grammatik definierte Art dieses Token. Die Integer-Konstanten zur Entschlüsselung in die angegebene Tokenbenennung werden von JavaCC automatisch in dem Interface <Parsername>Constants generiert. + int beginLine. Die Zeilennummer des ersten Zeichens dieses Token (>=1). + int beginColumn. Die Spaltennummer des ersten Zeichens dieses Token (>=1). + int endLine. Die Zeilennummer des letzten Zeichens dieses Token (>=1). + int endColumn. Die Spaltennummer des letzten Zeichens dieses Token (>=1). + String image. Alle Zeichen, die zum geparsten Token gehören, bilden diesen String. + Token next. Dieses Token referenziert das nachfolgende reguläre Token in der Listenstruktur, wenn es das Attribut eines ebenfalls regulären Token ist. Sollte es das letzte geparste Token sein, so hat das Attribut next den Wert null. Über die Verkettung der SpecialToken mittels des next-Attr. erfahren sie beim Attribut specialToken mehr. 88 KAPITEL 7. IMPLEMENTIERUNG + Token specialToken. Dieses Token referenziert das letzte SpecialToken vor einem regulären Token, wenn es das Attribut eines regulären Token ist. Andererseits referenziert es das vorhergehende SpecialToken, wenn es selbst das Attr. eines SpecialToken ist. In beiden Fällen enthält dieses Token den Wert null, falls kein davor platziertes Token geparst wurde. Die SpecialToken verweisen in ihrem next-Attr. wiederum auf das nachfolgende SpecialToken, bzw. auf null, wenn ein reguläres Token folgt. Die in der folgenden Liste erläuterten Methoden haben alle Objekte gemeinsam, die von der Klasse Token abgeleitet wurden: + toString() : String. Diese Methode überlädt die Methode der Klasse java.lang.Object, indem sie das Attribut image zurückgibt. Rückgabewert: der String des geparsten Token + getPosition() : int [ ]. Eine Methode, um die Positionen einfacher auslesen zu können. Rückgabewert: das Integerfeld mit den Positionsangaben des Token + setPosition(int bL,int bC,int eL,int eC) : void. Eine Methode, um die Positionen einfacher setzen zu können. Parameter: bL - ist die Startzeile bC - ist die Startspalte eL - ist die Endzeile eC - ist die Endspalte - increaseSpTkBeginLine(int lines) : void. Diese private Hilfsmethode erhöht die Anfangszeile des speziellen Token und ruft diese Methode rekursiv für alle vorstehenden Token auf. Parameter: lines - ist der Wert um den die Anfangszeile erhöht wird + increaseBeginLine(int lines) : void. Diese Methode erhöht die Anfangszeile des Token und ruft diese Methode rekursiv für alle nachstehenden Token auf. Außerdem werden auch die betroffenen SpecialToken verändert. Parameter: lines - ist der Wert um den die Anfangszeile erhöht wird - increaseSpTkBeginColumn(int col) : void. Diese private Hilfsmethode erhöht die Anfangsspalte des speziellen Token und ruft diese Methode rekursiv für alle vorstehenden Token auf. Parameter: lines - ist der Wert um den die Anfangsspalte erhöht wird 89 KAPITEL 7. IMPLEMENTIERUNG + increaseBeginColumn(int col) : void. Diese Methode erhöht die Anfangsspalte des Token und ruft diese Methode rekursiv für die nachstehenden Token in der selben Zeile auf. Außerdem werden auch die betroffenen SpecialToken verändert. Parameter: lines - ist der Wert um den die Anfangszeile erhöht wird + containsNL() : boolean. containsNL() überprüft ob das Token selbst, oder eines seiner vorhergehenden specialToken, ein newline Zeichen im Attribut image enthällt. Diese Methode kann für reguläre und spezielle Token aufgerufen werden und wird zum Beispiel in der Methode Modifier.insertToken(Token, Token, int, boolean) benutzt, um die Zeilenangaben der Token anpassen zu können. Rückgabewert: true, wenn "\n" enthalten ist; false sonst. + print(boolean choice) : String. Die Methode zur Ausgabe des im Attr. image gespeicherten Strings eines Token. Abhängig vom übergebenen Parameter kann die Rückgabe des Tokeninhalts beeinflußt werden. Diese Funktionalität wird sowohl von der getContent-, als auch von der doXMLExport-Methode der Klasse SimpleNode genutzt. Details hierzu stehen weiter unter beim Parameter choice. Parameter: choice - true =>reguläre Token und specialToken werden ausgegeben; false =>es werden nur reguläre Token ausgeben Rückgabewert: der Inhalt des Token plus vorgeschaltete specialToken + newToken(int ofKind) : Token. Eine statische Methode, die der von JavaCC generierte Parser standardmäßig zur Tokenerzeugung aufruft. Hierin können gemäß dem Factory-Pattern verschiedene benutzerdefinierte Unterarten von Token erzeugt werden. (Diese zusätzliche Funktionalität wird in der aktuellen Version noch nicht genutzt.) Parameter: ofKind - Integerwert, der zur Auswahl des Objekttyps genutzt werden kann Rückgabewert: das erzeugte Tokenobjekt Um die Beschreibung des Inhalt des Pakets parseplus.modifier abzuschließen, sei hier nun die Klasse ParseException noch vorgestellt. Sie entspricht in ihren Schnittstellen der gleichnamigen Klasse, die von JavaCC generiert wird. Lediglich die Formatierung der Ausgabe wurde in der Methode + getMessage() : String“ angepasst. Da ” diese Klasse unabhängig von den jeweiligen Erweiterungen ist, ist sie in dieses Paket integriert worden. Dadurch muss sie nicht für jede Erweiterung neu erzeugt werden, sondern es werden Exceptions von dieser zentralen Klasse abgeleitet. 7.5 Paket parseplus.sql92parser.parser Dieses Paket enthält die Klassen des SQL-92 Parsers, sowie das Interface mit den den definierten Token. Diese Klassen und das Interface sind auf Basis der Grammatik, die 90 KAPITEL 7. IMPLEMENTIERUNG in Kapitel 3 und im Anhang A beschrieben wird, mittels JavaCC erstellt worden. Da sie somit bis auf die Verwendung der Klassen und Interfaces des Abschnitts 7.4 keine Erweiterung der in Abschnitt 2.2.5 vorgestellten Klassen darstellen, konzentrieren sich die Erläuterungen in diesem Abschnitt auf eben diese Verwendung. Die standardmäßig erzeugten Methoden werden hier nicht noch einmal aufgelistet, da die von ihnen bereit gestellte Funktion schon im Kapitel 2.2.5 beschrieben wurden. Die globale Verwaltung aller möglichen Token der SQL-92 Grammatik ist durch das Interface SQL92Constants geregelt. Es kapselt einen statischen Integerwert je Token, der als Index des ebenfalls enthaltenen String–Arrays static final String[] ” tokenImage“ benutzt wird, um den jeweils durch das Token repräsentierten String zu bekommen. Die Klasse JavaCharStream wird für das Einlesen der einzelnen Zeichen der Eingabe benutzt. Außerdem liefert diese Klasse die Informationen zur aktuellen Position in Bezug auf die Zeile und Spalte der Eingabe an den lexikalen Scanner. Der lexikale Scanner dieses SQL-92 Compilers ist in der Klasse SQL92TokenManager implementiert. Durch diese Klasse werden die gefundenen Token als Objekte der Klasse parseplus.modifier.Token erzeugt und mit den entsprechenden Werten gefüllt. Diese Tokenobjekte können vom Parser mittels der Methode getNextToken“ angefordert werden. Diese Methode implementiert das Le” sen der Eingabe von links nach rechts mit der jeweiligen, in der Grammatik definierten Anzahl an Lookahead–Zeichen (Deterministischer Top–Down Parser). Genau diese beschriebene Methode ist es auch, die Exceptions von der Klasse TokenMgrError werfen kann. Für eine solche Exception, die durch den lexikalen Scanner ausgelöst worden ist, kann es vier verschiedene Ursachen geben. Deren Unterscheidung wird durch die folgenden statischen Integerkonstanten getroffen: • INVALID_LEXICAL_STATE: Während des lexikalischen Scannens wurde ein ungültiger Zustand (repräsentiert durch den Integerwert im Attribut curLexState der Klasse SQL92TokenManager ) erreicht. • LEXICAL_ERROR: Ein allgemeiner lexikalischer Fehler ist aufgetreten. • STATIC_LEXER_ERROR: Es wurde versucht, eine zweite Instanz des statischen SQL92TokenManagers zu erzeugen. • LOOP_DETECTED: Eine Endlosschleife wurde im SQL92TokenManager gefunden und beendet. Die Ausgabe der entsprechenden Fehlermeldung übernimmt die Methode getMessa” ge()“. Abschließend ist noch die Klasse SQL92 zu beschreiben, die die Interfaces SQLParser, SQL92Constants und SQL92TreeConstants implementiert. Diese Klasse ist 91 KAPITEL 7. IMPLEMENTIERUNG der Parser der SQL-92 Grammatik. Die Interfaces mit dem Präfix SQL92“ werden ” für den Zugriff auf die hinterlegten Konstanten in diese Klasse eingebunden. Das Interface SQLParser hingegen wird für den zentralen Zugriff in der Klasse ParsePlus benötigt. Die Schnittstellen, welche das Interface deklariert, werden in der Klasse SQL92 wie folgt implementiert: + SQLStatement() : AdvancedNode. Ist die Startproduktion der SQL-92 Grammatik und kann somit die Herleitung eines Create–, Insert–, Update–, Delete–, Commit–, Rollback–, SetTransaction– oder Select–Ausdrucks einleiten. Rückgabewert: der Wurzelknoten des geparsten Syntaxbaums + transformSQL(Object data) : Object. Löst bei den erweiterten Parsern die Umwandlung in SQl-92 Dialekt aus. In diesem Parser ist sie so implementiert, dass sie den Parameter data unverändert wieder zurück gibt, ohne eine Transformation zu veranlassen. Parameter: data - ein Objekt der Klasse Object, welches zur Steuerung der Transformation benutzt werden kann Rückgabewert: ein beliebiges Objekt der Klasse Object, welches nach Abschluss der Transformation zurückgegeben werden soll + getToken(int index) : Token. Durch diese Methode wird das nächste Token vom TokenManager angefordert. Der Parameter index stellt dabei die Position in der bisher erzeugten verketteten Tokenliste dar, beginnent mit dem Index 0 für das erste Token. Parameter: index - Position des angeforderten Token in der Listenstruktur Rückgabewert: das Objekt der Klasse Token an der spezifizierten Position der Liste 7.6 Paket parseplus.sql92parser.treebuilder In diesem Paket sind die Klassen und Interfaces enthalten, die zur Erstellung des abstrakten Syntaxbaums benötigt werden. Wie bereits im Abschnitt 2.3.2 beschrieben, existiert eine Klasse je Produktion, die in der Grammatikdatei definiert worden ist. Sie erben die Grundfunktionaliät von der Basisklasse (SimpleNode) der Baumstruktur. Die Namen dieser Klassen beginnen standardmäßig mit dem Präfix AST“ für Abstract Syntax Tree“. Da jede dieser ” ” Komponentenklassen die gleiche Struktur der Methoden aufweist – sie unterscheiden sich nur in der Benutzung der jeweiligen Konstruktoren – wird nachfolgend die Klasse ASTSQLStatement des Startsymbols stellvertretend für die insgesamt fünfzig enthaltenen Klassen vorgestellt: + ASTSQLStatement(int id). Dies ist einer der beiden Konstruktoren zur Erzeugung der Objekte dieser Klasse. Der übergebene Integerwert entspricht der 92 KAPITEL 7. IMPLEMENTIERUNG Konstante des Interfaces SQL92TreeConstants zu dieser Produktion des Startsymbols. Der Wert des Parameters wird dem Attribut int id“ der Elternklasse ” zugewiesen. Parameter: id - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt + ASTSQLStatement(SQL92 p, int id). Dies ist der zweite Konstruktor dieser Klasse, der, zusätzlich zum Typ des Knotens(int id), auch noch das aktuelle Parserobjekt(SQL92 p) übergeben bekommt. Beide Parameter werden den Attributen ind id“ bzw. SQLParser parser“ der Elternklasse zugewiesen. ” ” Parameter: p - Referenz auf das Parserobjekt, welches ein Objekt dieser Klasse erzeugen will id - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt + jjtCreate(int id) : AdvancedNode. Auch mittels dieser statischen Methode können Objekte der Klasse ASTSQLStatement erzeugt werden, die die Methode dann zurückgibt. Der übergebene Parameter wird wie bei den Konstruktoren zur Zuweisung der Knotenart genutzt. Parameter: id - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt Rückgabewert: die Referenz auf das erzeugte Knotenobjekt vom Typ AdvancedNode – kann in den Typ ASTSQLStatement gecastet werden. + jjtCreate(SQL92 p, int id) : AdvancedNode. Die zweite statische create– Methode mit der Knotenobjekte vom Typ ASTSQLStatement generiert werden können. Die Parameter werden wie beim zuvor beschriebenen zweiten Konstruktor benutzt. Parameter: p - Referenz auf das Parserobjekt, welches ein Objekt dieser Klasse erzeugen will id - ist die Integerkonstante, die diesem Knoten zugewiesen wird und somit seinen Typus festlegt Rückgabewert: die Referenz auf das erzeugte Knotenobjekt vom Typ AdvancedNode – kann in den Typ ASTSQLStatement gecastet werden. + postOrderAccept(SQL92Visitor visitor, Object data) : Object. Ein Durchlauf des Syntaxbaums in Postordnung, mit dem aufrufenden Knoten vom Typ ASTSQLStatement als Wurzel, wird mit dem Aufruf dieser Methode ausgeführt. Zurückgegeben wird der Rückgabewert der jeweiligen visit–Methode 93 KAPITEL 7. IMPLEMENTIERUNG des übergebenen Objekts visitor einer Visitorklasse. Diese Methode überlädt die Elternmethode. Parameter: visitor - ein Objekt einer Visitorklasse, welche das SQL92Visitor-Interface implementiert data - ein beliebiges Objekt, welches während des Durchlaufs von Knoten zu Knoten übergeben wird Rückgabewert: der Rückgabewert der jeweiligen Implementation der visit–Methode in der Visitorklasse + preOrderAccept(SQL92Visitor visitor, Object data) : Object. Ein Durchlauf des Syntaxbaums in Präordnung, mit dem aufrufenden Knoten vom Typ ASTSQLStatement als Wurzel, wird durch diese Methode ausgeführt. Zurückgegeben wird der Rückgabewert der jeweiligen visit–Methode des übergebenen Objekts visitor einer Visitorklasse. Diese Methode überlädt die Elternmethode. Parameter: visitor - ein Objekt einer Visitorklasse, welche das SQL92Visitor-Interface implementiert data - ein beliebiges Objekt, welches während des Durchlaufs von Knoten zu Knoten übergeben wird Rückgabewert: der Rückgabewert der jeweiligen Implementation der visit–Methode in der Visitorklasse + jjtAccept(SQL92Visitor visitor, Object data) : Object. Dies ist die dritte Methode des Visitor Pattern. Zusammen mit der automatische generierten Klasse RunThroughVisitor bietet diese Methode die Möglichkeit, zwischen den Aufrufen der einzelnen Knoten mittels deren visit–Methode, einzelne Kommandos einzubinden. Hierzu muss lediglich in die Methode runthrough(AdvancedNode node, Object data) eingegriffen werden. Die Methode jjtAccept“ überlädt die ” Elternmethode. Parameter: visitor - ist ein Objekt der Klasse, deren visit-Methoden zu benutzen sind data - ist ein beliebiges Objekt, welches während des Durchlaufs von Knoten zu Knoten übergeben wird Rückgabewert: der Rückgabewert der jeweiligen Implementation der visit-Methode + update(Observable o, Object arg) : void. Dies ist die Methode die immer dann aufgerufen wird, wenn sich der beobachtete Knoten verändert hat. Diese Methode überlädt die Elternmethode. 94 KAPITEL 7. IMPLEMENTIERUNG Parameter: o - ist das beobachtete Objekt, welches sich verändert hat arg - wird vom beobachteten Knoten als zusätzliche Information für die Beobachter mitgegeben Das Interface SQL92TreeConstants bietet den Zugriff auf die öffentlichen Integerkonstanten, von denen jede einer Produktion der zugrundeliegenden Grammatik zugeordnet ist. Die Benennungen der Produktionen der SQL-92 Grammatik für JJTree sind in einem Feld aus einzelnen Strings gespeichert, das durch die Array–Konstante jjtNodeName“ repräsentiert wird. Verwendet man beim Zugriff die genannten Inte” gerkonstanten als Indizes dieses Feldes, so wird der String geliefert, der der Benennung des NT auf der linken Seite der Produktion entspricht. Das zweite Interface, das im Paket parseplus.sql92parser.treebuilder enthalten ist, lautet SQL92Visitor. Es enthält die Deklaration je einer visit–Methode pro Produktion und entspricht somit dem, in Abschnitt 4.3 erläuterten, abstrakten Visitor des Visitor Pattern. Nachfolgend ist nun als Beispiel die Deklaration der Methode für das Startsymbol der Grammatik aufgeführt: + visit(ASTSQLStatement node, Object data) : Object. Die Beschreibung der realisierten Implementationen dieser Methode ist bei den nachfolgenden konkreten Visitorklassen BasicVisitor und RunThroughVisitor zu finden. Die konkrete Visitorklasse BasicVisitor implementiert das Interface SQL92Visitor. Der Methodenkörper aller visit–Methoden dieser Klasse ist identisch, er enthält lediglich die Rückgabe des übergebenen Parameters Object data“ ohne Veränderungen ” daran vorzunehmen. + visit(ASTSQLStatement node, Object data) : Object. Durch das späte Binden der polymorphen visit–Methoden an den jeweiligen Klassentyp des ersten Parameters wird die entsprechende Methode in der aufrufenden accept– Methode des Knotenobjekts ausgewählt. Der Methodenkörper besteht ausschließlich aus der Rückgabe des zweiten Parameters. Parameter: node - ist das Knotenobjekt der Wurzel des Syntaxbaums, welches diesen Visitor akzeptiert data - ein übergebenes Objekt, welches an die Kindknoten weitergegeben wird Rückgabewert: ein Objekt, welches zuvor an jeden Kindknoten zur Auswertung übergeben wurde Die zweite Implementation des abstrakten Visitors SQL92Visitor lautet RunThroughVisitor. Die visit–Methoden dieser Klasse enthalten alle den Aufruf der ebenfalls enthaltenen Methode runThrough, welche ihrerseits den Durchlauf des Syntaxbaums in Präordnung realisiert. Nachfolgend ist die angesprochene Methode sowie, stellvertretend für alle anderen, die visit–Methode des Startsymbols gelistet: 95 KAPITEL 7. IMPLEMENTIERUNG + runThrough(AdvancedNode node, Object data) : Object. Eine Methode für den Durchlauf des erzeugten Syntaxbaums in Präordnung ab dem übergebenen Knoten(Object node) eine Stufe weit hinunter. Der Besuch aller Kindknoten wird durch den Aufruf der jeweiligen Knotenmethode jjtAccept ausgeführt, bevor abschließend das übergebene Objekt data an die aufrufende visit–Methode zurückgegeben wird. Parameter: node - ist der Startknoten für den Durchlauf data - Datum, welches von einem Knoten zum nächsten übergeben wird Rückgabewert: ein Object, welches vom Elternknoten übergeben wurde und alle Kindknoten verändert haben können + visit(ASTSQLStatement node, Object data) : Object. Durch das späte Binden der polymorphen visit–Methoden an den jeweiligen Klassentyp des ersten Parameters wird die entsprechende Methode in der aufrufenden accept– Methode des Knotenobjekts ausgewählt. Diese Methode führt den Aufruf der Methode runThrough aus und gibt deren Rückgabewert an die aufrufende accept– Methode des Wurzelknotens zurück. Parameter: node - ist das Knotenobjekt der Wurzel des Syntaxbaums, welches diesen Visitor akzeptiert data - ein übergebenes Objekt, welches an die Kindknoten weitergegeben wird Rückgabewert: ein Objekt, welches zuvor an jeden Kindknoten zur Auswertung übergeben wurde Als letzte Klasse dieses Pakets sei hier nun die JJTSQL92State beschrieben. In ihr ist der abstrakte Datentyp Stack zur Speicherung der Knotenreferenzen während der Erzeugung des Syntaxbaums realisiert. Der Zugriff auf die Knotenobjekte in dieser Klasse geschieht durch die im Interface AdvancedNode spezifizierten Schnittstellen. Folgende Attribute weist die Klasse JJTSQL92State auf: - java.util.Stack<Integer> marks. Dieser Stack beinhaltet die internen Integer– Markierungen der Knoten auf dem Stack. Die Markierungen werden für die interne Verwaltung des NodeScope“ benötigt, der den Bereich der Kindknoten ” eines Knotens bezeichnet. - int mk. Dieser Integerwert ist die Markierung des zuletzt vom Stack gelöschten Knotenobjekts. Er wird zur Bestimmung der Anzahl der anzuhängenden Kindknoten benötigt. - boolean node created. Dieses Attribut ist true, wenn alle Kindknoten gemäß der Grammatik zu einem bestimmten Knoten hinzugefügt und danach vom Stack gelöscht wurden. 96 KAPITEL 7. IMPLEMENTIERUNG - java.util.Stack<AdvancedNode> nodes. Dieser Stack enthält die Knotenobjekte, die noch einem Elternknoten hinzugefügt werden müssen oder nur die Referenz auf den Wurzelknoten des Syntaxbaums. - int sp. Hierin ist die aktuelle Anzahl der Knoten auf dem Stack gespeichert. Auf die erläuterten Klassenattribute greifen die nachfolgend beschriebenen Methoden der selben Klasse zu: + JJTSQL92State(). Der Konstruktor initialisiert die beiden Attribute sp und mk mit 0. Außerdem erzeugt er zwei Objekte des Datentyps java.util.Stack und weist sie den privaten Eigenschaften nodes und marks zu. + closeNodeScope(AdvancedNode n, boolean condition) : void. In dieser Methode wird die Verkettung der Eltern- und Kindknoten realisiert. Das übergebene Knotenobjekt vom Typ AdvancedNode bekommt alle Knoten des Stacks als Kinder in der Reihenfolge in der Sie auf dem Stack liegen zugewiesen. Dies gilt, sofern der zweite Parameter(condition) gleich true ist. Diese Methode wird ausschließlich mit dem Aktualparameter true“ an zwei” ter Stelle der Parameterliste aufgerufen, da in der erstellten Grammatik keine conditional nodes“ erzeugt werden. ” Parameter: n - ist der Knoten dem alle bisher erzeugten Knoten als Kinder hinzugefügt werden sollen und der danach auf den Stack gelegt wird condition - ist das Prädikat, welches über das Hinzufügen der Kindknoten entscheidet + openNodeScope(AdvancedNode n) : void. Diese Methode wird vor dem Herleiten der jeweiligen Produktion für jedes erzeugte Knotenobjekt aus dem Parser SQL92 heraus aufgerufen. Es wird die aktuelle Markierung(Integerwert in mk) auf den Stack marks gelegt und die neue Markierung auf die Anzahl der Knoten auf dem Stack gesetzt. Danach folgt ein Aufruf der Methode jjtOpen des übergebenen Knotenobjekts, der zum Setzen des ersten Token der Produktion führt. Parameter: n - Knotenobjekt des NT, das auf der linken Seite der herzuleitenden Produktion steht + clearNodeScope(AdvancedNode n) : void. Durch den Aufruf dieser Methode werden alle Knotenobjekte bis zur aktuellen Markierung im Integerattribut mk vom Stack gelöscht. Parameter: n - übergebenes Knotenobjekt + nodeArity() : int. Eine Methode zur Bestimmung der Anzahl von Knoten auf dem Stack, die zur aktuellen node scope“ gehören. ” 97 KAPITEL 7. IMPLEMENTIERUNG Rückgabewert: die Anzahl der Kindknoten als Integerwert + popNode() : AdvancedNode. Der Aufruf dieser Methode führt zum Entfernen des obersten Knotens vom Stack und zu dessen Rückgabe an die aufrufende Instanz. Rückgabewert: das Knotenobjekt an der Spitze des Stacks + pushNode(AdvancedNode n) : void. Diese Methode fügt das übergebene Knotenobjekt als neuen Spitze des Stapels der Knotenobjekte ein. Parameter: n - ist das neue oberste Knotenobjekt des Stacks + reset() : void. Eine Methode, um beide Stapel zu löschen und die Integerwerte auf 0 zu setzen. + nodeCreated() : boolean. Die Methode liefert true, wenn zuvor in der Methode closeNodeScope die Verknüpfung von Eltern- und Kindknoten und das anschließende Legen“ des Elternknotens auf den Stack funktioniert hat. Andernfalls ” ist der Rückgabewert false. Rückgabewert: true oder false, je nach beschriebener Situation + rootNode() : AdvancedNode. Der Aufruf dieser Methode resultiert in der Rückgabe des untersten Knotenobjekts auf dem Stack nodes. Nach der erfolgreichen Beendigung des Parsings wird diese Methode dazu eingesetzt, den Wurzelknoten zu ermitteln. Rückgabewert: die Referenz auf das Knotenobjekt der Wurzel des Syntaxbaums (Instanz der Klasse ASTSQLStatement) + peekNode() : AdvancedNode. Die Methode zum Holen der Referenz auf den obersten Knoten des Stacks nodes, ohne den Knoten vom Stack zu entfernen. Rückgabewert: die Referenz auf den obersten Knoten des Stacks 7.7 Paket parseplus.dbcontent In diesem Paket sind die beiden Klassen zur Speicherung der Data Dictionary Daten und zur Verwaltung der DBS-Verbindung enthalten. Die abstrakte Klasse DBGlobals kapselt die Referenz auf die aktive Verbindung zur Datenbank, die Bezeichnung des Benutzernamens unter dem die Verbindung aufgebaut wurde und ein Objekt der Datenbank Metadaten die aus dem Verbindungsobjekt gewonnen wurden für den zentralen Zugriff innerhalb des erweiterbaren SQLCompilers. Nachfolgend werden die statischen Attribute dieser Klasse vorgestellt: 98 KAPITEL 7. IMPLEMENTIERUNG - Connection dbConn. Hierin wird die Referenz auf das übergebene ConnectionObjekt gespeichert, welches zum Zugriff auf das Data Dictionary benutzt wird. - DataBaseMetaData dbMeta. In diesem Attribut werden die Metadaten des DBS, zu dem die zuvor beschriebene Verbindung aufgebaut wurde, gespeichert. Diese Daten werden automatisch bei der Zuweisung eines neuen Verbindungsobjekts ermittelt. - String owner. Der Benutzername, unter dem die Verbindung aufgebaut wurde, wird in diesen String gespeichert. Das Stringobjekt wird mit dem Prozentzeichen initialisiert, da es für die Auswahl der korrekten Tabellenspalten aus dem Data Dictionary gebraucht wird. Hier nun die get/set-Methoden der beschriebenen statischen Attribute: + setDBSConnection(Connection conn) : void. Mittels dieser Methode wird das übergebene Connection-Objekt als Verbindung für anstehende DBS-Zugriffe gesetzt. Parameter: conn - stellt die Referenz auf die zu nutzende Verbindung dar + getDBSConnection() : Connection. Sie liefert das gespeicherte ConnectionObjekt für den Zugriff auf die Datenbankdaten. Rückgabewert: die Referenz auf das aktive Connection-Objekt + getDBSMetaData() : DatabaseMetaData. Eine Methode, um die Metadaten zu erhalten, die im Attribut dbMeta gespeichert sind. Rückgabewert: eine Referenz auf das Attribut dbMeta dieser abstrakten Klasse + setOwner(String owner) : void. Durch den Aufruf dieser Methode wird der String des Benutzernamens im privaten Attribut owner gesetzt. Parameter: owner - der String des Benutzernamens + getOwner() : String. Diese Methode gibt den Inhalt des Attributs owner zurück. Rückgabewert: ein String-Objekt des gespeicherten Benutzernamens Die andere Klasse dieses Pakets lautet DBContent und dient der Bestimmung und Verwaltung der ermittelten Data Dictionary Inhalte. In dieser Programmversion ist ausschließlich die Zuordnung des Tabellennamens zu den in der Tabelle enthaltenen Spaltennamen realisiert. Das einzige Attribut dieser abstrakten Klasse lautet - Hashtable hashTabToCol. Dieses Attribut ist die zentrale Hashtable zur Verwaltung der Zuordnung zwischen Tabellen und ihren Spalten in Bezug auf ihre Benennungen. 99 KAPITEL 7. IMPLEMENTIERUNG Die in der folgenden Auflistung beschriebenen Methoden stellen die Schnittstellen zum Einfügen neuer Einträge in die Hashtable und für den Zugriff auf existierende Einträge bereit. Zusätzlich dazu sind auch zwei Methoden enthalten, die die Schnittmenge zweier Spaltenmengen bestimmen. + fetchAllTabCol() : boolean. Holen der Dictionary-Daten und Füllen der Hashtable für alle Tabellen, die in all tab columns vermerkt sind. Rückgabewert: true, wenn die Anfrage erfolgreich war; false sonst + fetchOneTabCol(String tabName) : boolean. Diese Methode füllt die interne Hashtable mit den ermittelten Spalten der übergebenen Tabelle auf. Parameter: tabName - String des Tabellennamens Rückgabewert: true, wenn die Anfrage erfolgreich war; false sonst + getOneTabCol(String tabName) : Vector. Liefert alle Spalten der übergebenen Tabelle in Form eines Vektors, sofern unter diesem Tabellennamen Einträge gespeichert sind. Parameter: tabName - String des Tabellennamens Rückgabewert: ein Vector-Objekt der enthaltenen Spaltennamen, oder null. + getColVector(String query) : Vector. Liefert alle Ergebnisspalten der übergebenen Anfrage in Form eines Vektors. Zu diesem Zweck wird eine Ergebniszeile der Anfrage geholt und ihre Spaltenmenge ausgewertet. Parameter: query - die zu stellende Anfrage als String-Objekt Rückgabewert: ein Vector der ermittelten Spaltenamen oder null im Fehlerfall + getEqualColVector(String tab1, String tab2) : Vector. Ermittelt die Schnittmenge der Spaltennamen der beiden übergebenen Tabellen, deren Spaltennamen zuvor schon aus dem Data Dictionary geholt worden sein müssen. 100 KAPITEL 7. IMPLEMENTIERUNG Parameter: tab1 - ist der Name der ersten Tabelle tab2 - ist der Name der zweiten Tabelle, die mit der ersten verglichen werden soll Rückgabewert: ein Vector-Objekt, welches die gemeinsamen Spaltennamen enthält, oder null falls keine Schnittmenge existiert + getIntersectVector(Vector vec1, Vector vec2) : Vector. Diese Methode vergleicht den Inhalt der übergebenen Vectoren auf gleichen Inhalt der enthaltenen Strings(Groß- Kleinschreibung egal). Diese Methode arbeitet nur mit VectorObjekten korrekt, die Strings enthalten. Parameter: vec1 - der erste Vector aus Strings vec2 - der zweite Vector aus Strings Rückgabewert: ein Vector-Objekt, welches die Schnittmenge der beiden Vector-Objekte zurückgibt. + clearTabToCol() : void. Eine Methode zum Löschen aller Einträge aus dem Hashtable Attribut dieser Klasse. 7.8 Paket parseplus.semantics Dieses Paket enthält die Klassen zur Bestimmung der Bestandteile geparster SQL– Ausdrucks und zur Überprüfung der semantischen Korrektheit in Bezug auf die benutzten Tabellenspalten. Die abstrakte Klasse Alias in diesem Paket vergibt zur Laufzeit des Programms eindeutige Integerwerte, die zur Bestimmung von Aliasbezeichnungen innerhalb des SQL–Ausdrucks genutzt werden können. Der nächste zu vergebene Integerwert wird in diesem statischen Attribut gespeichert: - int index. Dieses Attribut wird mit dem Wert 0 initialisiert und jeweils um 1 erhöht, wenn eine neue Anforderung mittels der Methode getNextIndex getätigt worden ist. Die folgenden zwei statischen öffentlichen Methoden werden zur Verwaltung des Integerattributs index eingesetzt. + getNextIndex() : int. Liefert den aktuellen Wert von index und erhöht den Inhalt des Attributs danach um 1. Rückgabewert: der eindeutige Integerwert 101 KAPITEL 7. IMPLEMENTIERUNG + reset() : void. Diese Methode setzt das Attribut index wieder auf den Ausgangswert 0. Die abstrakte Klasse ClauseManager verwaltet die Klauseln eines geparsten SQL–Ausdrucks. Durch ihre statischen Methoden kann man Zugriff auf die unterschiedlichen Klauseln erlangen. Die Klauseln sind in statischen Hashtabellen vom Typ Hashtable gespeichert, welche durch einen Durchlauf des Syntaxbaums mit einem Objekt der Klasse ClauseVisitor der Erweiterung gefüllt werden. Das Löschen der Referenzen auf bereits entfernte Knoten des Syntaxbaums aus den Hashtables ist schon in die Methode Modifier.deleteNode(AdvancedNode) integriert. Werden neue Knoten mittels der Methode Modifier.insertNode(AdvancedNode, AdvancedNode, int) eingefügt, so ist auch hier die Aktualisierung der Klauselreferenzen gewährleistet. Nachfolgend werden die privaten Eigenschaften dieser Klasse und in ihr definierte Konstanten erläutert: + final int QUERYEXP. Diese Konstante muss zum Einfügen von Queries benutzt werden (Select ...). + final int SELECTLIST. Diese Konstante muss zum Einfügen des der Select Liste benutzt werden (*, Spaltennamen, ...). + final int FROMCLAUSE. Diese Konstante muss zum Einfügen von From– Klauseln benutzt werden (From ...). + final int WHERECLAUSE. Diese Konstante muss zum Einfügen von Where– Klauseln benutzt werden (Where ...). + final int GROUPBYCLAUSE. Diese Konstante muss zum Einfügen von Group–By–Klauseln benutzt werden (group by ...). + final int SETCLAUSE. Diese Konstante muss zum Einfügen von Set–Klauseln benutzt werden (Union, Except oder Intersect). + final int ORDERBYCLAUSE. Diese Konstante muss zum Einfügen von Order–By–Klauseln benutzt werden (order by ...). - Hashtable queryExp. Dies ist die private und statische Hashtable für Queries. Als Schlüssel dient der jeweilige Elternknoten, der jeweils nur eine Querie enthalten kann. - Hashtable selectList. Dies ist die private und statische Hashtable für die Select Listen. Als Schlüssel dient der jeweilige Elternknoten, der jeweils nur eine Select Liste enthalten kann. 102 KAPITEL 7. IMPLEMENTIERUNG - Hashtable fromCl. Dies ist die private und statische Hashtable für die From– Klauseln. Als Schlüssel dient der jeweilige Elternknoten, der jeweils nur eine From–Klausel enthalten kann. - Hashtable whereCl. Dies ist die private und statische Hashtable für die Where– Klauseln. Als Schlüssel dient der jeweilige Elternknoten, der jeweils nur eine Where–Klausel enthalten kann. - Hashtable groupByCl. Dies ist die private und statische Hashtable für die Group– By–Klauseln. Als Schlüssel dient der jeweilige Elternknoten, der jeweils nur eine Where–Klausel enthalten kann. - Hashtable setCl. Dies ist die private und statische Hashtable für die Set–Klauseln. Als Schlüssel dient der jeweilige Elternknoten, der jeweils nur eine Set–Klausel enthalten kann. - Hashtable orderByCl. Dies ist die private und statische Hashtable für die Order– By–Klauseln. Als Schlüssel dient der jeweilige Elternknoten, der jeweils nur eine Order–By–Klausel enthalten kann. Gemäß der SQL-92 Grammatik kann nur eine Order–By Anweisung pro SQL–Ausdruck enthalten sein, somit würde für diese Grammatik hier auch ein einzelnes statisches AdvancedNode Attribut ausreichen. Durch diese Modifizierung würde jedoch das allgemeine Klauselhandling komplizierter werden. - Hashtable nodeToVector. Allgemein Nutzbare Hashtable, die Knoten als Schlüssel und einen String-Vector als zugehörigen Wert beinhaltet. Die hier beschriebenen Methoden regulieren den Zugriff auf die privaten Hashtabellen, die soeben als Attribute vorgestellt worden sind. + putClause(AdvancedNode toAdd, int type) : boolean. Diese Methode fügt den übergebenen Knoten in die mittels Parameter type ausgewählte Hashtable ein. Als Schlüssel in der jeweiligen Hashtable wird der Elternknoten des übergebenen Knotens benutzt, weshalb eine Null-Referenz in diesem Attribut das Einfügen verhindert. Auch das Nutzen eines unbekannten Integerwertes als Aktualparameter für type führt zum Ablehnen der Einfügeoperation und zur Rückgabe von false. Bereits existierende Einträge unter einem Schlüssel werden durch diese Methode ersetzt. Parameter: toAdd - ist die Referenz auf den Klauselknoten der gespeichert werden soll type - ist die Integerkonstante, die über die Art des einzufügenden Knotens informiert (Klauselart) Rückgabewert: true, wenn die Einfügeoperation erfolgreich war; false sonst 103 KAPITEL 7. IMPLEMENTIERUNG + getClause(AdvancedNode key, int type) : AdvancedNode. Durch den Aufruf dieser Methode wird der Klauselknoten eines definierten Typs (Parameter type) geliefert. Der Parameter key dient dabei als Schlüssel, um in der Hashtable des angegebenen Typs die gesuchte Klausel eindeutig zu bestimmen. Gemäß der Logik der Einfügeroutine putClause muss im ersten Parameter demnach der Elternknoten der Klausel stehen. Sollte der Schlüsselknoten gleich null, oder kein Knoten unter dem Schlüssel hinterlegt sein, so wird null zurückgegeben. Parameter: key - ist der Knoten vom Typ AdvancedNode, der der Elternknoten des gesuchten Knotens ist type - ist die Integerkonstante, die über die Art des gesuchten Knotens informiert (Klauselart) Rückgabewert: das Knotenobjekt vom Typ AdvancedNode, welches als Wert unter dem übergebenen Knoten im übergebenen Klauseltyp gespeichert ist, oder null, falls kein Knoten gefunden wurde + getClauseEnum(int type) : Enumeration. Mit dieser Methode wird der Zugriff auf eine java.util.Enumeration über die enthaltenen Klauselknoten der gesuchten Art(Parameter type) ermöglicht. Durch diese Enumeration erhält man Zugriff auf die Referenzen der tatsächlichen Knoten des abstrakten Syntaxbaums und kann sie so modifizieren. Ist der Klauseltyp unbekannt, oder die ausgewählte Hashtable leer, wird null zurückgegeben. Parameter: type - ist die Integerkonstante, die über die Art der gesuchten Knoten informiert (Klauselart) Rückgabewert: eine java.util.Enumeration über alle gespeicherten Klauselknoten des angegebenen Typs, oder null falls der Typ unbekannt oder keine Klausel der angegebenen Art in den Hashtables enthalten war. + removeNode(AdvancedNode node) : boolean. Dies ist eine Methode zum Löschen des übergebenen Knotens aus allen Hashtables. Sowohl mögliche Klauseleinträge, als auch Schlüsseleinträge des übergebenen Knotens werden gelöscht. Die Methode gibt true zurück, falls mindestens eine in den Hashtables gespeicherte Referenz auf den übergebenen Knoten gelöscht werden konnte. Sollte keine Referenz gefunden worden, oder der übergebene Knoten gleich null gewesen sein, wird false geliefert. Parameter: node - ist der zu löschende Knoten Rückgabewert: true, falls der übergebene Knoten entweder als Schlüssel oder Wert aus mindestens einer der Hashtables entfernt wurde; false sonst. + removeClause(AdvancedNode clause) : AdvancedNode. Dies ist eine private Hilfsmethode zum Löschen der Referenz auf die übergebene Klausel (Pa- 104 KAPITEL 7. IMPLEMENTIERUNG rameter clause) aus einer der Hashtables, in der sie als Wert enthalten war. Es werden Wert und Schlüssel aus der jeweiligen Hashtable entfernt. War der Löschvorgang erfolgreich, so wird die Referenz auf den gelöschten Knoten zurückgegeben, ansonsten null. Parameter: clause - ist der Knoten dessen Referenz als Wert aus einer der statischen Hashtables gelöscht werden soll Rückgabewert: der gelöschte Knoten vom Typ AdvancedNode oder null, wenn der Knoten nirgendwo als Wert enthalten ist + getClauseColumn(AdvancedNode clause) : Vector. Diese Methode liefert einen Vector aller ColumnReference Knoten unterhalb des übergebenen Knotens, jedoch nicht innerhalb einer Unterabfrage. Parameter: clause - der Knoten, dessen Kinder untersucht werden sollen Rückgabewert: ein Vector aller ColumnReference Knoten unterhalb des übergebenen Knotens; aber nicht innerhalb einer Unterabfrage + clearClauseManager() : void. Durch den Aufruf dieser Methode werden alle Hashtables dieser abstrakten Klasse ClauseManager geleert. + getFirstPreNodeOfType(AdvancedNode node, int type) : AdvancedNode. Diese Methode ermittetlt ausgehend vom übergebenen Knoten den ersten Vorgängerknoten des angegebenen Typs. Der Knotentyp ist dabei auf die oben beschriebenen Konstanten der Klauseln beschränkt. Parameter: node - der Knoten an dem die Suche startet type - der Typ des zu suchenden Knotens Rückgabewert: null, oder der erste Knoten des gesuchten Typs + isSameFirstPreNodeOfType(AdvancedNode node1, AdvancedNode node2, int type) : boolean. Mit Hilfe dieser Methode können zwei Knoten dahingehend überprüft werden, ob sie den selben Vorgängerknoten vom übergebenen Typ haben. Parameter: node1 - der erste Knoten, dessen Vorgänger gesucht wird node2 - der zweite Knoten, dessen Vorgänger gesucht wird type - der Typ des zu suchenden Vorgängerknotens Rückgabewert: true, wenn selbes Knotenobjekt als Vorgänger gefunden wurde; false sonst + getFirstSucNodeOfType(AdvancedNode node, int type) : AdvancedNode. Diese Methode ermittetlt ausgehend vom übergebenen Knoten den linkesten 105 KAPITEL 7. IMPLEMENTIERUNG Nachfolgerknoten des angegebenen Typs. Der Knotentyp ist dabei auf die oben beschriebenen Konstanten der Klauseln beschränkt. Parameter: node - der Knoten an dem die Suche startet type - der Typ des zu suchenden Knotens Rückgabewert: der linkeste Folgeknoten(oder node selbst) des Knotens node vom übergebenen Typ, oder null bei einer erfolglosen Suche + setNodeToVector(AdvancedNode key, Vector value) : void. Fügt der privaten Hashtabelle nodeToVector einen neuen Eintrag hinzu. Hierzu wird der übergebene Knoten key als Schlüssel und der Vector value als Wert genommen. Parameter: key - der zu benutzende Schlüsselknoten value - ein Stringvektor als Wert des Knotenobjekts + getNodeToVector(AdvancedNode key) : Vector. Diese Methode gibt den eingetragenen Wert zum Schlüssel AdvancedNode key aus der Hashtabelle nodeToVector zurück. Parameter: key - der Schlüsselknoten, dessen Wert zurückgegeben werden soll Rückgabewert: der gefundene Vector zum übergebenen Schlüssel, oder null im Fall der erfolglosen Suche + clearNodeToVector() : void. Diese Methode löscht die Hashtable nodeToVector. Nun folgt die konkrete Visitor-Klasse ClauseVisitor zur Aktualisierung der globalen Klauselinformationen in der abstrakten Klasse ClauseManager. Die Klasse ClauseVisitor erbt von der Klasse treebuilder.RunThroughVisitor die visit–Methoden für die unterschiedlichen Komponententypen des abstrakten Syntaxbaums. Diese visit–Methoden rufen alle die zentrale Methode runThrough( AdvancedNode, Object) auf, welche den Baumdurchlauf in Präordnung realisiert. In der Klasse ClauseVisitor wird diese Methode nun überladen, so dass sie vor dem Knotendurchlauf jeden Knoten auf eine der sieben gewünschten Klauselarten hin überprüft und ihn dann gegebenenfalls im ClauseManager hinterlegt. Auf folgende Klauselarten hin, die den Benennungen der Produktionen in der Grammatik entsprechen, wird geprüft: • QueryExpression • SelectList • FromClause • WhereClause 106 KAPITEL 7. IMPLEMENTIERUNG • GroupByClause • SetClause • OrderByClause Die einzige Methode dieser Klasse sei hier nun beschrieben: + runThrough(AdvancedNode node, Object data) : Object. In dieser überladenen Methode wird zusätzllich zum Durchlauf der Knoten mittels der Methode acceptClauseVisitor(ClauseVisitor,Object) auch die Überprüfung auf die Klauselart durchgeführt. Sollte der Knoten(Parameter node) von einem der sieben Typen sein, die oben aufgelistet sind, wird eine Referenz auf ihn durch die Methode ClauseManager.putClause(AdvancedNode,int) gespeichert. Parameter: node - ist der Startknoten für den Durchlauf data - Datum, welches von einem Knoten zum nächsten übergeben wird Rückgabewert: ein Object, welches vom Elternknoten übergeben wurde und alle Kindknoten verändert haben können Die Klasse ColumnVisitor repräsentiert eine Visitor Klasse, die zur Ermittlung der Knoten der Spaltenreferenzen unterhalb eines zu definierenden Knotens benutzt wird. Dazu werden wiederum sämtliche Implementationen der visit–Methoden von der Klasse RunThroughVisitor geerbt und lediglich deren Methode runThrough in ihrer Funktionalität überladen. + runThrough(AdvancedNode node, Object data) : Object. Diese überladene Methode realisiert weiterhin den Präordnung–Durchlauf ab dem übergebenen Knoten node, jedoch wird zusätzlich noch jeder besuchte Knoten auf seinen Typ hin überprüft. Sollte der Knoten eine QueryExpression“ sein, so werden ” deren Kindknoten nicht besucht, da dieser Visitor–Durchlauf nur die Spaltenreferenzen auf der gleichen Anfrageebene liefern soll. Ist der Knoten jedoch eine ColumnReference“, so wird eine Referenz auf diesen Knoten an den übergebe” nen Vector von bereits ermittelten Spaltenreferenzen angehängt. Dieser Vector wird mittels des Parameters data von Knoten zu Knoten übergeben und enthält am Ende des Visitor–Durchlaufs alle gefundenen Spaltenreferenzen. Parameter: node - ist der Startknoten für den Durchlauf und der auf den Typus hin zu testende data - ist ein Vector-Object, welches alle bereits gefundenen Spaltenknoten enthält Rückgabewert: der Vector aller bisher gefundenen ColumnReference“ Knoten ” 107 KAPITEL 7. IMPLEMENTIERUNG Die Visitor Klasse SucNodeOfTypeVisitor wird zur Bestimmung des linkesten Nachfolgers, der von einem festgelegten Knotentyp ist, eines beliebigen Baumknotens benötigt. Die Klasse erbt sämtliche visit–Methoden von der Klasse RunThroughVisitor und überlädt deren Methode runThrough, um den beschriebenen Zweck zu erfüllen. Auch diese Visitor Klasse enthält somit lediglich eine eigene Methode: + runThrough(AdvancedNode node, Object data) : Object. Diese überladene Methode realisiert weiterhin den Präordnung–Durchlauf ab dem übergebenen Knoten node, jedoch wird zusätzlich noch jeder besuchte Knoten auf seinen Typ hin überprüft. Sollte der besuchte Knoten vom gewünschten Typ sein, so wird der Baumdurchlauf an dieser Stelle abgebrochen und eine Referenz auf diesen Knoten zurückgegeben. Entscheidender Bedeutung kommt hierbei dem Parameter Object data zu. Zu Beginn des Baumdurchlaufs enthält er noch eine Referenz auf das Stringobjekt, welches den Typ des gesuchten Knotens beinhaltet. Sobald jedoch der gewünschte Knoten gefunden wurde, wird diesem Parameter die Referenz auf das Knotenobjekt zugewiesen. Etwaige nachfolgend besuchte Nachbarn des ermittelten Knotens verhindern ihrerseits den Besuch ihrer Kindknoten, indem sie die aktuelle Klasseninstanz im Parameter Object überprüfen. Sollte diese Instanz nicht vom Typ String sein, so wird ein tiefer gehender Baumdurchlauf verhindert. Parameter: node - ist der Startknoten für den Durchlauf und der auf den Typus hin zu testende data - ist ein String- oder AdvancedNode-Objekt, welches die zu suchende Knotenart oder den gefundenen Knoten enthält Rückgabewert: ist im Falle einer erfolglosen Suche der String des gesuchten Knotentyps, oder im Erfolgsfall die Referenz auf den gefundenen Knoten Die Visitor Klasse SemanticVisitor dieses Pakets wird zur semantischen Überprüfung der geparsten SQL-92–Ausdrucks eingesetzt. Die Klasse verfügt über drei visit–Methoden die nachfolgend beschrieben werden. + visit(SimpleNode node, Object data) : Object. Diese Methode wird für alle Knotenobjekte aufgerufen, die nicht vom Typ ASTFromClause oder ASTQueryExpression sind. Ihr Methodenkörper besteht lediglich aus einer Anweisung, nämlich der Rückgabe des übergebenen Parameters data. Parameter: node - ist das zu besuchende Knotenobjekt data - ist das übergebene Object, welches im Fehlerfall den String der Meldung enthält Rückgabewert: das übergebene Object wird ohne Veränderungen zurückgegeben 108 KAPITEL 7. IMPLEMENTIERUNG + visit(ASTFromClause node, Object data) : Object. Für den Besuch des From–Klausel Knotens des Syntaxbaums ist diese Methode vorgesehen. Innerhalb dieser Methode wird die Spaltenmenge ermittelt, über die die enthaltenen Tabellen und Unterabfragen verfügen. Diese Spaltenmenge wird anschließend unter dem Schlüssel des übergeordneten QueryExpression Knotens abgespeichert. Parameter: node - ist das zu besuchende Knotenobjekt der From–Klausel data - ist das übergebene Object, welches im Fehlerfall den String der Meldung enthält Rückgabewert: das übergebene Object wird ohne Veränderungen zurückgegeben + visit(ASTQueryExpression node, Object data) : Object. Die letzte Methode dieser Klasse führt schließlich die Überprüfung der enthaltenen Klauseln aus. Jede Klausel wird dahingehend geprüft, ob sie nur Spaltenreferenzen beinhaltet, die durch die From-Klausel bereit gestellt werden. Ist dies nicht der Fall, so wird eine SemanticException geworfen, die über den aufgetretenen Fehler Auskunft gibt. Parameter: node - ist das zu besuchende Knotenobjekt der QueryExpression data - ist das übergebene Object, welches im Fehlerfall den String der Meldung enthält Rückgabewert: im Fehlerfall wird der String der Fehlermeldung geliefert; ansonsten der übergebene Parameter data Die Klasse SemanticException ist von der Klasse java.lang.Exception abgeleitet. Sie dient der Darstellung eines gefundenen semantischen Fehlers in Form eines Strings. Dieser String enthält immer die Beschreibung des ersten gefundenen Fehlers und wird schließlich als Rückgabewert der Methode ParsePlus.semanticizeSQL92Statement dem Nutzer des erweiterbaren SQL-Compilers bekannt gemacht. Konkrete Instanzen dieser Klasse werden in den zuvor beschriebenen visit–Methoden der Klasse SemanticVisitor erzeugt und geworfen. Um die Exception an den jeweiligen semantischen Fehler anzupassen, stehen die nachfolgend beschriebenen Attribute und Methoden der Klasse SemanticException bereit. - String column. In diesem String wird die Benennung der fehlerhaften Spaltenreferenz gespeichert. - String clause. Dieser String enthält den textuellen Zusammenhang der Klausel in der der Fehler aufgetreten ist. 109 KAPITEL 7. IMPLEMENTIERUNG + SemanticException(String column, String clause). Der Konstruktor dieser Exceptionklasse bekommt die Benennungen der Spalte und ihres textuellen Zusammenhangs übergeben. Die beiden Strings werden den gleichnamigen privaten Attributen dieser Klasse zugewiesen. Parameter: column - die Benennung des inhaltlich falsch benutzten Spaltennamens clause - der Text der Klausel in der der Fehler aufgetreten ist + getMessage() : String. Der Rückgabewert dieser Methode ist die Fehlermeldung der ausgelösten Exception. Er enthält die Angaben aus den privaten Eigenschaften column und clause der jeweiligen Exeptioninstanz. Rückgabewert: der String der Fehlermeldung bzgl. der semantischen Überprüfung 7.9 Erweiterung des Pakets org.javacc.jjtree Zur Anpassung der Java–Dateien, die durch JJTree erzeugte werden(s. Abschnitt 2.3.2), an den Datenfluss des erweiterbaren SQL-Compilers sind Änderungen am ursprünglichen Sourcecode des Präprozessors JJTree vollzogen worden. Diese betreffen die in diesem Abschnitt aufgeführten Methoden der beiden Klassen JJTreeState und NodeFiles. Nach dem Parsen der jjt-Grammatikdatei werden die Klassen zur Erstellung der Baumstruktur(s. Abschnitt 7.6) von JJTree in verschiedenen Java-Dateien erstellt. Hierfür werden die statischen Methoden der Klasse NodeFiles genutzt, die, bis auf die hier aufgelisteten, unverändert in diese Klasse innerhalb des erweiterbaren SQLCompilers übernommen wurden. + ensure(IO io, String nodeType) : void. Durch den Aufruf dieser Methode werden leere Java-Dateien im festgelegten Ausgabeverzeichnis erzeugt und die Erzeugung des beinhalteten Quellcodes initiiert. Deren Dateinamen werden durch den zweiten Parameter(nodeType) festgelegt. Durch die Änderungen an dieser Methode wird die Datei Node.java“ nicht mehr erzeugt, da ihre angepasste ” Version im Paket parseplus.modifier(Abschnitt 7.4) benutzt wird. Parameter: io - ist ein Objekt der finalen Klasse IO, welche die Ausgabeströme zentral verwaltet nodeType - ist der String des Dateinamens der zu erzeugenden Java-Datei + generateVisitor java() : void. Die Methode zur Erzeugung des abstakten Visitors in Form des Interfaces <Parsername>Visitor. Diese Methode wird direkt aus main-Methode von JJTree aufgerufen und wurde so verändert, dass die beiden konkreten Visitorklassen automatisch erzeugt werden. 110 KAPITEL 7. IMPLEMENTIERUNG - generateBasicVisitor java() : void. Diese private Hilfsmethode erzeugt die Visitorklasse BasicVisitor und wurde neu zur Klasse NodeFiles hinzugefügt. - generateRunThroughVisitor java() : void. Diese private Hilfsmethode erzeugt die Visitorklasse RunThroughVisitor und wurde neu zur Klasse NodeFiles hinzugefügt. - generateSimpleNode java(PrintWriter ostr) : void. Die Methode erzeugt die Basisklasse SimpleNode der Datenstruktur Syntaxbaum und wurde entsprechend der erweiterten Funktionalität dieser in Abschnitt 7.6 beschriebenen Klasse modifiziert. Parameter: ostr - das konkrete Objekt der Klasse PrintWriter, das auf die mit Quellcode zu füllende Datei gerichtet ist - generateMULTINode java(PrintWriter ostr, String nodeType) : void. Auch diese Methode ist Bestandteil der standardmäßigen Klasse NodeFiles in der JJTree Version 4.0. Ihr Zweck ist es, die Komponentenklassen des erzeugbaren Syntaxbaums zu erstellen. Mittels des zweiten Parameters(nodeType) wird der, aus der jeweiligen Produktion der Grammatik abgeleitete, Name der zu erzeugenden Klasse bestimmt. Parameter: ostr - das konkrete Objekt der Klasse PrintWriter, das auf die mit Quellcode zu füllende Datei gerichtet ist nodeType - ist der Klassenname der zu erzeugenden Komponentenklasse Die zweite erweiterte Klasse dieses Pakets lautet JJTreeState. An ihr ist die Erzeugung des Quellcodes der Klasse JJT<Parsername>State modifiziert worden. - insertState(PrintWriter ostr) : void. Diese Methode fügt den Quellcode der Klasse ein, die den Stack zur Zwischenspeicherung der erzeugten Knotenobjekte und die Methoden zu deren logischer Verknüpfung enthält. Die Änderungen betreffen hier ausschließlich die Umstellung der gemeinsamen Knotenschnittstelle vom Interface Node auf das Interface AdvancedNode im Paket parseplus.modifier(s. Abschnitt 7.4). Parameter: ostr - das konkrete Objekt der Klasse PrintWriter, das auf die mit Quellcode zu füllende Datei gerichtet ist 7.10 Erweiterung des Pakets org.javacc.parser Die Änderungen an diesem Paket des Parsergenerators JavaCC(Vers. 4.0) betreffen die Klasse OtherFilesGen, aus der die Anweisungen zur Erzeugung der Klasse ParseException und des Interfaces Token entfernt wurden. Die Java-Datei der Klasse ParseException samt Quellcode muss nicht mehr für jede Erweiterung generiert werden, da 111 KAPITEL 7. IMPLEMENTIERUNG gemäß des Konzepts die gleichnamige Exceptionklasse im Paket parseplus.modifier(s. Abschnitt 7.4) von den Parsermethoden referenziert wird. Für das Interface Token gilt das Gleiche, auch hier ist ein gleichnamiges, aber inhaltlich modifiziertes, Interface im Paket parseplus.modifier enthalten. Der Befehl zur Erzeugung dieser beiden Dateien ist aus der folgenden statischen Methode entfernt worden: + start() : void. Diese Methode startet die Erzeugung der Hilfsklassen und Interfaces des zu generierenden Parsers. Die entsprechenden Befehle für die beiden Java-Code Dateien ParseException.java und Token.java sind aus dieser Methode entfernt worden. 112 Kapitel 8 Ausblick • Ausbau der Zugriffsmöglichkeiten auf das Data Dictionary Im aktuellen Entwicklungsstadium des erweiterbaren SQL-Compilers werden schon die Möglichkeiten geboten, die Attributnamen einer Tabelle und Meta– Daten des DBS zu ermitteln. Diese Optionen könnten um die Überprüfung anderer Angaben wie etwa Datentypen eines Attributs oder Auflistung der gespeicherten Prozeduren erweitert werden. • Alternative Baumdurchläufe Neben den bereits implementierten Formen des Durchlaufs eines Syntaxbaums, können für bestimmte Anwendungen alternative Formen sinnvoll sein. Als Beispiele sind hier die Durchläufe in Stufenordnung oder nach dem Euler–Verfahren zu nennen. • Ausbau der Zugriffsmöglichkeiten auf Teilklauseln Die vorgefertigte Ermittlung einzelner Teilklauseln kann auf andere Teile der DML–Anweisungen als den bisher schon gelieferten ausgebaut werden. Denkbar wäre in diesem Zusammenhang eine Auflistung aller enthaltenen Funktionsaufrufe oder der direkte Zugriff auf die For–Update–Of–Klausel. 113 Anhang A SQL-92 Grammatik Im Kapitel A.1 des Anhangs ist die im Rahmen dieser Arbeit erstellte Untermenge der SQL-92 Grammatik anhand aller Produktionen, sowie terminalen und nichtterminalen Symbolen dargestellt. Die Grundlage für die Erstellung der SQL-92 Grammatik sind die Angaben in der Veröffentlichung [MS93] gewesen. A.1 A.1.1 Grammatik Nichtterminale Symbole N = { <LINE_COMMENT>, <MULTI_LINE_COMMENT>, <APPROX_NUM_LIT>, <EXACT_NUM_LIT>, <DIGIT>, <REG_IDENTIFIER>, <SIMP_LAT_LET>, <UNDERSCORE>, <Parameter_Name>, <CHAR_STRING_LITERAL>, <DELIM_IDENTIFIER>, <BasicDataTypeDeclaration>, <SQLStatement>, <CommitStatement>, <DeleteStatement>, <InsertStatement>, <RollbackStatement>, <SetTransactionStatement>, <UpdateStatement>, <CreateStatement>, <TableElement>, <ColumnDefinition>, <DefaultClause>, <SetClauseList>, <UpdateSource>, <ColumnReference>, <ActualIdentifier>, <CompOp>, <TableName>, <SelectStatement>, <QueryExpression>, <SelectList>, <SelectItem>, <TableSubQuery>, <SelectAlias>, <FromClause>, <TableReference>, <CorrelationName>, <WhereClause>, <GroupByClause>, <SetClause>, <OrderByClause>, <SortKey> ,<ForUpdateClause>, <SearchCondition>, <BooleanTerm>, <BooleanFactorPred>, <SelectStar>, <Predicate>, <ExpressionList>, <UNSIGNED_INT>, <ExistsPredicate>, <ComparisonOperatorExpression>, <InPredicate>, <BetweenPredicate>, <LikePredicate>, <NullPredicate>, <SimpleExpression>, <MultExpression>, <Factor>, <ValueExpressionPrimary>, <SetFunctionType>, <FunctionCall>, <colon>, <period>, <double_quote>, <left_paren>, <right_paren>, <asterisk>, <plus_sign>, <minus_sign>, <semicolon>, <equals_operator>, <concat_operator>, <less_than_operator>, <greater_than_operator>, <comma>, <not_equals_operator>, 114 ANHANG A. SQL-92 GRAMMATIK <greater_than_or_equals_operator>, <less_than_or_equals_operator>, <solidus> }, A.1.2 Terminale Symbole T = { space, eof, tab, return, newline, -, ", (, ), ,, ;, =, ., !=, <>, >, >=, <, <=, *, +, -, ||, /, ’, _, :, a, b, ..., z, A, B, ..., Z, 0, 1, ..., 9, ALL, AND, ANY, AS, ASC, AVG, BETWEEN, BINARY_INTEGER, BY, CHAR, CHARACTER, COMMIT, COUNT, CURRENT, CURRENT_USER, CREATE, DATE, DEC, DECIMAL, DELETE, DESC, DEFAULT, DISTINCT, DOUBLE, EXCEPT, EXISTS, FLOAT, FOR, FROM, GLOBAL, GROUP, HAVING, IN, INSERT, INTEGER, INTERSECT, INTO, IS, LIKE, LOCAL, MAX, MIN, NATURAL, NOT, NULL, NUMERIC, OF, ON, ONLY, OR, ORDER, PRECISION, PRESERVE, PRIOR, READ, REAL, ROLLBACK, ROWS, SELECT, SESSION_USER, SET, SMALLINT, SOME, SUM, SYSTEM_USER, TABLE, TEMPORARY, TIME, TIMESTAMP, TRANSACTION, UNION, UPDATE, USER, VALUES, VARCHAR, VARYING, WHERE, WITH, WORK, WRITE, ZONE }, A.1.3 Startsymbol S = { <SQLStatement> }, 115 ANHANG A. SQL-92 GRAMMATIK A.1.4 Produktionen P = { <SQLStatement> ::= ( | | | | | | | ) <CommitStatement> <DeleteStatement> <InsertStatement> <RollbackStatement> <SelectStatement> <SetTransactionStatement> <UpdateStatement> <CreateStatement> [ <semicolon> ] eof <CommitStatement> ::= COMMIT [ WORK ] <DeleteStatement> ::= DELETE FROM <TableName> [ WHERE ( ( CURRENT OF <ActualIdentifier> ) | <SearchCondition> ) ] <InsertStatement> ::= INSERT INTO <TableName> ( [ <left_paren> <ActualIdentifier> { <comma> <ActualIdentifier> } <right_paren> ] ( VALUES <left_paren> <ExpressionList> <right_paren> | <QueryExpression> ) ) | DEFAULT VALUES <RollbackStatement> ::= ROLLBACK [ WORK ] <SelectStatement> ::= <QueryExpression> [ <OrderByClause> ] [ <ForUpdateClause> ] <SetTransactionStatement> ::= SET TRANSACTION ( READ ( ONLY | WRITE ) ) <UpdateStatement> ::= UPDATE <TableName> SET <SetClauseList> [ WHERE ( ( CURRENT OF <ActualIdentifier> ) | <SearchCondition> ) ] 116 ANHANG A. SQL-92 GRAMMATIK <CreateStatement> ::= CREATE [ ( GLOBAL | LOCAL ) TEMPORARY ] TABLE <TableName> <left_paren> <TableElement> { <comma> <TableElement> } <right_paren> [ ON COMMIT ( DELETE | PRESERVE ) ROWS ] <TableElement> ::= <ColumnDefinition> <ColumnDefinition> ::= <ActualIdentifier> <BasicDataTypeDeclaration> [ <DefaultClause> ] <DefaultClause> ::= DEFAULT ( USER | CURRENT_USER | SESSION_USER | SYSTEM_USER | NULL ) <SetClauseList> ::= <ActualIdentifier> <equals_operator> <UpdateSource> { <comma> <ActualIdentifier> <equals_operator> <UpdateSource> } <UpdateSource> ::= <SimpleExpression> | NULL | DEFAULT <ColumnReference> ::= <ActualIdentifier> [ <period> <ActualIdentifier> [ <period> <ActualIdentifier> [ <period> <ActualIdentifier> ] ] ] <ActualIdentifier> ::= <REG_IDENTIFIER> | <DELIM_IDENTIFIER> <CompOp> ::= <equals_operator> | <not_equals_operator> | <greater_than_operator> | <greater_than_or_equals_operator> | <less_than_operator> | <less_than_or_equals_operator> <TableName> ::= <ActualIdentifier> [ <colon> <ActualIdentifier> [ <colon> <ActualIdentifier> ] ] <QueryExpression> ::= SELECT [ ALL | DISTINCT ] <SelectList> <FromClause> [ <WhereClause> ] [ <GroupByClause> ] [ <SetClause> ] <SelectList> ::= <asterisk> | <SelectItem> { <comma> <SelectItem> } <SelectItem> ::= <SelectStar> | <SimpleExpression> [ <SelectAlias> ] 117 ANHANG A. SQL-92 GRAMMATIK <SelectAlias> ::= [ AS ] ( <ActualIdentifier> )+ <SelectStar> ::= <ActualIdentifier> <period> <asterisk> | <ActualIdentifier> <period> <ActualIdentifier> <period> <asterisk> <FromClause> ::= FROM <TableReference> { <comma> <TableReference> } <TableReference> ::= ( <TableName> | <TableSubQuery> ) [ [ AS ] <CorrelationName> [ <left_paren> <ActualIdentifier> { <comma> <ActualIdentifier> } <right_paren> ] ] <TableSubQuery> ::= <left_paren> <QueryExpression> <right_paren> <CorrelationName> ::= <ActualIdentifier> <WhereClause> ::= WHERE <SearchCondition> <GroupByClause> ::= GROUP BY <ExpressionList> [ HAVING <SearchCondition> ] <SetClause> ::= ( ( UNION | EXCEPT | INTERSECT ) [ ALL ] ) ( <left_paren> <QueryExpression> <right_paren> ) | <QueryExpression> <OrderByClause> ::= ORDER BY <SortKey> [ ASC | DESC ] { <comma> <SortKey> [ ASC | DESC ] } <SortKey> ::= <ActualIdentifier> | <UNSIGNED_INT> <ForUpdateClause> ::= FOR ( READ ONLY | ( UPDATE [ OF <ColumnReference> { <comma> <ColumnReference> } ] ) ) <SearchCondition> ::= <BooleanTerm> { OR <BooleanTerm> } <BooleanTerm> ::= <BooleanFactorPred> { AND <BooleanFactorPred> } <BooleanFactorPred> ::= <ExistsPredicate> | ( [ NOT ] <Predicate> ) 118 ANHANG A. SQL-92 GRAMMATIK <ExistsPredicate> ::= [ NOT ] EXISTS <left_paren> <QueryExpression> <right_paren> <Predicate> ::= ( <left_paren> <ExpressionList> <right_paren> | <SimpleExpression> ) [ <ComparisonOperatorExpression> | <InPredicate> | <BetweenPredicate> | <LikePredicate> | <NullPredicate> ] <ExpressionList> ::= <SimpleExpression> { <comma> <SimpleExpression> } <ComparisonOperatorExpression> ::= <CompOp> ( ( [ ALL | ANY | SOME <left_paren> <QueryExpression> <right_paren> ) | <SimpleExpression> ) ] <InPredicate> ::= [ NOT ] IN <left_paren> ( <ExpressionList> | <QueryExpression> ) <right_paren> <BetweenPredicate> ::= [ NOT ] BETWEEN <SimpleExpression> AND <SimpleExpression> <LikePredicate> ::= [ NOT ] LIKE <SimpleExpression> <NullPredicate> ::= IS [ NOT ] NULL <SimpleExpression> ::= <MultExpression> { ( <plus_sign> | <minus_sign> | <concat_operator> ) <MultExpression> } <MultExpression> ::= <Factor> { ( <asterisk> | <solidus> ) <Factor> } 119 ANHANG A. SQL-92 GRAMMATIK <Factor> ::= [ <plus_sign> | <minus_sign> ] <ValueExpressionPrimary> <ValueExpressionPrimary> ::= | | | | | | | | COUNT <left_paren> <asterisk> <right_paren> <SetFunctionType> <left_paren> [ ALL | DISTINCT ] <SimpleExpression> <right_paren> <FunctionCall> <ColumnReference> <APPROX_NUM_LIT> <UNSIGNED_INT> <CHAR_STRING_LITERAL> <Parameter_Name> <left_paren> ( <SearchCondition> | <QueryExpression> ) <right_paren> <SetFunctionType> ::= SUM | AVG | MAX | MIN | COUNT <FunctionCall> ::= <REG_IDENTIFIER> <left_paren> <ExpressionList> <right_paren> <UNSIGNED_INT> ::= ( <DIGIT> )+ <APPROX_NUM_LIT> ::= <EXACT_NUM_LIT> ::= <DIGIT> ::= ( <UNSIGNED_INT> | <EXACT_NUM_LIT> ) [ "E" [ <minus_sign> , <plus_sign> ] <UNSIGNED_INT> ] <UNSIGNED_INT> <period> <UNSIGNED_INT> | <period> <UNSIGNED_INT> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 <REG_IDENTIFIER> ::= ( <SIMP_LAT_LET> )+ { <DIGIT> | <SIMP_LAT_LET> |<UNDERSCORE> } <Parameter_Name> ::= <colon> <REG_IDENTIFIER> [ <period> <REG_IDENTIFIER> ] <CHAR_STRING_LITERAL> ::= <quote> { ~[ <quote> ] } <quote> { <quote> { ~[ <quote> ] } <quote> } <DELIM_IDENTIFIER> ::= <double_quote> { ~[ newline, return , <double_quote> ] } <double_quote> 120 ANHANG A. SQL-92 GRAMMATIK <BasicDataTypeDeclaration> ::= ( ( CHAR [ VARYING ] | VARCHAR | CHARACTER [ VARYING ] ) [ <left_paren> <UNSIGNED_INT> <right_paren> ] ) | ( INTEGER | INT | SMALLINT | ( ( NUMERIC | DECIMAL | DEC ) [ <left_paren> <UNSIGNED_INT> [ <comma> <UNSIGNED_INT> ] <right_paren> ] ) ) | ( DOUBLE PRECISION | REAL | FLOAT [ <left_paren> <UNSIGNED_INT> <right_paren> ] ) | DATE | ( TIME | TIMESTAMP ) [ <left_paren> <UNSIGNED_INT> <right_paren> ] [ WITH TIME ZONE ] <SIMP_LAT_LET> ::= ( a ... z ) | ( A ... Z ) <UNDERSCORE> ::= _ <colon> ::= : <period> ::= . <double_quote> ::= " <quote> ::= ’ <left_paren> ::= ( 121 ANHANG A. SQL-92 GRAMMATIK <right_paren> ::= ) <asterisk> ::= * <plus_sign> ::= + <minus_sign> ::= <semicolon> ::= ; <equals_operator> ::= = <concat_operator> ::= || <less_than_operator> ::= < <greater_than_operator> ::= > <comma> ::= , <not_equals_operator> ::= <> <greater_than_or_equals_operator> ::= >= <less_than_or_equals_operator> ::= <= <solidus> ::= / <LINE_COMMENT> ::= <minus_sign> <minus_sign> { ~[ return , newline ] } <MULTI_LINE_COMMENT> ::= <solidus> <asterisk> { ~[ <asterisk> ] } <asterisk> { <asterisk> | ( ~[ <asterisk>, <solidus> ] { ~[ <asterisk> ] } <asterisk> ) } <solidus> } 122 Anhang B Handbuch für Erweiterungen In diesem Teil des Anhangs ist die im Abschnitt 4.6 beschriebene Vorgehensweise zur Erweiterung des SQL-Compilers Parseplus anhand eines Beispiels Schritt für Schritt erklärt. Schritt 0 – Entpacken der Programmdateien Zu Beginn eines Erweiterungsvorgangs muss die Archivdatei Parseplus.zip in das gewünschte Verzeichnis entpackt werden. Die folgenden vier Dateien sind in diesem Archiv enthalten: • parseplus v1.0.jar ist das ausführbare JAR–Archiv, welches den erweiterbaren SQL–Compiler enthält • parseplus.sh ist das Startskript des SQL–Compilers, dass im Schritt 1 Verwendung findet • javacc.jar ist das ausführbare JAR–Archiv des Java Compiler Compilers in der Version 4.0 • grammar.jjt ist die Grammatikdatei, die zur Erweiterung der SQL-92 Syntax benutzt werden soll Schritt 1 – Erweitern der Grammatik Der erste Schritt der Erweiterung ist die Modifikation der Grammatikdatei grammar.jjt gemäß der beliebig erweiterten SQL-92 Syntax. Die ausgewählten Optionen, die Methode transformSQL in der Parserklasse und die Startproduktion SQLStatement müssen erhalten bleiben, um die Schnittstellen zum erweiterbaren SQL-Compiler zu wahren. Der Name der Parserklasse und der Grammatikdatei, sowie die EBNF sämtlicher enthaltener Produktionen kann frei verändert werden. Natürlich ist es auch möglich, neue Produktionen hinzuzufügen. 123 ANHANG B. HANDBUCH FÜR ERWEITERUNGEN Schritt 2 – Ausführen des Startskripts Um den Parser, der im ersten Schritt erstellten, Grammatik zu erzeugen, muss das Shell–Skript parseplus.sh ausgeführt werden. Daraufhin wird eine GUI eingeblendet, in der die betreffende Grammatikdatei vom Typ .jjt“ ausgewählt werden kann. Auf ” der Basis der ausgewählten Datei werden anschließend die Klassen zur Syntaxbaumerzeugung und die Grammatikdatei(Typ .jj“) für JavaCC generiert. ” Die nun generierte Grammatikdatei muss in der zweiten GUI ausgewählt werden, die unmittelbar nach der Erzeugung der Syntaxbaumklassen eingeblendet wird. Sollte in der Syntax oder Semantik der zu Beginn gewählten Grammatikdatei ein Fehler entdeckt worden sein, so wird dieser dem Benutzer in der Kommandozeile angezeigt. Es ist auch möglich, die Auswahl in der ersten GUI abzubrechen, um eine erneute Erzeugung der Baumklassen zu verhindern. In jedem Fall wird jedoch nachfolgend die GUI zur Auswahl der von JavaCC verarbeiteten Grammatik geöffnet, deren Abbruch das Programm schließlich ohne die Erzeugung irgendeiner Klasse beenden würde. Schritt 3 – Einbinden des Parseplus –Archivs Nachdem als Ergebnis der Schritte null bis zwei sämtliche Klassen des erweiterten SQL-Compilers erzeugt worden sind, muss im vierten Schritt lediglich noch das Archiv parseplus v1.0.jar zu dem Klassenpfad dieses Compilers hinzugefügt werden. Schritt 4 – Überladen der visit–Methoden In diesem vorletzten Schritt der Erweiterung ist eine Visitor Klasse zu erzeugen, die die Implementation sämtlicher Methoden von einer der beiden generierten Klassen BasicVisitor oder RunThroughVisitor erbt. Die visit–Methoden der von der Erweiterung der Syntax betroffenen Knotenklassen werden in dieser neuen Klasse mit den entsprechenden Anweisungen zur Transformation überladen. Schritt 5 – Starten des Baumdurchlaufs Als letzter Schritt wird eine der accept–Methoden der Knotenklassen mit einem Objekt der neu erstellten Visitor Klasse aufgerufen. Dieser Aufruf ist in die hierfür vorbereitete Methode transformSQL der Parserklasse einzufügen. 124 ANHANG B. HANDBUCH FÜR ERWEITERUNGEN Im folgenden Ausschnitt des Quellcodes der im Abschnitt 4.7.1 vorgestellten SQL– Transformation ist der Aufruf der Methode postOrderAccept des Wurzelknotens des Syntaxbaums dargestellt: final public Object transformSQL(Object data) { AdvancedNode root = ParsePlus.getRootNode(); if (root == null) { return null; } else { ASTSQLStatement spzfdRoot = (ASTSQLStatement) root; // NEU Hinzugefügt spzfdRoot.postOrderAccept(new NJoinTrafoVisitor(), new Hashtable<AdvancedNode, Vector<String>>()); return data; } } Nachdem der Programmierer der Erweiterung die vorgestellten sechs Schritte fehlerfrei abgearbeitet hat, ist er nun in der Lage, mittels der statischen Methode startParsingExtension der Klasse ParsePlus einen Ausdruck in der erweiterten SQL– Syntax zu parsen und diesen anschließend durch den Aufruf der Methode transformExtdSQL in die SQL-92 Syntax umzuwandeln. 125 Abbildungsverzeichnis 1.1 Struktur eines erweiterten SQL-Compilers . . . . . . . . . . . . . . . 5 2.1 2.2 2.3 Durch JavaCC erzeugte Klassenhierarchie . . . . . . . . . . . . . . . Durch JJTree erzeugte Klassenhierarchie . . . . . . . . . . . . . . . . JJTree Baumstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 24 25 3.1 Ersetzen des nichtterminalen Symbols <table element list> . . . . . . 3.2 Auflösen einer Linksrekursion . . . . . . . . . . . . . . . . . . . . . . 32 32 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 Der abstrakte Syntaxbaum eines Delete–Ausdrucks . . Die Schnittstellen SimpleNode und AdvancedNode . . . Bestandteile des Visitor Pattern . . . . . . . . . . . . . Klassendiagramm des generierten Visitor Pattern . . . Hierarchieeinordnung des generierten Visitor Interfaces Tätigkeiten zur Erweiterung von Parseplus . . . . . . . Visitorklassen zur Umwandlung eines Natural Joins“ . ” Erweiterung der SQL-92 Grammatikdatei . . . . . . . . Die Visitorklasse NJoinTrafoVisitor . . . . . . . . . . . Einfügen eines ASTCorrelationName Knotens . . . . . Besuch eines ASTJoinClause Knotens . . . . . . . . . . Ermitteln der Join–Attribute . . . . . . . . . . . . . . . Teilbaum einer SQLSF–Anfrage . . . . . . . . . . . . . . . . . . . . . . . . . . 35 37 39 41 42 46 49 51 52 54 55 57 60 5.1 Ein SQL–Ausdruck und die entsprechende PAPL-Darstellung . . . . . 65 6.1 Sequenzen der semantischen Analyse im Fehlerfall . . . . . . . . . . . 68 7.1 Paketdiagramm der Applikation Parseplus . . . . . . . . . . . . . . . . 72 126 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturverzeichnis [ANT] AnTLR. T. Parr, http://www.antlr.org (22.04.2006). [GHJV95] E. Gamma, R. Helm, R. Johnson, J. Vlissides: Design Patterns – Elements of Reusable Object-Oriented Software. Addison-Wesley Professional Computing Series, Addison-Wesley, Reading, MA, 1995. [JCC] The Source for Java Technology Collaboration - JavaCC Project Home. Sun Microsystems, https://javacc.dev.java.net (10.04.2006). [MS93] J. Melton, A. R. Simon: Understanding the New SQL: A Complete Guide. Morgan Kaufmann Publishers, San Mateo, CA, 1993. [Par05] R. Parchmann. Script zur Vorlesung Programmiersprachen und Übersetzer SS2005. Institut für Informatik, Universität Hannover, 2005. [War06] H. Warneke: Anfragebearbeitung in föderierten räumlichen Datenbanksystemen. Master’s thesis, 2006. 127 Erklärung Hiermit versichere ich, daß ich die vorliegende Arbeit und die zugehörige Implementierung selbständig verfaßt und dabei nur die angegebenen Quellen und Hilfsmittel verwendet habe. Hannover, 07. August 2006 Lutz Steinleger 128