Erzeugung von dynamischem XML-Code

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

   

   

Bis jetzt haben wir eine Spezifikation für das Aussehen der Webseiten, für die XML-Daten jeder Seite und für die XSLT-Stylesheets, welche die Transformationen ausführen. Jetzt müssen wir uns um die Quelle der eigentlichen XML-Daten kümmern. Während des Entwurfs wurden die XML-Daten als eine Sammlung statischer Dateien betrachtet. Dadurch wird die Entwicklung der XSLT-Stylesheets enorm vereinfacht, denn die Autoren der Stylesheets können entkoppelt von der Datenbank und deren Zugriffsmechanismen schnell zu Ergebnissen kommen.

In einem realen System erfüllen statische XML-Dateien natürlich nicht unsere Anforderungen. Wir wollen Informationen aus einer relationalen Datenbank extrahieren und unmittelbar nach XML konvertieren, wenn eine Seite angefordert wird. Das macht unsere Anwendung lebendig, und Änderungen an der Datenbank werden für Nutzer unmittelbar sichtbar. Für den XSLT-Entwickler ist dieser Schritt belanglos, denn die XSLT-Transformationen arbeiten unabhängig davon, ob die XML-Daten einer Datei, einer relationalen Datenbank oder einer anderen Quelle entstammen.

Domänenklassen

Eine Domänenklasse ist eine Java-Klasse, die einen Zusammenhang in einem bestimmten Problemkreis beschreibt. Sie repräsentiert sozusagen das zu lösende Problem. In unserem Beispiel wird das Diskussionsforum durch eine Reihe von Java-Klassen beschrieben, die einen Puffer zwischen XML-Daten und der zugrundeliegenden relationalen Datenbank bilden. Diese Java-Klassen repräsentieren also die Daten des Diskussionsforums und können zusätzlich Geschäftslogik enthalten.

Die folgende Abbildung zeigt ein UML-Diagramm der Klassen im Paket com.oreilly.forum.domain. Sie enthalten weder Zugriffscode für die Datenbank noch können sie mit XML umgehen. Es handelt sich lediglich um einfache Datenstrukturen mit grundlegenden Funktionen. Dadurch kann die relationale Datenbank ohne Probleme durch eine andere Datenquelle ersetzt werden, ohne daß der Code für die XML-Erzeugung geändert werden müßte.

Wichtige Domänenklassen

Abbildung: Wichtige Domänenklassen

BoardSummary, MessageSummary und Message sind die Interfaces, die die Fähigkeiten des Diskussionsforums beschreiben. Zu jedem Interface existiert eine Impl-Klasse, die eine grundlegende Implementierung mit get- und set-Methoden enthält, die hier aber nicht gezeigt werden. Die Klassen MonthYear, DayMonthYear und DateUtil können ein Datum repräsentieren und auf einfache Weise manipulieren. Schließlich enthält die Klasse MessageTree Funktionen, mit denen eine Sammlung von Nachrichten, basierend auf Antworten und Erzeugungsdaten, sortiert und in einer hierarchischen Baumstruktur organisiert werden kann.

Das Interface BoardSummary aus dem folgenden Beispiel enthält Daten, die später benutzt werden, um die Homepage des Diskussionsforums zu erzeugen.

Beispiel: BoardSummary.java

package com.oreilly.forum.domain;

import java.util.Iterator;

/**
 * Informationen über das Nachrichten-Board.
 */
public interface BoardSummary {
  /**
   * @return eine eindeutige ID des Boards.
   */
  long getID( );
  
  /**
   * @return einen Namen für dieses Board.
   */
  String getName( );
  
  /**
   * @return eine Beschreibung dieses Boards.
   */
  String getDescription( );
  
  /**
   * @return einen Iterator von Objekten des Typs <code>MonthYear</code>.
   */
  Iterator getMonthsWithMessages( );
}

Das Interface BoardSummary wurde als nur lesbar entworfen. Dies ist eine wichtige Eigenschaft, da so eine aus der Datenbank erzeugte Instanz dieser Klasse nicht aus Versehen von einem Programmierer durch den Aufruf einer set-Methode geändert werden kann, da solche Änderungen nicht in der Datenbank gespeichert würden. Technisch gesehen könnte ein Nutzer dieser Klasse einen Iterator von Monaten mit Nachrichten erzeugen und die Methode remove( ) auf der Instanz von Iterator aufrufen. Man könnte zwar Maßnahmen ergreifen, um die Instanzen des Interface wirklich unveränderlich zu gestalten, aber das wäre sicherlich übertrieben.

Eine frühe Entscheidung beim Entwurf des Diskussionsforums war es, jedem Domänen-Objekt eine eindeutige long-Identifikation zuzuweisen, denn dadurch gestalten sich die SQL-Anfragen viel einfacher. (Der Code, der die eindeutigen IDs erzeugt, befindet sich in der Klasse DBUtil aus Beispiel DBUtil.java.) Diese Technik erlaubt es auch, Objekte in XHTML-Hyperlinks zu referenzieren, denn eine Identifikation kann in eine String-Repräsentation umgewandelt werden und umgekehrt.

Das nächste Interface im folgenden Beispiel stellt eine Zusammenfassung einer einzelnen Nachricht zur Verfügung.

Beispiel: MessageSummary.java

package com.oreilly.forum.domain;

import java.util.*;
/**
 * Informationen über eine Nachricht, ausgenommen dem Nachrichtentext.
 */
public interface MessageSummary extends Comparable {
  
  /**
   * @return die ID der Nachricht, auf die geantwortet wurde, oder
   * -1, wenn dies keine Antwort ist.
   */
  long getInReplyTo( );
  
  /**
   * @return die eindeutige ID der Nachricht.
   */
  long getID( );
  
  /**
   * @return wann diese Nachricht erzeugt wurde.
   */
  DayMonthYear getCreateDate( );
  
  /**
   * @return das Board, zu dem diese Nachricht gehört.
   */
  BoardSummary getBoard( );
  
  /**
   * @return der Betreff der Nachricht.
   */
  String getSubject( );
  
  /**
   * @return die E-Mail-Adresse des Autors (max. 80 Zeichen).
   */
  String getAuthorEmail( );
}

Das einzige im Interface MessageSummary fehlende Element ist der eigentliche Nachrichtentext. Das Interface Message ist von MessageSummary abgeleitet und enthält zusätzlich die Methode getText( ). Es ist im folgenden Beispiel zu sehen.

Beispiel: Message.java

package com.oreilly.forum.domain;

/**
 * Repräsentiert eine Nachricht einschließlich des Nachrichtentexts.
 */
public interface Message extends MessageSummary {
  /**
   * @return der Text der Nachricht.
   */
  String getText( );
}

Die Entscheidung, den Nachrichtentext in einem separaten Interface unterzubringen, beruht auf der Annahme, daß damit eine erhebliche Leistungssteigerung einhergeht. Denken Sie an eine Webseite, die eine hierarchische Ansicht aller Nachrichten eines Monats erzeugt. Diese Seite könnte hunderte Nachrichten enthalten und dabei nur die Informationen des Interface MessageSummary darstellen. Der Text jeder Nachricht kann tausende Wörter enthalten, die nur dann aus der Datenbank ermittelt werden, wenn die Nachricht tatsächlich dargestellt wird. In diesem Fall kann eine Instanz der Klasse, die Message implementiert, erzeugt werden.

