Implementierung des Servlets

(Auszug aus "Java und XSLT" von Eric M. Burke)

Die letzte zu nehmende Hürde besteht in der Koordination des Webbrowsers, der Datenbank, der Donänenobjekte, der JDOM-Erzeuger und der XSLT-Stylesheets. Diese Aufgabe wird bei der Implementierung des Servlets und zugehöriger Klassen gelöst. In einer XSLT-basierten Webanwendung übernimmt das Servlet nicht allzu viele Aufgaben. Es fungiert vielmehr als Vermittler zwischen all den anderen Aktivitäten der Anwendung.

Die folgenden Abbildung zeigt ein UML-Klassendiagramm für das Paket com.oreilly.forum.servlet. Es besteht aus wenigen Hauptklassen und vielen Unterklassen von Renderer und ReqHandler. Es existieren mehrere Unterklassen mit ähnlichem Namen, was ein Zeichen für den strukturierten Entwurf ist, der mit XML und XSLT möglich wird.

Entwurf des Servlets

Abbildung: Entwurf des Servlets

Der Entwurf des Servlets für diese Anwendung sieht wie folgt aus. Das ForumServlet fängt alle eingehenden Anfragen von den Clients ab. Diese Anfragen werden dann an Unterklassen von ReqHandler weitergeleitet, die die Anfragen für jeweils eine bestimmte Seite bearbeiten. Danach wählt eine Unterklasse von Renderer die XML-Daten und das passende XSLT-Stylesheet aus. Die eigentliche Transformation wird von XSLTRenderHelper ausgeführt, dann wird der XHTML-Code zurück zum Browser geschickt.

Dies ist kein Entwurf eines Frameworks für komplexe Webanwendungen, sondern eine einfache Sammlung von Vereinbarungen und Regeln zur Implementierung, die es erlauben, die Anwendung hochgradig modular zu gestalten. Es wäre einfach, die ReqHandler-Klassen durch mehrere Servlets zu ersetzen. Der Vorteil von expliziten Request-Handlern und Renderern ist das modularisierte Design, wodurch die Entwicklung in einem Team konsistent bleibt.

Der gesamte Kontrollfluß ist vermutlich der am schwersten zu verstehende Teil. Sobald er aber einmal verinnerlicht wurde, besteht die Implementierung nur aus dem Hinzufügen weiterer Request-Handler und Renderer. Die folgende Abbildung ist ein UML-Sequenzdiagramm, das die Abarbeitung einer einzelnen Anfrage eines Webbrowsers verdeutlicht.

Sequenzdiagramm

Abbildung: Sequenzdiagramm

Eine Anfrage eines Browsers ist immer an das Servlet gerichtet. Das Servlet lokalisiert dann aufgrund von Informationen aus der angeforderten URL den passenden Request-Handler. Dieser regelt die Interaktion mit der Datenabstraktionsschicht, die wiederum die Domänenobjekte erzeugt und aktualisiert und den entsprechenden Renderer generiert.

Sobald der Renderer existiert, ruft das Servlet dessen Methode render( ) auf, um seinen Inhalt darstellen zu lassen. Der Renderer benutzt den passenden JDOM-Erzeuger, um die XML-Daten zu generieren, und führt dann mit Hilfe des XSLT-Stylesheets die Transformation durch. Das Ergebnis der Transformation wird schließlich zum Clientbrowser gesendet.

Ein Request-Handler kann verschiedene Renderer verwenden. Wenn ein Nutzer z.B. eine neue Nachricht versenden will, werden seine Informationen an die Klasse PostMsgReqHandler gesendet. Wenn dieser feststellt, daß ein benötigtes Feld nicht ausgefüllt wurde, gibt er eine Instanz der Klasse PostMsgRenderer zurück, die dem Nutzer die Möglichkeit gibt, diese Felder auszufüllen. Wenn dagegen ein Fehler in der Datenbank auftritt, wird eine Instanz von ErrorRenderer zurückgegeben. Wenn die Nachricht schließlich erfolgreich verschickt wurde, wird ViewMsgRenderer zurückgegeben. Da unsere Request-Handler klar von den Renderern getrennt wurden, kann ein Request-Handler jeden beliebigen Renderer aufrufen.

Der Code des ForumServlet ist im folgenden Beispiel abgedruckt. Wie schon erwähnt, handelt es sich um das einzige Servlet in unserer Anwendung.

Beispiel: ForumServlet.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.ForumConfig;
import com.oreilly.forum.jdbcimpl.DBUtil;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Das einzige Servlet im Diskussionsforum.
 */
public class ForumServlet extends HttpServlet {
  private ReqHandlerRegistry registry;
  
