package javacodebook.xml.processing.dom.create;

import java.io.*;
import java.util.*;
import java.sql.*;

import javax.servlet.*;
import javax.servlet.http.*;

import org.w3c.dom.*;

// das sind Xerces-spezifischen Klassen die benötig werden, da
// deren Funktionsumfang vom W3C noch nicht spezifiziert ist.
import org.apache.xerces.dom.DocumentImpl;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;

/**
 * RDB2XMLConverter ist ein Servlet, welches in generischer Weise SQL-Datenbankanfragen in
 * XML-Datenstöme verwandeln kann.  Dabei werden Metainformationen wie Feld- und Tabellenamen und Typen
 * mitgeliefert.
 * Das Servlet unterstützt die HTTP-Get Methode.  Dabei benötigt es einen Parameter namens 'sql'
 * mit der gewünschten SQL-Anfrage.   Die Parameter 'driver', 'url', 'user' und 'pwd' sind
 * optional und können die Datenbankverbindung beschreiben auf die das SQL abgesetzt werden soll.
 * Zu beachten ist, dass dabei die entsprechende Treiberklasse im Klassenpfad der Servlet-Engine
 * zu finden sein muss.
 *
 * Dieses Servlet funktioniert nur, solange Spaltennamen nach well-formed XML benannt sind.  Dies kann bei Bedarf
 * sehr leicht geändert werden, indem der Spaltenname nicht über den Elementnamen, sondern über ein zu-
 * sätzliches Attribut modelliert wird.  Der Elementname könnte dann generisch, z.B. 'column' genannt werden.
 *
 * @author Christoph Leinemann
 */
