Java Blog

Inselraus: Anwendungen für FilterReader und FilterWriter

Unsere nächste Klasse bringt uns etwas näher an das HTML-Format heran. Wir wollen eine Klasse HTMLWriter entwerfen, die FilterWriter erweitert und Textausgaben in HTML konvertiert. In HTML werden Tags eingeführt, die vom Browser erkannt und besonders behandelt werden. Findet etwa der Browser im HTML-Text eine Zeile der Form <strong>Dick</strong>, so stellt er den Inhalt „Dick“ in fetter Schrift dar, da das <strong>-Element den Zeichensatz umstellt. Alle Tags werden in spitzen Klammern geschrieben. Daraus ergibt sich, dass HTML einige spezielle Zeichenfolgen (Entities genannt) verwendet. Wenn diese Zeichen auf der HTML-Seite dargestellt werden, muss dies durch spezielle Zeichensequenzen geschehen:

Kommen diese Zeichen im Quelltext vor, so muss unser HTMLWriter diese Zeichen durch die entsprechende Sequenz ersetzen. Andere Zeichen sollen nicht ersetzt werden.

Den Browsern ist die Struktur der Zeilen in einer HTML-Datei egal. Sie formatieren wiederum nach speziellen Tags. Zeilenvorschübe etwa werden mit <br/> eingeleitet. Unser HTMLWriter soll zwei leere Zeilen durch das Zeilenvorschub-Element <br/> markieren.

HTML-Dokument schreiben

Alle sauberen HTML-Dateien haben einen wohldefinierten Anfang und ein wohldefiniertes Ende. Das folgende kleine HTML-Dokument ist wohlgeformt und zeigt, was unser Programm später erzeugen soll:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
                       "http://www.w3.org/TR/html4/strict.dtd">
 <html><head><title>Superkreativer Titel</title></head>
 <body><p>
 Und eine Menge von Sonderzeichen: &lt; und &gt; und &amp;
 Zweite Zeile
 <br/>
 Leerzeile
 Keine Leerzeile danach
 </p></body></html>

Der Titel der Seite sollte im Konstruktor übergeben werden können. Hier ist nun das Programm für den HTMLWriter:

package com.tutego.insel.io.stream;
 
 import java.io.*;
 
 class HTMLWriter extends FilterWriter {
   
   private boolean newLine;
 
   /**
    * Creates a new filtered HTML writer with a title for the web page.
    *
    * @param out  a Writer object to provide the underlying stream.
    * @throws IOException if the header cannot be written
    */
   public HTMLWriter( Writer out, String title ) throws IOException {
     super( out );
 
     out.write( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"" +
            " \"http://www.w3.org/TR/html4/strict.dtd\">\n"  );
     out.write( "<html><head><title>" + title + "</title></head>\n<body><p>\n" );
   }
 
   /**
    * Creates a new filtered HTML writer with no title for the web page.
    *
    * @param out  a Writer object to provide the underlying stream.
    */
   public HTMLWriter( Writer out ) {
     super( out );
   }
 
   /**
    * Writes a single character.
    */
   @Override
   public void write( int c ) throws IOException {
     switch ( c ) {
       case '<':
         out.write( "&lt;" );
         newLine = false;
         break;
       case '>':
         out.write( "&gt;" );
         newLine = false;
         break;
       case '&':
         out.write( "&amp;" );
         newLine = false;
         break;
       case '\n':
         if ( newLine ) {
           out.write( "<br/>\n" );
           newLine = false;
         }
         else
           out.write( "\n" );
         newLine = true;
         break;
       case '\r':
         break; // ignore
 
       default :
         out.write( c );
         newLine = false;
     }
   }
 
   /**
    * Writes a portion of an array of characters.
    *
    * @param  cbuf Buffer of characters to be written
    * @param  off  Offset from which to start reading characters
    * @param  len  Number of characters to be written
    * @exception   IOException If an I/O error occurs
    */
   @Override
   public void write( char[] cbuf, int off, int len ) throws IOException {
     for ( int i = off; i < len; i++ )
       write( cbuf[ i ] );
   }
 
   /**
    * Writes a portion of a string.
    *
    * @param  str  String to be written.
    * @param  off  Offset from which to start reading characters
    * @param  len  Number of characters to be written
    * @exception   IOException If an I/O error occurs
    */
   @Override
   public void write( String str, int off, int len ) throws IOException {
     for ( int i = off; i < len; i++ )
       write( str.charAt( i ) );
   }
 
   /**
    * Closes the stream.
    *
    * @throws IOException If the prolog can not be written or the underlying stream 
    * not be closed
    */
   @Override
   public void close() throws IOException {
     try {
       out.write( "</p></body></html>" );
     }
     finally {
       out.close();  // Ignoriere, falls out.close() und out.write() knallt
     }
   }
 }

