Bachelorarbeit Entwicklung eines erweiterbaren SQL

Werbung
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
Herunterladen