package de.fuberlin.projectci.grammar;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.fuberlin.commons.util.LogFactory;
/**
*
* Implementierung eines GrammarReaders für Grammatiken in einer simplen Backus-Naur-Form.<br />
* Zu beachten:<br />
* <ul>
* <li> jede Produktion in einer Zeile</li>
* <li> Zeichenfolge ::= als Definitionssymbol</li>
* <li> vertikaler Strich | als Zeichen für eine Alternative</li>
* <li> Nichtterminale in spitzen Klammern eingeschlossen <...></li>
* <li> Terminale in doppelten Anführungszeichen "..."</li>
* <li> Startsymbol der Grammatik ist das Nichtterminal der linken Regelseite(LHS) der 1. eingelesenen Produktion</li>
* <li> Das leere Wort wird mit dem Terminal "@" dargestellt.</li>
* <li> Leerzeilen zwischen den Produktionen möglich</li>
* <li> Leerzeichen innerhalb von Produktionen möglich</li>
* </ul>
* @see <a href="http://de.wikipedia.org/wiki/Backus-Naur-Form">Wikipedia: Backus-Naur-Form</a>
*/
public class BNFGrammarReader implements GrammarReader{
private static Logger logger=LogFactory.getLogger(BNFGrammarReader.class);
/**
* Liest von einem Reader-Objekt die BNF-Grammatik ein und erstellt daraus ein neues
* Grammar-Objekt.
* @param r Reader-Objekt der BNF-Grammatik. Produktionen müssen mit Zeilenumbrüchen getrennt sein.
* @return Grammatik-Object mit eingelesenen Produktionen.
* @throws BNFParsingErrorException Falls das Reader-Objekt keine gültige BNF-Grammatik
* einlesen konnte.
*/
public Grammar readGrammar(Reader r) throws BNFParsingErrorException {
logger.fine("Reading grammar file...");
Grammar grammar = new Grammar();
int foundProductions = 0;
int lineNumber = 0;
try {
// Textdatei zeilenweise einlesen
BufferedReader reader = new BufferedReader(r);
String line = null;
while((line = reader.readLine()) != null) {
++lineNumber;
if(line.length() > 0) { // Leerzeilen ignorieren
// pro Zeile genau eine Produktion parsen
List<Production> productions = parseProduction(line, grammar);
for(Production production : productions){
grammar.addProduction(production);
++foundProductions;
// Annahme: erste Produktion enthält Startsymbol
if(foundProductions == 1)
grammar.setStartSymbol(production.getLhs());
}
}
}
} catch (FileNotFoundException e) {
// Textdatei nicht gefunden oder nicht zugreifbar
throw new BNFParsingErrorException(e);
} catch (IOException e) {
// Lesefehler z.B. durch Interrupt
throw new BNFParsingErrorException(e);
} catch (BNFParsingErrorException e) {
// falls beim Parsen einer Zeile ein Fehler auftritt, wird Zeilennummer mit angegeben
throw new BNFParsingErrorException(e.getMessage()+" (Line "+lineNumber+")");
}
if(foundProductions == 0)
logger.info("WARNING Created grammar contains no productions!");
logger.info("Grammar successfully built.");
return grammar;
}
/**
* Erstellt einen Reader für ein gegebenes File-Objekt, welches die BNF-Grammatik enthält
* und erstellt daraus ein neues Grammar-Objekt.
* @param file File-Objekt, das auf eine BNF-Grammatik-Datei verweist.
* @return Grammatik-Object mit eingelesenen Produktionen.
* @throws BNFParsingErrorException Falls das File-Objekt keine gültige BNF enthält
* oder nicht gelesen werden konnte.
*/
public Grammar readGrammar(File file) throws BNFParsingErrorException{
FileReader reader = null;
try {
reader = new FileReader(file);
} catch (FileNotFoundException e) {
// Textdatei nicht gefunden oder nicht zugreifbar
throw new BNFParsingErrorException(e);
}
return readGrammar(reader);
}
/**
* Liest eine Textdatei mit enthaltener BNF-Grammatik und erstellt daraus ein neues
* Grammar-Objekt.
* Die Textdatei darf auch Leerzeilen enthalten, welche beim Parsen ignoriert werden.
* @param filename Pfad zur Textdatei, welche die Eingabegrammatik in BNF enthält.
* @return Grammatik-Object mit eingelesenen Produktionen.
* @throws BNFParsingErrorException Falls die Textdatei keine gültige BNF ist.
*/
public Grammar readGrammar(String filename) throws BNFParsingErrorException {
FileReader reader = null;
try {
reader = new FileReader(filename);
} catch (FileNotFoundException e) {
// Textdatei nicht gefunden oder nicht zugreifbar
throw new BNFParsingErrorException(e);
}
return readGrammar(reader);
}
/**
* Untersucht einen String nach einer gültigen Produktion.
* @param string Eingabestring
* @param grammar Eingabegrammatik
* @return Produktions-Objekt
* @throws BNFParsingErrorException Falls der String keine gültige Produkton ist.
*/
private List<Production> parseProduction(String string, Grammar grammar) throws BNFParsingErrorException{
// alle Whitespaces entfernen
string = string.replaceAll("\\s", "");
// Reguläre Ausdrücke
String regexNonterminal = "(<[^<>]+>)"; // mind. 1 Zeichen außer "< und >" muss innerhalb <...> liegen
String regexSymbol = "("+regexNonterminal+"|(\"[^\"]+\"))"; // Nichterminale und Terminale
String regexRightHandSite = "("+regexSymbol+"+\\|)*"+regexSymbol+"+";
Pattern patternNonterminal = Pattern.compile(regexNonterminal);
Pattern patternSymbol = Pattern.compile(regexSymbol+"|\\|"); // Nichterminale, Terminale und "|"
Pattern patternProduction = Pattern.compile(regexNonterminal+"::="+regexRightHandSite); // auf generelles Schema prüfen
// Variablen für zu findende LHS, RHS und Liste für Produktionen
NonTerminalSymbol leftHandSite = null;
List<Symbol> rightHandSite = null;
List<Production> productions = new LinkedList<Production>();
// Parsen
Matcher matcher = patternProduction.matcher(string);
if(matcher.matches()){ // Produktion gültig? (LHS::=RHS1|RHS2...)
// Linke Regelseite bestimmen (muss 1 Nonterminal enthalten)
matcher = patternNonterminal.matcher(string);
if(matcher.find()){ // Nonterminal gefunden
// zu LHS Variable hinzufügen
String leftHandSiteName = matcher.group();
leftHandSiteName = leftHandSiteName.replaceAll("[<>]", "");
leftHandSite = grammar.createNonTerminalSymbol(leftHandSiteName);
// Rechte Regelseite abarbeiten
rightHandSite = new LinkedList<Symbol>(); // lege Symbolliste an
string = string.substring(leftHandSiteName.length()+5); // verkürze String ab Postition nach "::="
// einzelne Symbole parsen und bei "|" neue Produktionen erstellen
matcher = patternSymbol.matcher(string);
while(matcher.find()){
String symbol = matcher.group();
if(symbol.startsWith("<")){ // Nonterminal zu RHS hinzufügen
symbol = symbol.replaceAll("[<>]", "");
// lasse von Grammar entweder Neues erstellen oder Referenz auf Vorhandenes zurückgeben
rightHandSite.add(grammar.createNonTerminalSymbol(symbol));
} else if(symbol.startsWith("\"")) { // Terminal zu RHS hinzufügen
symbol = symbol.replaceAll("\"", "");
rightHandSite.add(grammar.createTerminalSymbol(symbol));
} else { // Alternative "|" gematched, lege neue Produktion an
Production p = new Production(leftHandSite, rightHandSite);
productions.add(p);
rightHandSite = new LinkedList<Symbol>();
logger.finer("Produktion hinzugefügt: " +p);
}
}
Production p = new Production(leftHandSite, rightHandSite);
productions.add(p);
logger.finer("Produktion hinzugefügt: " +p);
}
} else {
throw new BNFParsingErrorException("Illegal production!");
}
return productions;
}
}