Es ist noch nicht so lange her, da lebte die Computer Industrie in einem Sprachwirrwar biblischen Ausmaßes. Als wäre der Turm zu Babel direkt über den Computerterminals zusammengebrochen, ersannen alle Organisationen und Hersteller ihre eigenen Zeichensatzkodierungen.
Das allgegenwärtige Problem hieß, wie bekomme ich alle meine Zeichen in ein Byte. Die unteren 127 Werte, waren üblicherweise schon für ASCII Codierungen verwendet, also blieben nur die oberen 127 Werte. Es oblag der Willkür der Entwickler, ob sich dort Umlaute, kyrillische Zeichen, mathematische Symbole oder sonstige Spezialitäten tummelten. Dieser Wahnsinn endete, als jemand auf die Idee kam, Zeichen in zwei Bytes zu kodieren. Unicode war geboren.
Warum dieser Diskurs? Der GedcomReader aus Stammbäume für Entwickler liest die Datei mit einem Reader ein, den ich auf die korrekte Kodierung einstelle. Genauer gesagt, verwende ich nur GEDCOM Dateien, die UTF-8 oder ASCII kodiert sind. Der GEDCOM Standard von 1999 versteht natürlich mehr und daher wäre die Unterstützung von ASCII, UTF-8, UTF-16 und ANSEL schön. Aber was ist ANSEL?
ANSEL ist eine Zeichensatzkodierung der National Information Standards Organization und kodiert neben den 127 ASCII Zeichen noch einige weitere. Wer neugierig ist, findet die Details auf Wikipedia. Alternativ ist diese Kodierung auch unter dem Namen ISO-IR 231 bekannt.
Damit der GedcomReader diese Kodierung versteht, könnte er selbst den einlaufenden InputStream konvertieren. Dieser Ansatz würde aber mit einigen Nachteilen verbunden sein. Der bisher verwendete Reader müsste erweitert werden, die Lösung wäre kaum wiederverwendbar und der Java Standard hat einen besseren Lösungsweg parat.
Sollen in Java Binardaten in Zeichen umgewandelt werden, dann kommen die Charset Klassen zum Einsatz. Für die üblichen Verdächtigen wie Unicode, ASCII und ISO-Latin und viele andere stehen Implementierungen bereit, für Exotisches können eigene Implementierungen hinzugefügt werden.
Um ein eigenes Charset nutzen zu können, benötigen wir eine Subklasse, die den von uns gewünschten Namen der Kodierung und ggf. Aliase kennt. Da uns die Basisklasse vieles abnimmt, ist unsere Implementierung kurz.
public class AnselCharset extends Charset { protected AnselCharset() { super("ANSEL", new String[] { "ISO-IR-231" }); } @Override public CharsetDecoder newDecoder() { return new AnselCharsetDecoder(this); } @Override public CharsetEncoder newEncoder() { throw new UnsupportedOperationException("we don't like to write ANSEL"); } }
Die ganze Arbeit verrichten der CharsetDecoder
und der CharsetEncoder
. Da wir uns nur mit dem Einlesen beschäftigen, implementieren wir auch nur den CharsetDecoder
.
Da unser Decoder nichts wirklich aufregendes macht benötigen wir nur die Methode
CoderResult CharsetDecoder#decodeLoop(ByteBuffer, CharBuffer)
die Bytes aus dem ByteBuffer
nimmt und entsprechende Zeichen in den CharBuffer
schreibt. Die möglichen Sonderfälle, kein Platz mehr im CharBuffer
und unbekannte Bytewerte, werden durch spezielle Rückgabewerte signalisiert. Der Algorithmus ist trivial, da die ersten 127 Werte der ASCII Codierung entsprechen, müssen wir nur den Wert von byte
auf char
casten. Für die anderen 127 Zeichen greifen wir auf ein Array zu, in dem ein entsprechendes Mapping vorgenommen wird. In Java sind Bytewerte vorzeichenbehaftet, daher können wir die beiden Gruppen dadurch unterscheiden, dass die ersten 127 Zeichen einem positiven Bytewert entsprechen und die anderen einem negativen Bytewert. Damit sieht die Methode wie folgt aus.
@Override protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { while (in.hasRemaining()) { if (!out.hasRemaining()) { return CoderResult.OVERFLOW; } byte b = in.get(); if (b < 0) { char result = highTable[b + 128]; if (result == 0) { return CoderResult.malformedForLength(1); } out.append(result); } else { out.append((char) b); } } return CoderResult.UNDERFLOW; }
Mit der bisherigen Implementierung, sind wir nun schon in der Lage unsere InputStreamReader
korrekt auf ANSEL kodierte Dateien anzuwenden. Und wenn wir eine weitere Klasse GedcomCharset
mit modifiziertem Mapping-Array erzeugen, dann können wir auch endlich unsere GEDCOM Dateien mit ANSEL Kodierung lesen.
BufferedReader reader = new BufferedReader(new InputStreamReader(anselInput, new GedcomCharset())); String line = reader.readLine();
Als letztes Sahnehäubchen statten wir unsere kleine Bibliothek noch mit einem
Service Provider Interface aus. Dann müssen wir unsere Charsets
nicht mehr direkt instanziieren, sondern einfach über ihren Namen anfordern.
Dafür benötigen wir einen kleinen CharsetProvider
, der unsere Charsets
kennt und müssen seinen Namen, den Konventionen folgend, in der Datei META-INF/services/java.nio.charset.spi.CharsetProvider hinterlegen.
public class AnselCharsetProvider extends CharsetProvider { private List<Charset> charsets = Arrays.asList(new AnselCharset(), new GedcomCharset()); @Override public Iterator<Charset> charsets() { return charsets.iterator(); } @Override public Charset charsetForName(String charsetName) { return charsets.stream().filter(c -> c.name().equals(charsetName)).findFirst().orElse(null); } }
Mit diesen wenigen zusätzlichen Zeilen Code kann der InputStreamReader
ganz Java-typisch das Datei Encoding über den Namen bestimmen.
BufferedReader reader = new BufferedReader(new InputStreamReader(anselInput, "GEDCOM")); String line = reader.readLine();
Viele Möglichkeiten, dass erworbene Wissen zu nutzen, wird es wohl nicht mehr geben. Die Zahl der Zeichenkodierungen verringert sich langsam aber stetig. Immer mehr Exoten sterben aus und irgendwann sprechen wir nur noch Unicode.
Der Code zu diesem Beitrag findet sich auf GitLab im Projekt ansel-encoding.