[Note: The source code for this article is here. Ed.]
Introduction
Unlike Java Applets, which after all the hype proved to be just toys in
the corporate world, Java Servlets are rapidly capturing the attention of
Information Technology on many companies.
But what are Java Servlets? They are server-side Java applications,
just like Applets are client-side (browser) applications. Servlets work in
collaboration with some kind of server to provide a service to client
applications.
We'll focus on a special kind of servlet, the HttpServlet. It's a
Servlet specialized to work with a Web Server. You can think about a Java
Servlet as an easier replacement for a Java CGI application. The standard
Java Extension API for Servlets provides Java classes the ability to
perform the same operations that the standard CGI interface permits any
external application to do:
- Reply to a Web Browser request for an URL
- Get the request details, such as the client IP address or user-agent
- Get form data send by the browser
- Get and set cookies
- Send an HTML page or other kind of document (such as an GIF image)
as the reply to the browser
- Send HTTP headers to the browsers, causing it to redirect to another
URL or not store the reply on its cache
So far I could just write a JAVA CGI application to do the work. But the
Java VM startup time is too long to provide a quick reply to browser
requests. Besides, I must write a CMD wrapper so my web server can run my
Java application as a CGI application.
The Java Servlet API supposes the web server contains a Java VM so it
won't take the time to start a new VM for each browser request. But better
than that, the Servlet API provides means by which your application can
preserve state between browser requests. (Remember, the CGI interface is
stateless!) So, time-consuming operations like connecting to a data base
can be performed just one time during Servlet initialization instead of
for every request.
Programming Java Servlets is also much easier than programming Java or
C/C++ CGI applications. This is because the Servlet API provides methods
for easy decoding of form data, URL information and so on.
Configuring Java Servlet support
Let's use Lotus Domino GoWebserver (it's just the old known IBM
Internet Connection Server, now sold using the Lotus brand). I downloaded
release 4.6.2.2 from IBM Visual Age Developer's Domain:
http://www7.software.ibm.com/vad.nsf/
Lotus GoWebserver is still free, only the secure (SSL) components
expire after 60 days and need registration. Servlet support is available
since ICS release 4.2.1, but you should check the supported release of
both the JDK and the Servlet API if you do not want to upgrade your web
server. I am using JDK 1.1.6 and GoWebserver 4.6.2.2 supports the Servlet
API version 1.0.
The Apache OS/2 web server also supports Servlet, through the JServ
module. I am not aware of other OS/2 web servers with Servlet support, if
you know about one please mail me as lozano@blnet.com. Few other commercial
web servers have native Servlet support (two exceptions are O'Reilly
WebSite Pro 2.2 and the Novonyx Fastrack Server for Netware) but Sun
provides on its own JSDK (Java Servlet Development Kit) support for
Netscape Fasttrack and Enterprise servers, besides Apache. You can also
buy from third-parties, such as IBM WebSphere, Servlet support for
Microsoft IIS and other servers. The Servlet Domain is a nice source of
information on Servlet support, the URL is:
http://www.servletcentral.com/
I installed GoWebserver in E:\WWW, and put all related files inside this
directory, so I have E:\WWW\HTML, E:\WWW\CGI-BIN and E:\WWW\SERVLETS.
Before being able to run servlets on Lotus GoWebserver you need to edit
the file \MPTN\ETC\servlet.cnf and change the following lines (please
replace "e:\WWW" with the directory you chose during installation):
ServletLog e:\WWW\LOGS\servlet-log
JavaPath e:\java11\bin
JavaLibPath e:\java11\dll
JavaClassPath e:\java11\lib\classes.zip;e:\www\cgi-bin\icsclass.zip;
e:\www\servlets\public
Restart GoWebserver and check its error log to see if everything is ok.
Now let's run one of the sample Servlets provided with GoWebserver to see
if we really have Servlet support operational.
First, enter the directory E:\WWW\SERVLETS\PUBLIC. Then try the
following command:
[E:\www\servlets\public] javac ReqInfoServlet.java
You should have a clean compile and the file ReqInfoServlet.class should
be generated. If something goes wrong, check the release number of your
JDK (it must be at least 1.1.4 for GoWebserver 4.6.2.2), check your
CLASSPATH in CONFIG.SYS (it should have at least the same contents of the
JavaClassPath in servnet.cnf) and check if you put in \MPTN\ETC\servet.cnf
the correct paths for your installation of JDK 1.1.x (tip: it is NOT
"\javaOS2").
Let's test the compiled servlet. Open your favorite web browser and try
the following URL:
http://localhost/servlet/ReqInfoServlet
(You did configure "localhost" in your HOSTS database, didn't you?) The
result should be a page like this:
Request protocol is HTTP/1.0
Request scheme is http
Remote host is null
Remote address is 127.0.0.1
Receiving server is localhost on port number 80
Header data is:
name = CONNECTION; value = Keep-Alive
name = ACCEPT; value = image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
name = USER-AGENT; value = Mozilla/3.0 (OS/2; I)
name = AUTHORIZATION; value = Basic YWRtaW46YWRtaW4=
name = HOST; value = localhost
Request parameters are:
If it didn't work, recheck all previous steps of installing JDK 1.1.6,
LotusGoWebserver 4.6.2.2 and editing servlet.cnf.
A simple Servlet
Now that we have the ability to run servlets, let's see how to build
one.
At first, we need to import the classed from the Servlet API:
import javax.servlet.*;
import javax.servlet.http.*;
As you will most of the time send text (or HTML text) back to the browser,
we need the java.io core package:
import java.io.*;
A servlet is just a Java class that inherits from one of the Servlet
classes let's use the specialized HttpServlet:
public class Sample1 extends HttpServlet
{
Our servlet should implement at least one method: service(). This method
is called for every request the web server receive from the browser asking
for the servlet URL:
public void service(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
The service() method have to be declared as above. Its parameters provide
access to the browser request and allow us to build our reply to the
request. The first thing we have to do is to is to tell what kind of
information we are about to send to the browser:
res.setContentType("text/html");
So we are about to send ASCII text but the browser should interpret HTML
tags inside it. To actually send the HTML text to the browser, we need an
output stream, which we get from the Response argument:
PrintWriter pw = new PrintWriter(res.getOutputStream());
And now just dump plain HTML on the stream:
pw.println("<HTML>");
pw.println("<HEAD>");
pw.println("<TITLE>Sample Servlet #1</TITLE>");
pw.println("</HEAD>");
pw.println("<BODY>");
pw.println("<H1>Sample Servlet #1<HR></H1>");
pw.println("<P>Hi There!");
pw.println("</BODY>");
pw.println("</HTML>");
Now let's finish with the output stream, and we are done!
pw.flush();
pw.close();
}
}
Here is our complete servlet example:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
public class Sample1 extends HttpServlet
{
public void service(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
pw.println("<HTML>");
pw.println("<HEAD>");
pw.println("<TITLE>Sample Servlet #1</TITLE>");
pw.println("</HEAD>");
pw.println("<BODY>");
pw.println("<H1>Sample Servlet #1<HR></H1>");
pw.println("<P>Hi There!");
pw.println("</BODY>");
pw.println("</HTML>");
pw.flush();
pw.close();
}
}
Save it as Sample1.java and compile with the command:
[E:\fernando] javac Sample1.java
Then copy the generated .class file to the servlets directory of your web
server:
[E:\fernando] copy Sample1.class \www\servlets\public
Now you can run the servlet using the following URL:
http://localhost/servlet/Sample1
And the result should be a page that resembles the following:
Sample Servlet #1
----------------------------------
Hi There!
If something goes wrong and you have to recompile your servlet, be sure to
restart also Lotus GoWebserver. Each servlet is loaded only once by the
web server, so it won't get the new .class file unless your stop the web
server and restart it.
More about the Servlet API
Although most servlet tutorials will instruct you to use the service()
method, just as we have already done, it is not advisable to do so. The
service() method is called for any request received for the servlet URL.
This request could be a GET, a POST, a PUT, a DELETE and others can be
defined by newer HTTP standards.
The HttpServlet class has request-specific methods that are the
preferable way to code our servlets. The main ones are doGet() and
doPost(). The GET request is sent by the browser when it just wants to
load a page, and the POST request is sent by the browser when there is too
much form data to be kept as part of the URL. Actually, it's the HTML
coding of the form that instructs the browser to use GET or POST when the
Submit button is pressed, and I recommend you always use POST for HTML
forms.
Let's rewrite out example to use the doGet() method instead of
service():
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
public class Sample2 extends HttpServlet
{
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
pw.println("<HTML>");
pw.println("<HEAD>");
pw.println("<TITLE>Sample Servlet #2</TITLE>");
pw.println("</HEAD>");
pw.println("<BODY>");
pw.println("<H1>Sample Servlet #2<HR></H1>");
pw.println("<P>Hi There!");
pw.println("</BODY>");
pw.println("</HTML>");
pw.flush();
pw.close();
}
}
Save it as Sample2.java, compile it and copy Sample2.class to the servlet
public directory of your web server. Load it with the URL:
http://localhost/servlet/Sample2
And see it's result. Note that we just changes "service" for "doGet" and
changed "Sample 1" to "Sample 2". Very easy!
But before we can write an useful servlet, we need to know how to get
form data. Create the following simple form, which has just an text entry
field and a submit button:
<HTML>
<HEAD>
<TITLE>Form for Sample Servlet #3</TITLE>
</HEAD>
<BODY>
<H1>Form for Sample Servlet #3<HR></H1>
</BODY>
<FORM method="post" action="/servlet/Sample3">
Please enter your name:
<BR>
<INPUT type="text" name="name" size=30>
<P>
<INPUT type="submit" value="Ok"><HR>
</FORM>
</HTML>
A servlet can obtain form data using the getParameter() method of the
HttpRequest class. You refer to each form element value by using it's name
attribute. As our text field has name="name", we can call getParameter as:
String name = req.getParameter ("name");
Here is the simple Sample3.java servlet that receives the form data and
displays a greeting message:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
public class Sample3 extends HttpServlet
{
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
pw.println("<HTML>");
pw.println("<HEAD>");
pw.println("<TITLE>Sample Servlet #3</TITLE>");
pw.println("</HEAD>");
pw.println("<BODY>");
pw.println("<H1>Sample Servlet #3<HR></H1>");
pw.println("<P>Hi, " + req.getParameter("name") + "!");
pw.println("<BR>Nice to meet you!");
pw.println("</BODY>");
pw.println("</HTML>");
pw.flush();
pw.close();
}
}
Have you seen how easy is to work with form data? So now let's do more
interesting things, using database-driven servlets!
Getting data from a database
As some readers may be suspecting, I'll use mSQL as the database
back-end for the rest of this article. I could use any other database:
Oracle, Sybase, Solid, DB2 and others have OS/2 versions and JDBC drivers.
I could also used other web servers, but GoWebserver + mSQL gives to me
and you the following benefits:
- Both are FREE!
- Small download, even 28.8K modem users can get the two packages
and start working
- Small footprint, no one will need Pentium 266 + 64Mb RAM to follow
my examples.
You have to look at my two previous articles, "mSQL Revisited" and
"Accessing databases using Java and JDBC" to get the background I need and
to know how to build the work environment for the rest of this tutorial.
Instead of using mSQL 2.0.1B by Dirk Ohme, you may download and install
the new mSQL port for OS/2, release 2.0.4.1 Beta3 by Yuri Dario. It won't
have the easy install script (just instructions in the file README.EMX) but
although being labeled a beta and a "developer's release", it is already
more stable than Dirk's port. Visit The mSQL PC Home Page at
http://www.blnet.com/msqlpc
To download the new mSQL 2.0.4.1 and to know about new developments.
Besides all steps necessary to compile and run Java applications using
the mSQL JDBC driver, you need to change the CLASSPATH the servlet gets
when started from the web server. Edit \MPTN\ETC\servlet.cnf as follows:
JavaClassPath e:\java11\jdbc\msql-jdbc_1.0b3\imaginary.zip;
e:\java11\lib\classes.zip;
e:\www\cgi-bin\icsclass.zip;e:\www\servlets\public
And don't forget to restart your web server.
If you can already run the previous servlets and you can also run the
samples from the JDBC article, we can start with a simple servlet that
just lists all entries from our address book. Here is the source code:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.URL;
import java.sql.*;
public class AddrBook extends HttpServlet
{
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
// we will send a HTML page
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
// start the page and the included table
pw.println("<HTML>");
pw.println("<HEAD>");
pw.println("<TITLE>Address Book Servlet</TITLE>");
pw.println("</HEAD>");
pw.println("<BODY>");
pw.println("<H1>Address Book<HR></H1>");
pw.println("<TABLE border align=\"center\">");
pw.println("<TR>");
pw.println("<TH>Name");
pw.println("<TH>Company");
pw.println("<TH>Title");
pw.println("<TH>E-mail");
try {
// connect to mSQL database pim
Class.forName ("com.imaginary.sql.msql.MsqlDriver");
String url = "jdbc:msql://localhost:1114/pim";
Connection con = DriverManager.getConnection (url, "borg", "");
// execute the query
Statement stmt = con.createStatement ();
ResultSet cur = stmt.executeQuery (
"SELECT company, first_name, last_name, title, e_mail "
+ "FROM addr_book "
+ "ORDER BY last_name, first_name"
);
// list the returned rows
while (cur.next ()) {
pw.println ("<TR>");
pw.println ("<TD>" + cur.getString ("last_name")
+ ", " + cur.getString ("first_name"));
pw.println ("<TD>" + cur.getString ("company"));
pw.println ("<TD>" + cur.getString ("title"));
pw.println ("<TD>" + cur.getString ("e_mail"));
}
// clean-up
stmt.close ();
con.close ();
}
catch (Exception e) {
pw.println("<TR><TD colspan=4>");
pw.println (e.getMessage ());
// this one will go to the web server error log
e.printStackTrace ();
pw.println("</TABLE>");
pw.println("</BODY>");
pw.println("</HTML>");
pw.flush();
pw.close();
}
// finish the HTML page
pw.println("</TABLE>");
pw.println("</BODY>");
pw.println("</HTML>");
pw.flush();
pw.close();
}
}
If you have deleted the PIM database we've used in the previous articles,
the source code of this article, available on EDM/2 web site, has a SQL
script that creates the table, sequence, index and populate then with some
test data.
You can see that this application is very simple: I basically got the
code inside the main() method of the PimSearch app presented in the June
1998 issue and put inside the servlet doGet() method, changing "System.out"
by "pw". That's all: a complete servlet that lists records from an mSQL
database.
But I won't recommend coding a database-driven servlet the way this
example shows. It has too much code in just one method, and has redundant
code (see how we finish the page two times). Worse yet, imagine your
complete address book, with dozens or even hundred of names, being loaded
by your web browser through a 28.8K modem. So, let's rewrite out app to be
better structured and also to be better adapted to the Internet
environment.
The natural structure for an address book is to show only the names
with a given starting letter. So here is the revised AddrBook servlet:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.URL;
import java.sql.*;
public class AddrBook2 extends HttpServlet
{
public void printRowOfLetters(PrintWriter pw, char letter,
char start, char end)
{
pw.println("<TT>");
char l = start;
while (l <= end )
{
if (l == letter) {
pw.print(" " + letter + " ");
}
else {
pw.print(
" <A href=\"/servlet/AddrBook2?letter="
+ l + "\">" + l + "</A> "
);
}
++l;
}
pw.println("</TT>");
}
public void printLinks(PrintWriter pw, char letter)
{
pw.println("<P><BIG><B>");
printRowOfLetters(pw, letter, 'A', 'M');
pw.println("<BR>");
printRowOfLetters(pw, letter, 'N', 'Z');
pw.println("</B></BIG>");
}
public void printHeader (PrintWriter pw, String letter)
{
pw.println("<HTML>");
pw.println("<HEAD>");
pw.println("<TITLE>Address Book Servlet</TITLE>");
pw.println("</HEAD>");
pw.println("<BODY>");
pw.println("<H1>Address Book<HR></H1>");
printLinks(pw, letter.charAt(0));
pw.println("<P>");
pw.println("<TABLE border align=\"center\">");
pw.println("<TR>");
pw.println("<TH>Name");
pw.println("<TH>Company");
pw.println("<TH>Title");
pw.println("<TH>E-mail");
}
public void printFooter (PrintWriter pw)
{
pw.println("</TABLE>");
pw.println("</BODY>");
pw.println("</HTML>");
pw.flush();
pw.close();
}
public void printRows (PrintWriter pw, ResultSet cur)
throws java.sql.SQLException
{
boolean empty = true;
while (cur.next ()) {
empty = false;
pw.println ("<TR>");
pw.println ("<TD>" + cur.getString ("last_name")
+ ", " + cur.getString ("first_name"));
pw.println ("<TD>" + cur.getString ("company"));
pw.println ("<TD>" + cur.getString ("title"));
pw.println ("<TD>" + cur.getString ("e_mail"));
}
if (empty) {
pw.println ("<TR><TD colspan=4>No records found");
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
String letter = req.getParameter("letter");
printHeader(pw, letter);
try {
// connect to mSQL database pim
Class.forName ("com.imaginary.sql.msql.MsqlDriver");
String url = "jdbc:msql://localhost:1114/pim";
Connection con = DriverManager.getConnection (url, "borg", "");
// execute the query
Statement stmt = con.createStatement ();
ResultSet cur = stmt.executeQuery (
"SELECT company, first_name, last_name, title, e_mail "
+ "FROM addr_book "
+ "WHERE last_name LIKE '" + letter + "%' "
+ "ORDER BY last_name, first_name"
);
printRows (pw, cur);
// clean-up
stmt.close ();
con.close ();
}
catch (Exception e) {
pw.println("<TR><TD colspan=4>");
pw.println (e.getMessage ());
// this one will go to the web server error log
e.printStackTrace ();
printFooter(pw);
}
printFooter(pw);
}
}
When building servlets, I like to structure the code mirroring the
structure of the generated web page. This case, our page has a header, a
footer and a table displaying selected rows from the database, so we have
the methods printHeader(), printRows() and printFooter().
The header also contains a lot of links that just provide a mean to
switch to another letter, and this set of links is generated by
printLinks(), which is called from inside printHeader(). It could be a
nice idea to use a set of nice bitmaps, each one representing a notebook
tab, just like Lotus Organizer. I'm sure the reader will have no trouble
to adapt my code to this design.
Now I feel compelled to confess I am not a proficient Java programmer.
If anyone knows a clearer and easier way to write the code, I'll be glad
do see it and share with other EDM2 readers.
Look how we pass parameters to the servlet using the URL and the GET
method. To use this servlet, we need to specify an URL like:
http://localhost/servlet/AddrBook2?letter=A
And when you select another letter, a similar URL will be generated by
the form. This URL is looks like the one generated by search engines, for
example AltaVista, and the fact that the parameters are inside the URL
makes it easy to link a set of related servlets, each one generating a
different page that is part of a bigger application.
Object-Oriented Programming for the Web
We could just go on and write more methods and more Servlets until we
get the complete Address Book Web application working. But this way we'd
not be taking advantage of Java features and would not know how they could
be used for real-life projects.
Unfortunately, most programmers in IT departments work this way. They
don't use "object-oriented programming", they just do "programing using
objects". I do not pretend to teach all the dos and don'ts of
object-oriented programming, but I think I can give you useful tips and
techniques.
The first way to take advantage of OO is to build a super-class for
all our Address Book servlets. Why? All then will share some common visual
elements: a title and a navigation bar. Real servlets would have the
company logo, copyright messages, webmaster e-mail and the like. We can put
all these elements in a common superclass from which all servlets inherits
the standard page layout. So, let's create AddrBookServlet class.
We need a form to insert or edit Address Book entries, and a servlet to
actually make the changes to the database (when the form is submitted).
And to finish our application, we need a way to remove an entry. This
could be done by the same servlet that shows the entries list, as the user
will have the immediate visual feedback of the entry disappearing.
Why don't we do the same to the form? Because an entry form can have
many validation rules, and nothing guarantees that the inserted (or
edited) entry will stay on the current letter.
Then we have three classes to derive from AddrBookServlet: AddrBook3
(the listing class), AddrBookForm and AddrBookEntry (the one that does
insert or update).
This is the external, user-interface structure of our application, but
a nice OO design will separate data from application logic. This way we
design classes for the data entities we use. The only one is an
AddressBook entry, which we call Entry. If an Entry had any processing on
its own, we would create an abstract type (class) Entry and another class
to fetch and store the Entry on the database, but as an Entry has only its
data fields, we'll use this class to interface to the database for only
one entry (inserting, updating and deleting).
Limit these data classes to the real business functions of your
application. This way, if someday I write a traditional GUI Address Book
application, or if I create another application (what about a complete
contact management?) I'll be able to reuse the same data classes.
We do not just manipulate one isolated Entry. We need to manipulate
sets of entries. How would we get all the entries starting with letter
'E'? And if we implemented a view by company, someone has to filter and
sort the entries. My preferred approach is to define one or more collection
classes to do these tasks, so I have EntryCollection class.
But the data needs to be presented to the user and somehow the data the
user types must get into the data classes. So we need UI classes specific
for web. I defined the HttpEntry class with methods to render an Entry in
HTML as a form or table row, and with the ability to initialize itself
from HTML form data.
Note that we have three layers of classes in this design:
application-specific classes, data entity classes and data visualization
classes. A layered approach is at the heart of all maintainable and
expandable software, and you can note that my layers resembles the CVM
(Controller/View/Model) design from Smalltalk and nice OO methodologies.
Have you ever wondered that the same principles worked outside the GUI
world?
Let's summarize our Address Book Web application design:
Data Entry EntryCollection
|
|
View HttpEntry
Application AddrBookServlet
|
+---------------+--------------+
| | |
AddrBook3 AddrBookForm AddrBookEntry
This diagram shows the specialization (inheritance) relationship of the
classes. We could build diagram showing other aspects of our design, like
the "contains" relationships and the "use" relationships. The bigger your
application the more you benefit from these additional views of your design.
Design and implementation considerations
I agree that, for this simple application, such an object-oriented
design may be overkill. Most programmers like to just sit and type the
code (which is a bad idea anyway) but they may be much more comfortable
using a traditional procedural design. I won't argument that my design is
better than any other, but as we are talking about Java, an OO programming
language, I felt compelled to talk about object-oriented design. There are
so few sources of info on the subject that uses workable examples that you
can build, run and actually USE, and even fewer for web application.
Some people may argue that, as servlets do not reload for every
browser request (as CGI applications does), we do not need to break the
application in many small apps, one for each web page. They would prefer
to have just one servlet for all operations, and that only servlet would
get as a parameter the operation to be performed.
I do not like this monolithic design. Java was made so your application
is made of small collaborating classes. This is useful for network
computing, distributed applications (as the classes are loaded only on
demand) and also for easing the burden of maintenance.
If we decide to change the standard lay-out of the pages, I just need
to edit and recompile the AddrBookServlet class, as long as the new design
fits on the Header-Body-Footer structure of the original layout design. I
do not have to recompile all dependent classes (as I would need to do if
I used C++), just drop the new AddrBookServlet.class on my web server
servlets directory and presto, the existing servlets inherits the layout
changes.
But that's too much about theory. Let's see the code!
The Data layer
Let's see the classes on the first layer: Entry and HttpEntry. These
only encapsulate SQL statements against the database (using JDBC) and
provide an interface based on the application data entities and the
operations supported by then.
/*
* Entry - encapsulates an Address Book entry (i.e., an Address
* Book record or row)
*/
import java.net.URL;
import java.sql.*;
public class Entry
{
public String id;
public String first_name;
public String last_name;
public String title;
public String company;
public String e_mail;
protected void emptyEntry ()
{
id = "";
first_name = "";
last_name = "";
title = "";
company = "";
e_mail = "";
}
public Entry ()
{
emptyEntry();
}
private void entryFromRow (ResultSet res)
throws java.sql.SQLException
{
/*
* As we are inserting spaces instead of empty strings or
* NULL values, we need to remove these extra spaces.
* THIS IS A HACK!
*/
id = res.getString("id").trim();
last_name = res.getString("last_name").trim();
first_name = res.getString("first_name").trim();
company = res.getString("company").trim();
title = res.getString("title").trim();
e_mail = res.getString("e_mail").trim();
}
public Entry (ResultSet res)
throws java.sql.SQLException
{
entryFromRow(res);
}
public Entry (Connection con, String myId)
throws java.sql.SQLException
{
emptyEntry();
id = myId;
if (myId.length() > 0) {
Statement stmt = con.createStatement();
ResultSet res = stmt.executeQuery(
"SELECT id, company, first_name, last_name, title, e_mail "
+ "FROM addr_book "
+ "WHERE id = " + myId
);
if (res.next()) {
entryFromRow(res);
}
stmt.close();
}
}
public void validate ()
throws java.lang.Exception
{
if (last_name.length() == 0 || e_mail.length() == 0) {
Exception e = new Exception(
"You should provide at least "
+ "a last name and an e-mail"
);
throw(e);
}
/*
* mSQL JDBC driver gets lost if it gets an empty string
* from the database and you get no rows. THIS IS A HACK!
* The correct solution would be to insert NULL values for
* the empty fields
*/
if (first_name.length() == 0) first_name = " ";
if (title.length() == 0) title = " ";
if (company.length() == 0) company = " ";
}
public static void delete (Connection con, String id)
throws java.sql.SQLException
{
Statement stmt = con.createStatement ();
stmt.executeUpdate(
"DELETE FROM addr_book "
+ "WHERE id = " + id
);
}
public void insert (Connection con)
throws java.sql.SQLException
{
Statement stmt = con.createStatement ();
ResultSet res = stmt.executeQuery("SELECT _seq FROM addr_book");
res.next();
id = res.getString("_seq");
stmt.executeUpdate(
"INSERT INTO addr_book "
+ "(id, first_name, last_name, title, company, e_mail) "
+ "VALUES (" + id + ", "
+ "'" + first_name +"', '" + last_name + "', "
+ "'" + title +"', '" + company + "', "
+ "'" + e_mail +"')"
);
stmt.close();
}
public void update (Connection con)
throws java.sql.SQLException
{
Statement stmt = con.createStatement ();
stmt.executeUpdate(
"UPDATE addr_book SET "
+ "first_name = '" + first_name + "', "
+ "last_name = '" + last_name + "', "
+ "title = '" + title + "', "
+ "company = '" + company + "', "
+ "e_mail = '" + e_mail + "' "
+ "WHERE id = " + id
);
stmt.close();
}
}
Note two interesting things about the Entry class: first, we use the
validate() method to test if the data provided to initialize an instance
(object) of this class. We defined that any address book entry should have
at least a last name and an e-mail address. So far so good. But, instead of
returning a boolean value, we return nothing and throw an exception if the
instance is not valid.
Java follows the philosophy that error and exception processing should
not be part of the regular course of action of the application. Note that
nowhere our program checked status values for errors. We check for empty
result sets, but this is a NORMAL occurrence in our code. This leads to
cleaner code and less work testing for errors. This also allows a single
point for displaying error messages.
If you know some part of the code may get run-time errors, put it
inside an try block and do all error processing in the catch block. And if
your code may identify errors, throw Exception objects for then and let the
try/catch blocks handles them: for example, displaying an error message
(instead of many pieces of code everywhere for displaying these messages).
The other thing is a hack. Every real application has its hacks, so our
wouldn't be different. Here's the cause of the hack: if the returned rows
from any select contains empty strings, the JDBC driver returns no rows!
All you get is an empty result set. If you run the same query through the
mSQL monitor, you get all the rows that match the select statement. Too
bad, you can't leave blank fields on the insert/edit form.
We could change validate() to force all fields to have some content,
but my requirements are for allowing empty fields. The correct value for an
empty field in relational databases is to have a NULL value, not an empty
string, so I guess that's why this bug had its way inside mSQL JDBC driver.
The real solution for the problem would be to insert or update the
empty fields with NULL values (ex: update addr_book ser company = NULL
where ...") but I'd have to write too much code for generating the correct
SQL statement, because a non-empty value is delimited by quotes, while a
NULL value has no delimiter. It's easier to put a white space if the field
is empty. This is what we do inside validate(). You shouldn't insert or
update any data to the database without validate() before, so this is a
nice place for the hack.
After this hack, I may get many field containing only white space from
the database. It will be not nice to have an input field containing an
space. This could also lead to bad formatting of HTML. So I play safe
trim() white space from the fields when initializing the object from
database.
See how an object-oriented design can help even when hacking: I had a
clear place to put the hack, and I can be assured the hack will not impact
any other code. Better yet, I won't start altering many lines of code
spread through many modules because I need to insert and remove white
space.
Another important thing about the Entry class is the static method
delete(). This is because I do not need an in-memory initialized Entry for
deleting, I just need to be able to identify the entry about to be
deleted. Why should I instantiate an object that will just be discarded?
Also thing about fetching data from the database that will not be used.
That's enough about the Entry class. Let's see the EntryCollection
class:
/*
* EntryCollection - encapsulates the result of a database query
* against Address Book entries
*/
import java.net.URL;
import java.sql.*;
public class EntryCollection
{
private Statement stmt;
private ResultSet res;
public EntryCollection (Connection con, String firstLetter)
throws java.sql.SQLException
{
stmt = con.createStatement ();
res = stmt.executeQuery(
"SELECT id, company, first_name, last_name, title, e_mail "
+ "FROM addr_book "
+ "WHERE last_name LIKE '" + firstLetter + "%' "
+ "ORDER BY last_name, first_name"
);
}
public Entry next ()
throws java.sql.SQLException
{
if (res.next()) {
return new Entry(res);
}
else {
return null;
}
}
public void close ()
throws java.sql.SQLException
{
res.close();
}
}
The EntryCollection class is much simpler, so I guess it does not need any
comments.
The View Layer
Our view layer is very small. Just one class: HttpEntry.
/*
* HttpEntry - initializes or render an Entry using HTTP
*/
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
public class HttpEntry extends Entry
{
public HttpEntry()
{
emptyEntry();
}
public HttpEntry(Entry entry)
{
id = entry.id;
first_name = entry.first_name;
last_name = entry.last_name;
title = entry.title;
company = entry.company;
e_mail = entry.e_mail;
}
public HttpEntry(HttpServletRequest req)
{
// as the user may have typed extra spaces, we remove then
id = req.getParameter("id").trim();
first_name = req.getParameter("first_name").trim();
last_name = req.getParameter("last_name").trim();
title = req.getParameter("title").trim();
company = req.getParameter("company").trim();
e_mail = req.getParameter("e_mail").trim();
}
public String getName ()
{
return(last_name + ", " + first_name);
}
public void printForm (PrintWriter pw)
{
pw.println("<P><HR>");
pw.println("<FORM method=\"post\" "
+ "action=\"/servlet/AddrBookEntry\">");
pw.println("<INPUT type=\"hidden\" name=\"id\" "
+ "value=\"" + id + "\">");
pw.println("<TABLE>");
pw.println("<TR><TD>Last Name");
pw.println("<TD><INPUT type=\"text\" name=\"last_name\" "
+ "value=\"" + last_name + "\">");
pw.println("<TR><TD>First Name");
pw.println("<TD><INPUT type=\"text\" name=\"first_name\" "
+ "value=\"" + first_name + "\">");
pw.println("<TR><TD>Company");
pw.println("<TD><INPUT type=\"text\" name=\"company\" "
+ "value=\"" + company + "\">");
pw.println("<TR><TD>Title");
pw.println("<TD><INPUT type=\"text\" name=\"title\" "
+ "value=\"" + title + "\">");
pw.println("<TR><TD>E-mail");
pw.println("<TD><INPUT type=\"text\" name=\"e_mail\" "
+ "value=\"" + e_mail + "\">");
pw.println("</TABLE>");
pw.println("<P>");
pw.println("<INPUT type=\"submit\" ");
if (id.length() > 0) {
pw.println("value=\"Update Entry\">");
}
else {
pw.println("value=\"Insert Entry\">");
}
pw.println("<INPUT type=\"reset\" value=\"Clear Form\">");
pw.println("</FORM>");
}
public void printTableRow (PrintWriter pw)
{
pw.println("<TR>");
pw.println("<TD>" + getName());
pw.println("<TD>" + company);
pw.println("<TD>" + title);
pw.println("<TD>" + e_mail);
}
public void printTableRow (PrintWriter pw, String editURL,
String deleteURL)
{
pw.println ("<TR>");
pw.println (
"<TD>" + "<A href=\"" + editURL + "id=" + id + "\">"
+ getName() + "</A>"
);
pw.println("<TD>" + company);
pw.println("<TD>" + title);
pw.println("<TD>" + e_mail);
pw.println(
"<TD align=\"center\">" + "<A href=\"" + deleteURL
+ "id=" + id + "\">" + "[X]</A>"
);
}
}
This is a utility class that helps writing applications that manipulate
an address book entry. It would be used by any web application, enabling
code reuse. The data layer may be used by non-web applications. Here's the
heart of OO design: build for reuse.
See that I have methods for initializing an HttpEntry from a web form
(a HttpServletRequest) and for displaying it as a table row or as a form.
We also need to initialize it from a regular entry. This helps the view
interface simpler, because it won't need to include constructors that know
about databases. This would also allow us to store presentation properties
on the view (a different color, for example).
If we displayed partial contents of an entry, we would need additional
methods. When you build a new application that needs a different HTML
presentation change (or extends) this class, do not build this logic
inside your new application, else you won't enable code reuse and
consequently will turn maintaining or enhancing the code.
Of course, you would design many view classes for a single entity,
instead of just one class as I did.
Bigger applications would also benefit from one or more
HttpCollectionView classes. My design isn't pure as some view logic is
inside the application.
The Application Layer
Now the application layer. The first class is a generic AddrBookServlet
from which all other servlet classes inherits the standard page layout and
the navigational links. If later I decide to include a search function on
the address book, I'd have to modify only this class to get the search
link on every page generated by the Address Book Web Application.
/*
* AddrBookServlet - standard page layout for all AddrBook servlets
*/
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
public class AddrBookServlet extends HttpServlet
{
private void printRowOfLetters (PrintWriter pw, char letter,
char start, char end)
{
pw.println("<TT>");
char l = start;
while (l <= end )
{
if (l == letter) {
pw.print(" " + letter + " ");
}
else {
pw.print(
" <A href=\"/servlet/AddrBook3?letter="
+ l + "&id=\">" + l + "</A> "
);
}
++l;
}
pw.println("</TT>");
}
private void printLinks (PrintWriter pw, char letter)
{
pw.println("<P><CENTER><TABLE width=\"100%\">");
pw.println("<TR><TD>");
pw.println("<BIG><B>");
printRowOfLetters(pw, letter, 'A', 'M');
pw.println("<BR>");
printRowOfLetters(pw, letter, 'N', 'Z');
pw.println("</B></BIG>");
pw.println("<TD>");
pw.println("<A href=\"/servlet/AddrBookForm?id=\">");
pw.println("Insert new entry</A>");
pw.println("</TABLE></CENTER>");
}
protected void printHeader (PrintWriter pw, String title, String letter)
{
pw.println("<HTML>");
pw.println("<HEAD>");
// the servlet-generated pages should not be stored on cache
pw.println("<META http-equiv=\"pragma\" content=\"no-cache\">");
pw.println("<META http-equiv=\"expires\" content=\"10-JAN-1980\">");
pw.println("<TITLE>" + title + "</TITLE>");
pw.println("</HEAD>");
pw.println("<BODY>");
pw.println("<H1>" + title + "<HR></H1>");
printLinks(pw, letter.charAt(0));
}
protected void printFooter (PrintWriter pw)
{
pw.println("</TABLE>");
pw.println("</BODY>");
pw.println("</HTML>");
pw.flush();
pw.close();
}
}
Now the main servlet: the Address Book Listing, which I named AddrBook3 so
you do not have to worry about renaming each version of the listing.
/*
* AddrBook3 - lists address book entries by letter and deletes
* the selected (clicked) entry
*/
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.URL;
import java.sql.*;
public class AddrBook3 extends AddrBookServlet
{
protected void printHeader (PrintWriter pw, String title, String letter)
{
super.printHeader(pw, title, letter);
pw.println("<P>");
pw.println("<TABLE border width=\"100%\">");
pw.println("<TR>");
pw.println("<TH>Name");
pw.println("<TH>Company");
pw.println("<TH>Title");
pw.println("<TH>E-mail");
pw.println("<TH>Remove");
}
private void printRows (PrintWriter pw, Connection con, String letter)
throws java.sql.SQLException
{
EntryCollection entries = new EntryCollection(con, letter);
Entry entry = entries.next();
if (entry == null) {
pw.println("<TR><TD colspan=5>No records found");
}
else {
while (entry != null)
{
HttpEntry httpEntry = new HttpEntry(entry);
httpEntry.printTableRow(pw, "/servlet/AddrBookForm?",
"/servlet/AddrBook3?letter=" + letter + "&");
entry = entries.next();
}
}
entries.close();
}
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws IOException
{
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
String letter = req.getParameter("letter");
String id = req.getParameter("id");
printHeader(pw, "Address Book Servlet", letter);
try {
Class.forName("com.imaginary.sql.msql.MsqlDriver");
String url = "jdbc:msql://localhost:1114/pim";
Connection con = DriverManager.getConnection(url, "borg", "");
if (id.length() > 0) {
Entry.delete(con, id);
}
printRows(pw, con, letter);
con.close();
}
catch (Exception e) {
pw.println("<TR><TD colspan=5>");
pw.println("Unexpected error while listing Address Book");
pw.println("Entries");
pw.println("<P>" + e.getMessage ());
// this one will go to the web server error log
e.printStackTrace();
printFooter(pw);
}
printFooter(pw);
}
}
This is an example of bad design as the application (servlet) code
implements view functionality. I'll leave as an exercise to the reader
implementing a better design (tip: look at the printHeader() and
printRows() methods).
This servlet implements two functions of our application: browsing
(listing address book entries by initial letter) and deleting entries. I
did this way to have immediate feedback as the entry disappears from the
list. I could also have a separate AddrBookDelete servlet and make this
servlet just redirect the browser to the listing servlet instead to get
the same visual effect.
Now the AddrBookForm servlet:
/*
* AddrBookForm - displays an empty form for inserting or a
* filled for updating a given address book entry
*/
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.URL;
import java.sql.*;
public class AddrBookForm extends AddrBookServlet
{
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
String id = req.getParameter("id");
printHeader(pw, "Address Book Form", " ");
try {
Class.forName ("com.imaginary.sql.msql.MsqlDriver");
String url = "jdbc:msql://localhost:1114/pim";
Connection con = DriverManager.getConnection (url, "borg", "");
Entry entry = new Entry (con, id);
HttpEntry httpEntry = new HttpEntry (entry);
httpEntry.printForm(pw);
printFooter(pw);
}
catch (Exception e) {
pw.println("<P><HR><P>Unexpected error while generating the");
pw.println("Entry form");
pw.println("<P>" + e.getMessage());
// this one will go to the web server error log
//e.printStackTrace();
printFooter(pw);
}
}
}
We connect to the database, create (instantiate) the objects and let then
do the work. Ideally all useful programs would be that simple. But the
form does not perform the insertions or updates. Let's see who really does
these:
/*
* AddrBookEntry - inserts or updates an address book entry using the
* data received from a HTML form
*/
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.URL;
import java.sql.*;
public class AddrBookEntry extends AddrBookServlet
{
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException
{
res.setContentType("text/html");
PrintWriter pw = new PrintWriter(res.getOutputStream());
printHeader(pw, "Address Book Form Results", " ");
try {
HttpEntry entry = new HttpEntry(req);
entry.validate();
Class.forName ("com.imaginary.sql.msql.MsqlDriver");
String url = "jdbc:msql://localhost:1114/pim";
Connection con = DriverManager.getConnection (url, "borg", "");
if (entry.id.length() > 0) {
pw.println(
"<P><HR>Entry <B>" + entry.getName()
+ "</B> updated successfully."
);
entry.update(con);
}
else {
entry.insert(con);
pw.println(
"<P><HR>Entry <B>" + entry.getName()
+ "</B> inserted successfully."
);
}
con.close();
printFooter(pw);
}
catch (Exception e) {
pw.println("<P><HR><P>Error while inserting or updating entry:");
/*
pw.println(
+ "<UL><LI>id ='" + req.getParameter("id") + "'"
+ "<LI>last name ='" + req.getParameter("last_name") + "'"
+ "<LI>first name ='" + req.getParameter("first_name") + "'"
+ "</UL>"
);
*/
pw.println("<P>" + e.getMessage());
pw.println("<P>Please press the [Back] button to correct");
// this one will go to the web server error log
//e.printStackTrace();
printFooter(pw);
}
}
}
Just let the instantiated objects to the work and displays a feedback
message. Why not returning to the listing? Because the inserted or updated
entry would show under a different letter, and I am too lazy to write the
code to direct to the correct listing. Well, if the users ask too much...
And we are done! We have three servlets and four supporting, reusable
classes that makes a complete address book web application. Enjoy!
How to Run the Application
Copy all the .class files generated by compiling the Java classes
presented above to the public servlets directory of your web server
(E:\WWW\SERVLETS\PUBLIC) and type the following URL on the browser:
http://localhost/servlet/AddrBook3?letter=A&id=
A last tip: most web servers have an error log, and the standard error
from servlets is generally directed to this error log. Lotus GoWebserver
also shows the error log in real time inside it's window. Although this is
terrible for a production server, it is very nice for a development server
as you can insert debug messages and watch them on the error log window.
How to write to the standard error?
import java.io.*
...
System.err.println("This should appear in the web server error log!");
If you have some problems and start inserting debug code in your applets,
do not forget to restart the web server, so the modified classes get
loaded.
Before we finish, a little home work for the reader: extend the Address
Book to allow a search by part of a name. You should be able to do this by
just changing the AddrBookServlet class (so the new function appears on
the links all pages shows), defining (or extending or inheriting) the
EntryCollection class and building one or two servlets for the search
function.
Maybe you can even imagine a design clever than mine and build a
generic AddrBook view superclass from which the "letter" view, the search
results view and a view by company could be derived. And please do not
repeat my bad design, create an HttpEntryCollection View-layer class!
Oh, I almost forgot! Please visit The mSQL PC Home Page at:
http://www.blnet.com/msqlpc
And help me share knowledge about mSQL, databases and web programming
in general!
|