Solche Entscheidungen können beim Entwurf nicht isoliert gemacht werden. Unabhängig davon, wie sauber XSLT und XML Präsentation und zugrundeliegende Daten trennen, sollten stark frequentierte Webseiten einen Einfluß auf den Entwurf der Datenstruktur haben. Dabei ist es wichtig, sich nicht zu sehr und zu früh auf Optimierungen zu konzentrieren und dabei einen klaren Entwurf zu vernachlässigen. In diesem Fall war die potentiell sehr große Anzahl von sehr langen Nachrichten Grund genug für ein separates Interface für Message.

Unsere drei Referenzimplementierungen sind die Klassen MessageImpl, Message-SummaryImpl und BoardSummaryImpl. Die JDBC-Datenabstraktionsschicht (siehe Abschnitt »Datenabstraktionsschicht«) erzeugt neue Instanzen der Klassen, die diese Interfaces implementieren, und liefert diese zurück. Falls in der Zukunft auf eine andere Datenquelle umgestellt werden soll, können diese Klassen benutzt oder neue Klassen geschrieben werden, die die entsprechenden Interfaces implementieren.

Die letzte Klasse dieses Paketes, MessageTree, ist im folgenden Beispiel abgebildet.

Beispiel: MessageTree.java

package com.oreilly.forum.domain;

import java.util.*;

/**
 * Arrangiert eine Sammlung von MessageSummary-Objekten in einer Baumstruktur.
 */
public class MessageTree {
  private List topLevelMsgs = new ArrayList( );
  
  // bilde IDs auf MessageSummary-Objekte ab
  private Map idToMsgMap = new HashMap( );
  
  // bilde Antwort-IDs auf Listen von MessageSummary-Objekten ab
  private Map replyIDToMsgListMap = new HashMap( );
  
  /**
   * Erzeuge einen neuen Nachrichtenbaum aus einem Iterator von MessageSummary-
   * Objekten.
   */
  public MessageTree(Iterator messages) {
    while (messages.hasNext( )) {
      // Speichere Nachrichten in einer Map, für einen schnellen ID-Zugriff
      MessageSummary curMsg = (MessageSummary) messages.next( );
      this.idToMsgMap.put(new Long(curMsg.getID( )), curMsg);
      
      // erzeuge eine invertierte Map, die Antwort-IDs auf
      // Listen von Nachrichten abbildet
      Long curReplyID = new Long(curMsg.getInReplyTo( ));
      List replyToList =
          (List) this.replyIDToMsgListMap.get(curReplyID);
      if (replyToList == null) {
        replyToList = new ArrayList( );
        this.replyIDToMsgListMap.put(curReplyID, replyToList);
      }
      replyToList.add(curMsg);
    }
  
    // Erzeuge die Liste von Top-Level-Nachrichten, die
    // eine der folgenden Kriterien erfüllen müssen:
    // - ihre Antwort-ID ist -1
    // - ihre Antwort-ID wurde nicht in der Liste von Nachrichten gefunden. Das
    // ist der Fall, wenn es sich um eine Antwort auf eine Nachricht des
    // Vormonats handelt
    Iterator iter = this.replyIDToMsgListMap.keySet().iterator( );
    while (iter.hasNext( )) {
      Long curReplyToID = (Long) iter.next( );
      if (curReplyToID.longValue( ) == -1
        || !this.idToMsgMap.containsKey(curReplyToID)) {
      List msgsToAdd =
        (List) this.replyIDToMsgListMap.get(curReplyToID);
      this.topLevelMsgs.addAll(msgsToAdd);
      }
    }
    Collections.sort(this.topLevelMsgs);
  }

  public Iterator getTopLevelMessages( ) {
    return this.topLevelMsgs.iterator( );
  }

  /**
   * @return einen Iterator von MessageSummary-Objekten der Antworten auf
   * die übergebene Nachricht.
   */
  public Iterator getReplies(MessageSummary msg) {
    List replies = (List) this.replyIDToMsgListMap.get(
      new Long(msg.getID( )));
    if (replies != null) {
      Collections.sort(replies);
      return replies.iterator( );
    } else {
      return Collections.EMPTY_LIST.iterator( );
    }
  }
}

Die Klasse MessageTree organisiert eine Liste von Nachrichten entsprechend dem Ablauf der Diskussion. Wenn Sie sich noch einmal den Code von MessageSummary ansehen, stellen Sie fest, daß jede Nachricht die ID ihres Vorgängers speichert:

public interface MessageSummary extends Comparable {
   ...
   long getInReplyTo( );
   ...
 }

Handelt es sich um eine Top-Level-Nachricht, ist die Antwort-ID –1. Ansonsten verweist sie auf eine andere Nachricht. Da eine Nachricht keine Methode zur Ermittlung ihrer Antworten besitzt, muß die Klasse MessageTree diese Liste für jede Nachricht erstellen. So werden die baumartigen Datenstrukturen der Klasse MessageTree erzeugt:

private List topLevelMsgs = new ArrayList( );
private Map idToMsgMap = new HashMap( );
private Map replyIDToMsgListMap = new HashMap( );

Beim Erzeugen des MessageTree erhält dieser einen Iterator aller Nachrichten eines Monats. Aus diesem Iterator wird die Datenstruktur idToMsgMap erzeugt. In idToMsgMap werden alle Nachrichten gespeichert, so daß diese schnell anhand ihrer ID abgefragt werden können. Während idToMsgMap erzeugt wird, erstellt der Konstruktor auch eine replyIDToMsgListMap. Die Schlüssel dieser Map sind Antwort-IDs, und die Werte sind Listen von Nachrichten-IDs. Mit anderen Worten, jeder Schlüssel wird auf eine Liste von Antworten abgebildet.

Nachdem die ersten beiden Datenstrukturen erzeugt wurden, wird eine Liste von Top-Level-Nachrichten angelegt. Dazu wird über alle Schlüssel der idToMsgMap iteriert und dabei nach Nachrichten mit Antwort-ID –1 gesucht. Außerdem werden Nachrichten, deren Antwort-ID nicht gefunden wurde, ebenfalls als Top-Level-Nachrichten betrachtet. Dies ist der Fall, wenn es sich um eine Antwort auf eine Nachricht des Vormonats handelt. Der ganze Code befindet sich im Konstruktor von MessageTree.

Datenabstraktionsschicht

Die Verbindung einer objektorientierten Klassenbibliothek und einer physischen Datenbank gestaltet sich oft recht schwierig. Enterprise JavaBeans (EJB) können für diesen Zweck eingesetzt werden. Dies erschwert jedoch den Einsatz des Diskussionsforums auf einem typischen Service-Provider. Indem die Anwendung auf Servlets und eine relationale Datenbank beschränkt bleibt, ist es möglich, unter mehreren ISPs zu wählen, die sowohl Servlets als auch JDBC-Zugriff auf Datenbanken wie MySQL ermöglichen.

Zusätzlich zu Software-Beschränkungen vieler Service-Provider spielt die Flexibilität des Entwurfes eine große Rolle. Heute stellt der direkte Zugriff auf eine MySQL-Datenbank die bevorzugte Lösung dar. In der Zukunft könnte eine komplette EJB-Lösung mit einer anderen Datenbank favorisiert werden. Oder wir könnten uns dazu entschließen, Nachrichten in Dateien und nicht in einer Datenbank abzulegen. Alle diese Möglichkeiten hält man sich durch die abstrakte Klasse DataAdapter offen. Diese wird in der folgenden Abbildung mit verschiedenen zugehörigen Klassen abgebildet.

Entwurf der Datenabstraktionsschicht

Abbildung: Entwurf der Datenabstraktionsschicht