public class RDB2XMLConverter
    extends HttpServlet {

  private static final String CONTENT_TYPE = "text/xml";

  private Connection connection = null;

  private String defaultDriver;
  private String defaultUrl;
  private String defaultUser;
  private String defaultPwd;

  public void init(ServletConfig config) {
    defaultDriver = config.getInitParameter("defaultDriver");
    defaultUrl = config.getInitParameter("defaultUrl");
    defaultUser = config.getInitParameter("defaultUser");
    defaultPwd = config.getInitParameter("defaultPwd");
  }

  /**
   * Die doGet-Methode ist überschreiben, damit das Servlet http-Get unterstützt.
   * In der doGet-Methode wird der http-Parameter 'sql' ausgelesen.  Der ausgelesene
   * String wird als SQL entweder an die Default-Datenbank oder an eine andere Datenbank,
   * die über zusätzliche http-Parameter beschrieben wurde, abgesetzt.
   *
   * @param request
   * @param response
   * @throws ServletException
   * @throws IOException
   */
  public void doGet(HttpServletRequest request, HttpServletResponse response) throws
      ServletException, IOException {
    response.setContentType(CONTENT_TYPE);
    PrintWriter out = response.getWriter();
    String sql = request.getParameter("sql");
    // falls kein http-Parameter namens 'sql' übergeben wird, soll eine Fehlermeldung
    // zurück gegeben werden.
    if (sql == null || sql.length() == 0) {
      out.println(
          "<message code=\"-1\">please provide http-get-parameter named \'sql\'</message>");
      return;
    }

    try {
      // eine Datenbankverbindung wird aufgebaut
      Connection con = getConnection(request);
      Document doc = null;

      // da kein Connectionpool implementiert ist, sollte an dieser Stelle vorsichtshalber
      // sichergestellt sein, dass nicht mehr als ein Thread die Connection verwendet.
      synchronized (con) {
        doc = createDocument(con, sql);
      }
      // Auf Basis des Document-Objektes wird ein OutputFormat-Objekt erzeugt.  Hierbei muss das richige
      // Encoding gewählt werden. Der letze boolsche Wert im Konstruktor gibt an, ob das
      // XML eingerückt ausgegeben werden soll oder nicht.

      OutputFormat format = new OutputFormat(doc, "ISO-8859-1", true);

      // Es wird ein XMLSerializer auf Basis des Ausgabestroms zum Client
      // und des OutputFormat-Objekts instanziiert.
      XMLSerializer serial = new XMLSerializer(out, format);

      // Nun kann das Dokument zum Client geschrieben werden.
      serial.serialize(doc);
      out.flush();
      out.close();

    }
    catch (Exception e) {
      //Im Ausnahmefall wird eine Fehlermeldung zurück gegeben.
      out.println("<message code=\"-1\">" + e + "</message>");
    }
  }

  /**
   * Diese Methode liefert auf Basis der Parameter, die in dem HttpServletRequest-Objekt
   * gekapselt sind, eine neue oder die schon bestehende Datenbankverbindung.  An dieser
   * Stelle würde in einer professionellen Anwendung ein Connectionpool eingesetzt
   * werden.
   * Falls keine Parameter 'url', 'driver', 'user' und 'pwd' übergeben wurden, wird die
   * Default-Datenbankverbindung genommen, die über die web.xml konfiguriert wurde.
   *
   * @param request
   * @return java.sql.Connection
   * @throws Exception
   */
  private Connection getConnection(HttpServletRequest request) throws Exception {
    String driver = null, url = null, user = null, pwd = null;
    Connection con = null;

    // wenn der Parameter 'url' übergeben wurde, soll das SQL auf einer anderem als der
    // Default-Datenbank ausgeführt werden, also werden die anderen Parameter auch
    // noch ausgelesen.
    if ( (url = request.getParameter("url")) != null && url.length() != 0) {
      driver = request.getParameter("driver");
      user = request.getParameter("user");
      pwd = request.getParameter("pwd");

      // eine neue Verbindung wird mit den entsprechenden Parametern geholt.
      return getConnection(driver, url, user, pwd);
    }
    else {

      // ansonsten wird eine Datenbankverbindung mit den default Werten erzeugt
      // oder die schon vorhandene Verdinung zurück gegeben.
      if (connection == null) {
        connection = getConnection(defaultDriver, defaultUrl, defaultUser, defaultPwd);
      }
      return connection;
    }
  }

  /**
   * Diese Methode dient zur Erzeugung einer neuen Datenbankverbindung auf Basis
   * der übergebenen Parameter.
   *
   * @param driver
   * @param url
   * @param user
   * @param pwd
   * @return java.sql.Connection
   * @throws Exception
   */
  private Connection getConnection(String driver, String url, String user,
                                   String pwd) throws Exception {
    Class.forName(driver);
    Connection con = DriverManager.getConnection(url, user, pwd);
    return con;
  }

  /**
   * In dieser Methode wird zunächst eine Datenbankabfrage durchgeführt und dann
   * das erhaltene Resulat in ein XML-Dokument umgewandelt.  Diese Umwandlung geschieht
   * immer nach dem gleichen Schema.
   *
   * @param con
   * @param sql
   * @return org.w3c.Document
   * @throws Exception
   */
  private Document createDocument(Connection con, String sql) throws Exception {

    // Es wird ein Statement-Objekt zum Absetzen der Anfrage benötigt.
    Statement stmt = con.createStatement();

    // Nun wird die Anfrage ausgeführt und Ergebnisse in form eines ResultSet-Objekts
    // zurückgegeben.
    ResultSet rs = stmt.executeQuery(sql);

    // Über das ResultSet kann ein ResultSetMetaData-Objekt erzeugt werden was dazu
    // dient allerlei Metainformation über das Ergebnis und die Datenbank zu erhalten,
    // wie z.B. Spalten- und Tabellennamen.
    ResultSetMetaData rsmd = rs.getMetaData();

    // Als Rückgabewert wird ein neues DocumentImpl-Objekt benötigt welches
    // das W3C-Document-Interface implementiert.  Der W3C-Standard definiert nicht
    // wie man Objekte erzeugen soll, die das Document-Interface implementieren.
    // Deswegen benutzen wir hier die Xerces-spezifischen Objekte.  Die folgende
    // Zeile müsste also ersetzt werden, sollte man sich in Zukunft für
    // einen anderen Parser entscheiden.
    Document doc = new DocumentImpl();

    // Als Root-Element wird ein Element namens ResultSet erzeugt
    Element root = doc.createElement("ResultSet");

    // Ein Kommentar soll eingefügt werden.
    Comment comment=doc.createComment("Das ResultSet Element kapselt das Resultat der SQL-Abfrage.");
    // Der Kommentar und das Root-Element muss an das Document-Objekt angehängt werden.
    doc.appendChild(comment);
    doc.appendChild(root);


    // Es wird ein Attr-Objekt erzeugt, welches ein Attribut namens 'onSql' repräsentiert
    Attr sqlAttr = doc.createAttribute("onSql");

    // Der Wert diese Attributes wird mit dem SQL belegt, das durch das
    // XML-Dokument beantworted werden soll.
    sqlAttr.setNodeValue(sql);

    // Nun muss das Attr-Objekt noch an das Root-Element angehängt werden.
    root.setAttributeNode(sqlAttr);

    // In den folgenden Schleifen wird die Anzahl der Spalten benötigt, die
    // das Ergebnis der Anfrage hat.
    int columnCount = rsmd.getColumnCount();

    while (rs.next()) {

      // Für jeden Datensatz des Ergebnisses wird ein Element namens 'RowSet' erzeugt.
      Element row = doc.createElement("RowSet");

      for (int i = 1; i < columnCount; i++) {

        // für jeden Wert in jedem der Datensätze der Ergenismenge soll ein Element
        // mit dem Namen der betreffenden Spalte erzeugt werden.
        Element column = doc.createElement(rsmd.getColumnName(i));

        Object object = rs.getObject(i);
        Text textNode = null;

        // Falls der Wert null war, wird an das Element der Text 'null' gehängt.
        if (object == null) {
          textNode = doc.createTextNode("null");
        }

        // Ansonsten wird verschiedene Metainformation als Attribut gesetzt.
        // Die hier angewandte Methode zum setzten von Attributen
        // ist wesentlich komfortabler als die oben angewandte.
        else {

          column.setAttribute("type", rsmd.getColumnTypeName(i));
          column.setAttribute("length",
                              new Integer(rsmd.getPrecision(i)).toString());
          column.setAttribute("table", rsmd.getTableName(i));
          column.setAttribute("java-type", object.getClass().getName());

          // Für den eigentlichen Wert muss nun ein Text-Knoten
          // erstellt werden.
          textNode = doc.createTextNode(object.toString());

        }
        // Der Text-Knoten muss als Unterknoten an das Column-Element angehängt werden.
        column.appendChild(textNode);
        // Das Column-Element muss wiederum an das RowSet-Element angehängt werden.
        row.appendChild(column);
      }
      // Letztendlich muss auch noch jedes entstandene RowSet-Element and
      // das Root-Element angehängt werden.
      root.appendChild(row);
    }

    // Das zusammengesetzte Dokument wird zurückgegeben.
    return doc;

  }
}