Du sprechen ANSEL?

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 instantiieren, 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.