Die Klasse DataAdapter definiert die Schnittstelle zu einer Backend-Datenquelle. Wie aus dem Klassendiagramm ersichtlich wird, handelt es sich bei FakeDataAdapter und JdbcDataAdapter um Unterklassen. Diese implementieren die Datenschicht mit einfachen Dateien beziehungsweise einer relationalen Datenbank. An dieser Stelle läßt sich in der Zukunft ein EJBDataAdapter einfügen. ForumConfig wird verwendet, um zu bestimmen, welche Unterklasse von DataAdapter instantiiert werden soll. Die Klasse DBUtil kapselt einige oft benutzte JDBC-Funktionen.

Der Quellcode von ForumConfig ist im folgenden Beispiel abgebildet. Es handelt sich um eine einfache Klasse, die Konfigurationsdaten an einer Stelle vereint. Wie weiter unten in diesem Kapitel noch gezeigt wird, werden hier alle konfigurierbaren Einstellungen im Anwendungsdeskriptor des Servlets abgespeichert, so daß sie nicht fest einprogrammiert werden müssen. Das Servlet liest als erstes diese Werte und speichert sie in ForumConfig. (Für diesen Zweck kann auch JNDI verwendet werden. Es erfordert allerdings einen höheren Aufwand bei der Konfiguration und läßt sich evtl. schwerer auf einen ISP übertragen.)

Beispiel: ForumConfig.java

package com.oreilly.forum;

/**
 * Definiert Konfigurationsinformationen der Anwendung. Das Servlet
 * muß die Methode setValues( ) aufrufen, bevor eine der get-
 * Methoden dieser Klasse verwendet werden kann.
 */
public class ForumConfig {
  // Maximalgrößen verschiedener Felder in der Datenbank
  public static final int MAX_BOARD_NAME_LEN = 80;
  public static final int MAX_BOARD_DESC_LEN = 255;
  public static final int MAX_MSG_SUBJECT_LEN = 80;
  public static final int MAX_EMAIL_LEN = 80;
  
  private static String jdbcDriverClassName;
  private static String databaseURL;
  private static String adapterClassName;
  
  public static void setValues( 
    String jdbcDriverClassName,
    String databaseURL,
    String adapterClassName) {
    ForumConfig.jdbcDriverClassName = jdbcDriverClassName;
    ForumConfig.databaseURL = databaseURL;
    ForumConfig.adapterClassName = adapterClassName;
  }
  
  /**
   * @return der Klassenname des JDBC-Treibers.
   */
  public static String getJDBCDriverClassName( ) {
    return ForumConfig.jdbcDriverClassName;
  }
  
  /**
   * @return die URL der JDBC-Datenbank.
   */
  public static String getDatabaseURL( ) {
    return ForumConfig.databaseURL;
  }
  
  /**
   * @return den Klassennamen der Datenabstraktionsimplementierung.
   */
  public static String getAdapterClassName( ) {
    return ForumConfig.adapterClassName;
  }
  
  private ForumConfig( ) {
  }
}

Die Klasse DataException ist eine Ausnahme, die ein Problem mit der zugrundeliegenden Datenquelle anzeigt. Sie verbirgt spezifische Ausnahmefehler der Datenbank vor dem Client und ermöglicht so zukünftige Implementierungen, die nicht auf einer Datenbank beruhen. Es könnte z.B. eine EJB-Schicht hinzugefügt werden, die anstelle einer SQLException eine RemoteException oder EJBException erzeugt. Deshalb wird jede spezifische Ausnahme in eine Instanz von DataException verpackt, bevor sie an den Aufrufer weitergeleitet wird.

Der Code von DataAdapter im folgenden Beispiel zeigt, wie jede Methode eine DataException auslöst. Diese Klasse ist das Herzstück der Datenabstraktionsschicht, die die Domänenklassen von der zugrundeliegenden Datenbank trennt.

Beispiel: DataAdapter.java

package com.oreilly.forum.adapter;

import com.oreilly.forum.*;
import com.oreilly.forum.domain.*;
import java.util.*;

/**
 * Definiert die Schnittstelle zur Datenquelle.
 */
public abstract class DataAdapter {
  private static DataAdapter instance;
  
  /**
   * @return die einzige Instanz dieser Klasse.
   */
  public static synchronized DataAdapter getInstance( )
      throws DataException {
    if (instance == null) {
      String adapterClassName = ForumConfig.getAdapterClassName( );
      try {
        Class adapterClass = Class.forName(adapterClassName);
        instance = (DataAdapter) adapterClass.newInstance( );
      } catch (Exception ex) {
        throw new DataException("Kann nicht instantiieren: "
            + adapterClassName);
      }
    }
    return instance;
  }

  /**
   * @param msgID muß eine gültige Nachrichten-ID sein.
   * @return die Nachricht mit der spezifizierten ID.
   * @throws DataException, falls die msgID nicht existiert oder ein Fehler
   * in der Datenbank auftritt.
   */
  public abstract Message getMessage(long msgID) throws DataException;

  /**
   * Füge eine Antwort auf eine bestehende Nachricht ein.
   *
   * @throws DataException, wenn ein Fehler in der Datenbank auftritt oder
   * ein Parameter ungültig ist.
   */
  public abstract Message replyToMessage(long origMsgID, String msgSubject,
    String authorEmail, String msgText) throws DataException;

  /**
   * Versende eine neue Nachricht.
   *
   * @return die neu erzeugte Nachricht.
   * @throws DataException, wenn ein Fehler in der Datenbank auftritt oder
   * ein Parameter ungültig ist.
   */
  public abstract Message postNewMessage(long boardID, String msgSubject,
    String authorEmail, String msgText) throws DataException;

  /**
   * Liefere einen leeren Iterator, wenn für das angegebene Board
   * und den Monat keine Nachricht existiert.
   * @return einen Iterator von <code>MessageSummary</code>-Objekten.
   * @throws DataException, wenn die boardID ungültig ist oder ein Fehler in
   * der Datenbank auftritt.
   */
  public abstract Iterator getAllMessages(long boardID, MonthYear month)
    throws DataException;

  /**
   * @return einen Iterator über alle <code>BoardSummary</code>-Objekte.
   */
  public abstract Iterator getAllBoards( ) throws DataException;

  /**
   * @return eine Zusammenfassung des Boards mit der angegebenen ID.
   * @throws DataException, wenn die boardID ungültig ist oder ein Fehler in
   * der Datenbank auftritt.
   */
  public abstract BoardSummary getBoardSummary(long boardID)
    throws DataException;
}

Die Klasse DataAdapter besteht aus abstrakten Methoden und der statischen Methode getInstance( ). Hier wird das Entwurfsmuster Singleton implementiert und eine Instanz einer Unterklasse von DataAdapter zurückliefert. (Siehe Gamma et al., Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software, Addison-Wesley, 2001.) Der eigentliche Rückgabetyp wird in der Klasse ForumConfig spezifiziert, und mit den Java Reflection-APIs wird das Objekt instantiiert:

String adapterClassName = ForumConfig.getAdapterClassName( );
try {
  Class adapterClass = Class.forName(adapterClassName);
  instance = (DataAdapter) adapterClass.newInstance( );
} catch (Exception ex) {
  throw new DataException("Kann nicht instantiieren: "
    + adapterClassName);
}

Alle restlichen Methoden sind abstrakt und werden entsprechend den Interfaces des Paketes com.oreilly.forum.domain definiert. Eine Nachricht kann zum Beispiel über ihre ID ausgelesen werden:

