Servlet examples
Examples include:
The "membership" example has a simple form used to enter data, a servlet that receives the data and organizes the response, a "bean" that is responsible for data transfer to/from the database, and a simple database. The webapp structure is:
The main form takes data on the name, age, gender and email address of new subscribing members to some interest group.
The Servlet owns a connection to a database; the example code is set up to use an ODBC database (ODBC Data Source named MembersOnly mapped to an ODBC database), and a small Microsoft Access database is available. This database connection is established when the servlet is first loaded into the servlet engine. If a database connection cannot be established, later attempts to use the servlet get redirected to a page appologising for disrupted database service.
The servlet creates a SubscriberRecord object and copies form-input data into this "bean". The SubscriberRecord class has the code that validates inputs. If the data are invalid, the servlet generates a a response page with text identifying the data inputs that were missing or invalid and supplying a new data entry form with any apparently valid inputs already filled in.
If the submitted data appear valid, a new entry is made in the database and a response page is generated welcoming the new member. This has links to a couple of token HTML pages that supposedly represent a part of the members site ('this week events', and 'welcome').
This helper class holds the data elements defining a subscriber and handles transfers to/from a database (its load/store methods take an argument that defines the database connection). The class is bean like - it has "get" and "set" functions for each of the data-elements that it looks after.
import java.sql.*;
public class SubscriberRecord {
# static members used to define constraints such a limits on length of names
private static final int NAMELENGTH = 30;
...
# instance data members
private String givenName, familyName, eMail;
private int age;
private String sex;
private int id;
# business methods, validation check and "get/set" methods for each data member
# (except id - this is set from the database)
public boolean isValid() { ... }
public String getGivenName() { return givenName; }
public void setGivenName(String aName) { ... }
# similar methods for other data members
...
# database methods
public boolean loadFromDatabase(int idNumber, Connection db) {
...
}
public int createInDatabase( Connection db) {
...
}
}
The "set" methods apply business rules that check the input data. A SubscriberRecord will be left in an invalid state if any input data item is unacceptable.
public void setSex(String gender) {
sex = null;
if(gender.equals("Male")) sex = gender;
else
if(gender.equals("Female")) sex = gender;
}
The isValid method verifies that all inputs were acceptable.
public boolean isValid() {
return
(givenName != null) &&
(familyName != null) &&
(eMail != null) &&
(sex != null) &&
((age>=MINAGE) && (age <= MAXAGE));
}
The servlet can simply put all input data from the form into a subscriber record and rely on it to then confirm whether the data were acceptable.
The createInDatabase function accesses one of the database tables to
determine the next membership number to allocate, then inserts a new record in the main
database table. The servlet owns a single database connection and there is a
possibility that two threads might simultaneously try to use this connection; the
code actually using the connection is therefore within a synchronization lock.
public int createInDatabase( Connection db) {
int idnumber = -1;
try {
synchronized(db) {
Statement stmt = db.createStatement ();
String request =
"select nextnumber from memnum where fortable='members'";
ResultSet rset = stmt.executeQuery(request);
int value = 0;
if(rset.next()) {
value = rset.getInt("NEXTNUMBER");
}
else { stmt.close(); return -1; }
value++;
request = "update memnum set nextnumber=" +
value + " where fortable='members'";
stmt.executeUpdate(request);
stmt.close();
// Prepared statement would be preferable
// PreparedStatement pstmt =
// db.prepareStatement(
// "insert into members values( ?, ?, ?, ?, ?, ?)");
// pstmt.setInt(1, value);
// pstmt.setString(2, givenName);
// pstmt.setString(3, familyName);
// pstmt.setString(4, eMail);
// pstmt.setString(5, sex);
// pstmt.setInt(6, age);
// pstmt.executeUpdate();
// Microsoft Access can't handle prepared statements, so give it a
// simple statement and hope that don't have any pesky names with single quote
// characters to cause problems!
// If really were serious should use a StringBuffer to assemble the request.
Statement maStmt = db.createStatement();
String insertStr = "insert into members values (" + Integer.toString(value);
insertStr = insertStr + ", '" + givenName;
insertStr = insertStr + "', '" + familyName;
insertStr = insertStr + "', '" + eMail;
insertStr = insertStr + "', '" + sex;
insertStr = insertStr + "', " + Integer.toString(age) + ")";
maStmt.executeUpdate(insertStr);
maStmt.close();
idnumber = value;
db.commit();
}
}
catch (Exception e) {
try { db.rollback(); } catch(Exception whatnow) { }
// Error message to log file
System.out.println("Error creating a subscriber record - membership servlet");
System.out.println(e.toString());
}
id = idnumber;
return idnumber;
}
The code is updating two tables; these two updates are handled as a single transaction.
The database has two tables - memnum and members. The memnum table
is used to allocated membership numbers; it has rows with "nextnumber" (integer) and "fortable" (string
identifier) fields. The nextnumber field contains the data used to determine the next unique
identifier number that is to be assigned. (Many database systems have autoincrement data types
that handle this allocation task automatically; unfortunately implementations vary. An alternative
way of finding the next number to allocated would be to select the maximum id currently used in
a table and add one.) The main table is the members table with its records of members - as
in the Microsoft Access table supplied:
membernum GIVENNAME FAMILYNAME EMAIL SEX AGE 1 James Dane JD432 Male 32 2 Sandra Wood SW03 Female 30 3 Jack Dancer jd432 Male 54 4 Sam Gamges sg22 Male 24
(The SQL definition would be something like:
create table members ( membernum integer, Givenname varchar(50), Familyname varchar(50), Email varchar(50), Sex varchar(8), Age integer );
The InfoServlet servlet defines:
The data members include the single database connection, an strings that hold the username and password for the database (not applicable with Microsof Access but generally necessary). The username and password data are taken as initialization parameters defined in the web.xml deployment file. The functions used to set up (and close) the database connection are:
public class InfoServlet extends HttpServlet {
private String userName;
private String userPassword;
private static final String dbDriverName =
"sun.jdbc.odbc.JdbcOdbcDriver";
private static final String dbURL =
"jdbc:odbc:MembersOnly";
private Connection dbConnection;
private void connectToDatabase() {
try {
Class.forName (dbDriverName);
dbConnection = DriverManager.getConnection(
dbURL,
userName, userPassword);
// Take charge of transactions programmatically
dbConnection.setAutoCommit(false);
}
catch(Exception e) {
System.out.println("InfoServlet - failed to connect to database");
System.out.println(e.toString());
}
}
public void init() {
//System.out.println("Infoservlet initializing");
userName = getInitParameter("User");
userPassword = getInitParameter("Password");
connectToDatabase();
}
public void destroy() {
if(dbConnection != null) {
try {
dbConnection.close();
} catch(Exception e) { }
}
}
The doPost method creates its SubscriberRecord and fills it with the form data, checks for validity and maybe persists it to the database, and then generates an appropriate response page
public void doPost (HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
if(dbConnection==null) {
response.sendRedirect("/membership/NoDB.html");
return;
}
SubscriberRecord aRecord = new SubscriberRecord();
aRecord.setGivenName(request.getParameter("GivenName"));
aRecord.setFamilyName(request.getParameter("FamilyName"));
int anAge = 0;
try {
String ageStr = request.getParameter("Age");
anAge = Integer.parseInt(ageStr.trim());
}
catch(NumberFormatException nfe) { }
// Similar code for other inputs
...
if(!aRecord.isValid()) {
// send error response with partly filled form
sendRetryResponse(response, aRecord);
return;
}
int membernum = aRecord.createInDatabase(dbConnection);
if(membernum<1) {
// must have had a real database error
response.sendRedirect("/membership/NoDB.html");
return;
}
sendWelcome(response, membernum, request.getParameter("GivenName"));
}
The private methods sendRetryResponse and sendWelcome both
set the response type to text/html and then output large amounts of standard
text and HTML markup tags.
The only interesting feature of the deployment file is its use of initialization parameters:
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
<servlet>
<servlet-name>infoservlet</servlet-name>
<servlet-class>InfoServlet</servlet-class>
<init-param>
<param-name>User</param-name>
<param-value>HSimpson</param-value>
</init-param>
<init-param>
<param-name>Password</param-name>
<param-value>Duh</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>infoservlet</servlet-name>
<url-pattern>/subscriber</url-pattern>
</servlet-mapping>
</web-app>
Servlets can generate graphic response pages. It is simply a matter of:
image/gif or image/jpg,BufferedImage structure,You have two choices for the codec that does the image conversion:
Acme.JPM.Encoders.GifEncoder; com.sun.image.codec.jpeg.*;The GIF encoder is part of a package that can be downloaded from Acme Acme.com (yes, there really is an Acme on the web, but it doesn't deal in items that would appeal to the "Road Runner" from the TV cartoons). Sun's JPEG codec is part of the standard Java runtime, it simply isn't documented anywhere. It is probably easiest to use the Sun codec.
The Graphic webapp is particularly simple. The following code
assumes that it will be created as the webapp graphic;
the servlet will be invoked by pointing a browser at
http://localhost:8080/graphic/UtilityHistogram.
This webapp requires only a web.xml
deployment file, and the HistogramServlet. The
web.xml file merely identifies the servlet:
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
<servlet>
<servlet-name>graphicservlet</servlet-name>
<servlet-class>HistogramServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>graphicservlet</servlet-name>
<url-pattern>/UtilityHistogram</url-pattern>
</servlet-mapping>
</web-app>
The servlet code contains instructions for both codecs with one commented out.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.awt.*;
import java.awt.image.*;
//import Acme.JPM.Encoders.GifEncoder;
import com.sun.image.codec.jpeg.*;
public class HistogramServlet extends HttpServlet {
public void doGet (HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
Graphics g = null;
try {
ServletOutputStream out = response.getOutputStream();
// response.setContentType("image/gif");
response.setContentType("image/jpeg");
BufferedImage bi = new BufferedImage(400,600,
BufferedImage.TYPE_INT_RGB);
g = bi.getGraphics();
g = bi.getGraphics();
g.setColor(Color.white);
g.fillRect(0,0,400,600);
g.setColor(Color.black);
g.drawString("Usage Histogram (jpeg)", 20,20);
// g.drawString("Usage Histogram (gif)", 20,20);
g.setColor(Color.black);
g.drawString("Apr 2000", 10, 40);
g.setColor(Color.blue);
g.fillRect(80, 30, 100, 20);
g.setColor(Color.black);
g.drawString("Apr 2001", 10, 64);
g.setColor(Color.red);
g.fillRect(80, 54, 54, 20);
g.setColor(Color.black);
g.drawString("Apr 2002", 10, 88);
g.setColor(Color.cyan);
g.fillRect(80, 78, 155, 20);
g.setColor(Color.black);
g.drawString("Apr 2003", 10, 112);
g.setColor(Color.yellow);
g.fillRect(80, 102, 185, 20);
//GifEncoder encoder = new GifEncoder(bi, out);
//encoder.encode();
JPEGImageEncoder encoder =
JPEGCodec.createJPEGEncoder(out);
JPEGEncodeParam param =
encoder.getDefaultJPEGEncodeParam(bi);
// Quality - 100% 1.0; good 0.75, medium 0.5, smudge 0.25
float quality = 0.75f;
param.setQuality(quality, true);
encoder.encode(bi, param);
encoder = null;
} finally { if(g!=null) g.dispose(); } }
}
If you want to use the GIF encoder, download the package from Acme (
as a tar.gz file), then
decompress and untar it in your webapps/graphic/WEB-INF/classes directory
(WinZip understands how to deal with tar.gz files).
If you are running your Tomcat on a "headless server", you are likely to encounter problems at the point where you try to create the BufferedImage. You will receive a null pointer instead of an image area.
What is a "headless server" and what is the problem?
Problem is caused by java.awt code asking for details of the screen where the image is to be displayed before it creates the BufferedImage. It shouldn't as the screen doesn't matter. A "headless server" is a machine that does not have a console screen associated with the process that is making the request. So problem is likely to arise if you are running on a shared server machine, or you are connected to a workstation using something like rlogin or ssh that gives you a text only connection.
Try the following: before starting Tomcat, set the environment variable as follows (this if for Unix, make suitable substitution for Windows):
export CATALINA_OPTS="-Djava.awt.headless=true"
This example is a reworking of the micro-Amazon
example in the PHP section. It uses cookies (or URL rewriting if necessary) to identify
a client. It uses Session data - a "shopping cart" that gets filled in
with items that a customer wishes to order.
The web site is a members only sales cooperative dealing with computers, books, and furniture. Only people who have established a membership, using the membership servlet illustrated above, are allowed to shop.
The site has:
members
table in the MembersOnly database created earlier. If the data are invalid, the
servlet redirects the client browser to a page refusing entry.
doGet method and a private sendForm
method. The doGet function checks to determine that a session is defined; if
the user has navigated directly to the servlet without entering via the login page, there
will be no session. Clients who aren't associated with sessions are redirected to the login page.
sendForm method is invoked. This
generates a text/html page with a form that allows items to be selected. Data entered
in these forms are handled by the PurchaseServlet;
links on the page permit navigation between the different books/furniture/computers form pages.
doPost method, and two private methods -
processOrderItems and sendPage.doPost method again starts by checking that the servlet has been invoked
from within a session, redirecting clients to the login page if necessary.processOrderItems method collects data from the posting form (the identifier
keys for selected items) and adds these to the saved session data. Session data
are essentially held in an associative array that is looked after by the servlet engine.
The code in the PurchaseServlet asks for the "shoppingtrolley" session object.
This is simply a java.util.Vector that is used to hold the identifier Strings of
the ordered items. (If this is the first invocation of PurchaseServlet in this
session, the shoppingtrolley will not exist; in this case it is defined as
an empty Vector.)sendPage method generates a response page that has links allowing
the user to resume shopping for books/computers/furniture or to proceed to checkout.doGet method that prints the contents of the shoppingtrolley (it
is invoked by following an <a href=...> link - hence the doGet
method.) Although it could, the code does not terminate the session; it
would be reasonable to invalidate the session object after
the final response page has been printed. The servlet engine will
eventually notice discarded and forgotten sessions and tidy them up
anyway.The code of these servlets uses URL rewriting to encode the session identifier into all URLs in the HTML pages that they generate. Consequently, the system will work even for a user who has disabled cookies in their browser. A cookieless customer will receive pages like:
<p> You may continue shopping or proceed to checkout. <ul> <li><a href="/shop1/Furniture;jsessionid=799EFFC9DD76850CDC3167BF7929F13E">Furniture</a></li> ... </ul>
instead of a page with simple links
<li><a href="/shop1/Furniture">Furniture</a></li>
and a HTTP header that sets the jsessionid cookie. (You can
test your implementation of URL rewriting for cookieless customers by
temporarily disabling acceptance of cookies by your browser.)
The LoginServlet etablishes the session once it has validated the user name and membership number entered in the login form. The code required is simply:
HttpSession current = request.getSession(true);
This is executed just before the start of the code that generates a response page. The servlet infrastructure will arrange for a "cookie" to be added to the header of the generated response.
The code generating URLs that are to appear in pages generated by these servlets has the following typical form:
out.print("<li><a href=\"");
deptcode = response.encodeURL("/shop1/Computers");
out.print(deptcode);
out.println("\">Computers</a>");
The response.encodeURL() method may add the session id to the supplied URL.
The first page returned to the client will contain both a cookie and the
rewritten URLs. The client's next response determines how the servlet engine proceeds.
If a cookie is returned with the session identifier, subsequent calls to encodeURL
are no-ops. If however the session id was instead found only in the request path of the
invoked servlet, then the servlet engine will stop trying to add cookies and simply
rely on the URL rewriting.
The PurchaseServlet creates and subsequently uses the actual session data:
private void processOrderItems(HttpSession current,
HttpServletRequest request) throws ServletException
{
Vector v = (Vector) current.getAttribute("shoppingtrolley");
if(v==null) v = new Vector();
String[] items = request.getParameterValues("buys");
if(items!=null) {
int len = items.length;
for(int i=0;i<len;i++)
v.addElement(items[i]);
current.setAttribute("shoppingtrolley", v);
}
}
A simple Vector of Strings is a somewhat oversimplified version of the typical shopping cart but it is sufficient for this illustrative example.
The web.xml file is a little lengthier now because it has to define several servlets:
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
<servlet>
<servlet-name>loginservlet</servlet-name>
<servlet-class>LoginServlet</servlet-class>
<init-param>
<param-name>User</param-name>
<param-value>HSimpson</param-value>
</init-param>
<init-param>
<param-name>Password</param-name>
<param-value>Duh</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>booksservlet</servlet-name>
<servlet-class>BooksServlet</servlet-class>
</servlet>
...
and some more
...
<servlet-mapping>
<servlet-name>loginservlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
and some more
...
<servlet-mapping>
<servlet-name>checkoutservlet</servlet-name>
<url-pattern>/Checkout </url-pattern>
</servlet-mapping>
</web-app>
Older versions of Tomcat were fairly tolerant of web.xml files that did things like declare one servlet and its mappings, then declare a second servlet and its mappings. The current Tomcat is much more pedantic insisting that the web.xml file obey the rules.
The rules (the "document type definition") really are at http://java.sun.com/j2ee/dtds/web-app_2_2.dtd. The rule defining the correct order of the main elements in a web.xml file is:
<!ELEMENT web-app (icon?, display-name?, description?, distributable?, context-param*, servlet*, servlet-mapping*, session-config?, mime-mapping*, welcome-file-list?, error-page*, taglib*, resource-ref*, security-constraint*, login-config?, security-role*, env-entry*, ejb-ref*)>
This says that all servlet entries must come before the servlet-mapping
entries.
This webapplication consists of several HTML pages, a deployment file, and the servlets. It is however more clearly defined than the collection of PHP scripts and static HTML pages used for the corresponding example in the PHP section. The deployment file identifies the components and there is a defined form for the web archive file that can be used to distribute components.
This web archive is atypical. The code isn't compiled. You
will have to get in and unzip and then edit and compile
it; you have to edit at least the database details. Although this (zipped) war file does not really a
package a distributable
servlet it illustrates
the principle of distributing all the components of a webapplication as
a single entity. (Zipping a war file? File is already compressed so don't gain much further
compression. This is an attempt to get around a proble that
occurs with some browsers that think a ".war"
file is some strange kind of text file.) The webapp should be installed as the "context" shop1
(the term "context" really just identifies the name of the tom/webapps/ sub directory with the
application files).
The final example illustrates how access to servlets can be restricted to particular classes of users (users in specific "roles") and how the behavior of individual servlets can be made to adapt to the role of the user. It also illustrates application data that are shared by all the servlets in a webapp.
The example is the one where all employees in a company can create database records of the number of hours that they work on tasks; the entries in the database identify the employee, the number of hours of work, and the type of work (selected from a predefined list). (The Acme company whose records are used in this example has no relation to that which owns the www.acme.com web site!) Employees can obtain summaries of all the work hours that are recorded for them and the pay that they are due. A manager can also obtain details of the hours recorded for each person that they manage. Finally, the boss can change the rates paid for different types of work, and can define new types of task.
This webapp has three servlets and makes use of two helper classes. The servlets are
The helper classes are DBInfo and RatesRecord.
The DBInfo class helps
to isolate details of the database used from the rest of the application. The
RatesRecord class
is a helper bean used to store current pay rates.
The structure for the webapplication is:
The webapp uses the security "roles" - worker, manager, and boss. The web.xml deployment file contains deployment restrictions on the servlets; these restrictions identify the "role" that must be filled for a user to access a servlet, and a login mechanism that allows users to identify themselves (user name and password).
The application requires a simple database with three tables (example Access database). The tables in the Access database are:
employee manager David Claire Susan Claire Terry Claire Martin Samuel Leila Samuel Keith Samuel Claire Colin Samuel Colin
(Table definition is simply two varchar fields.)
name Activity Hours Claire managing 18 Claire sales presentations 8 Claire meetings 7 David design 6 David coding 7 David testing 9 Colin conference 6 Colin golf 4 Colin client entertainment 6 Colin business travel 8 Colin networking 5 Colin conference 5 Terry coding 18 Leila design 6
(Table definition is two varchar fields and a double.)
Activity Rate thinking 1.5 design 2.5 documenting 2.5 coding 5 testing 3 managing 15 conference 125 networking 60 business lunch 150 business travel 200 customer liason 25 debugging 7.5 meetings 20 sales presentations 30 golf 115 client entertainment 180
(Table definition is a varchar field and a double.)
The users for the webapp and their permitted roles are defined by entries in the
tom/conf/tomcat-users.xml file:
<?xml version='1.0' encoding='utf-8'?> <tomcat-users> <role rolename="worker" description="humble employee"/> <role rolename="boss" description="overpaid "/> <role rolename="tomcat"/> <role rolename="role1"/> <role rolename="manager"/> <role rolename="admin"/> <group groupname="workers" description="Happy employees at Acme"/> <user username="Susan" password="sue" fullName="Susan Songster" roles="worker"/> <user username="Martin" password="mm" fullName="Martin Maybe"/> <user username="Peter" password="retep" fullName="Peter Stone" roles="worker"/> <user username="Samuel" password="leumas" fullName="Sam Gamges" roles="worker,manager"/> <user username="tomcat" password="tomcat" roles="tomcat"/> <user username="David" password="dai" fullName="David Ventables" roles="worker"/> <user username="Claire" password="erialc" fullName="Claire Babbs" roles="worker,manager"/> <user username="both" password="tomcat" roles="tomcat,role1"/> <user username="role1" password="tomcat" roles="role1"/> <user username="Leila" password="aliel" fullName="Leila Khalin" roles="worker"/> <user username="Terry" password="MOI" fullName="Terrence Mulligan" roles="worker"/> <user username="Colin" password="Password" fullName="Colin The Boss" roles="worker,manager,boss"/> <user username="admin" password="Nqv8st" roles="admin,manager"/> <user username="Keith" password="k001" fullName="Keith Bond" roles="worker"/> </tomcat-users>
It is simplest just to edit the tomcat-users.xml file. You can use the "Administrator" utility in Tomcat to define users, roles, and groups; but there doesn't appear to be a dialog in the current version that allows you to assign roles to users. (The role "manager" used in the example fortuitously grants Acme's managers the right to manage your Tomcat.)
Entries in the web.xml file identify the roles that are relevant to a web-app and the specific constraints that apply to servlets:
<servlet>
<servlet-name>
HoursServlet
</servlet-name>
<servlet-class>
WorkerServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>
RatesServlet
</servlet-name>
<servlet-class>
RateChangeServlet
</servlet-class>
</servlet>
more specifications
...
<servlet-mapping>
<servlet-name>
RatesServlet
</servlet-name>
<url-pattern>
Rates
</url-pattern>
</servlet-mapping>
this constraint identifies the "Hours" URL, which is mapped to the WorkerServlet
as being accessible to all persons in role worker
<security-constraint>
<web-resource-collection>
<web-resource-name>
AcmeCompany
</web-resource-name>
<url-pattern>
/Hours
</url-pattern>
<http-method>
GET
</http-method>
<http-method>
POST
</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>
worker
</role-name>
</auth-constraint>
</security-constraint>
This one identifies that the /Rates url, mapped to the RateChangeServlet
can only be used by those in boss role
<security-constraint>
<web-resource-collection>
<web-resource-name>
AcmeCompany
</web-resource-name>
<url-pattern>
/Rates
</url-pattern>
<http-method>
GET
</http-method>
<http-method>
POST
</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>
boss
</role-name>
</auth-constraint>
</security-constraint>
other stuff
specification of roles relevant to this webapp
<security-role>
<role-name>
worker
</role-name>
</security-role>
<security-role>
<role-name>
manager
</role-name>
</security-role>
<security-role>
<role-name>
boss
</role-name>
</security-role>
The typical mechanism for simple login checks done with Tomcat (or other servlet engine) is for the application programmer to define a login page (and an errors page that is shown if a password is invalid), and for the servlet engine to actually enforce the login mechanism.
A login page has a form with defined input fields and action:
<HTML> <TITLE>Acme Record's Login</TITLE> <BODY> <FORM METHOD="POST" ACTION="j_security_check"> <table align="center" border="2"> <caption>Enter your name and password</caption> <tr> <th ALIGN="RIGHT"><b>Name</b></th> <TD><INPUT TYPE="TEXT" NAME="j_username" VALUE="" SIZE="15"></TD> </tr> <TR> <TH ALIGN="RIGHT"><B>Password:</B></th> <TD> <INPUT TYPE=PASSWORD NAME="j_password" VALUE="" SIZE=15> </TD> </TR> <TR> <TD COLSPAN=2 Align=CENTER> <INPUT TYPE=submit VALUE=" OK "> </TD> </TR> </TABLE> </FORM> </BODY> </HTML>
This inputs from this form are handled by a built-in j_security_check part
of the servlet engine.
The web.xml file has a login-config element that identifies the pages
that the web-application author has supplied for handling the login data entry and error
reporting:
<login-config> <auth-method> FORM </auth-method> <form-login-config> <form-login-page> /loginpage.html </form-login-page> <form-error-page> /errorpage.html </form-error-page> </form-login-config> </login-config>
For example, consider a request for http://localhost/combo/Rates; this is handled roughly as follows:
combo webapp.Rates URL is associated with a constraint requiring that the user
is in role bosslogin-config and sees that it is to use a login form.boss role. If the user is not a boss, an HTTP unauthorized response is sent.Don't try to go directly to the login page (http://localhost:8080/combo/loginpage.html) even if
your browser offers you this as a short cut. It won't work that way. It will submit a request that
asks to run a combo/j_security_check servlet, and there isn't any such beast so you
get a page not found response.
These servlets all need to use the data from the "Rates" table that define job types and pay rates. These data don't often change. It could be costly to constantly go back to the disk and reload the data. So, a memory resident copy is used. (It is also used as a way of getting a simple illustration of shared application data.)
The RatesRecord class is the memory resident copy of the data. Its definition is:
import java.util.*;
import java.sql.*;
public class RatesRecord {
private Hashtable rates = new Hashtable();
public Enumeration keys() { return rates.keys(); }
public double getRate(String task) {
double rate = 0.0;
try {
Double data = (Double) rates.get(task);
rate = data.doubleValue();
}
catch (Exception e) { }
return rate;
}
public RatesRecord() {
loadTable();
}
private void loadTable() {
Connection db = DBInfo.connectToDatabase();
if(db != null) {
try {
Statement stmt =
db.createStatement ();
String request =
"select * from rates";
ResultSet rset = stmt.executeQuery(request);
while(rset.next()) {
String key = rset.getString("ACTIVITY");
double val = rset.getDouble("RATE");
Double dval = new Double(val);
rates.put(key,dval);
}
stmt.close();
db.close();
}
catch(Exception e) { }
}
}
}
When instantiated, it loads its data from the table (it grabs a temporary connection to the database to do this.) It holds the rates data in a hashtable, and provides access via a method that provides an Enumeration of all the different task types and a method that returns the rate for a particular task.
The webapp, i.e. the collection of three servlets, need a single copy
of a RatesRecord. This is thus a simple instance of what servlets refer to
as ServletContext data and JSPs refer to as application data. The ServletContext
is a structure that holds information about a webapp. Like the Session object
used earlier, a ServletContext can act as a kind of associative array holding data objects
that are associated with an identifier key. Here we need a RatesRecord object, the key is
RatesTable. A servlet can ask for the "RatesTable" object:
ServletContext ctx = getServletContext();
RatesRecord rates = (RatesRecord)
ctx.getAttribute("RatesTable");
if(rates==null) {
rates = new RatesRecord();
ctx.setAttribute("RatesTable", rates);
}
The code picks up a reference to the context, asks the context object for the RatesTable. If there is none, then this is the first servlet to run; it had better create a RatesRecord (which automatically loads its own data) and place it in the ServletContext.
Typically, the RatesRecord object will be created by a user invoking http://localhost:8080/combo/Hours (the URL that invokes the servlet that records hours worked) or http://localhost:8080/combo/ShowRecord (the URL that invokes the servlet for viewing a person's work record). Once created, the RatesRecord remains in memory.
If the boss choose to run the RateChangeServlet, then this will update the database by changing an existing rate or adding a new task type and rate. This servlet uses the existing RatesRecord to determine whether it is adding new data or updating old data. Once it has done its task, it removes the RatesRecord from the shared ServletContext (application) area. A new copy with the updated information will be instantiated as soon as it is required.
The DBInfo class represents a first step at moving details of the database out of the application.
import java.sql.*;
public class DBInfo {
public static final String userName = "HSimpson";
public static final String userPassword = "Duh";
/*
public static final String dbDriverName =
"oracle.jdbc.driver.OracleDriver";
public static final String dbURL =
"jdbc:oracle:thin:@wraith:1521:csci";
*/
private static final String dbDriverName =
"sun.jdbc.odbc.JdbcOdbcDriver";
private static final String dbURL =
"jdbc:odbc:Acme";
public static final Connection connectToDatabase() {
Connection dbConnection = null;
try {
Class.forName (dbDriverName);
dbConnection = DriverManager.getConnection(
dbURL,
userName, userPassword);
}
catch(Exception e) { }
return dbConnection;
}
}
The class is simply static code that provides the database connection. Different segments can be commented out allowing it to use an ODBC database or an Oracle database depending on where the application is to be deployed.
The JDBC library is evolving and code like this that loads drivers and uses the DriverManager to get a connection will eventually be deprecated. The preferred style is to use DataSources; these offer a more complete and effective method of separating deployment issues regarding databases from coding issues. An application simply has a symbolic name for the database that it is to use. At run time, it contacts some form of directory service to find out what database is mapped to this name. It gets back an object that was previously registered with the directory service. This object is a pooled connection to the appropriate database, any drivers will also have been loaded.
This is the JNDI mechanism to which you will see references in the Tomcat documentation. JNDI is simply Java's common interface that allows applications to use a variety of different directory services where details of databases (and other resources) may be stored.
The use of JNDI and DataSources was pioneered in the more sophisticated Enterprise Java Beans environment but it is now being adopted for servlets. The Administrator utility with Tomcat provides some mechanisms for setting up such datasources.
It isn't appropriate to use JNDI and datasources in your first servlet exercises. But you should look to these extensions if you plan more serious servlet develpments.
The behaviors of the WorkerServlet and ShowRecordServlet are adapted to the roles of their users. For
example, the list of tasks that appear in the hours worked form that is generated by the doGet
method of the WorkerServlet must be appropriate to the user. Ordinary programmers see tasks such as coding
and documentation, managers see tasks like managing and customer relations, the boss sees tasks like travel,
business lunches,
and customer entertainment.
This is achieved by code that checks the role of the user. The
showOptions routine is used to generate a set of option
entries in a select input field of a generated form. It uses the role of
the user to determine the appropriate options according to pay rate (as obtained from
the shared "application" or "ServletContext" RatesRecord object)
private void showOptions(HttpServletRequest req, PrintWriter out)
{
ServletContext ctx = getServletContext();
RatesRecord rates = (RatesRecord)
ctx.getAttribute("RatesTable");
if(rates==null) {
rates = new RatesRecord();
ctx.setAttribute("RatesTable", rates);
}
double low = 0.0;
double high = Double.MAX_VALUE;
if(req.isUserInRole("boss")) {
low = 25.0;
}
else
if(req.isUserInRole("manager")) {
low = 5.0;
high = 30.0;
}
else high = 5;
Enumeration activities = rates.keys();
while(activities.hasMoreElements()) {
String activity = (String) activities.nextElement();
double dval =rates.getRate(activity);
if((dval<low) || (dval>high)) continue;
out.print("<option ");
out.print("value=\"" + activity +"\">");
out.println(activity);
}
}
Sometimes it is necessary to know the identity of the user. In this example, this matters in the CheckRecords servlet. When used by a user with role manager, this prompts for an employees name. If the name entered is that of the manager then it proceeds to report on the manager's own work record, otherwise it must check that the name entered is that of an employee directly supervised by the manager.
The code in this case is:
public void doPost (HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
// Pick up name entered in form
String person = request.getParameter("ENAME");
if(person==null) {
response.sendRedirect("/combo/BadData.html");
return;
}
// Pick up name of user (getRemoteUser may be deprecated in future,
// use alternative based on getUserPrincipal)
String client = request.getRemoteUser();
if(client.equals(person)) {
generateReport(client,response);
return;
}
if(!request.isUserInRole("manager")) {
response.sendRedirect("/combo/NoAccess.html");
return;
}
String mm = getManager(person);
if(!client.equals(mm)) {
response.sendRedirect("/combo/NoAccess.html");
return;
}
generateReport(person,response);
}
This is getting into the realm of a moderately serious webapplication with a variety of servlets, helper classes, and a sophisticated deployment that specifies a login authentication mechanism and authorization controls on sevlets. Everything can be neatly packaged up in a single deployable "war" resource (once again, a zipped war file that hopefully will be correctly downloaded by all browsers.). The war file is again not a deployable application; you must edit database details and recompile the code.