/*
*
* Copyright 2012 lexergen.
* This file is part of lexergen.
*
* lexergen is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* lexergen is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with lexergen. If not, see <http://www.gnu.org/licenses/>.
*
* lexergen:
* A tool to chunk source code into tokens for further processing in a compiler chain.
*
* Projectgroup: bi, bii
*
* Authors: Daniel Rotar
*
* Module: Softwareprojekt Übersetzerbau 2012
*
* Created: Apr. 2012
* Version: 1.0
*
*/
package de.fuberlin.bii.regextodfaconverter;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Stellt grundlegende Funktionen zum Arbeiten mit regulären Ausdrücken bereit.
*
* @author Daniel Rotar
*
*/
public class Regex {
/**
* Die Metazeichen.
*/
private static char[] META_CHARS = { '[', ']', '(', ')', '{', '}', '|',
'?', '+', '-', '*', '^', '$', '\\', '.' };
// ASCII control characters: character code 0-31
// ASCII printable characters: character code 32-127
// The extended ASCII codes: character code 128-255
/**
* Das erste Zeichen aus dem ASCII Zeichensatz, das im Alphabet enthalten
* sein soll.
*/
private static int FIRST_ASCII_CHAR = 4;
// Die ersten vier Zeichen werden von dem direkten DFA-Übersetzter als Hilfzeichen benötigt und können daher nicht verwendet werden.
// Der indirekte Weg unterstützt theoretisch das vollständige Alphabet.
/**
* Das letzte Zeichen aus dem ASCII Zeichensatz, das im Alphabet enthalten
* sein soll.
*/
private static int LAST_ASCII_CHAR = 255;
/**
* Reduziert den angebenen regulären Ausdruck auf die Grundoperationen und
* klammert diesen anschließend korrekt und vollständig.
*
* @param regex
* Der zu reduzierende und zu klammernde reguläre Ausdruck.
* @return Ein äquivalenter reduzierter und geklammerte regulärer Ausdruck.
* @throws RegexInvalidException
* Wenn der angegebene regulärer Ausdruck ungültig ist oder
* nicht unterstützt wird.
*/
public static String reduceAndBracketRegex(String regex)
throws RegexInvalidException {
return bracketRegex(reduceRegex(regex));
}
/**
* Reduziert den angebenen regulären Ausdruck auf die Grundoperationen.
*
* @param regex
* Der zu reduzierende reguläre Ausdruck.
* @return Ein äquivalenter reduzierter regulärer Ausdruck.
* @throws RegexInvalidException
* Wenn der angegebene regulärer Ausdruck ungültig ist oder
* nicht unterstützt wird.
*/
public static String reduceRegex(String regex) throws RegexInvalidException {
// Alle Chars überprüfen, ob sie Teil des Alphabets oder ein Metazeichen
// sind.
for (int i = 0; i < regex.length(); i++) {
if (!isMetaOrAlphaChar(regex.charAt(i))) {
throw new RegexInvalidException(
"Der angegebene reguläre Ausdruck enthält ein ungültiges Zeichen: '"
+ regex.charAt(i) + "'!");
}
}
// Überprüfen ob der angegebene Regex gültig ist.
try {
Pattern.compile(regex);
} catch (PatternSyntaxException e) {
throw new RegexInvalidException(
"Der angegebene reguläre Ausdruck '" + regex
+ "' ist ungültig! ");
}
// Überprüfen ob im angegebenen Regex ein $- oder ^-Operator vorkommt.
// Diese beiden Operatoren machen im Anwendungskontext keinen Sinn.
for (int i = 0; i < regex.length(); i++) {
char c = regex.charAt(i);
if (c == '\\') {
i++;
} else if (c == '$' || c == '^') {
throw new RegexInvalidException(
"Der "
+ c
+ "-Operator wird im Anwendungskontext nicht unterstützt und kann einfach weggelassen werden!");
}
}
// Überprüfen ob im angegebenen Regex ein *?-, +?, ?? oder
// {n,m}?-Operator vorkommt.
// Diese Operatoren machen im Anwendungskontext keinen Sinn.
for (int i = 0; i < regex.length(); i++) {
char c = regex.charAt(i);
if (c == '\\') {
i++;
} else if (c == '*' || c == '+' || c == '?' || c == '}') {
if (i + 1 < regex.length()) {
i++;
if (regex.charAt(i) == '?') {
throw new RegexInvalidException(
"Der "
+ c
+ regex.charAt(i)
+ "-Operator wird im Anwendungskontext nicht unterstützt und kann einfach weggelassen werden!");
}
}
}
}
// Verarbeitung starten
String output = regex;
// 1. Eckige Klammern reduzieren.
output = replaceSquareBrackets(output);
// 2. Punkte reduzieren (muss nach (1) gemacht werden).
output = replaceDots(output);
// 3. Backshlashes reduzieren (muss nach (1) gemacht werden).
output = replaceBackslashes(output);
// 4. ? durch {0,1} ersetzten (muss nach (1) gemacht werden).
output = replaceQuestionMarks(output);
// 5. + durch {1,} ersetzten (muss nach (1) gemacht werden).
output = replacePlusSigns(output);
// 6. Geschweifte klammern reduzieren.
output = replaceBraces(output);
return output;
}
/**
* Klammert den angebenen regulären Ausdruck korrekt und vollständig.
*
* @param regex
* Der zu klammernde reguläre Ausdruck (darf nur die
* Grundoperationen A|B, AB und A* enthalten).
* @return Ein äquivalenter geklammerte regulärer Ausdruck.
* @throws RegexInvalidException
* Wenn der angegebene regulärer Ausdruck ungültig ist oder
* nicht unterstützt wird.
*/
protected static String bracketRegex(String regex)
throws RegexInvalidException {
// 1. Bei Eingabe eines leeren Regex "()" zurückgeben
if (regex.length() == 0) {
return "()";
}
// 2. Überprüfen ob der angegebene reguläre Ausdruck nur die Basisoperationen enthält.
if (!containsOnlyBasicOperations(regex)) {
throw new RegexInvalidException(
"Der angegebene reguläre Ausdruck darf nur die Grundoperationen enthalten!");
}
// 3. Sonderfälle für das leere Zeichen behandeln:
// 3.1 ...(|...
if (regex.startsWith("|"))
{
// Kann nur bei einem Unterausdruck mit "(|" vorkommen.
regex = "()" + regex;
}
// 3.2 ...|)...
if (regex.endsWith("|"))
{
// Kann nur bei einem Unterausdruck mit "|)" vorkommen.
regex = regex + "()";
}
// 3.3 ...||...
Pattern pattern = Pattern.compile("[^\\\\]\\|\\|"); //[^\\]\|\|
Matcher matcher = pattern.matcher(regex);
if (matcher.find())
{
String match = matcher.group();
regex = regex.replace(match, match.substring(0,1) + "|()|");
}
// 4. ArrayList erstellen
ArrayList<String> regexTasks = new ArrayList<String>();
// 5. ArrayList befüllen
for (int i = 0; i < regex.length(); i++) {
char c = regex.charAt(i);
if (isBasicMetaCharacter(c)) {
if (c == '(') {
// Der regex enthält bereits einen geklammerten Ausdruck!
// Geklammerten Regex rekursiv auflösen.
int opened = 1;
StringBuilder subRegex = new StringBuilder();
while (opened != 0) {
i++;
if (i == regex.length()) {
throw new RegexInvalidException(
"Der angegebene reguläre Ausdruck ist ungültig geklammert");
}
if (regex.charAt(i) == '(') {
subRegex.append(regex.charAt(i));
opened++;
} else if (regex.charAt(i) == ')') {
opened--;
if (opened > 0) {
subRegex.append(regex.charAt(i));
}
} else if (regex.charAt(i) == '\\') {
subRegex.append(regex.charAt(i));
i++;
subRegex.append(regex.charAt(i));
} else {
subRegex.append(regex.charAt(i));
}
}
regexTasks.add(bracketRegex(subRegex.toString()));
} else if (c == ')') {
throw new RegexInvalidException(
"Der angegebene reguläre Ausdruck ist ungültig geklammert");
} else if (c == '|' || c == '*') {
regexTasks.add("" + c);
} else {
throw new RegexInvalidException(
"Unbekannter Ausnahmefehler. Fehlercode: f-i1-e");
}
} else if (c == '\\') {
regexTasks.add("(" + c + "" + regex.charAt(i + 1) + ")");
i++;
} else if (isAlphaChar(c) || c=='-') {
regexTasks.add("(" + c + ")");
} else {
throw new RegexInvalidException(
"Der angegebene reguläre Ausdruck enthält ungültige Zeichen: '"
+ c + "'");
}
}
// 6. ArrayList abarbeiten, bis nur noch ein Eintrag übrig ist.
if (regexTasks.size() == 0) {
throw new RegexInvalidException(
"Unbekannter Ausnahmefehler. Fehlercode: r-0");
} else {
// 6.1 closure
for (int i = 0; i < regexTasks.size(); i++) {
if (regexTasks.get(i).equals("*")) {
regexTasks.set(i - 1, "(" + regexTasks.get(i - 1) + "*)");
regexTasks.remove(i);
i--;
}
}
// 6.2 concat
for (int i = 0; i < regexTasks.size(); i++) {
if (i + 1 < regexTasks.size()) {
// '*' kann nicht mehr kommen, da bereits vollständig
// abgearbeitet.
if ((!regexTasks.get(i).equals("|"))
&& (!regexTasks.get(i + 1).equals("|"))) {
regexTasks.set(i,
"(" + regexTasks.get(i) + regexTasks.get(i + 1)
+ ")");
regexTasks.remove(i + 1);
i--;
}
}
}
// 6.3 union
for (int i = 0; i < regexTasks.size(); i++) {
if (regexTasks.get(i).equals("|")) {
regexTasks.set(i - 1, "(" + regexTasks.get(i - 1) + "|"
+ regexTasks.get(i + 1) + ")");
regexTasks.remove(i + 1);
regexTasks.remove(i);
i = i - 2;
}
}
return regexTasks.get(0);
}
}
/**
* Gibt an, ob der angegebene reguläre Ausdruck nur die Grundoperationen
* enthält.
*
* @param regex
* Der zu überprüfende reguläre Ausdruck.
* @return true, wenn der reguläre Ausdruck nur die Grundoperationen
* enthält, sonst false.
*/
public static boolean containsOnlyBasicOperations(String regex) {
for (int i = 0; i < regex.length(); i++) {
if (isExtendedMetaCharacter(regex.charAt(i))) {
return false;
}
if (regex.charAt(i) == '\\') {
// Nach einem Escape-Char darf nur ein Meta-Zeichen kommen
i++;
if (regex.length() == i) {
return false;
} else {
if (!isMetaCharacter(regex.charAt(i))) {
return false;
}
}
}
}
return true;
}
/**
* Reduziert im angebenen regulären Ausdruck alle eckigen Klammern (die
* nicht escapted wurden) auf die Grundoperationen.
*
* @param regex
* Der zu reduzierende reguläre Ausdruck.
* @return Ein äquivalenter reduzierter Ausdruck.
*/
private static String replaceSquareBrackets(String regex) {
String output = "_" + regex; // Workaround
Pattern pattern = Pattern.compile("[^\\\\]\\[.*?[^\\\\]\\]"); // [^\\]\[.*?[^\\]\]
Matcher matcher = pattern.matcher(output);
while (matcher.find()) {
String match = matcher.group();
String subRegex = match.substring(1);
StringBuilder sb = new StringBuilder("(");
for (int i = FIRST_ASCII_CHAR; i <= LAST_ASCII_CHAR; i++) {
String toCeck = "" + ((char) i);
if (toCeck.matches(subRegex)) {
if (isMetaCharacter((char) i)) {
sb.append("|\\" + toCeck);
} else {
sb.append("|" + toCeck);
}
}
}
sb.append(")");
if (sb.length() > 2) {
// Wenn nicht "()"
sb.delete(1, 2); // erstes "|" entfernen.
} else {
// Wenn "()"
sb = new StringBuilder(""); // "()" vollständig weglassen.
}
output = output.replace(match,
match.substring(0, 1) + sb.toString());
}
return output.substring(1);
}
/**
* Reduziert im angebenen regulären Ausdruck alle Punkte (die nicht escapted
* wurden) auf die Grundoperationen.
*
* @param regex
* Der zu reduzierende reguläre Ausdruck.
* @return Ein äquivalenter reduzierter Ausdruck.
*/
private static String replaceDots(String regex) {
String output = regex;
for (int i = 0; i < output.length(); i++) {
char c = output.charAt(i);
if (c == '\\') {
i++;
} else if (c == '.') {
String subRegex = ".";
StringBuilder sb = new StringBuilder("(");
for (int j = FIRST_ASCII_CHAR; j <= LAST_ASCII_CHAR; j++) {
String toCeck = "" + ((char) j);
if (toCeck.matches(subRegex)) {
if (isMetaCharacter((char) j)) {
sb.append("|\\" + toCeck);
} else {
sb.append("|" + toCeck);
}
}
}
sb.append(")");
if (sb.length() > 2) {
// Wenn nicht "()"
sb.delete(1, 2); // erstes "|" entfernen.
} else {
// Wenn "()"
sb = new StringBuilder(""); // "()" vollständig weglassen.
}
output = replaceRangeInString(output, i, i + 1, sb.toString());
}
}
return output;
}
/**
* Reduziert im angebenen regulären Ausdruck alle Zeichen die durch ein
* Backslash escapted wurden auf die Grundoperationen wenn möglich.
*
* @param regex
* Der zu reduzierende reguläre Ausdruck.
* @return Ein äquivalenter reduzierter Ausdruck.
*/
private static String replaceBackslashes(String regex) {
String output = regex;
for (int i = 0; i < output.length(); i++) {
if (output.charAt(i) == '\\') {
if (i + 1 < output.length()) {
String subRegex = "" + output.charAt(i)
+ output.charAt(i + 1);
StringBuilder sb = new StringBuilder("(");
for (int j = FIRST_ASCII_CHAR; j <= LAST_ASCII_CHAR; j++) {
String toCeck = "" + ((char) j);
if (toCeck.matches(subRegex)) {
if (isMetaCharacter((char) j)) {
sb.append("|\\" + toCeck);
} else {
sb.append("|" + toCeck);
}
}
}
sb.append(")");
if (sb.length() > 2) {
// Wenn nicht "()"
sb.delete(1, 2); // erstes "|" entfernen.
} else {
// Wenn "()"
sb = new StringBuilder(""); // "()" vollständig
// weglassen.
}
output = replaceRangeInString(output, i, i + 2,
sb.toString());
}
i++;
}
}
return output;
}
/**
* Reduziert im angebenen regulären Ausdruck alle Fragezeichen (die nicht
* escapted wurden) auf {0,1}.
*
* @param regex
* Der zu reduzierende reguläre Ausdruck.
* @return Ein äquivalenter reduzierter Ausdruck.
*/
private static String replaceQuestionMarks(String regex) {
String output = regex;
for (int i = 0; i < output.length(); i++) {
char c = output.charAt(i);
if (c == '\\') {
i++;
} else if (c == '?') {
output = replaceRangeInString(output, i, i + 1, "{0,1}");
}
}
return output;
}
/**
* Reduziert im angebenen regulären Ausdruck alle Pluszeichen (die nicht
* escapted wurden) auf {1,}.
*
* @param regex
* Der zu reduzierende reguläre Ausdruck.
* @return Ein äquivalenter reduzierter Ausdruck.
*/
private static String replacePlusSigns(String regex) {
String output = regex;
for (int i = 0; i < output.length(); i++) {
char c = output.charAt(i);
if (c == '\\') {
i++;
} else if (c == '+') {
output = replaceRangeInString(output, i, i + 1, "{1,}");
}
}
return output;
}
private static String replaceBraces(String regex) {
String output = regex;
Pattern pattern;
Matcher matcher;
// 1. {0,} mit * ersetzen.
// Ersetzung möglich ohne auf escape Char zu überprüfen, da \{0,} nicht
// möglich!
output = output.replace("{0,}", "*");
// 2. {n} mit {n,n} ersetzen.
pattern = Pattern.compile("\\{\\d+\\}"); // \{\d+\}
matcher = pattern.matcher(output);
while (matcher.find()) {
String match = matcher.group();
int n = Integer.valueOf(match.substring(1, match.length() - 1));
output = output.replace(match, "{" + n + "," + n + "}");
}
// 3. Nach Pattern {n,m} suchen
pattern = Pattern.compile("\\{\\d+,\\d+\\}"); // \{\d+,\d+\}
matcher = pattern.matcher(output);
while (matcher.find()) {
String match = matcher.group();
// 3.1 Element vor der geschweiften Klammer berechnen.
String lastElement = getLastElement(output.substring(0,
matcher.start()));
// 3.2 min und max ermitteln.
int min = Integer.valueOf(match.substring(1, match.indexOf(",")));
int max = Integer.valueOf(match.substring(match.indexOf(",") + 1,
match.indexOf("}")));
// 3.3 Neuen Regex aufbauen.
StringBuilder sb = new StringBuilder("(");
for (int i = min; i <= max; i++) {
StringBuilder subSb = new StringBuilder("(");
for (int j = 1; j <= i; j++) {
subSb.append(lastElement);
}
subSb.append(")");
sb.append('|');
sb.append(subSb);
}
sb.append(")");
if (sb.length() > 2) {
// Wenn nicht "()"
sb.delete(1, 2); // erstes "|" entfernen.
if (sb.toString().equals("(())")) {
sb = new StringBuilder(""); // "(())" vollständig weglassen.
}
} else {
// Wenn "()"
sb = new StringBuilder(""); // "()" vollständig weglassen.
}
// 3.4 Alten Regex ersetzen.
output = output.replace(lastElement + match, sb.toString());
}
// 4. Nach Pattern {n,} suchen
pattern = Pattern.compile("\\{\\d+,\\}"); // \{\d+,\}
matcher = pattern.matcher(output);
while (matcher.find()) {
// n>0, wegen Schritt 1.
String match = matcher.group();
// 4.1 Element vor der geschweiften Klammer berechnen.
String lastElement = getLastElement(output.substring(0,
matcher.start()));
// 4.2 min ermitteln.
int min = Integer.valueOf(match.substring(1, match.indexOf(",")));
// 4.3 Neuen Regex aufbauen.
StringBuilder sb = new StringBuilder("(");
for (int i = 1; i <= min; i++) {
sb.append(lastElement);
}
sb.append("(" + lastElement + ")*");
sb.append(")");
// 4.4 Alten Regex ersetzen.
output = output.replace(lastElement + match, sb.toString());
}
return output;
}
/**
* Gibt das letzte zusammenhängende Element in dem angegebenen regulären
* Ausdruck zurück.
*
* @param regex
* Der reguläre Ausdruck, von dem das letzte Element
* zurückgegeben werden soll.
* @return Das letzte zusammenhängende Element in dem angegebenen regulären
* Ausdruck.
*/
private static String getLastElement(String regex) {
String lastElement = "";
if (regex.length() == 1) {
return "" + regex.charAt(0);
}
if (regex.charAt(regex.length() - 2) == '\\') {
lastElement = "" + regex.charAt(regex.length() - 2)
+ regex.charAt(regex.length() - 1);
} else {
if (regex.charAt(regex.length() - 1) == ')') {
int closed = 1;
lastElement = ")";
for (int i = regex.length() - 2; i >= 0; i--) {
char c = regex.charAt(i);
if (c == '(') {
if (i - 1 >= 0) {
if (regex.charAt(i - 1) == '\\') {
lastElement = "\\(" + lastElement;
} else {
lastElement = "(" + lastElement;
closed--;
if (closed == 0) {
break;
}
}
} else {
lastElement = "(" + lastElement;
closed--;
if (closed == 0) {
break;
}
}
} else if (c == ')') {
closed++;
lastElement = ")" + lastElement;
} else {
lastElement = c + lastElement;
}
}
} else {
lastElement = "" + regex.charAt(regex.length() - 1);
}
}
return lastElement;
}
/**
* Gibt an, ob es sich bei dem angegebenen Zeichen um ein Metazeichen
* handelt.
*
* @param c
* Das zu überprüfende Zeichen
* @return true, wenn es sich um ein Metazeichen handelt, sonst false.
*/
protected static boolean isMetaCharacter(char c) {
for (char mc : META_CHARS) {
if (mc == c) {
return true;
}
}
return false;
}
/**
* Gibt an ob das angebene Zeichen zum Alphabet (und nicht zu den
* Metazeichen) gehört.
*
* @param c
* Das zu überprüfende Zeichen
* @return true, wenn das angegebene Zeichen zum Alphabet (und nicht zu den
* Metazeichen) gehört, sonst false.
*/
protected static boolean isAlphaChar(char c) {
return c >= FIRST_ASCII_CHAR && c <= LAST_ASCII_CHAR
&& (!isMetaCharacter(c));
}
/**
* Gibt an ob das angebene Zeichen zum Alphabet oder zu den Metazeichen
* gehört
*
* @param c
* Das zu überprüfende Zeichen
* @return true, wenn das angegebene Zeichen zum Alphabet oder zu den
* Metazeichen gehört, sonst false.
*/
protected static boolean isMetaOrAlphaChar(char c) {
return isAlphaChar(c) || isMetaCharacter(c);
}
/**
* Gibt an ob es sich bei dem angegebenen Zeichen um ein erweitertes
* Metazeichen handelt.
*
* @param c
* Das zu überprüfende Zeichen
* @return true, wenn es sich um eins der nachfolgenden Metazeichen handelt
* '[', ']', '{', '}', '?', '+', '^', '$', '.', sonst false.
*/
private static boolean isExtendedMetaCharacter(char c) {
char[] extendedMetaChars = { '[', ']', '{', '}', '?', '+', '^', '$', '.' };
for (char rc : extendedMetaChars) {
if (rc == c) {
return true;
}
}
return false;
}
/**
* Gibt an ob es sich bei dem angegebenen Zeichen um ein Basis-Metazeichen
* handelt.
*
* @param c
* Das zu überprüfende Zeichen
* @return true, wenn es sich um eins der nachfolgenden Metazeichen handelt
* '(', ')', '|', '*', sonst false.
*/
private static boolean isBasicMetaCharacter(char c) {
char[] basicMetaChars = { '(', ')', '|', '*' };
for (char rc : basicMetaChars) {
if (rc == c) {
return true;
}
}
return false;
}
/**
* Ersetzt in dem angegebenen String den angegebenen Bereich mit dem
* angegebenen Inhalt.
*
* @param inputString
* Der String, in dem der Bereich ersetzt werden soll.
* @param beginIndex
* Der Start-Index des Bereichs, der ersetzt werden soll.
* @param endIndex
* Der End-Index des Bereichs, der ersetzt werden soll.
* @param replaceString
* Der Inhalt, mit dem der angegebene Berech ersetzt werden soll.
* @return
*/
private static String replaceRangeInString(String inputString,
int beginIndex, int endIndex, String replaceString) {
return inputString.substring(0, beginIndex) + replaceString
+ inputString.substring(endIndex);
}
}