  /**
   * Registriert alle Request-Handler und initialisiert
   * das Objekt ForumConfig.
   */
  public void init(ServletConfig sc) throws ServletException {
    super.init(sc);
    
    // hole die Initialisierungsparameter vom
    // Anwendungsdeskriptor (web.xml)
    
    String jdbcDriverClassName = sc.getInitParameter("jdbcDriverClassName");
    String databaseURL = sc.getInitParameter("databaseURL");
    String adapterClassName = sc.getInitParameter("adapterClassName");
    ForumConfig.setValues(jdbcDriverClassName, databaseURL, adapterClassName);
    
    try {
      // lade alle Request-Handler
      this.registry = new ReqHandlerRegistry(new HomeReqHandler( ));
      this.registry.register(new PostMsgReqHandler( ));
      this.registry.register(new ViewMonthReqHandler( ));
      this.registry.register(new ViewMsgReqHandler( ));
    } catch (Exception ex) {
      log(ex.getMessage( ), ex);
      throw new UnavailableException(ex.getMessage( ), 10);
    }
  }
  
  /**
   * Schließe alle Datenbankverbindungen. Diese Methode wird
   * beim Beenden des Servlets aufgerufen.
   */
  
  public void destroy( ) {
    super.destroy( );
    DBUtil.closeAllConnections( );
  }
  
  protected void doPost(HttpServletRequest request,
      HttpServletResponse response) throws IOException,
      ServletException {
    ReqHandler rh = this.registry.getHandler(request);
    Renderer rend = rh.doPost(this, request, response);
    rend.render(this, request, response);
  }
  
  protected void doGet(HttpServletRequest request,
      HttpServletResponse response) throws IOException,
      ServletException {
    ReqHandler rh = this.registry.getHandler(request);
    Renderer rend = rh.doGet(this, request, response);
    rend.render(this, request, response);
  }
}

ForumServlet überschreibt die Methode init( ), um eine einmalige Initialisierung durchzuführen, bevor eine Client-Anfrage abgearbeitet wird. Hier werden die Kontext-Initialisierungsparameter aus dem Anwendungsdeskriptor gelesen und in einer Instanz von ForumConfig gespeichert:

String jdbcDriverClassName = sc.getInitParameter("jdbcDriverClassName");
String databaseURL = sc.getInitParameter("databaseURL");
String adapterClassName = sc.getInitParameter("adapterClassName");
ForumConfig.setValues(jdbcDriverClassName, databaseURL, adapterClassName);

Die Methode init( ) initialisiert auch eine Instanz von jedem Request-Handler. Diese werden durch die Klasse ReqHandlerRegistry registriert, mit der unsere Request-Handler später angesprochen werden können.

In der Methode destroy( ), die beim Beenden des Servlets aufgerufen wird, werden alle Datenbankverbindungen geschlossen:

public void destroy( ) {
  super.destroy( );
  DBUtil.closeAllConnections( );
}

Dies hat in der jetzigen Version der Anwendung keine Auswirkungen, aber in späteren Versionen könnte ein Pool von Datenbankverbindungen genutzt werden, so daß die Anwendung beim Beenden diese Verbindungen schließen müßte.

Die verbleibenden Methoden doGet( ) und doPost( ) sind nahezu identisch. Sie lokalisieren eine passende Instanz eines Request-Handlers, die dann ein GET bzw. POST ausführt. Die Antwort wird dann mittels eines Renderers erzeugt und zum Client gesendet.

Der Code von ReqHandler.java ist im folgenden Beispiel abgedruckt. Es handelt sich um eine abstrakte Klasse mit den Methoden doGet( ) und doPost( ). Standardmäßig liefern diese Methoden Fehlermeldungen zurück zum Client, so daß eine abgeleitete Klasse eine oder beide Methoden überschreiben muß, um HTTP GET- und/oder POST-Anfragen zu bearbeiten. Nach Beendigung der Methode muß die abgeleitete Klasse eine Instanz von Renderer zurückliefern, die die nächste Seite darstellt.

Beispiel: ReqHandler.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Alle Request-Handler müssen von dieser Klasse abgeleitet werden.
 */
public abstract class ReqHandler {
  protected abstract String getPathInfo( );
  
  protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    return new ErrorRenderer("GET wird nicht unterstützt");
  }
  
  protected Renderer doPost(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    return new ErrorRenderer("POST wird nicht unterstützt");
  }
}

Die Klasse Renderer ist im folgenden Beispiel zu sehen. Es handelt sich wie bei ReqHandler ebenfalls um eine abstrakte Klasse. Die abgeleiteten Klassen sind dafür verantwortlich, den Seiteninhalt für die HttpServletResponse bereitzustellen. Jede Seite des Diskussionsforums wird von einer Unterklasse von Renderer erzeugt.

Beispiel: Renderer.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Alle Seiten-Renderer müssen von dieser Klasse abgeleitet werden.
 */
