/**
* Fußnoten-Sortierprogramm mit Ausrichtung auf eine möglichst hohe Effizienz
* und Skalierbarkeit sowie einem geringen Speicherverbrauch.
* <p>
* Zugunsten der Verarbeitungsgeschwindigkeit wurde auf die Nutzung von
* regulären Ausdrücken (<code>Pattern</code>/<code>Matcher</code>),
* sowie des <code>PrintWriter</code>s verzichtet.<br>
* Zugunsten des Speicherverbrauchs arbeitet das Programm Stream-basiert,
* was jedoch die re-nummerierung der im Text befindlichen Verweise in
* der Reihenfolge der Verweisziele aus dem Fußnotenabschnitt ohne
* zweiten Durchlauf unmöglich macht. Des Weiteren wurde auf die Verwendung
* eines <code>Comparator</code>s verzichtet, der die Vorhaltung aller
* Verweisziele im Speicher bedingt hätte um die Sortierung durchführen zu
* können.<br>
* Nachteil der Optimierungen ist ein deutlich längerer Code - die Klassen
* <code>FootnoteFinder</code> und <code>Footnotes</code>, sowie die vielen
* <code>writer.write()</code> würde es ohne diese Optimierungen nicht geben.
* <p>
* Die Formatierung des Codes, die Stern-Imports, das fehlende
* Exception-Handling, sowie die Verwendung innerer Klassen dienen
* ausschließlich der möglichst kompakten Umsetzung der gestellten Aufgabe
* und kann nicht als Beispiel guten Codes dienen.
* <p>
* Dieser Code wurde mit Eclipse 3.4 / Java 6 unter Kubuntu 8.04 erstellt. :-)
*
* @author Oliver Siegmar
*/
public class FootnoteProcessor {
/** Fußnotenmarkierung **/
private static final String FOOT_MARKER = "@footnote:";
/** Zeichensatz der Quelldatei **/
private static final String CHARSET = "ISO-8859-15";
/** Zeichen, mit dem die Fußnoten begonnen werden **/
private static final char FOOTNOTE_OPEN = '[';
/** Zeichen, mit dem die Fußnoten geschlossen werden **/
private static final char FOOTNOTE_CLOSE = ']';
/**
* Speicher für die neu nummerierten Fußnoten
* (Key = alte Nummer, Value = neue Nummer)
**/
private final Map<Integer, Integer> noteMap =
new HashMap<Integer, Integer>();
/** Writer für die Ausgabe des re-nummerierten Textes **/
private final Writer writer;
public static void main(final String[] args) throws IOException {
if (args.length == 0) {
System.err.println("No file specified. " +
"Usage: FootnoteProcessor input.txt > output.txt");
System.exit(1);
}
new FootnoteProcessor().processFile(new File(args[0]));
}
/**
* Einstiegsmethode zur Verarbeitung der Daten.
*
* @param file die zu verarbeitende Quelldatei.
* @throws IOException tritt bei Lese- oder Schreibfehlern auf.
*/
private void processFile(final File file) throws IOException {
final BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream(file), CHARSET));
Scanner scanner = new BodyScanner();
for (String line; (line = br.readLine()) != null;) {
if (FOOT_MARKER.equals(line)) {
scanner = new FootScanner();
writer.write(FOOT_MARKER);
writer.write(NEWLINE);
} else {
scanner.process(line);
}
}
br.close();
writer.close();
}
/**
* Klasse zur effizienten Ermittlung der Position von Fußnoten
*/
private class FootnoteFinder {
/** Zu untersuchende Zeile **/
private final char[] ca;
/** Startposition des Fußnotenmarkers ([) **/
private int start;
/** Endposition des Fußnotenmarkers (]) **/
private int end;
/**
* Initialisierung.
*
* @param line die zu untersuchende Zeile.
*/
public FootnoteFinder(final String line) {
ca = line.toCharArray();
}
/**
* Sucht nach der nächsten Fußnote
*
* @return Start- und Endposition der Fußnote, oder <code>null</code>,
* wenn keine (weitere) Fußnote gefunden wurde.
*/
private int[] find() {
boolean open = false;
int[] ret = null;
for (int i = end; i < ca.length; i++) {
if (!open) {
if (ca[i] == FOOTNOTE_OPEN) {
start = i + 1;
open = true;
}
} else {
if (ca[i] == FOOTNOTE_CLOSE) {
if (i > start) {
ret = new int[] { start, i };
}
end = i + 1;
break;
} else if (ca[i] < '0' || ca[i] > '9') {
break;
}
}
}
return ret;
}
}
/** Interface für den zeilenbasierten Fußnotenscanner **/
private interface Scanner {
/**
* Verarbeitet eine Zeile der Quelldatei.
* @param line eingelesene Zeile der Quelldatei.
* @throws IOException tritt bei einem Schreibfehler auf.
*/
void process(String line) throws IOException;
}
/** Von den Scannerimplementierungen gemeinsam genutzte Funktionalität **/
private abstract class AbstractScanner implements Scanner {
public void process(final String line) throws IOException {
final FootnoteFinder matcher = new FootnoteFinder(line);
for (int[] matches; (matches = matcher.find()) != null;) {
process(line, matches[0], matches[1]);
}
postProcess(line);
}
/**
* Diese Methode wird für jeden Treffer einer Fußnote aufgerufen.
*
* @param line die zu verarbeitende Zeile.
* @param start Startposition der Fußnote.
* @param end Endposition der Fußnote.
* @throws IOException tritt bei einem Schreibfehler auf.
*/
abstract void process(String line, int start, int end)
throws IOException;
/**
* Diese Methode wird aufgerufen, sobald alle Treffer für eine
* Zeile abgearbeitet sind.
*
* @param line die zu verarbeitende Zeile.
* @throws IOException tritt bei einem Schreibfehler auf.
*/
void postProcess(final String line) throws IOException {}
}
/** Scannerimplementierung, welche die Verweise im Text untersucht **/
private class BodyScanner extends AbstractScanner {
private int lastNumber = 0;
private int lastEnd = 0;
@Override
public void process(final String line, final int begin, final int end)
throws IOException {
final Integer number = new Integer(line.substring(begin, end));
/** Scannerimplementierung, welche die Verweisziele untersucht **/
private class FootScanner extends AbstractScanner {
private final Footnotes footnotes = new Footnotes();
@Override
public void process(final String line, final int begin, final int end)
throws IOException {
final Integer number = new Integer(line.substring(begin, end));
final String text = line.substring(end + 2);
final Integer remap = noteMap.get(number);
footnotes.add(remap, text);
}
}
/** Speicher-, Sortier- und Ausgabelogik für Verweisziele **/
private class Footnotes {
/** Zwischenspeicher für Fußnoten, die noch nicht ausgegeben wurden **/
private final Map<Integer, String> footnotes =
new HashMap<Integer, String>();
/** Ziffer der Fußnote, die als nächstes ausgegeben werden muss **/
private Integer nextNumber = 1;
/**
* Fügt dem Zwischenspeicher eine Fußnote hinzu. Prüft anschließend,
* ob die hinzugefügte Fußnote (sowie evtl. bereits gespeicherte)
* Fußnoten ausgegeben werden können.
*
* @param number Nummer der hinzuzufügenden Fußnote.
* @param footnote Text der hinzuzufügenden Fußnote.
* @throws IOException tritt bei einem Schreibfehler auf.
*/
public void add(final Integer number, final String footnote)
throws IOException {
footnotes.put(number, footnote);
if (number.equals(nextNumber)) {
printFootnotes();
}
}
/**
* Gibt alle im Zwischenspeicher befindlichen Fußnoten in
* sortierter Reihenfolge aus. Bricht ab, wenn der Zwischenspeicher
* noch keine Fußnote der nächsten Nummer findet.
*
* @throws IOException tritt bei einem Schreibfehler auf.
*/
private void printFootnotes() throws IOException {
String nextFootnote;
while ((nextFootnote = footnotes.get(nextNumber)) != null) {
writer.write(FOOTNOTE_OPEN);
writer.write(nextNumber.toString());
writer.write(FOOTNOTE_CLOSE);
writer.write(" ");
writer.write(nextFootnote);
writer.write(NEWLINE);
nextNumber++;
footnotes.remove(nextFootnote);
}
}
}