public abstract Message getMessage(long msgID) throws DataException;

Indem der Code entsprechend dem Interface Message geschrieben wird, kann später eine neue Klasse geschrieben werden, die Message auf andere Weise implementiert. In der Klasse DataAdapter wird immer dann eine DataException geworfen, wenn eine ID ungültig ist oder die Datenbank einen Fehler liefert.

Die Beispielimplementierung des Diskussionsforums enthält eine Attrappen-Implementierung von DataAdapter und eine Implementierung für JDBC. Die Datenbank-Implementierung wurde mit Microsoft Access und MySQL getestet und sollte mit jeder relationalen Datenbank arbeiten, die einen JDBC-Treiber bereitstellt. Die folgende Abbildung zeigt den Entwurf der Datenbank und die Verwendung der Klasse JdbcDataAdapter.

Entwurf der Datenbank

Abbildung: Entwurf der Datenbank

Die Datenbank ist relativ einfach. Jede Tabelle enthält die Spalte id, die eine eindeutige Identifikation und somit den Primärschlüssel darstellt. Message.inReplyToID enthält eine Referenz auf die Nachricht, auf die geantwortet wird, oder –1, wenn es sich um eine Top-Level-Nachricht handelt. Das Datum einer Nachricht wird in Monat, Tag und Jahr zerlegt. Die Anwendung könnte das Datum und die Zeit auch in einem anderen Format abspeichern, die gewählte Lösung ermöglicht aber leicht Anfragen wie diese:

SELECT subject
FROM Message
WHERE createMonth=3 AND createYear=2001

Die Spalte Message.boardID ist der Fremdschlüssel, der festlegt, zu welchem Board eine Nachricht gehört. Die Spalte Message.msgText kann eine unbegrenzte Menge Text enthalten, alle anderen Felder haben beschränkte Längen.

Wenn Sie MySQL verwenden, können Sie mit der »Dump«-Datei aus dem folgenden Beispiel und dem Import-Werkzeug die Datenbank erstellen.

Beispiel: Dump-Datei von MySQL

# MySQL dump 8.8
#
# Host: localhost Database: forum
#--------------------------------------------------------
# Server version3.23.23-beta

#
# Struktur der Tabelle 'board'
#

CREATE TABLE board (
  id bigint(20) DEFAULT '0' NOT NULL,
  name char(80) DEFAULT '' NOT NULL,
  description char(255) DEFAULT '' NOT NULL,
  PRIMARY KEY (id)
);

#
# Füllen der Tabelle 'board'
#

INSERT INTO board VALUES (0,'XSLT Grundlagen','XSLT-Stylesheets und -Prozessoren');
INSERT INTO board VALUES (1,'JAXP: Programmiertechniken','JAXP 1.1 verwenden');

#
# Struktur der Tabelle 'message'
#

CREATE TABLE message (
  id bigint(20) DEFAULT '0' NOT NULL,
  inReplyToID bigint(20) DEFAULT '0' NOT NULL,
  createMonth int(11) DEFAULT '0' NOT NULL,
  createDay int(11) DEFAULT '0' NOT NULL,
  createYear int(11) DEFAULT '0' NOT NULL,
  boardID bigint(20) DEFAULT '0' NOT NULL,
  subject varchar(80) DEFAULT '' NOT NULL,
  authorEmail varchar(80) DEFAULT '' NOT NULL,
  msgText text DEFAULT '' NOT NULL,
  PRIMARY KEY (id),
  KEY inReplyToID (inReplyToID),
  KEY createMonth (createMonth),
  KEY createDay (createDay),
  KEY boardID (boardID)
);

Die Klasse DBUtil aus dem folgenden Beispiel enthält einige Hilfsfunktionen, die den Umgang mit relationalen Datenbanken erleichtern.

Beispiel: DBUtil.java

package com.oreilly.forum.jdbcimpl;

import java.io.*;
import java.sql.*;
import java.util.*;

/**
 * Hilfsmethoden für den Zugriff auf eine relationale Datenbank mit JDBC.
 */
public class DBUtil {
  
  // Eine Abbildung von Tabellennamen auf maximale ID-Nummern
  private static Map tableToMaxIDMap = new HashMap( );
  
  /**
   * Beende eine Anweisung und Verbindung.
   */
  public static void close(Statement stmt, Connection con) {
    if (stmt != null) {
      try {
        stmt.close( );
      } catch (Exception ignored1) {
      }
    }
    if (con != null) {
      try {
        con.close( );
      } catch (Exception ignored2) {
      }
    }
  }
  
  /**
   * @return eine neue Verbindung zur Datenbank.
   */
  public static Connection getConnection(String dbURL)
      throws SQLException {
    // Vorschlag: die Implementierung eines Verbindungs-Pools wäre eine
    // sinnvolle Erweiterung
    return DriverManager.getConnection(dbURL);
  }
  
  /**
   * Beende alle offenen Verbindungen. Das Servlet ruft
   * diese Methode aus seiner Methode destroy( ) auf.
   */
  public static void closeAllConnections( ) {
    // sollte einmal ein Verbindungs-Pool implementiert werden, müssen
    // die Verbindungen hier beendet werden.
  }
  
  /**
   * Speichere einen langen Text in der Datenbank. Der Text einer Nachricht
   * ist z.B. zu lang, um mit der Methode setString() von JDBC
   * gespeichert zu werden.
   */
  public static void setLongString(PreparedStatement stmt,
      int columnIndex, String data) throws SQLException {
    if (data.length( ) > 0) {
      stmt.setAsciiStream(columnIndex,
        new ByteArrayInputStream(data.getBytes( )),
        data.length( ));
    } else {
      // Dieser 'else'-Zweig wurde eingeführt, weil der Code von
      // 'setAsciiStream' bei MS Access dazu führt, daß ein
      // "function sequence error" auftritt, wenn die String-Länge
      // null ist.
      stmt.setString(columnIndex, "");
    }
  }
  /**
   * @return ein langes Textfeld aus der Datenbank.
   */
  public static String getLongString(ResultSet rs, int columnIndex)
      throws SQLException {
    try {
      InputStream in = rs.getAsciiStream(columnIndex);
      if (in == null) {
        return "";
      }
        
      byte[] arr = new byte[250];
      StringBuffer buf = new StringBuffer( );
      int numRead = in.read(arr);
      while (numRead != -1) {
        buf.append(new String(arr, 0, numRead));
        numRead = in.read(arr);
      }
      return buf.toString( );
    } catch (IOException ioe) {
      ioe.printStackTrace( );
      throw new SQLException(ioe.getMessage( ));
    }
  }
  /**
   * Berechne eine neue eindeutige ID. Es wird angenommen, daß in der
   * spezifizierten Tabelle die Spalte 'id' vom Typ 'long' existiert.
   * Jeder Programmteil muß diese Methode verwenden, um neue IDs zu erzeugen.
   * @return die nächste verfügbare eindeutige ID für die Tabelle
   */
  public static synchronized long getNextID(String tableName,
      Connection con) throws SQLException {
    Statement stmt = null;
        
    try {
      // wenn das Maximum bereits aus der Tabelle ausgelesen wurde,
      // berechne die nächste ID, ohne in der Datenbank nachzuschauen
      if (tableToMaxIDMap.containsKey(tableName)) {
        Long curMax = (Long) tableToMaxIDMap.get(tableName);
        Long newMax = new Long(curMax.longValue( ) + 1L);
        tableToMaxIDMap.put(tableName, newMax);
        return newMax.longValue( );
      }
      stmt = con.createStatement( );
      ResultSet rs = stmt.executeQuery(
        "SELECT MAX(id) FROM " + tableName);
      long max = 0;
      if (rs.next( )) {
        max = rs.getLong(1);
      }
      max++;
      tableToMaxIDMap.put(tableName, new Long(max));
      return max;
    } finally {
      // beende die Anweisung
      close(stmt, null);
    }
  }
}