public abstract class Renderer {
  public abstract void render(HttpServlet servlet,
    HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException;
}

Der einfachste Renderer ist der ErrorRenderer, der im folgenden Beispiel zu sehen ist. Diese Klasse stellt eine Fehlermeldung im Webbrowser dar, wobei der HTML-Code durch println( )-Anweisungen erzeugt wird. Anders als die anderen Teile der Anwendung benutzt die Klasse ErrorRenderer weder XML noch XSLT. Der Grund dafür ist, daß viele Fehlermeldungen ihre Ursache in einem im CLASSPATH falsch konfigurierten XML-Parser haben. (CLASSPATH-Probleme werden unter Entwicklungsumgebungen, Testen und Performance detailliert besprochen.) In diesem Fall wäre unser einfacher Renderer nicht betroffen.

Hinweis:
Auch der ErrorRenderer kann mit XML und XSLT realisiert werden, vorausgesetzt, daß ein try/catch-Block die Transformationsfehler abfängt und gegebenenfalls auf println( )-Anweisungen zurückgreift.

Beispiel: ErrorRenderer.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Stellt eine Fehlerseite dar. Da Fehler oft durch falsch konfigurierte
 * JAR-Dateien verursacht werden, wird in dieser Klasse weder XML noch XSLT benutzt.
 * Ansonsten würde der CLASSPATH-Fehler, der die ursprüngliche
 * Exception verursacht hat, dazu führen, daß auch diese Seite einen Fehler verursacht.
 */
public class ErrorRenderer extends Renderer {
  private String message;
  private Throwable throwable;
  
  public ErrorRenderer(Throwable throwable) {
    this(throwable, throwable.getMessage( ));
  }
  
  public ErrorRenderer(String message) {
    this(null, message);
  }
  
  public ErrorRenderer(Throwable throwable, String message) {
    this.throwable = throwable;
    this.message = message;
  }
  public void render(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    response.setContentType("text/html");
    PrintWriter pw = response.getWriter( );
    // stelle eine einfache Fehlerseite dar.
    pw.println("<html>");
    pw.println("<body>");
    pw.println("<p>");
    pw.println(this.message);
    pw.println("</p>");
    if (this.throwable != null) {
      pw.println("<pre>");
      this.throwable.printStackTrace(pw);
      pw.println("</pre>");
    }
    pw.println("</body></html>");
  }
}

Bei XSLTRenderHelper aus dem folgenden Beispiel handelt es sich um eine Hilfsklasse, die von den anderen Renderen genutzt wird. Sie realisiert die XSLT-Transformationen und beseitigt so identischen Code in den einzelnen Renderern. XSLTRenderHelper verwaltet auch einen Cache von Dateinamen von Stylesheets, so daß diese nicht immer wieder durch die Methode ServletContext.getRealPath( ) angesprochen werden müssen.

Beispiel: XSLTRenderHelper.java

package com.oreilly.forum.servlet;

import com.oreilly.javaxslt.util.StylesheetCache;
import java.io.*;
import java.net.URL;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import org.jdom.*;
import org.jdom.output.*;

/**
 * Eine Hilfsklasse, die das Rendern mit XSLT erleichtert. Dadurch
 * wird identischer Code für das Rendern der einzelnen Webseiten
 * an einer Stelle zusammengefaßt.
 */
public class XSLTRenderHelper {
  private static Map filenameCache = new HashMap( );
  
  /**
   * Führe eine XSLT-Transformation durch.
   *
   * @param servlet erlaubt den Zugriff auf den ServletContext, damit
   * das XSLT-Verzeichnis bestimmt werden kann.
   * @param xmlJDOMData JDOM-Repräsentation des XML-Dokuments.
   * @param xsltBaseName der Name des Stylesheets ohne Verzeichnisnamen.
   * @param response die Antwort des Servlets, in die die Ausgabe geschrieben wird.
   */
  public static void render(HttpServlet servlet, Document xmlJDOMData,
    String xsltBaseName, HttpServletResponse response)
    throws ServletException, IOException {
      
  String xsltFileName = null;
  try {
    // ermittle den kompletten Dateinamen des XSLT-Stylesheets
    synchronized (filenameCache) {
      xsltFileName = (String) filenameCache.get(xsltBaseName);
      if (xsltFileName == null) {
        ServletContext ctx = servlet.getServletContext( );
        xsltFileName = ctx.getRealPath(
          "/WEB-INF/xslt/" + xsltBaseName);
        filenameCache.put(xsltBaseName, xsltFileName);
      }
    }
      
    // schreibe die JDOM-Daten in einen StringWriter
    StringWriter sw = new StringWriter( );
    XMLOutputter xmlOut = new XMLOutputter("", false, "ISO-8859-1");
    xmlOut.output(xmlJDOMData, sw);#
      
    response.setContentType("text/html");
    Transformer trans = StylesheetCache.newTransformer(xsltFileName);
      
    // übergib einen Parameter an das XSLT-Stylesheet
    trans.setParameter("rootDir", "/forum/");
      
    trans.transform(new StreamSource(new StringReader(sw.toString( ))),
      new StreamResult(response.getWriter( )));
    } catch (IOException ioe) {
      throw ioe;
    } catch (Exception ex) {
      throw new ServletException(ex);
    }
  }

