Fehlerhafte (Längen-) Validierung in JSF

Vor einigen Tagen habe ich untersucht, wie schwer oder einfach es ist, die Internationalisierung einer Java-Anwendung zu erweitern, so dass sie Japanisch unterstützt. Die Internationalisierung der Oberfläche arbeitet mit Properties-Dateien, so dass hier keine Probleme zu befürchten waren. Das mögliche Problem lag darin, dass die Datenbank mit UTF-8 arbeitet und die Datenbankspalten je nachdem, für welchen Inhalt sie gedacht sind, für maximal ein oder drei Bytes pro UTF-8 Zeichen ausgelegt sind.

Da die Zeichen-Codierungsverfahren in diesem Artikel eine wichtige Rolle spielen, hierzu eine kurze Erklärung: UTF-8 arbeitet mit einer variablen Anzahl von Bytes pro Zeichen. Die Zeichen, die häufig verwendet werden, wie z.B. die lateinischen Buchstaben, (arabische) Ziffern, etc., können mit einem einzelnen Byte dargestellt werden. Je seltener die Zeichen verwendet werden, desto mehr Bytes pro Zeichen benötigt man. Damit ein Computer unterscheiden kann, wie viele Bytes zusammen gehören, gibt es eine spezielle Codierung, über die z.B. auch Übertragungsfehler erkannt werden können. Auf Wikipedia ist die Codierung von UTF-8 sehr anschaulich beschrieben. Der Nachteil an dieser flexiblen Codierung ist jedoch, dass für die Codierung der Information, wie viele Bytes zusammen gehören, und für die Fehlererkennung einige Bits verwendet werden müssen, die dann nicht mehr für die eigentliche Zeichencodierung verwendet werden können. Damit sinkt dann also die Anzahl der nutzbaren Bits, je mehr Bytes man für ein Zeichen verwenden muss.

Java verwendet intern nicht UTF-8 sondern UTF-16, welches einen ähnlichen Ansatz verfolgt, jedoch im Unterschied zu UTF-8 nur eine Unterscheidung zwischen Zeichen mit zwei oder vier Byte Länge macht. Dadurch ist man zwar weniger flexibel, hat aber gleichzeitig mehr nutzbare Bits.

Für meine Untersuchung der Unterstützung für japanische Schriftzeichen habe ich mich zunächst darüber informiert, wie diese denn mit UTF-8 codiert werden. Die meisten, gebräuchlichsten Zeichen können anscheinend mit drei Bytes codiert werden, einige “weniger gebräuchliche” Zeichen benötigen jedoch vier Bytes. Die Aussage “weniger gebräuchlich” ist aber anscheinend eher theoretischer Natur, wenn man diesem Eintrag bei Stackexchange glauben will. Schlussendlich bin ich dazu übergegangen, einfach zu schauen was passiert, wenn man ein japanisches Schriftzeichen auf der Oberfläche eingibt, das als UTF-8 Zeichen vier Byte verbraucht. Zunächst schien es so, als wenn alles ohne Probleme funktionieren würde, jedoch bin ich dann über zwei Probleme bei der Eingabevalidierung gestolpert.

Wie gesagt gibt es in der Anwendung verschiedene Arten von Eingabefeldern, bei denen unterschiedliche Zeichen erlaubt sind. Die Eingabevalidierung wurde über die jew. UTF-8 Blöcke erledigt, in denen die einzelnen Zeichen liegen. Grob aus dem Gedächtnis gecoded kam dabei eine Funktion zum Einsatz, die in etwa so aussah:

public List<String> getIllegalCharacters( String uiInput, UnicodeBlock[] allowedBlocks ) {
  List<String> result = new ArrayList<String>();

  for ( int i=0; i < uiInput.length(); i++ ) {
    char uiChar = uiInput.charAt( i );
    if ( isIllegalCharacter( uiChar, allowedBlocks ) ) {
      result.add( String.valueOf( uiChar ) );
    }
  }

  return result;
}

Auf den ersten Blick scheint der Code genau das zu tun, was er soll, nämlich den String zeichenweise durch zu gehen und eine Liste der nicht erlaubten Zeichen zu erstellen. Was hierbei jedoch nicht berücksichtigt wurde ist, dass der primitive Datentyp char nur 16bit groß ist, ein UTF-16 Zeichen jedoch auch doppelt so groß werden kann. Bei der Eingabe eines einzelnen, japanischen Schriftzeichens bekommt man folglich von dieser Funktion eine Liste mit zwei Strings zurück, die jedoch beide keine gültigen UTF-16 Zeichen enthalten. Was noch erschwerend hinzukommt ist, dass die Funktion String.length() nicht die tatsächliche Anzahl von Zeichen zurückliefert, sondern die Länge des char Arrays, welches intern die Zeichen des Strings enthält. Die Java-API weist hierauf auch korrekterweise hin, dennoch dürften die wenigsten wirklich darüber gestolpert sein: “Returns the length of this string. The length is equal to the number of Unicode code units in the string.“. An dieser Stelle muss man bei der Internationalisierung also einige Gedanken mehr investieren, um es richtig zu machen:

public List<String> getIllegalCharacters( String uiInput, UnicodeBlock[] allowedBlocks ) {
    List<String> result = new ArrayList<String>();

    int unicodeChars = Character.codePointCount( uiInput, 0, uiInput.length() );
    for ( int i=0; i < unicodeChars; i++ ) {
        int unicodeChar = Character.codePointAt( uiInput, i );
        if ( isIllegalCharacter( unicodeChar, allowedBlocks ) ) {
            result.add( String.valueOf( Character.toChars( unicodeChar ) ) );
        }
    }

    return result;
}

Nach dieser Korrektur wurden dann zumindest die nicht erlaubten UnicodeBlocks erkannt. Während der weiteren Tests stellt sich dann aber heraus, dass irgend etwas mit der Validierung der maximal zulässigen Länge für die Textfelder auch nicht stimmte. Für die Längenvalidierung wurde der normale LengthValidator aus Java Server Faces verwendet. Ein Blick in den Quellcode des Validators zeigte, dass die Länge des Strings ebenfalls mit length() ermittelt wird, was somit auch zu unverhofften Ergebnissen führt. Aus der Sicht des Programmierers mag das insbesondere hinsichtlich der Spaltengröße in Bytes in der Datenbank sinnvoll sein. Aus der Sicht des Endanwenders ist es jedoch verwirrend, wenn ich sechs (japanische) Schriftzeichen eingebe und mir die Anwendung sagt, dass ich das Maximum von 10 Zeichen überschritten hätte.

Insgesamt muss man also immer das Gesamtpaket betrachten: Datenbankmodell und die Spaltengrößen, Zeichensatz und die Validatoren und deren Implementierung. Schlussendlich entschied der Kunde jedoch, dass es zu aufwändig wäre, die Anwendung so umzubauen, dass sie mit japanischene Schriftzeichen umgehen kann, und wir haben den Code nur so angepasst, dass er mit Sprachen zurecht kommt, deren Zeichen bis zu drei Bytes in UTF-8 benötigen.

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.