DBUtil besitzt die private Klassenvariable tableToMaxIDMap, das sich die größte eindeutige ID jeder Tabelle merkt. Dies geschieht im Zusammenspiel mit der Methode getNextID( ), welche die nächste verfügbare eindeutige ID für einen Tabellennamen liefert. Durch das Zwischenspeichern der IDs in der Map wird die Anzahl der Datenbankzugriffe verringert. Es muß aber darauf verwiesen werden, daß dieser Ansatz fehlschlägt, wenn jemand eine neue ID manuell einfügt und dabei diese Methode umgeht.

Die Methode close( ) wird benötigt, weil bei JDBC ein Statement und eine Connection fast immer beendet werden müssen. Deshalb sollte diese Methode immer aus einem finally-Block aufgerufen werden, denn ein solcher wird immer abgearbeitet, egal ob eine Ausnahme ausgelöst wurde oder nicht. Zum Beispiel:

Connection con = null;
Statement stmt = null;
try {
    // Code zum Aufbau der Verbindung und der Anweisung
    ...
    // Zugriff auf die Datenbank
    ...
} finally {
    DBUtil.close(stmt, con);
}

Wenn JDBC-Ressourcen nicht in einem finally-Block freigegeben werden, können Connections aus Versehen über längere Zeit bestehen bleiben, was insofern problematisch ist, als daß die Performance der Datenbank darunter leiden kann und einige Datenbanken die Anzahl gleichzeitiger Verbindungen limitieren.

In dieser Version unterstützt DBUtil zwar keinen Pool für Verbindungen, die Klasse enthält aber die folgende Methode:

public static Connection getConnection(String dbURL)

In einer späteren Version dieser Klasse könnte diese Methode eine Instanz einer Connection aus einem Pool zurückliefern, anstatt bei jedem Aufruf eine neue Verbindung zu erzeugen. Entsprechend könnte die Methode DBUtil.close( ) die Connection wieder im Pool ablegen und sie nicht einfach schließen. Das sind Überlegungen für zukünftige Erweiterungen, die aus Platzgründen hier nicht umgesetzt wurden.

Die Methoden setLongString( ) und getLongString( ) werden zum Speichern bzw. Auslesen der Nachrichtentexte verwendet. Da diese Texte sehr lang werden können, müssen sie anders als kurze Strings abgespeichert werden. In manchen Datenbanken werden sie CLOB-Spalten genannt. MS Access benutzt den Typ MEMO, während MySQL den Datentyp TEXT verwendet. Da Datenbanken in diesem Punkt unterschiedlich implementiert sein können, wird der Zugriffscode in der Klasse DBUtil untergebracht. Wenn eine Anpassung für eine bestimmte Datenbank gemacht werden muß, wird sie an einer einzigen Stelle im Code und nicht in jede SQL-Anweisung eingearbeitet.

Wir wollen nun die Klasse JdbcDataAdapter im folgenden Beispiel vorstellen. Es handelt sich um eine Implementierung der Klasse DataAdapter, die mit fast jeder relationalen Datenbank zusammenarbeiten sollte.

Beispiel: JdbcDataAdapter.java

package com.oreilly.forum.jdbcimpl;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import java.sql.*;
import java.util.*;

/**
 * Eine Implementierung von DataAdapter für JDBC.
 */
public class JdbcDataAdapter extends DataAdapter {
  
  private static String dbURL = ForumConfig.getDatabaseURL( );
  /**
   * Konstruiere den Datenadapter und lade den JDBC-Treiber.
   */
  public JdbcDataAdapter( ) throws DataException {
    try {
      Class.forName(ForumConfig.getJDBCDriverClassName( ));
    } catch (Exception ex) {
      ex.printStackTrace( );
      throw new DataException("Der JDBC-Treiber kann nicht geladen werden: "
        + ForumConfig.getJDBCDriverClassName( ));
    }
  }
  
  /**
   * @param msgID muß eine gültige Nachrichten-ID sein.
   * @return die Nachricht mit der angegebenen ID.
   * @throws DataException, wenn die msgID nicht existiert oder ein
   * Datenbankfehler auftritt.
   */
  public Message getMessage(long msgID) throws DataException {
    Connection con = null;
    Statement stmt = null;
    try {
      con = DBUtil.getConnection(dbURL);
      stmt = con.createStatement( );
      ResultSet rs = stmt.executeQuery(
        "SELECT inReplyToID, createDay, createMonth, createYear, "
        + "boardID, subject, authorEmail, msgText "
        + "FROM Message WHERE id="
        + msgID);
      if (rs.next( )) {
        long inReplyToID = rs.getLong(1);
        int createDay = rs.getInt(2);
        int createMonth = rs.getInt(3);
        int createYear = rs.getInt(4);
        long boardID = rs.getLong(5);
        String subject = rs.getString(6);
        String authorEmail = rs.getString(7);
        String msgText = DBUtil.getLongString(rs, 8);
        
        BoardSummary boardSummary = this.getBoardSummary(boardID, stmt);
        
        return new MessageImpl(msgID,
          new DayMonthYear(createDay, createMonth, createYear),
          boardSummary, subject, authorEmail, msgText,
          inReplyToID);
      } else {
        throw new DataException("Ungültige msgID");
      }
      } catch (SQLException sqe) {
        sqe.printStackTrace( );
        throw new DataException(sqe.getMessage( ));
      } finally {
        DBUtil.close(stmt, con);
      }
    }
    /**
     * Füge eine Antwort auf eine bestehende Nachricht ein.
     * @throws DataException, wenn ein Datenbankfehler auftritt oder
     * ein Parameter ungültig ist.
     */
    public Message replyToMessage(long origMsgID,
        String msgSubject, String authorEmail, String msgText)
        throws DataException {
      Message inReplyToMsg = this.getMessage(origMsgID);
      return insertMessage(inReplyToMsg.getBoard( ), origMsgID,
        msgSubject, authorEmail, msgText);
          
    }
    /**
     * Versende eine neue Nachricht.
     *
     * @return die neue Nachricht.
     * @throws DataException, wenn ein Datenbankfehler auftritt oder
     * ein Parameter ungültig ist.
     */
  
    public Message postNewMessage(long boardID, String msgSubject,
        String authorEmail, String msgText) throws DataException {
        
      BoardSummary board = this.getBoardSummary(boardID);
      return insertMessage(board, -1, msgSubject, authorEmail, msgText);
    }
  