  private XSLTRenderHelper( ) {
  }
}

XSLTRenderHelper führt die XSLT-Transformation durch, indem das JDOM-Document in einen String mit XML-Daten geschrieben wird, der dann an einen zu JAXP kompatiblen XSLT-Prozessor übergeben wird. Das ist nicht gerade der effizienteste Weg, JDOM und JAXP zu integrieren, aber er arbeitet verläßlich mit der Beta-Version von JDOM zusammen. Wenn Sie dieses Buch lesen, wird JDOM eine standardisierte API für die Integration mit JAXP besitzen.

Eine weitere Hilfsklasse, ReqHandlerRegistry, ist im folgenden Beispiel zu sehen. Diese Klasse lokalisiert Instanzen von ReqHandler aufgrund von Pfadinformationen aus der URL der Anfrage. Pfadinformationen sind der Text nach dem Slash (/), der auf das Servlet-Mapping folgt. HttpServletRequest beinhaltet die Methode getPathInfo( ) zum Ermitteln der Pfadinformation.

Beispiel: ReqHandlerRegistry.java

package com.oreilly.forum.servlet;

import java.util.*;
import javax.servlet.http.*;

/**
 * Eine Hilfsklasse, die Instanzen von Request-Handlern aufgrund
 * von zusätzlicher Pfadinformation lokalisiert.
 */
public class ReqHandlerRegistry {
  private ReqHandler defaultHandler;
  private Map handlerMap = new HashMap( );
  
  public ReqHandlerRegistry(ReqHandler defaultHandler) {
    this.defaultHandler = defaultHandler;
  }
  
  public void register(ReqHandler handler) {
    this.handlerMap.put(handler.getPathInfo( ), handler);
  }
  
  public ReqHandler getHandler(HttpServletRequest request) {
    ReqHandler rh = null;
    String pathInfo = request.getPathInfo( );
    if (pathInfo != null) {
      int firstSlashPos = pathInfo.indexOf('/');
      int secondSlashPos = (firstSlashPos > -1) ?
        pathInfo.indexOf('/', firstSlashPos+1) : -1;
      
      String key = null;
      if (firstSlashPos > -1) {
        if (secondSlashPos > -1) {
          key = pathInfo.substring(firstSlashPos+1, secondSlashPos);
        } else {
          key = pathInfo.substring(firstSlashPos+1);
        }
      } else {
        key = pathInfo;
      }
      if (key != null && key.length( ) > 0) {
        rh = (ReqHandler) this.handlerMap.get(key);
      }
    }
    return (rh != null) ? rh : this.defaultHandler;
  }
}

In unserem Diskussionsforum sehen URLs wie folgt aus:

"http://hostname:port/forum/main/home"

In dieser URL steht forum für die Webanwendung und den zugehörigen Namen der WAR-Datei. Der nächste Teil der URL, main, ist eine Abbildung auf ForumServlet. Da sich weder WAR-Datei noch Servlet ändern, bleibt dieser Teil der URL konstant. Beim restlichen Teil, nämlich /home, handelt es sich um Pfadinformation. Dieser Teil der URL wird von ReqHandlerRegistry benutzt, um Instanzen von ReqHandler zu lokalisieren. Wenn die Pfadinformation null ist oder auf keinen Request-Handler abgebildet werden kann, wird ein Default-Request-Handler zurückgegeben. Der Nutzer gelangt in diesem Fall zurück zur Homepage.

Der erste wirkliche Request-Handler, HomeReqHandler, ist im folgenden Beispiel zu sehen. Diese einfache Klasse liefert eine Instanz von HomeRenderer zurück. Der Code ist einfach, weil die Homepage keine andere Funktion hat, als alle Nachrichten-Boards darzustellen. Andere Request-Handler sind komplizierter, da sie Parameter aus dem HttpServletRequest verarbeiten müssen.

Beispiel: HomeReqHandler.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Das ist der 'Default'-Request-Handler der Anwendung. Die
 * erste eingehende Anfrage wird an eine Instanz dieser Klasse
 * geleitet, die den Renderer der Homepage zurückliefert.
 */
public class HomeReqHandler extends ReqHandler {
  
  protected String getPathInfo( ) {
    return "home";
  }
  
  protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    return new HomeRenderer( );
  }
}

Alle Request-Handler müssen die Methode getPathInfo( ) überschreiben. Sie ermittelt die Pfadinformationen aus der URL, und jeder Request-Handler muß einen eindeutigen String zurückliefern.

