ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Don Bales

Database Access Using Lightweight Applets

by Donald Bales, author of Java Programming with Oracle JDBC
02/20/2002

Using HTTP as the communication protocol and a servlet peer for database queries, you can write a rich-content user interface applet that can dynamically interact with a database, yet remain small enough in size to perform well. In last month's article, "Dynamic Database Access from Client-side JavaScript", we looked at an architecture for utilizing the services of an applet-servlet pair to provide dynamic database access for client-side JavaScript.

There were, however, some drawbacks to this approach, the most important being the fact that this architecture leaves access to the database wide open. You can work around this issue by restricting which database objects can be accessed from the user id used to log into the database from SqlServlet, or by restricting a sub-classed version of SqlServlet to the access of a particular table; however, both of these strategies fall short of allowing you to access confidential data. Another approach is needed. We'll discuss a transparent approach shortly.

A second drawback was browser compatibility. JavaScript compatibility varies wildly from browser to browser. To work around this problem, you can simply abandon HTML and JavaScript use for your client-side GUI, and instead create a rich-content GUI using a lightweight applet. A lightweight applet is an applet designed to display a single screen. It uses HTTP as a protocol to communicate with a database through SqlServlet.

Designing an applet this way greatly reduces the size of the applet, because almost all of the classes it needs to execute already exist on the client as part of a browser's installation. All that needs to be downloaded is your custom applet and the SqlApplet classes. The typical lightweight applet is around 8K in size. Not much bigger than an HTML page embedded with enough JavaScript to produce equivalent functionality. Accordingly, I'll show you how to rewrite SqlServlet and SqlApplet so they can be readily sub-classed to create a specialized SQL servlet or lightweight applet. We'll also cover some strategies for making SqlServlet and SqlApplet secure. Then, we'll finish up with a working example.

In contrast to my previous article, we're now using an applet-servlet-database architecture, minus the HTML and JavaScript, as shown in Figure 1. In this architecture, our dynamic SQL servlet, SqlServlet, receives a SQL statement as a URL parameter, sql, executes the SQL statement, and then returns its results as tab-delimited text. SqlServlet's peer, SqlApplet, parses the tab-delimited data, making it available as a series of rows and columns.

Diagram.
Figure 1. Applet-Servlet-Database Architecture

We have two modifications to make to SqlServlet. The first is to move the dynamic query capability of the servlet to a protected method. This will allow you to sub-class the servlet in order to create a specialized query servlet. A second modification is to add security to the servlet so that only authorized use of the servlet is possible. Then we have two possible modifications for SqlApplet. The first is to remove its GUI console, and the second is to make its custom public methods protected, to prevent generic use through JavaScript. Let's start from the ground up by tackling SqlServlet first.

Dynamic Database Access from Client-Side JavaScript -- Imagine the advantages of being able to dynamically access a database from client-side JavaScript. You could dynamically query a database from JavaScript for client-side field validation, or dynamically populate a drop-down select list, to name just a few possibilities. In fact, you can do this with the help of an applet-servlet pair. This article describes the applet-servlet pair architecture and offers several sample applications.

Rewriting SqlServlet to Enable Sub-classing

You can re-code SqlServlet so that it is suitable for sub-classing by moving the code that dynamically executes a SQL statement, and returns the results as tab-delimited data, into a protected method, execute(), as I have done in Example 1. In addition, if you wish to disable generic use of SqlServlet, you can code a call to the sendError() method in the beginning of its doGet() method.

Adding Security to SqlServlet

Related Reading

Java Programming with Oracle JDBC
By Donald Bales

There are several tactics you can use to add security to SqlServlet. For example, you can have it check headers for Basic authorization, like a Web server does when security is enabled. You would then have to add functionality to SqlApplet so that it would send a properly-formatted response to the authorization header from SqlServlet. There is a good example of this in Java Servlet Programming. The problem with this method is that a Base-64-encoded userid and password are passed clear over the wire, so it's fairly easy for a malicious user to discover what userid and password are being used by using a sniffer. You could always use your own custom encoding, but there's a better approach.