    /**
     * Liefere einen leeren Iterator zurück, wenn für das Board und den Monat
     * keine Nachricht existiert
     * @return einen Iterator von Objekten des Typs <code>MessageSummary</code>.
     * @throws DataException, wenn die Board-ID ungültig ist oder ein
     * Datenbankfehler auftritt.
     */
    public Iterator getAllMessages(long boardID, MonthYear month)
        throws DataException {
      List allMsgs = new ArrayList( );
          
      Connection con = null;
      Statement stmt = null;
      try {
        con = DBUtil.getConnection(dbURL);
        stmt = con.createStatement( );
        
        BoardSummary boardSum = this.getBoardSummary(boardID, stmt);
        
        ResultSet rs = stmt.executeQuery(
          "SELECT id, inReplyToID, createDay, "
          + "subject, authorEmail "
          + "FROM Message WHERE createMonth="
          + month.getMonth( )
          + " AND createYear="
          + month.getYear( )
          + " AND boardID="
          + boardID);
        
       while (rs.next( )) {
         long msgID = rs.getLong(1);
         long inReplyTo = rs.getLong(2);
         int createDay = rs.getInt(3);
         String subject = rs.getString(4);
         String authorEmail = rs.getString(5);
         
         DayMonthYear createDMY = new DayMonthYear(
           createDay, month.getMonth(), month.getYear( ));
         
         allMsgs.add(new MessageSummaryImpl(msgID, createDMY,
           boardSum,
           subject, authorEmail, inReplyTo));
       }
       return allMsgs.iterator( );
     } catch (SQLException sqe) {
       sqe.printStackTrace( );
       throw new DataException(sqe);
     } finally {
       DBUtil.close(stmt, con);
     }
   }
  
  /**
   * @return einen Iterator aller Objekte des Typs <code>BoardSummary</code>.
   */
  public Iterator getAllBoards( ) throws DataException {
    List allBoards = new ArrayList( );
    
    Connection con = null;
    Statement stmt = null;
    Statement stmt2 = null;
    try {
      con = DBUtil.getConnection(dbURL);
      stmt = con.createStatement( );
      stmt2 = con.createStatement( );
      ResultSet rs = stmt.executeQuery(
        "SELECT id, name, description FROM Board "
        + "ORDER BY name");
      
      while (rs.next( )) {
        long id = rs.getLong(1);
        String name = rs.getString(2);
        String description = rs.getString(3);
        
        // Hole die Monate mit Nachrichten. Es wird ein anderes
        // Objekt für die Anweisung benutzt, weil das Ergebnis-Set
        // der ersten Anweisung bearbeitet wird.
        // List monthsWithMessages =
          this.getMonthsWithMessages(id, stmt2);
        
        allBoards.add(new BoardSummaryImpl(id, name, description,
          monthsWithMessages));
      }
      return allBoards.iterator( );
    } catch (SQLException sqe) {
      sqe.printStackTrace( );
      throw new DataException(sqe);
    } finally {
      if (stmt2 != null) {
        try {
          
          stmt2.close( );
        } catch (SQLException ignored) {
        }
      }
      DBUtil.close(stmt, con);
    }
  }
  
  /**
   * @return eine Zusammenfassung des Boards mit der übergebenen ID.
   * @throws DataException, wenn die boardID ungültig ist oder
   * ein Datenbankfehler auftritt.
   */
  public BoardSummary getBoardSummary(long boardID)
      throws DataException {
    Connection con = null;
    Statement stmt = null;
    try {
      con = DBUtil.getConnection(dbURL);
      stmt = con.createStatement( );
      return getBoardSummary(boardID, stmt);
    } catch (SQLException sqe) {
      sqe.printStackTrace( );
      throw new DataException(sqe);
    } finally {
      DBUtil.close(stmt, con);
    }
  }
  
  private BoardSummary getBoardSummary(long boardID, Statement stmt)
      throws DataException, SQLException {
    ResultSet rs = stmt.executeQuery(
      "SELECT name, description FROM Board WHERE id=" + boardID);
        
    if (rs.next( )) {
      String name = rs.getString(1);
      String description = rs.getString(2);
      
      List monthsWithMessages = getMonthsWithMessages(boardID, stmt);
      
      return new BoardSummaryImpl(boardID, name, description,
        monthsWithMessages);
    } else {
      throw new DataException("Unbekannte boardID");
    }
  }
  
  /**
   * @return eine Liste von Objekten des Typs MonthYear
   */
  private List getMonthsWithMessages(long boardID, Statement stmt)
      throws SQLException {
        
    List monthsWithMessages = new ArrayList( );
    ResultSet rs = stmt.executeQuery(
      "SELECT DISTINCT createMonth, createYear "
      + "FROM Message "
      + "WHERE boardID=" + boardID);
    while (rs.next( )) {
      monthsWithMessages.add(new MonthYear(
        rs.getInt(1), rs.getInt(2)));
    }
    return monthsWithMessages;
  }
  
  private Message insertMessage(BoardSummary board, long inReplyToID,
      String msgSubject, String authorEmail,
      String msgText) throws DataException {
    // verhindere einen Überlauf der maximalen Spaltenbreite der Datenbank
    if (msgSubject.length( ) > ForumConfig.MAX_MSG_SUBJECT_LEN) {
      msgSubject = msgSubject.substring(0,
        ForumConfig.MAX_MSG_SUBJECT_LEN);
    }
    if (authorEmail.length( ) > ForumConfig.MAX_EMAIL_LEN) {
      authorEmail = authorEmail.substring(0,
        ForumConfig.MAX_EMAIL_LEN);
    }

    DayMonthYear createDate = new DayMonthYear( );

    Connection con = null;
    PreparedStatement stmt = null;
    try {
      con = DBUtil.getConnection(dbURL);
      long newMsgID = DBUtil.getNextID("Message", con);
      stmt = con.prepareStatement("INSERT INTO Message "
        + "(id, inReplyToID, createMonth, createDay, createYear, "
        + "boardID, subject, authorEmail, msgText) "
        + "VALUES (?,?,?,?,?,?,?,?,?)");
      stmt.setString(1, Long.toString(newMsgID));
      stmt.setString(2, Long.toString(inReplyToID));
      stmt.setInt(3, createDate.getMonth( ));
      stmt.setInt(4, createDate.getDay( ));
      stmt.setInt(5, createDate.getYear( ));
      stmt.setString(6, Long.toString(board.getID( )));
      stmt.setString(7, msgSubject);
      stmt.setString(8, authorEmail);
      DBUtil.setLongString(stmt, 9, msgText);
      stmt.executeUpdate( );
      
      return new MessageImpl(newMsgID, createDate,
        board, msgSubject, authorEmail,
        msgText, inReplyToID);
      
    } catch (SQLException sqe) {
      sqe.printStackTrace( );
      throw new DataException(sqe);
    } finally {
      DBUtil.close(stmt, con);
    }
  }
}

Da dies kein Buch über den Zugriff auf relationale Datenbanken mit Java ist, wollen wir an dieser Stelle nicht auf die elementaren Details von JDBC eingehen. Der SQL-Code ist bewußt einfach gehalten, damit die Klasse für verschiedene relationale Datenbanken eingesetzt werden kann. Die URL und der Klassenname des JDBC-Treibers der Datenbank werden aus der Klasse ForumConfig abgerufen:

private static String dbURL = ForumConfig.getDatabaseURL( );
/**
* Erzeuge den Datenadapter und lade den JDBC-Treiber.
*/
public JdbcDataAdapter( ) throws DataException {
  try {
    Class.forName(ForumConfig.getJDBCDriverClassName( ));
  } catch (Exception ex) {
    ex.printStackTrace( );
    throw new DataException("JDBC-Treiber kann nicht geladen werden: "
        + ForumConfig.getJDBCDriverClassName( ));
  }
}

Mit Hilfe der Klasse DBUtil werden Verbindungen zur Datenbank hergestellt:

Connection con = null;
try {
    con = DBUtil.getConnection(dbURL);

Wie schon erwähnt, kann diese Lösung in einer späteren Ausbaustufe durch einen Pool für Verbindungen erweitert werden. Ein solcher Pool müßte in der Klasse DBUtil an nur einer Stelle eingefügt werden. Wenn Verbindungen und Anweisungen nicht mehr benötigt werden, sollten sie immer in einem finally-Block geschlossen werden:

} finally {
   DBUtil.close(stmt, con);
}