Der Renderer der Homepage, siehe das folgende Beispiel, ist auch relativ einfach. Genau wie der Request-Handler der Homepage besitzt er nur einen Arbeitsmodus. Wie andere Renderer bekommt diese Klasse Daten von der Klasse DataAdapter und beauftragt den JDOM-Erzeuger, diese Daten nach XML zu konvertieren. Schließlich wird dem XSLTRenderHelper mitgeteilt, welches XSLT-Stylesheet für die Transformation herangezogen werden soll.

Beispiel: HomeRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Stellt die Homepage dar.
 */
public class HomeRenderer extends Renderer {
  
  public void render(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    try {
      // hole die Daten für die Homepage
      DataAdapter adapter = DataAdapter.getInstance( );
      
      // ein Iterator von BoardSummary-Objekten
      Iterator boards = adapter.getAllBoards( );
      
      // konvertiere die Daten nach XML (in ein JDOM-Dokument)
      Document doc = new Document(HomeJDOM.produceElement(boards));
      
      // wende das passende Stylesheet an
      XSLTRenderHelper.render(servlet, doc, "home.xslt", response);
    } catch (DataException de) {
      new ErrorRenderer(de).render(servlet, request, response);
    }
  }
}

Der ViewMonthReqHandler aus dem folgenden Beispiel ist etwas komplexer als der Request-Handler der Homepage. Da er die Board-ID, die Monatsnummer und die Jahreszahl als Parameter benötigt, muß vor der Bearbeitung der Anfrage eine Validierung durchgeführt werden.

Beispiel: ViewMonthReqHandler.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Request-Handler zum Anzeigen eines Monats aus einem Nachrichten-Board.
 */
public class ViewMonthReqHandler extends ReqHandler {
  
  protected String getPathInfo( ) {
    return "viewMonth";
  }
  
  protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    try {
      DataAdapter adapter = DataAdapter.getInstance( );
      
      // das sind alle benötigten Parameter
      long boardID = 0L;
      int month = 0;
      int year = 0;
      try {
        boardID = Long.parseLong(request.getParameter("boardID"));
        month = Integer.parseInt(request.getParameter("month"));
        year = Integer.parseInt(request.getParameter("year"));
      } catch (Exception ex) {
        return new ErrorRenderer("Ungültige Abfrage");
      }
      BoardSummary board = adapter.getBoardSummary(boardID);
      if (board == null) {
        return new ErrorRenderer("Ungültige Anfrage");
      }
        
      return new ViewMonthRenderer(board, new MonthYear(month, year));
    } catch (DataException de) {
      return new ErrorRenderer(de);
    }
  }
}

In unserer Anwendung wird eine scheinbar recht drastische Fehlerbehandlung durchgeführt. Wenn eine »unmögliche« Anfrage erkannt wird, erhält der Nutzer eine prägnante Fehlermeldung:

try {
  boardID = Long.parseLong(request.getParameter("boardID"));
  month = Integer.parseInt(request.getParameter("month"));
  year = Integer.parseInt(request.getParameter("year"));
} catch (Exception ex) {
  return new ErrorRenderer("Ungültige Anfrage");
}

Sicherheit von Webanwendungen
In der Klasse ViewMonthRegHandler wird eine NumberFormatException geworfen, wenn einer der Parameter nicht numerisch oder null ist. Es gibt prinzipiell nur zwei mögliche Ursachen für einen solchen Fehler. Zum einen kann eines der XSLT-Stylesheets fehlerhaft sein und einen der benötigten Parameter nicht übergeben. In diesem Fall sollte der Entwickler den Fehler während der Entwicklungs- und Testphase entdecken und beheben. Zum anderen könnte jemand manuell Parameter eingeben, ohne die XHTML-Nutzerschnittstelle zu verwenden. Dann handelt es sich vermutlich um einen Hacker, der unsere Site angreift, um einen Anwendungsfehler zu verursachen. Also weisen wir die Anfrage einfach zurück.
Eigenständige GUI-Anwendungen müssen diese Themen nicht berücksichtigen, da die Nutzerschnittstelle ungültige Eingaben verhindern kann. Webanwendungen sind dagegen für die ganze Welt offen, so daß der Entwickler einen besonders defensiven Programmierstil annehmen sollte. Wenn das Verhindern von Angriffen nicht an erster Stelle steht, könnte der Nutzer im Falle einer ungültigen Anfrage einfach zurück auf die Homepage verwiesen werden. Es ist prinzipiell eine gute Idee, beim Auftreten eines Fehlers die IP-Adresse, von der die Anfrage kam, und weitere relevante Informationen in einer Log-Datei zu protokollieren. Log-Einträge sind auch für die Diagnose von Fehlern der Webanwendung nützlich.

Das Hauptinteresse bei der Fehlerbehandlung sollte sich auf Einbruchsversuche von Hackern richten. Es ist für einen Nutzer sehr einfach zu bestimmen, welche Parameter an eine Webanwendung übergeben werden, um dann, durch das Eingeben von Permutationen dieser Parameter, Schaden anzurichten. Indem ungültige Parameter erkannt und einfach zurückgewiesen werden, kann eine Webanwendung wesentliche Sicherheitslücken schließen.