You can authenticate a user with a separate login screen, and store an authentication flag in their HttpSession object. You can then check for the authentication flag before executing a SQL statement in SqlServlet. That's what I have done in the beginning of the SqlServlet's execute() method. First, I get the current session object. Next, I attempt to retrieve a String by the name of authenticated from the session object. If the authentication flag authenticated does not exist, SqlServlet returns a 503 error (forbidden) to SqlApplet. This tactic requires you to add authentication to your Web application, but prevents unauthorized access to the database through SqlServlet. It works transparently for both the generic and sub-classed use of SqlServlet.


Example 1. SqlServlet

import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SqlServlet extends HttpServlet {
  public void doGet(
   HttpServletRequest request,
   HttpServletResponse response)
   throws IOException, ServletException {
    // Moved the code to execute a SQL statement
    // to a private method: execute(...).
    // You can disable its use without being
    // sub-classed by sending a forbidden error.
    /*
    response.sendError(
     HttpServletResponse.SC_FORBIDDEN);
    return;
    */
    // Process the SQL statement
    String sql = request.getParameter("sql");
    if (sql != null) {
      execute(request, response, sql);
    }
    else {
      response.sendError(
       HttpServletResponse.SC_BAD_REQUEST);
    }
  }
  public void doPost(
   HttpServletRequest request,
   HttpServletResponse response)
   throws IOException, ServletException {
    doGet(request, response);
  }
  protected void execute(
   HttpServletRequest request,
   HttpServletResponse response,
   String sql)
   throws IOException, ServletException {
    // Check authentication.  If the session variable
    // "authenticated" does not exist, send a 401 error
    HttpSession session       = request.getSession();
    String      authenticated =
     (String)session.getAttribute("authenticated");
    if (authenticated == null) {
      response.sendError(
       HttpServletResponse.SC_FORBIDDEN);
      return;
    }
    // Normally, I'd never get a connection
    // for a servlet this way, but it's OK
    // for an example.
    // Load the JDBC driver
    try {
      Class.forName("oracle.jdbc.driver.OracleDriver");
    }
    catch (ClassNotFoundException e) {
      System.err.print(e.getMessage());
      response.sendError(
       HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
       "Unable to load class " +
       "oracle.jdbc.driver.OracleDriver");
      return;
    }
    // Get a database connection
    Connection conn = null;
    try {
      conn = DriverManager.getConnection(
       "jdbc:oracle:thin:@dssw2k01:1521:orcl",
       "scott",
       "tiger");
    }
    catch (SQLException e) {
      System.err.print(e.getMessage());
      response.sendError(
       HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
       e.getMessage());
      return;
    }
    int               cols = 0;
    int               stat = 0;
    int               rows = 0;
    ResultSet         rset = null;
    ResultSetMetaData rsmd = null;
    Statement         stmt = null;
    // This StringBuffer will hold the output until
    // we're ready to send it.
    StringBuffer      data = new StringBuffer(8192);
    try {
      // Create a Statemetn object from the
      // Connection object
      stmt = conn.createStatement();
      // Execute the SQL statement.
      // The execute() method will return
      // a true if a result set is avaiable.
      if (stmt.execute(sql)) {
        // Get the result set
        rset = stmt.getResultSet();
        // Get meta data (data about the data)
        // from the result set.
        rsmd = rset.getMetaData();
        // Get the number of columns
        cols = rsmd.getColumnCount();
        // Walk the result set
        // tab delimiting the column
        // data as you go into the
        // StringBuffer, data.
        while(rset.next()) {
          rows++;
          if (rows > 1) {
            data.append("\n");
          }
          for(int col = 1;col <= cols;col++) {
            if (col > 1) {
              data.append("\t");
            }
            data.append(rset.getString(col));
          }
        }
        // Let go of the meta data object
        rsmd = null;
        // Close and let go of the result set
        rset.close();
        rset = null;
      }
      else {
        // If there's no result set
        // then the execute() method
        // returns the number of rows
        // affected by the SQL statement.
        rows = stmt.getUpdateCount();
      }
      // Close a let go of the statement
      stmt.close();
      stmt = null;
    }
    catch (SQLException e) {
      System.out.println(
       "Can't execute query: " + sql + ".");
      System.out.println(e.getMessage());
      stat = e.getErrorCode();
    }
    finally {
      // Make sure the result set
      // and statement objects
      // are close if there is a
      // SQLException.
      if (rset != null) {
        try {
          rset.close();
        }
        catch (SQLException ignore) {
        }
      }
      if (stmt != null) {
        try {
          stmt.close();
        }
        catch (SQLException ignore) {
        }
      }
    }
    // Close the connection
    try {
      conn.close();
    }
    catch (SQLException ignore) {
    }
    // Get the output stream
    PrintWriter out = response.getWriter();
    // Set the content type
    response.setContentType("text/plain");
    // Set the "custom" headers:
    // Sql-Stat returns any SQLException
    // error code.
    response.setHeader(
     "Sql-Stat",Integer.toString(stat));
    // Sql-Rows returns the number of rows
    response.setHeader(
     "Sql-Rows",Integer.toString(rows));
    // Sql-Cols returns the number of columns
    response.setHeader(
     "Sql-Cols",Integer.toString(cols));
    // Send the data
    out.print(data.toString());
    out.flush();
  }
}

