JSPs, Version 2
If you look at the original approach then
you will no doubt be able to discern the complexity. While not a
problem for someone like me who understands HTML as well as Java
and JavaScript, it would be enough to give others nightmares! A
different tack is one which completely separates the HTML author
from the Java programmer. The general concept is that the JSPs
don't include any Java code and the Java code doesn't generate
any HTML.
This is an intelligent approach, allowing each group to focus on
their particular area of expertise. It doesn't always work out so
cleanly in the execution. After reading an excellent treatise on
Advanced JavaServer Pages
I marvelled at the clean division achieved in the early sections
of the book. The case study demonstrated that it's not always that
simple. I went back to the original code to see how I could clean
it up. Here's the resulting JSP:
<%@ taglib uri="tc.tld" prefix="tc" %>
<head>
<title>Demo Page</title>
</head>
<body>
<tc:updateSkills/>
<form name="testForm" method="POST">
<p>
<h2>Select skill</h2>
<p>
<tc:getUnselectedSkills/>
<p>
<h2>Select skill level</h2>
<p>
<tc:getSelectedSkills/>
<p>
Click <input type="submit" name="submit" value=">>"> to go to next page.
</form>
</body>
|
So where did all the complexity go? It went into three custom
tags. The tags are described in the tc.tld file which looks like
this:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN"
"http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">
<taglib>
<tag>
<name>getSelectedSkills</name>
<tagclass>tags.GetSelectedSkills</tagclass>
<bodycontent>JSP</bodycontent>
</tag>
<tag>
<name>getUnselectedSkills</name>
<tagclass>tags.GetUnselectedSkills</tagclass>
<bodycontent>JSP</bodycontent>
</tag>
<tag>
<name>updateSkills</name>
<tagclass>tags.UpdateSkills</tagclass>
<bodycontent>empty</bodycontent>
</tag>
</taglib>
|
Now before I include the classes which implement the tags, it should
be mentioned that there is a particular directory structure for web
applications. The WAR files used by both J2EE and servlet containers
such as Tomcat encapsulate the structure. Here's my structure, starting
at the application root:
+-getSkill.jsp
+-tc.tld
+-WEB-INF
+-web.xml
+-lib
| +-oracle.jar
+-classes
+-beans
| +-SkillLevel.java
+-tags
+-UpdateSkills.java
+-GetSelectedSkills.java
+-GetUnselectedSkills.java
|
The oracle.jar file contains the classes required to interface to the
local Oracle database and exists within the application structure in
order to be accessible to application elements. The beans directory
name is a bit of a misnomer as SkillLevel.java doesn't extend
java.beans.Beans
at all. It extends java.lang.Object to provide me with a convenience
class. The gist of the application is located in the tags directory.
The three tags extend javax.servlet.jsp.tagext.BodyTagSupport even
though the updateSkills tag specifies that the body is empty.
It was just easier to be consistent. I should also mention that I use a
SELECT as well as various FORM
fields in this demonstration. You might also note that you don't see
the field names specified anywhere. This is because these particular
tags are designed to work together. I could have cobbled together more
flexible naming but it would have come at the cost of more cumbersome
specification on the part of the HTML author. I'll come back to the
customization issue later. So here's what UpdateSkills.jsp looks like:
package tags;
import java.util.Vector;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;
import beans.SkillLevel;
public class UpdateSkills extends BodyTagSupport {
private static final String prefix = "skill_";
private static final String select = "skills";
private static final String deleteCmd = "Delete";
private static final String addCmd = "Add";
public int doStartTag() throws JspException {
HttpServletRequest req = (HttpServletRequest)
pageContext.getRequest();
HttpServletResponse resp = (HttpServletResponse)
pageContext.getResponse();
HttpSession sess = req.getSession( true );
String user =
(String) pageContext.getAttribute( "user" );
int userId = 0;
try {
userId = Integer.parseInt( user );
}
catch( NumberFormatException e ) {
throw( new JspException( e.toString() ) );
}
/*
* if we don't already have a vector of skills
* then load them from the database
*/
Vector skills = (Vector) sess.getValue( "skills" );
if( skills == null ) {
try {
skills = getSkills( userId );
}
catch( SQLException e ) {
throw( new JspException( getClass().getName() +
": SQLException: " + e.getSQLState() ) );
}
catch( ClassNotFoundException e ) {
throw( new JspException(
getClass().getName() + ": " + e.getMessage() +
": not found" ) );
}
sess.putValue( "skills", skills );
}
String cmd = req.getParameter( "submit" );
String target = null;
Enumeration e = req.getParameterNames();
Connection conn = null;
boolean modified = false;
while( e.hasMoreElements() ) {
String name = (String) e.nextElement();
if( ! name.startsWith( prefix ) )
continue;
String vals[] = req.getParameterValues( name );
name = name.substring( prefix.length() );
for( int i = 0; i < vals.length; i++ ) {
if( vals[i].equals( deleteCmd ) )
conn = setSkill( conn, skills, userId,
name, -1 );
else
conn = setSkill( conn, skills, userId,
name, vals[i] );
modified = true;
}
}
if( ( cmd != null ) && ( cmd.equals( addCmd ) ) ) {
target = req.getParameter( select );
conn = setSkill( conn, skills, userId, target, 3 );
modified = true;
}
if( conn != null ) {
try {
conn.close();
}
catch( SQLException f ) {
throw( new JspException( getClass().getName() +
": SQLException: " + f.getSQLState() ) );
}
}
if( modified )
sess.putValue( "skills", skills );
return( SKIP_BODY );
}
public int doEndTag() throws JspException {
return( EVAL_PAGE );
}
}
|
I didn't include getSkills or setSkill
since it's standard JDBC code. It would be properly done with
beans but the important concept here is that I create a Vector
of SkillLevel objects which I then store in the session
context. And you can also see why I didn't leave it up to the
HTML programmer to choose field identifiers. I require certain
patterns for the application to function properly. Here's the
SkillLevel.java source:
package beans;
public class SkillLevel {
private int id;
private String desc;
private int level;
public SkillLevel( int id, String desc, int level ) {
this.id = id;
this.desc = desc;
this.level = level;
}
public int getId() {
return( id );
}
public String getDesc() {
return( desc );
}
public int getLevel() {
return( level );
}
public void setLevel( int level ) {
this.level = level;
}
}
|
So it's just a container for a skill identifier, description and
the level of a particular user. Now we need to create the selection
for those skills which haven't already been selected by the user.
Here's that code:
package tags;
import beans.SkillLevel;
import java.io.IOException;
import java.util.Vector;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;
public class GetUnselectedSkills extends BodyTagSupport {
public int doStartTag() throws JspException {
HttpServletRequest req = (HttpServletRequest)
pageContext.getRequest();
HttpServletResponse resp = (HttpServletResponse)
pageContext.getResponse();
HttpSession sess = req.getSession( true );
String user =
(String) pageContext.getAttribute( "user" );
if( user == null ) {
throw( new JspException( "No user defined" ) );
}
Vector skills = (Vector) sess.getValue( "skills" );
if( skills == null ) {
throw( new JspException( "No skills defined" ) );
}
JspWriter out = pageContext.getOut();
SkillLevel skill = null;
try {
out.println( "<select name=\"skills\">" );
for( int i = 0; i < skills.size(); i++ ) {
skill = (SkillLevel) skills.elementAt( i );
if( skill.getLevel() >= 0 )
continue;
out.println( "<option value=\"" +
skill.getId() + "\">" + skill.getDesc() +
"</option>" );
}
out.println( "</select>" );
out.println( "<input type=\"submit\" name=\"submit\" value=\"Add\">" );
}
catch( IOException e ) {
throw( new JspException( getClass().getName() + ": " +
e.getMessage() ) );
}
return( SKIP_BODY );
}
public int doAfterBody() throws JspException {
if( true )
return( EVAL_BODY_BUFFERED );
else
return( SKIP_BODY );
}
public int doEndTag() throws JspException {
return( EVAL_PAGE );
}
}
|
This is pretty simple: we obtain the Vector of SkillLevels
from the session context and walk through it, creating an option for any
skills which the user has not previously selected. I use the skill level
of -1 to indicate unselected which is a fairly common practice. The code
for GetSelectedSkills isn't that much more complex:
package tags;
import beans.SkillLevel;
import java.io.IOException;
import java.util.Vector;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;
public class GetSelectedSkills extends BodyTagSupport {
public int doStartTag() throws JspException {
HttpServletRequest req = (HttpServletRequest)
pageContext.getRequest();
HttpServletResponse resp = (HttpServletResponse)
pageContext.getResponse();
HttpSession sess = req.getSession( true );
String user =
(String) pageContext.getAttribute( "user" );
if( user == null ) {
throw( new JspException( "No user defined" ) );
}
Vector skills = (Vector) sess.getValue( "skills" );
if( skills == null ) {
throw( new JspException( "No skills defined" ) );
}
JspWriter out = pageContext.getOut();
SkillLevel skill = null;
String skillName = null;
try {
boolean printed = false;
for( int i = 0; i < skills.size(); i++ ) {
skill = (SkillLevel) skills.elementAt( i );
if( skill.getLevel() < 0 )
continue;
if( !printed ) {
out.println( "<table border=\"2\">" );
out.println( "<tr>" );
out.println( "<th rowspan=\"2\">Skill</th>" );
out.println( "<th colspan=\"5\" align=\"center\">Level of experience</th>" );
out.println( "<th rowspan=\"2\">Remove</th>" );
out.println( "</tr>" );
out.println( "<tr>" );
out.println( "<th align=\"center\">Exposure</th>" );
out.println( "<th> </th>" );
out.println( "<th align=\"center\">Working</th>" );
out.println( "<th> </th>" );
out.println( "<th align=\"center\">Expert</th>" );
out.println( "</tr>" );
printed = true;
}
skillName = "skill_" + skill.getId();
out.println( "<tr>" );
out.println( "<td>" + skill.getDesc() + "</td>" );
for( int j = 1; j <= 5; j++ ) {
out.println( "<td width=\"15%\" align=\"center\">" );
out.print( "<input type=\"RADIO\" name=\"" + skillName + "\" value=\"" + j + "\"" );
if( skill.getLevel() == j )
out.print( " checked" );
out.println( ">" );
out.println( "</td>" );
}
out.println( "<td>" );
out.println( "<input type=\"Submit\" name=\"" +
skillName + "\" value=\"Delete\">" );
out.println( "</td>" );
out.println( "</tr>" );
}
if( printed )
out.println( "</table>" );
else
out.println( "<h3>No skills selected</h3>" );
}
catch( IOException e ) {
throw( new JspException( getClass().getName() + ": " +
e.getMessage() ) );
}
return( SKIP_BODY );
}
public int doAfterBody() throws JspException {
if( true )
return( EVAL_BODY_BUFFERED );
else
return( SKIP_BODY );
}
public int doEndTag() throws JspException {
return( EVAL_PAGE );
}
}
|
If you ignore the code which generates the actual HTML for the table,
it's essentially the same as GetUnselectedSkills except for
the insertion of the selection criteria. I also skip the table completely
if no skills have been selected and generate a level 3 header.
So you might be wondering what's left for the HTML programmer. Quite
a bit, actually! Remember, their concern is the "look and feel" of
pages and ensuring consistency within a website. This JSP can be
easily included as merely a component of a complete page which
might include fancy headers, navigation bars, etc. There's also the
ability to customize appearance using cascading stylesheets (CSS).
I used the following in my testing:
<style type="text/css">
th { font: bold 16px Arial }
td { font: 14px Arial }
</style>
|
Very simple but it serves to prove the point. I could have set things
like the default paragraph font, background colours or images, etc.
The HTML author actually has quite a lot of control, even though they
can't choose the field names. It would be simple to modify the
custom tags to generate custom style identifiers for the generated
elements, providing even more customization possibilities.
This was a fairly simple example but I feel that it demonstrates
both the capabilities and limitations of JSP. It's difficult to
achieve a completely clean demarcation between JSP and the tags.
On balance, I'd rather that the Java programmer be required to
generate HTML tags than expect HTML programmers to learn the
Java language so that they can insert it into their own JSP code.
But then that's just my personal opinion.
Copyright © 2002 by Phil Selby