ViewMonthRenderer ist im folgenden Beispiel zu sehen. Es handelt sich um eine einfache Klasse, die einen ganzen Monat mit den Nachrichen eines Boards darstellt. Obwohl der XHTML-Code der Seite sehr komplex werden kann, wird die meiste Arbeit vom JDOM-Erzeuger und vom XSLT-Stylesheet bewältigt, so daß der Java-Code minimal bleibt.

Beispiel: ViewMonthRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Rendert eine Seite mit allen Nachrichten eines Monats.
 */
public class ViewMonthRenderer extends Renderer {
  
  private BoardSummary board;
  private MonthYear month;
  
  public ViewMonthRenderer(BoardSummary board, MonthYear month) {
    this.board = board;
    this.month = month;
  }
  
  public void render(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    try {
      // konvertiere die Daten nach XML (in ein JDOM-Dokument)
      Document doc = new Document(ViewMonthJDOM.produceElement(
        this.board, this.month));
      
      // verwende das passende Stylesheet
      XSLTRenderHelper.render(servlet, doc,
        "viewMonth.xslt", response);
    } catch (DataException de) {
      throw new ServletException(de);
    }
  }
}

Die Klasse ViewMsgReqHandler im folgenden Beispiel benötigt den Parameter msgID. Ist dieser ungültig, wird wie sonst auch eine Fehlerseite dargestellt. Ansonsten wird eine Instanz von ViewMsgRenderer an das Servlet zurückgegeben.

Beispiel: ViewMsgReqHandler.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Request-Handler zum Anzeigen einer Nachricht.
 */
public class ViewMsgReqHandler extends ReqHandler {
  
  protected String getPathInfo( ) {
    return "viewMsg";
  }
  
  protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    try {
      DataAdapter adapter = DataAdapter.getInstance( );
      
      // msgID ist ein zwingend benötigter Parameter, der gültig sein muß
      String msgIDStr = request.getParameter("msgID");
      
      if (msgIDStr == null) {
        servlet.log("Fehlender Parameter 'msgID'");
        return new ErrorRenderer("Ungültige Anfrage");
      }
        
      Message msg = adapter.getMessage(Long.parseLong(msgIDStr));
      MessageSummary inResponseTo = null;
      if (msg.getInReplyTo( ) > -1) {
        inResponseTo = adapter.getMessage(msg.getInReplyTo( ));
      }
      return new ViewMsgRenderer(msg, inResponseTo);
    } catch (NumberFormatException nfe) {
      servlet.log("Parameter 'msgID' war keine Zahl");
      return new ErrorRenderer("Ungültige Anfrage");
    } catch (DataException de) {
      return new ErrorRenderer(de);
    }
  }
}

Der zugehörige Renderer ViewMsgRenderer ist im folgenden Beispiel abgebildet. Diese Klasse folgt demselben allgemeinen Ansatz wie die anderen Renderer: Sie erzeugt ein JDOM-Dokument und benutzt XSLTRenderHelper für die XSLT-Transformation.

Beispiel: ViewMsgRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Stellt eine Nachricht dar.
 */
public class ViewMsgRenderer extends Renderer {
  
  private Message message;
  private MessageSummary inResponseTo;
  
  public ViewMsgRenderer(Message message, MessageSummary inResponseTo) {
    this.message = message;
    this.inResponseTo = inResponseTo;
  }
  
  public void render(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
        
    // konvertiere die Daten nach XML (in ein JDOM-Dokument)
    Document doc = new Document(ViewMessageJDOM.produceElement(
      this.message, this.inResponseTo));
        
    // wende das passende Stylesheet an
    XSLTRenderHelper.render(servlet, doc, "viewMsg.xslt", response);
  }
}

Im folgenden Beispiel ist die Klasse PostMsgReqHandler abgebildet. In der Methode doGet( ) wird der Parameter mode verwendet, um zu unterscheiden, ob der Nutzer eine neue Nachricht versenden oder auf eine bestehende Nachricht antworten will. Die Methode doGet( ) wird bei einer HTTP GET-Anfrage aufgerufen, also wenn der Nutzer einen Link betätigt oder eine URL eingibt.

Beispiel: PostMsgReqHandler.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Bearbeitet GET- und POST-Anfragen für die Seite, auf der der
 * Nutzer neue Nachrichten versenden oder auf bestehende antworten kann.
 */
public class PostMsgReqHandler extends ReqHandler {
  
  protected String getPathInfo( ) {
    return "postMsg";
  }
  