Rewriting SqlApplet to Enable Sub-classing

Our dynamic query applet, SqlApplet, has two possible modifications. The first is to remove the console code from it. This is the code that existed in its init() method, which displayed the number of columns and rows, and the error code after each call to execute(). In Example 2, I've removed the code from init() and instead added a formatted call to System.out.println() to display that data in the Java console of the browser.

Adding Security to SqlApplet?

Download example files for this article here.

In order to prevent JavaScript access to a database through SqlApplet, you can make its methods protected instead of public. If they are protected, they can still be called by a class that extends SqlApplet, but they will no longer be visible to client-side JavaScript. For the sake of compatibility with the previous use of SqlApplet, I have not made this change to Example 2.


Example 2. SqlApplet

import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
public class SqlApplet extends Applet {
 int        cols      = 0;
 int        row       = 0;
 int        rows      = 0;
 int        stat      = 0;
 String[][] tokens    = new String[1][1];
 private String nvl(String value, String substitute) {
  return (value != null) ? value : substitute;
 }
 // You can disable SqlApplet's use without
 // being sub-classed by making the following
 // methods protected instead of public.
 public boolean next() {
  row++;
  return (row < rows) ? true : false;
 }
 public String getString(int col) {
  return (row < rows) ? tokens[row][col - 1] : "";
 }
 public int getColumnCount() {
  return cols;
 }
 public int getRowCount() {
  return rows;
 }
 public int execute(String sql) {
  BufferedReader br   = null;
  InputStream    in   = null;
  URLConnection  conn = null;
  URL            url  = null;
  try {
   String servlet = nvl(getParameter("servlet"),
    "http://localhost:8080/root/servlet/SqlServlet");
   url = new URL(servlet + "?sql=" + URLEncoder.encode(sql));
   conn = url.openConnection();
   conn.setUseCaches(false);
   in   = conn.getInputStream();
   stat = conn.getHeaderFieldInt("Sql-Stat", -1);
   rows = conn.getHeaderFieldInt("Sql-Rows", -1);
   cols = conn.getHeaderFieldInt("Sql-Cols", -1);
   // I've eliminated the console code in the
   // init() method, so I've added this code
   // to display the execute() method's status
   // in the Browser's Java console.
   System.out.println(
    "Columns: " + cols +
    ", Rows: " + rows +
    ", Status: " + stat);
   br = new BufferedReader(new InputStreamReader(in));
   int    beginIndex = 0;
   int    index      = 0;
   int    col        = 0;
   String line       = null;
   tokens            = new String[rows][cols];
   row = 0;
   while ((line = br.readLine()) != null) {
    beginIndex = 0;
    col = 0;
    while ((index = line.indexOf('\t', beginIndex)) != -1) {
     tokens[row][col] = line.substring(beginIndex, index);
     beginIndex = index + 1;
     col++;
    }
    if (beginIndex < line.length()) {
     tokens[row][col] = line.substring(beginIndex);
    }
    row++;
   }
   row = -1;
   br.close();
   br = null;
   in.close();
   in = null;
  }
  catch (IOException e) {
   System.out.println("Can't execute servlet.");
   System.out.println(conn.getHeaderField(0));
   System.out.println(e.getMessage());
  }
  finally {
   if (br != null)
    try { br.close(); } catch (IOException ignore) {}
   if (in != null)
    try { in.close(); } catch (IOException ignore) {}
  }
  return stat;
 }
}