Ein Demo-Programm soll die aufbereiteten Daten in einen StringWriter schreiben:

StringWriter sw = new StringWriter();

try ( HTMLWriter html = new HTMLWriter( sw, "Superkreativer Titel" );

      PrintWriter pw = new PrintWriter( html ) ) {

  pw.println( "Und eine Menge von Sonderzeichen: < und > und &" );

  pw.println( "Zweite Zeile" );

  pw.println();

  pw.println( "Leerzeile" );

  pw.println( "Keine Leerzeile danach" );

}

System.out.println( sw );

HTML-Tags mit einem speziellen Filter überlesen

Unser nächstes Beispiel ist eine Klasse, die den FilterReader so erweitert, dass HTML-Tags überlesen werden. Die Klasse FilterReader deklariert den notwendigen Konstruktor zur Annahme des Reader, der die wirklichen Daten liefert, und überschreibt zwei read(…)-Methoden. Die read()-Methode ohne Parameter – die ein int für ein gelesenes Zeichen zurückgibt – legt einfach ein 1 Zeichen großes Feld an und ruft dann die zweite überschriebene read(char[], int, int)-Methode auf, die die Daten in ein Feld liest. Da dieser Methode neben dem Feld auch noch die Größe übergeben werden kann, müssen wirklich so viele Zeichen gelesen werden. Es reicht einfach nicht aus, die übergebene Anzahl von Zeichen vom tiefer liegenden Reader zu lesen, sondern hier müssen wir beachten, dass eingestreute Tags nicht zählen. Die Zeichenkette <p>Hallo<p> ist ja nur fünf Zeichen lang und nicht elf!

 package com.tutego.insel.io.stream;
 
 import java.io.*;
 
 public class HTMLReader extends FilterReader {
   private boolean inTag = false;
 
   public HTMLReader( Reader in ) {
     super( in );
   }
 
   @Override
   public int read() throws IOException {
     char[] buf = new char[ 1 ];
     return read( buf, 0, 1 ) == –1 ? –1 : buf[ 0 ];
   }
 
   @Override
   public int read( char[] cbuf, int off, int len ) throws IOException {
     int numchars = 0;
 
     while ( numchars == 0 ) {
       numchars = in.read( cbuf, off, len );
 
       if ( numchars == –1 ) // EOF?
         return –1;
 
       int last = off;
 
       for ( int i = off; i < off + numchars; i++ ) {
         if ( ! inTag ) {
           if ( cbuf[ i ] == '<' )
             inTag = true;
           else
             cbuf[ last++ ] = cbuf[ i ];
         }
         else if ( cbuf[ i ] == '>' )
           inTag = false;
       }
       numchars = last – off;
     }
     return numchars;
   }
 }

Ein Beispielprogramm soll die Daten aus einem StringReader ziehen. Der HTMLReader bekommt diesen StringReader und wird selbst von Scanner genutzt, damit wir die komfortable nextLine()-Methode nutzen können. Da hier keine externen Ressourcen vorkommen, müssen wir nichts schließen, und ein try mit Ressourcen kann entfallen.

String s = "<html>Hallo! <b>Ganz schön fett.</b> "
            + "Ah, wieder normal.</html>";
 
 Reader sr = new StringReader( s );
 Reader hr = new HTMLReader( sr );
 Scanner scanner = new Scanner( hr );
 while ( scanner.hasNextLine() )
   System.out.println( scanner.nextLine() );

Es produziert dann die einfache Ausgabe:

Hallo! Ganz schön fett. Ah, wieder normal.