  /**
   * Bei einer HTTP GET-Anfrage wird die Webseite das erste Mal
   * dargestellt.
   */
  protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
    try {
      // Modus: "postNewMsg" oder "replyToMsg"
      String mode = request.getParameter("mode");
      
      DataAdapter adapter = DataAdapter.getInstance( );
      if ("replyToMsg".equals(mode)) {
        long origMsgID = Long.parseLong(
          request.getParameter("origMsgID"));
        Message inResponseToMsg = adapter.getMessage(origMsgID);
        if (inResponseToMsg != null) {
          return new PostMsgRenderer(inResponseToMsg);
        }
      } else if ("postNewMsg".equals(mode)) {
        long boardID = Long.parseLong(
            request.getParameter("boardID"));
        BoardSummary board = adapter.getBoardSummary(boardID);
        if (board != null) {
          return new PostMsgRenderer(board);
        }
      }
      
      return new ErrorRenderer("Ungültige Anfrage");
    } catch (NumberFormatException nfe) {
      return new ErrorRenderer(nfe);
    } catch (DataException de) {
      return new ErrorRenderer(de);
    }
  }
  
  /**
   * Bearbeitet eine HTTP POST-Anfrage, die auftritt, wenn der Nutzer
   * das Formular ausgefüllt und auf "Senden" geklickt hat.
   */
  protected Renderer doPost(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
        
    // Klickt der Nutzer auf "Abbrechen", wird zur Homepage zurückgekehrt.
    if (request.getParameter("cancelBtn") != null) {
      return new HomeRenderer( );
    }
        
    // Fehlerprüfungen...
    if (request.getParameter("submitBtn") == null) {
      servlet.log("Parameter 'submitBtn' war nicht vorhanden");
      return new ErrorRenderer("Ungültige Anfrage");
    }
  
    // Ein null-Parameter ist ein Hinweis auf einen Hacker-Angriff oder
    // einen Syntaxfehler im HTML-Code.
    String mode = request.getParameter("mode");
    String msgSubject = request.getParameter("msgSubject");
    String authorEmail = request.getParameter("authorEmail");
    String msgText = request.getParameter("msgText");
    if (mode == null || msgSubject == null || authorEmail == null
        || msgText == null) {
      return new ErrorRenderer("Ungültige Anfrage");
    }
    // einer davon könnte null sein
    String origMsgIDStr = request.getParameter("origMsgID");
    String boardIDStr = request.getParameter("boardID");
    if (origMsgIDStr == null && boardIDStr == null) {
      return new ErrorRenderer("Ungültige Anfrage");
    }

    long origMsgID = 0;
    long boardID = 0;
      try {
        origMsgID = (origMsgIDStr != null) ? Long.parseLong(origMsgIDStr) : 0;
        boardID = (boardIDStr != null) ? Long.parseLong(boardIDStr) : 0;
      } catch (NumberFormatException nfe) {
        return new ErrorRenderer("Ungültige Anfrage");
      }

      // Entferne überschüssigen Whitespace und prüfe, ob alle benötigten
      // Felder ausgefüllt wurden.
      msgSubject = msgSubject.trim( );
      authorEmail = authorEmail.trim( );
      msgText = msgText.trim( );
        try {
          DataAdapter adapter = DataAdapter.getInstance( );
          if (msgSubject.length( ) == 0
              || authorEmail.length( ) == 0
              || msgText.length( ) == 0) {
            BoardSummary board = (boardIDStr == null) ? null
              : adapter.getBoardSummary(boardID);
            MessageSummary inResponseToMsg = (origMsgIDStr == null) ? null
              : adapter.getMessage(origMsgID);
          
            return new PostMsgRenderer(board, inResponseToMsg,
              true, msgSubject, authorEmail, msgText);
            }

           //
           // An diesem Punkt wurden alle Fehlertests erfolgreich bestanden, so
           // daß die neue Nachricht bzw. die Antwort eingetragen werden kann
           //
           Message msg = null;
           if ("replyToMsg".equals(mode)) {
             msg = adapter.replyToMessage(origMsgID, msgSubject,
               authorEmail, msgText);
           } else if ("postNewMsg".equals(mode)) {
             msg = adapter.postNewMessage(boardID, msgSubject,
               authorEmail, msgText);
           }

           if (msg != null) {
             MessageSummary inResponseTo = null;
             if (msg.getInReplyTo( ) > -1) {
               inResponseTo = adapter.getMessage(msg.getInReplyTo( ));
             }
             return new ViewMsgRenderer(msg, inResponseTo);
           }
           return new ErrorRenderer("Ungültige Anfrage");
         } catch (DataException dex) {
           return new ErrorRenderer(dex);
         }
       }
     }

Anders als die anderen Request-Handler dieser Anwendung besitzt der PostMsgReqHandler die Methode doPost( ). Die Methode doGet( ) liefert einen Renderer, der das XHTML-Formular darstellt, während die Methode doPost( ) das ausgefüllte Formular verarbeitet. Da unser XHTML-Formular einige benötigte Felder und Buttons besitzt, ist die Methode doPost( ) weit komplizierter als doGet( ). Wie aus dem Code ersichtlich wird, liegt diese Komplexität zu großen Teilen in aufwendigen Fehlertests und Validierungen.