Solche finally-Blocks werden unabhängig vom Auftreten einer Ausnahme immer abgearbeitet.

Die Erzeugung von XML mit JDOM

Das Diskussionsforum kann bis jetzt Daten aus einer relationalen Datenbank extrahieren und Instanzen von Java-Domänenklassen erzeugen. Im nächsten Schritt sollen diese Objekte nach XML konvertiert werden, das wiederum mittels XSLT transformiert wird. Dazu verwenden wir die Klassenbibliothek JDOM. Dabei handelt es sich um Open-Source-Software, die unter jdom.org heruntergeladen werden kann. Obwohl JDOM etwas einfacher im Umgang ist und zu übersichtlicherem Code führt, kann natürlich auch mit der DOM-API gearbeitet werden. (Ein Beispiel zu DOM finden Sie im Beispiel XML-Erzeugung mit DOM in der Klasse BibliothekDOMCreator.)

Das Grundmuster besteht aus mehreren JDOM-Erzeugerklassen, von denen jede eine oder mehrere Domänenobjekte nach XML konvertieren kann. Dieser Ansatz nutzt die rekursive Natur von XML, indem jede Klasse eine Instanz eines JDOM-Elements erzeugt. Einige dieser Element-Instanzen repräsentieren ganze Dokumente, während andere kleine XML-Fragmente darstellen. Diese Fragmente können rekursiv in andere Element-Instanzen eingebettet werden, um komplexere Strukturen zu erzeugen.

Die XML-Daten außerhalb der Domänenobjekte unterzubringen, ist in vielerlei Hinsicht sinnvoll:

  • JDOM-Erzeugerklassen können durch DOM-Erzeuger oder eine andere Technologie ersetzt werden.
  • Es können zusätzliche Erzeugerklassen geschrieben werden, die anderen XML-Code generieren, ohne die bestehenden Domänenobjekte oder XML-Erzeuger zu ändern.
  • Domänenobjekte können durch Java-Interfaces mit mehreren verschiedenen Implementierungen repräsentiert werden. Indem die XML-Erzeugung separat gehalten wird, kann derselbe Erzeuger mit allen Implementierungen der Domänen-Interfaces arbeiten.

Die Klasse HomeJDOM aus dem folgenden Beispiel ist verhältnismäßig einfach. Sie erzeugt lediglich ein <home>-Element, das eine Liste von <board>-Elementen enthält. Da die <board>-Elemente von einem separaten JDOM-Erzeuger angelegt werden, muß die Klasse HomeJDOM diese XML-Fragmente nur zu einer größeren Struktur zusammensetzen.

Beispiel: HomeJDOM.java

package com.oreilly.forum.xml;

import com.oreilly.forum.domain.*;
import java.util.*;
import org.jdom.*;

/**
 * Erzeuge die JDOM-Daten für die Homepage.
 */
public class HomeJDOM {
  
  /**
   * @param boards ein Iterator von <code>BoardSummary</code>-Objekten.
   */
  public static Element produceElement(Iterator boards) {
    Element homeElem = new Element("home");
    while (boards.hasNext( )) {
      BoardSummary curBoard = (BoardSummary) boards.next( );
      homeElem.addContent(BoardSummaryJDOM.produceElement(curBoard));
    }
    
    return homeElem;
  }
  
  private HomeJDOM( ) {
  }
}

Weitere Optionen von JDOM
Die Verwendung von statischen Methoden, die in diesem Kapitel demonstriert wurde, ist nicht der einzige Weg, JDOM-Daten zu erzeugen. Man könnte auch Unterklassen der Klasse Element von JDOM schreiben. Der Konstruktor einer solchen Unterklasse bekommt dann ein Domänenobjekt als Parameter. Die XML-Daten werden dann nicht durch den Aufruf einer statischen Methode, sondern folgendermaßen erzeugt:

Iterator boards = ...
Element homeElem = new HomeElement(boards);

Des weiteren kann der JDOM-Code in die Domänenobjekte integriert werden. In diesem Fall würde Ihr Code so aussehen:

BoardSummary board = ...
Element elem = board.convertToJDOM( );

Dieser Ansatz hat natürlich den Nachteil, daß der JDOM-Code eng mit den Domänenklassen verbunden wird. Er funktioniert auch nicht in solchen Fällen, in denen XML-Daten nicht aus einem einzigen, sondern aus einer ganzen Gruppe von Domänenobjekten erzeugt werden.

Unabhängig von der angewendeten Methode ist Konsistenz das wichtigste Kriterium. Wenn alle Klassen nach demselben Muster geschrieben werden, muß das Entwicklungsteam nur ein Beispiel verstehen, um mit dem ganzen System vertraut zu sein.

   

Der Konstruktor der Klasse HomeJDOM ist privat deklariert. Dadurch wird eine Instantiierung der Klasse verhindert, was zur Effizienzsteigerung beiträgt. Da alle Klassen des Diskussionsforums zur Erzeugung von JDOM zustandslos und somit Thread-sicher sind, kann die Methode produceElement( ) static deklariert werden. Das bedeutet, daß es einfach unnötig ist, Instanzen des JDOM-Erzeugers anzulegen, denn dieselbe Methode wird von mehreren nebenläufigen Threads verwendet. Außerdem gibt es keine gemeinsame Basisklasse, weil jede der Methoden produceElement( ) andere Parametertypen übernimmt.

Der Code von ViewMonthJDOM ist im folgenden Beispiel zu sehen. Diese Klasse erzeugt die XML-Daten für einen ganzen Monat mit Nachrichten.

Beispiel: ViewMonthJDOM.java

package com.oreilly.forum.xml;

import java.util.*;
import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import org.jdom.*;

/**
 * Erzeugt JDOM für die Monatsübersicht des Boards.
 */
public class ViewMonthJDOM {
  
  /**
   * @param board das Nachrichten-Board, für das JDOM erzeugt werden soll.
   * @param month der Monat und das Jahr, die angezeigt werden sollen.
   */
  public static Element produceElement(BoardSummary board,
      MonthYear month) throws DataException {
    Element viewMonthElem = new Element("viewMonth");
    viewMonthElem.addAttribute("month",
      Integer.toString(month.getMonth( )));
    viewMonthElem.addAttribute("year",
      Integer.toString(month.getYear( )));
        
    // erzeuge das Element <board> ...
    Element boardElem = BoardSummaryJDOM.produceNameIDElement(board);
    viewMonthElem.addContent(boardElem);
        
    DataAdapter adapter = DataAdapter.getInstance( );
        
    MessageTree msgTree = new MessageTree(adapter.getAllMessages(
      board.getID( ), month));
        
    // hole einen Iterator für die MessageSummary-Objekte
    Iterator msgs = msgTree.getTopLevelMessages( );
        
    while (msgs.hasNext( )) {
      MessageSummary curMsg = (MessageSummary) msgs.next( );
      Element elem = produceMessageElement(curMsg, msgTree);
      viewMonthElem.addContent(elem);
     }
        
     return viewMonthElem;
   }
  