Creating a Specialized SQL Servlet

In order to demonstrate sub-classing of SqlServlet, I've created a specialized servlet, OrganizationDDLBServlet (Example 3), that expects part of a WHERE clause to be passed instead of an entire SQL statement. This prevents someone from using the servlet for a task other than that which it was intended.

OrganizationDDLBServlet extends SqlServlet, which gives it access to SqlServlet's protected method execute(). The servlet's doGet() method dynamically builds a SQL statement using the WHERE clause passed as a URL get parameter, and then calls its execute() method.


Example 3. OrganizationDDLBServlet

import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class OrganizationDDLBServlet
 extends SqlServlet {
  public void doGet(
   HttpServletRequest request,
   HttpServletResponse response)
   throws IOException, ServletException {
    String sql = request.getParameter("sql");
    if (sql == null) {
      response.sendError(
       HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
    String select =
     "select organization_id, " +
     "       name " +
     "from   ORGANIZATION " +
     "where  " + sql + " " +
     "order by name";
    execute(request, response, select);
  }
}

Creating a Lightweight Applet

Now that we've made the necessary modifications to SqlServlet and SqlApplet, we have a working foundation we can use to create a lightweight applet. Creating a lightweight applet is now a simple matter of sub-classing SqlApplet, adding the necessary GUI code, and deciding how we are going to initialize and save the applet's values.

There are two ways you can set the applet's initial field values. First, you can pass each of the values as an applet parameter, or you can pass a primary key value as a parameter and then query the database for the remaining values. In Example 4, OrganizationDDLBApplet, I use the second tactic. Here, I've have created a three-level, hierarchically-related drop-down list box dialog. For this applet, it's not necessary for me to pass a primary key as a parameter. Instead, the applet queries the database for all of the high-level organizations when its init() method is executed. Let's take a detailed look.

OrganizationDDLBApplet extends SqlApplet, which gives it access to SqlApplet's abilities to execute a SQL statement against a database. It also implements the ActionListener and ItemListener interfaces, so it can detect when the OK button is pressed, or when an item in one of the drop-down list boxes has been selected. At the top of the program I declare a handful of variables. The Button, ok, and Choice array, choice, contains GUI components for the OK button and three drop-down list boxes. The Vector array, id, keeps track of the primary keys for the values in each Choice. When an item is selected from a Choice, the same index value can be used to find the appropriate primary key for the selected organization. The Frame, frame, is used to set the cursor to an hourglass while the database is being queried. The int, levels, determines how many Choice lists the applet will display. The String, parent_organization_id, will hold the last organization ID selected from one of the drop-down list boxes (DDLB).

Briefly, in the init() method, the GUI is initialized. I call a private method, getFrame(), to walk the component hierarchy until I get the address of the browser's frame. I do this so I can later call the setCursor() method on the frame. Next, I get the background color for the applet from an applet tag parameter. You can make similar calls to get default or initial values for the applet. I do this sometimes when I generate the HTML that displays the applet dynamically from a JSP or servlet, to pass a primary key value for data to be retrieved from the database, or to set all initial values. Next, being lazy, I set the layout manager to null to enable fixed positioning of the applet's components. (Have you ever tried to get multiple drop down list boxes to line up with one of the standard layout managers?) Last, I add the components to the GUI.

Screen shot.
Figure 2. OrganizationDDLBApplet

In the start() method, I make a call to a specialized SqlServlet servlet, OrganizationDDLBServlet (Example 3), in order to initialize the top-level DDLB with all top-level organizations in the database. At this point, the applet displays a screen like the one shown in Figure 2. Each time an item is selected in one of the DDLBs, another call is made to the database via OrganizationDDLBServlet. When the OK button is clicked, the applet calls SnoopServlet, passing it the selected organization ID as a URL get parameter.

Once again, you have two ways you can save the applet's modified data. First, you can pass the items as URL get parameters, or you can execute INSERT, UPDATE, or DELETE statements against the database.


Example 4. OrganizationDDLBApplet

/*
 OrganizationDDLBApplet
 by Donald Bales
 on 2/2/2002
 */
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.*;
public class OrganizationDDLBApplet
 extends     SqlApplet
 implements  ActionListener,
             ItemListener {
  Button   ok;
  Choice[] choice;
  Frame    frame;
  int      levels = 3;
  String   parent_organization_id;
  Thread   loader;
  Vector[] id;
  private Frame getFrame(Component component) {
    Component frame = component;
    while (!(frame instanceof Frame)) {
      frame = frame.getParent();
    }
    return (Frame)frame;
  }
  public void init() {
    int col1Width  = 56;
    int col2Width  = col1Width * 8;
    int col1X      = 0;
    int col2X      = col1Width;
    int itemHeight = 21;
    int rowHeight  = 24;
    int y          = 0;
    choice         = new Choice[levels];
    id             = new Vector[levels];
    frame          = getFrame(this);
    Color  bgColor = Color.white;
    String BGCOLOR = getParameter("BGCOLOR");
    if (BGCOLOR != null) {
     try {
      bgColor = Color.decode(BGCOLOR);
     }
     catch (NumberFormatException e) {
      bgColor = Color.white;
     }
    }
    setBackground(bgColor);
    Font arialBold11 =
     new Font("Arial", Font.BOLD, 11);
    Font courier11   =
     new Font("Courier", Font.PLAIN, 11);
    setLayout(null);
    Label label = null;
    for (int i=0;i < levels;i++) {
      label = new Label("Level " +
       Integer.toString(i + 1) + ":", Label.RIGHT);
      label.setFont(arialBold11);
      label.setSize(
       new Dimension(col1Width, itemHeight));
      label.setLocation(col1X, y);
      add(label);
      id[i]     = new Vector();
      choice[i] = new Choice();
      choice[i].setFont(courier11);
      choice[i].setSize(
       new Dimension(col2Width, itemHeight));
      choice[i].setLocation(col2X, y);
      add(choice[i]);
      empty(choice[i],id[i]);
      choice[i].addItemListener(this);
      y += rowHeight;
    }
    ok = new Button(" OK ");
    ok.setFont(arialBold11);
    ok.setSize(
     new Dimension(col1Width, itemHeight));
    ok.setLocation(col1X, y);
    ok.addActionListener(this);
    add(ok);
  }
  public void start() {
    load(null, choice[0], id[0]);
  }
  public void actionPerformed(ActionEvent ignore) {
    AppletContext ac = getAppletContext();
    String  codeBase = getCodeBase().toString();
    StringBuffer url = new StringBuffer();
    if (!parent_organization_id.equals("")) {
      url.append(codeBase.substring(0,
       codeBase.length() - "applet/".length()));
      url.append("servlet/SnoopServlet");
      url.append("?parent_organization_id=");
      url.append(parent_organization_id);
      try {
        System.out.println("url=" + url);
        ac.showDocument(new URL(url.toString()));
      }
      catch (MalformedURLException e) {
      }
    }
    else {
      ac.showStatus(
       "Please select an organization");
    }
  }
  public void itemStateChanged(ItemEvent ie) {
    Object currentChoice   = ie.getSource();
    int currentIndex       = 9;
    parent_organization_id = null;
    if (ie.getStateChange() == ItemEvent.SELECTED) {
      for (int i=0;i < choice.length;i++) {
        if (choice[i] == currentChoice) {
          currentIndex = i;
          int selectedIndex =
           choice[i].getSelectedIndex();
          parent_organization_id =
           (String)(id[i].elementAt(selectedIndex));
          System.out.println(parent_organization_id);
        }
        if (i > currentIndex) {
          empty(choice[i], id[i]);
          System.out.println("emptying " + i);
        }
      }
      if ((currentIndex + 1) < levels &&
          !(parent_organization_id.equals(""))) {
        load(
         parent_organization_id,
         choice[currentIndex + 1],
         id[currentIndex + 1]);
      }
    }
  }
  public void empty(
   Choice emptyChoice,
   Vector emptyId) {
    emptyChoice.removeAll();
    //                   0        1         2         3
             4         5         6
    //                  
    123456789012345678901234567890123456789012345678901234567890
    emptyChoice.addItem("                                       
                         ");
    emptyId.removeAllElements();
    emptyId.addElement(new String(""));
  }
  public void load(
   String parent_organization_id,
   Choice loadChoice,
   Vector loadId) {
    Cursor cursor = frame.getCursor();
    ((Component)frame).setCursor(
     new Cursor(Cursor.WAIT_CURSOR));
    empty(loadChoice, loadId);
    String sql =
     (parent_organization_id != null) ?
    "parent_organization_id = " +
     parent_organization_id :
    "parent_organization_id is null";
    if (execute(sql) == 0) {
      while (next()) {
        System.out.println(
         "organization_id=" + getString(1));
        loadId.addElement(getString(1));
        System.out.println("name=" + getString(2));
        loadChoice.addItem(getString(2));
      }
    }
    ((Component)frame).setCursor(cursor);
  }
}

Example 5 is an HTML file you can use to display OrganizationDDLBApplet. Example 6, Login, is a servlet you can use to set the authenticated flag in the current session to enable the use of SqlServlet.


Example 5. OrganizationDDLBApplet.html

<html>
<head>
<title>Organization</title>
</head>
<body BGCOLOR="#BFBFBF">
<applet
  code="OrganizationDDLBApplet.class"
  codebase="http://dssw2k01:8080/learn/applet/"
  height="550"
  width="750" >
<!-- Tell the applet where its peer is located -->
<param
  name="servlet"
  value="http://dssw2k01:8080/learn/servlet/OrganizationDDLBServlet" />
<param
  name="BGCOLOR"
  value="#BFBFBF" />
</applet>
</body>
</html>

Example 6. Login

import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class Login extends HttpServlet {
  public void doGet(
   HttpServletRequest request,
   HttpServletResponse response)
   throws IOException, ServletException {
    // Get the current session or create a new one
    HttpSession session       = request.getSession();
    // Get the authentication flag
    String      authenticated =
     (String)session.getAttribute("authenticated");
    // If it's valid then remove it
    if (authenticated != null) {
      session.removeAttribute("authenticated");
    }
    // Get the userid and password
    String      userid   =
     request.getParameter("userid");
    String      password =
     request.getParameter("password");
    PrintWriter out      =
     response.getWriter();
    response.setContentType("text/html");
    // Display a login screen if needed
    if (userid == null || password == null) {
     out.println("<html>");
     out.println("<head>");
     out.println("<title>Login</title>");
     out.println("<body>");
     out.println("<form action\"../servlet/Login\" 
                  method=\"post\">");
     out.println("Please enter your useid and password 
                  then submit to log-in:<br/>");
     out.println("Userid: <input name=\"userid\" 
                  size=\"30\" type=\"text\"><br/>");
     out.println("Password: <input name=\"password\" 
                  size=\"30\" type=\"password\"><br/>");
     out.println("<input type=\"submit\"");
     out.println("</form>");
     out.println("</body>");
     out.println("</html>");
    }
    else {
     // Add authetication here!!!
     session.setAttribute("authenticated", "");
     out.println("<html>");
     out.println("<head>");
     out.println("<title>Login</title>");
     out.println("<body>");
     out.println("<h3>You have logged-in successfully.</h3>");
     out.println("</body>");
     out.println("</html>");
    }
  }
  public void doPost(
   HttpServletRequest request,
   HttpServletResponse response)
   throws IOException, ServletException {
    doGet(request, response);
  }
}

Conclusion

Using lightweight applet technology, you can bring the full weight of Java's GUI capability, and the advantages of client-side dynamic database access, to your Web applications. Because the technology employs HTTP to communicate with a database via SqlServlet, lightweight applets perform well yet remain small enough to have acceptable download times.

Java Programming with Oracle JDBC

Related Reading

Java Programming with Oracle JDBC
By Donald Bales

You can get a copy of the source code for this article here. For more information on applets, look at Learning Java. For HTTP communications read Java I/O. For servlets, check out the totally excellent Java Servlet Programming. And for more information on Oracle's implementation of JDBC, check out my book, Java Programming with Oracle JDBC.

Donald Bales is a Systems Architect, Computer Applications Consultant, and Business Analyst specializing in the analysis, design, and programming of web-based, client-server, and distributed applications, the internationalization of existing applications, systems integration, and data warehousing.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.