Die Methode doPost( ) prüft zuerst auf ungültige bzw. unmögliche Parameter und gibt gegebenenfalls eine Fehlerseite zurück. Als nächstes werden die Eingaben des Nutzers geprüft. Wurde ein benötigtes Feld ausgelassen, enthält der Parameterwert einen leeren String und nicht null. Natürlich müssen auch Leerzeichen am Anfang und am Ende des Strings abgeschnitten werden:

msgSubject = msgSubject.trim( );
authorEmail = authorEmail.trim( );
msgText = msgText.trim( );

Ist eines dieser Felder leer, wird der PostMsgRenderer zurückgegeben, wobei die bereits eingegebenen Felder voreingestellt werden:

return new PostMsgRenderer(board, inResponseToMsg,
    true, msgSubject, authorEmail, msgText);

Der Nutzer erhält somit die Möglichkeit, die fehlenden Felder auszufüllen und das Formular noch einmal abzuschicken. Wenn alles in Ordnung ist, wird eine Instanz von ViewMsgRenderer zurückgegeben. Der Nutzer kann die gerade gesendete Nachricht noch einmal anschauen.

Der Quellcode von PostMsgRenderer ist im folgenden Beispiel zu sehen.

Beispiel: PostMsgRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Stelle die Webseite dar, auf der der Nutzer eine neue Nachricht bzw. eine
 * Antwort auf eine bestehende Nachricht versenden kann.
 */
public class PostMsgRenderer extends Renderer {
  private MessageSummary inResponseToMsg;
  private BoardSummary board;
  private String msgSubject;
  private String authorEmail;
  private String msgText;
  private boolean showError;
  
  /**
   * Dieser Konstruktor wird beim Antworten auf eine bestehende
   * Nachricht verwendet.
   */
  public PostMsgRenderer(Message inResponseToMsg) {
    this.board = inResponseToMsg.getBoard( );
    this.inResponseToMsg = inResponseToMsg;
    this.showError = false;
    this.msgSubject = "Re: " + inResponseToMsg.getSubject( );
    this.authorEmail = "";
    
    StringTokenizer st = new StringTokenizer(
        inResponseToMsg.getText( ), "\n");
    StringBuffer buf = new StringBuffer( );
    buf.append("\n");
    buf.append("\n> -----Originalnachricht-----");
    buf.append("\n> Gesendet von ");
    buf.append(inResponseToMsg.getAuthorEmail( ));
    buf.append(" am ");
    buf.append(inResponseToMsg.getCreateDate().toString( ));
    buf.append("\n");
    while (st.hasMoreTokens( )) {
      String curLine = st.nextToken( );
      buf.append("> ");
      buf.append(curLine);
      buf.append("\n");
    }
    buf.append("> ");
    this.msgText = buf.toString( );
  }
  /**
   * Dieser Konstruktor wird beim Versenden einer neuen Nachricht verwendet.
   */
  public PostMsgRenderer(BoardSummary board) {
    this(board, null, false, "", "", "");
  }
  
  /**
   * Dieser Konstruktor wird verwendet, wenn der Nutzer ein Formular
   * abgeschickt hat, bei dem benötigte Einträge fehlen.
   */
  public PostMsgRenderer(BoardSummary board,
      MessageSummary inResponseToMsg,
      boolean showError,
      String msgSubject,
      String authorEmail,
      String msgText) {
    this.board = board;
    this.inResponseToMsg = inResponseToMsg;
    this.showError = showError;
    this.msgSubject = msgSubject;
    this.authorEmail = authorEmail;
    this.msgText = msgText;
  }
  public void render(HttpServlet servlet, HttpServletRequest request,
      HttpServletResponse response)
      throws IOException, ServletException {
        
    // konvertiere die Daten nach XML (in ein JDOM-Dokument)
    Document doc = new Document(PostMessageJDOM.produceElement(
      this.board,
      this.inResponseToMsg,
      this.showError,
      this.msgSubject,
      this.authorEmail,
      this.msgText));
    // wende das passende Stylesheet an
    XSLTRenderHelper.render(servlet, doc, "postMsg.xslt", response);
  }
}

Die Klasse besitzt verschiedene Konstruktoren, die unterschiedliche Operationsmodi unterstützen. Der erste Konstruktor wird für das Antworten auf bestehende Nachrichten verwendet und rückt die Originalnachricht wie viele E-Mail-Clients mit >-Zeichen ein. Abgesehen von den verschiedenen Konstruktoren funktioniert der Renderer wie alle anderen in unserer Anwendung. Der JDOM-Erzeuger und das XSLT-Stylesheet erledigen die meiste Arbeit und unterscheiden dabei die verschiedenen Operationsmodi.

   

zum Seitenanfang

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Java & XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Java und XSLT" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de