   /**
    * Erzeuge ein XML-Fragment für eine bestimmte Nachricht. Das
    * ist eine rekursive Funktion.
    */
   private static Element produceMessageElement(MessageSummary msg,
       MessageTree msgTree) {
     Element msgElem = new Element("message");
     msgElem.addAttribute("id", Long.toString(msg.getID( )));
     msgElem.addAttribute("day",
       Integer.toString(msg.getCreateDate().getDay( )));
     msgElem.addContent(new Element("subject")
       .setText(msg.getSubject( )));
     msgElem.addContent(new Element("authorEmail")
       .setText(msg.getAuthorEmail( )));
         
     Iterator iter = msgTree.getReplies(msg);
     while (iter.hasNext( )) {
       MessageSummary curReply = (MessageSummary) iter.next( );
       
       // Erzeuge den XML-Code rekursiv für alle Antworten
       msgElem.addContent(produceMessageElement(curReply, msgTree));
     }
     return msgElem;
   }
  
   private ViewMonthJDOM( ) {
   }
}

Der einzige schwierige Code in ViewMonthJDOM ist die rekursive Methode, die die <message>-Elemente erzeugt. Da die <message>-Elemente eingerückt werden, bilden die XML-Daten eine rekursive Baumstruktur, die beliebig tief werden kann. JDOM unterstützt dies dadurch, daß ein JDOM-Element weitere verschachtelte Elemente enthalten kann. Die Methode produceMessageElement( ) erzeugt die benötigten XML-Daten.

Die im folgenden Beispiel dargestellte Klasse ist recht einfach. Sie erzeugt eine XML-Ansicht einer bestimmten Nachricht.

Beispiel: ViewMessageJDOM.java

package com.oreilly.forum.xml;

import com.oreilly.forum.domain.*;
import java.util.Date;
import org.jdom.*;
import org.jdom.output.*;

/**
 * Erzeuge JDOM-Daten für die Nachrichtenansicht.
 */
public class ViewMessageJDOM {
  
  /**
   * @param message die Nachricht, die angezeigt werden soll.
   * @param inResponseTo die Nachricht, auf die mit dieser geantwortet wurde,
   * bzw. null, wenn es sich um eine Top-Level-Nachricht handelt.
   */
  public static Element produceElement(Message message,
    MessageSummary inResponseTo) {
    Element messageElem = new Element("message");
    messageElem.addAttribute("id", Long.toString(message.getID( )));
    DayMonthYear d = message.getCreateDate( );
    messageElem.addAttribute("month", Integer.toString(d.getMonth( )));
    messageElem.addAttribute("day", Integer.toString(d.getDay( )));
    messageElem.addAttribute("year", Integer.toString(d.getYear( )));
      
    Element boardElem = BoardSummaryJDOM.produceNameIDElement(
      message.getBoard( ));
    messageElem.addContent(boardElem);
      
    if (inResponseTo != null) {
      Element inRespToElem = new Element("inResponseTo")
        .addAttribute("id", Long.toString(inResponseTo.getID( )));
      inRespToElem.addContent(new Element("subject")
        .setText(inResponseTo.getSubject( )));
      messageElem.addContent(inRespToElem);
    }
      
    messageElem.addContent(new Element("subject").setText(message.getSubject( )));
    messageElem.addContent(new Element("authorEmail").setText(message.getAuthorEmail( )));
    messageElem.addContent(new Element("text").setText(message.getText( )));
    return messageElem;
  }
  
  private ViewMessageJDOM( ) {
  }
}

Der JDOM-Erzeuger BoardSummaryJDOM aus dem folgenden Beispiel generiert die XML-Daten für ein BoardSummary-Objekt. Diese Klasse erzeugt kein ganzes XML-Dokument. Statt dessen werden die von ihr erzeugten Elemente in andere XML-Seiten der Anwendung eingebettet. Zum Beispiel ist auf der Homepage eine Liste aller <board>-Elemente des Systems zu sehen, wobei jedes von BoardSummaryJDOM erzeugt wird. Wenn Sie ein eigenes System entwerfen, stoßen Sie wahrscheinlich auch auf XML-Fragmente, die auf mehreren Seiten verwendet werden können. Schreiben Sie in diesem Fall eine gemeinsame Hilfsklasse anstatt den Code zu duplizieren.

Beispiel: BoardSummaryJDOM.java

package com.oreilly.forum.xml;

import com.oreilly.forum.domain.*;
import java.util.*;
import org.jdom.*;

/**
 * Erzeugt JDOM für ein BoardSummary-Objekt.
 */
public class BoardSummaryJDOM {
  public static Element produceNameIDElement(BoardSummary board) {
    // erzeuge das Folgende:
    // <board id="123">
    // <name>der Board-Name</name>
    // <description>eine Board-Beschreibung</description>
    // </board>
    Element boardElem = new Element("board");
    boardElem.addAttribute("id", Long.toString(board.getID( )));
    boardElem.addContent(new Element("name").setText(board.getName( )));
    boardElem.addContent(new Element("description")
      .setText(board.getDescription( )));
    return boardElem;
  }
  
  public static Element produceElement(BoardSummary board) {
    Element boardElem = produceNameIDElement(board);
    
    // füge die Liste von Nachrichten hinzu
    Iterator iter = board.getMonthsWithMessages( );
    while (iter.hasNext( )) {
      MonthYear curMonth = (MonthYear) iter.next( );
      Element elem = new Element("messages");
      elem.addAttribute("month", Integer.toString(curMonth.getMonth( )));
      elem.addAttribute("year", Integer.toString(curMonth.getYear( )));
      boardElem.addContent(elem);
    }
    
    return boardElem;
  }
  
  private BoardSummaryJDOM( ) {
  }
}

Der letzte JDOM-Erzeuger, PostMessageJDOM , ist im folgenden Beispiel zu sehen. Die Methode produceElement( ) übernimmt viele Parameter, mit denen XML-Code zum Versenden neuer Nachrichten oder zum Antworten auf eine Nachricht erzeugt wird. Es können auch die Werte für den Betreff, die E-Mail-Adresse des Autors und den Nachrichtentext im XML voreingestellt werden. Die Anwendung nutzt dies, wenn das HTML-Formular für einen Nutzer mit den voreingestellten Werten noch einmal dargestellt werden muß.

Beispiel: PostMessageJDOM.java

package com.oreilly.forum.xml;

import com.oreilly.forum.domain.*;
import org.jdom.*;

/**
 * Erzeuge JDOM für die Seite "Nachricht senden".
 */
public class PostMessageJDOM {
  
  public static Element produceElement(
      BoardSummary board,
      MessageSummary inResponseToMsg,
      boolean showError,
      String subject,
      String authorEmail,
      String msgText) {
    Element messageElem = new Element("postMsg");
        
    // benutze die Klasse BoardSummaryJDOM, um ein
    // XML-Fragment zu erzeugen
    messageElem.addContent(BoardSummaryJDOM.produceNameIDElement(board));
        
    if (inResponseToMsg != null) {
      Element inRespTo = new Element("inResponseTo")
        .addAttribute("id", Long.toString(inResponseToMsg.getID( )));
      inRespTo.addContent(new Element("subject")
        .setText(inResponseToMsg.getSubject( )));
      messageElem.addContent(inRespTo);
    }
        
    if (showError) {
      messageElem.addContent(new Element("error")
        .addAttribute("code", "ALL_FIELDS_REQUIRED"));
    }
        
    Element prefill = new Element("prefill");
    prefill.addContent(new Element("subject").setText(subject));
    prefill.addContent(new Element("authorEmail").setText(authorEmail));
    prefill.addContent(new Element("message").setText(msgText));
    messageElem.addContent(prefill);
        
    return messageElem;
  }
  
  private PostMessageJDOM( ) {
  }
}
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