Java Web Books
— print (last updated: Nov 14, 2009) print

Select font size:
Download the WebBooks.zip archive and install this as a Web Application from Existing Sources. You will need to add these libraries: This project assumes, like other Books-related examples, that the test database is accessible via the guest user with no password. See the Java Books Application for details. The models package plus all hibernate-related files are the same as those used in the Java Books Application. There is no "hibernate search" setup in this application.

Beans

This application uses a number of Java Bean classes. Our JSP files always instantiate the relevant beans with a "jsp:useBean" tag. In some cases the bean is then used related servlet handler, which is normally intended to be called after a JSP file which instantiates it. Nevertheless, the servlet must somehow deal with the possiblity that the bean was not instantiated before the servlet was called.

A "null bean" turns out to be a very difficult error to track down. With a few exceptions, the servlet should test if the bean is not instantiated, i.e., is null, and then

The nav bean

The nav bean controls the navigational links using the class:

util.Nav
package util; import java.util.*; public class Nav { public enum Choices { NONE, ANON, LOGIN, ADMIN }; private Choices choice = Choices.NONE; private Map<String, String> anon_links = new LinkedHashMap<String, String>(), admin_links = new LinkedHashMap<String, String>(), login_links = new LinkedHashMap<String, String>(); public Nav() { anon_links.put(".", "Home"); anon_links.put("cart.jsp", "View Cart"); anon_links.put("admin.jsp", "Admin Access"); login_links.put(".", "Home"); admin_links.put(".", "Home"); admin_links.put("admin.jsp", "Admin Access"); admin_links.put("add.jsp", "Add A Book"); admin_links.put("logout.jsp", "Admin Logout"); } public void setChoice(Choices choice) { this.choice = choice; } public Map<String, String> getLinks() { if (choice == Choices.ANON) { return anon_links; } if (choice == Choices.ADMIN) { return admin_links; } if (choice == Choices.LOGIN) { return login_links; } return null; } }
All scripts which include nav.jsp must define this bean:
<jsp:useBean id="nav" scope="session" class="util.Nav" />
and set a choice
nav.setChoice(util.Nav.Choices.____); // one of: ANON, ADMIN, LOGIN
The navigational link generator script is this:

nav.jsp
<%@ page import="java.util.*" %> <% // NetBeans flags this as an error because the // definition of "nav" is outside the page Map<String,String> links = nav.getLinks(); if (links == null) return; // Stacking the navigational actions makes debugging easier. // Set to false for final version. boolean stackNav = true; %> <table class="nav"> <tr> <% for( Map.Entry<String,String> entry: links.entrySet()) { String target = entry.getKey(); String label = entry.getValue(); %> <td> <% if (stackNav) { %> <a href="<%=target%>"><%=label%></a> <% } else { %> <a href="javascript:location.replace('<%=target%>')"><%=label%></a> <% } %> <% } %> </td> </tr> </table>

The cart bean

The application features the primitive shopping cart which simply associates a book id number to a quantity (the number of books). The cart is maintained by the session bean:
<jsp:useBean id="cart" scope="session" class="java.util.HashMap" />
The cart is a Map, and the intended usage of it is as a Map<Integer,Integer> representing an association of book ids to a quantity. However, the useBean declaration does not permit generic specifications.

The authadmin bean

Access to the administrative features of delete and add must be preceded by authentication. In its simplest form, authentication is controlled by a session-based boolean object initially set to false. Upon the user's confirmation that he/she knows some "password", the application sets the boolean object's value to true.

The bean declaration is this:
<jsp:useBean id="authadmin" scope="session" class="util.AuthInfo" />
using this "boolean wrapper" class:

util.AuthInfo
package util; public class AuthInfo { private boolean status = false; public AuthInfo(){ } public boolean getStatus() { return status; } public void setStatus(boolean status) { this.status = status; } }
The AuthInfo class is used instead of the more obvious (java.lang) Boolean class because AuthInfo objects are mutable, meaning that the class supports get/set methods to read/write the internal boolean value. In contrast, Boolean objects are immutable in the sense that once initialized, the object's value cannot be changed.

An AuthInfo object used as a bean has the initial value false and the successful authentication action will set:
authadmin.setStatus(true);
The test for authentication is thus:
if (authadmin.getStatus()) { /* .. authenticated .. */ }

The db bean

The db bean is simply an instantiation of the models.DB class propagated throughout the session whenever Hibernate database access in needed. It serves as a convenience and, in some sense, an efficiency.

Anonymous Display

Anonymous display is the application's home page: displaying a list of books where the user can see individual books and add them to a cart (see next section).

index.jsp
<%@ page contentType="text/html" pageEncoding="UTF-8"%> <jsp:useBean id="db" scope="session" class="models.DB" /> <jsp:useBean id="nav" scope="session" class="util.Nav" /> <jsp:useBean id="cart" scope="session" class="java.util.HashMap" /> <%@ page import="java.util.List" %> <%@ page import="org.hibernate.*" %> <%@ page import="models.*" %> <%@ page import="util.Escape" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <% String page_title = "Home Page"; %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title><%=page_title%></title> <link rel="stylesheet" href="css/common.css" type="text/css" /> <script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/errors.js"></script> <script type="text/javascript" src="js/display.js"></script> <script type="text/javascript" src="js/cart.js"></script> </head> <body> <% nav.setChoice(util.Nav.Choices.ANON); %> <%@ include file="nav.jsp" %> <h1><%=page_title%></h1> <% Session sess = db.getSession(); Criteria crit = sess.createCriteria(Book.class); List<Book> books = crit.list(); %> <select id="sel"> <option value="0">-- choose --</option> <% for (Book book : books) { int id = book.getId(); String title = book.getTitle(); title = Escape.html(title); %> <option value="<%=id%>"><%=title%></option> <% } %> </select> <br /><br /> <div id="output"> <table cellpadding="5px"> <tr><td>id:</td> <td id="id"></td></tr> <tr><td>title:</td> <td id="title"></td></tr> <tr><td>type:</td> <td id="type"></td></tr> <tr><td>qty:</td> <td id="qty"></td></tr> </table> <button id="addtocart">Add To Cart</button> </div> </body> </html>

display.js
function initSelection() { $("#sel").val(0) $("#output").css("visibility","hidden") $("#delete").attr("disabled", true) } $(function() { // initialize selection on loading the document initSelection(); // selection change $("#sel").change(function() { var selval = $("#sel").val() if (selval == 0) { initSelection() return } $.get( "GetBook", { id: selval }, function(data) { $("#output").css("visibility","visible") $("#delete").attr("disabled", false) $("#id").html(data.id) $("#title").html(data.title) $("#type").html(data.type) $("#qty").html(data.qty) }, "json" ) }) })

servlet.GetBook
package servlet; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.hibernate.*; import models.*; import util.Escape; public class GetBook extends HttpServlet { protected void processRequest( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { String id = request.getParameter("id"); int num_id = Integer.parseInt(id); DB db = (DB) request.getSession().getAttribute("db"); if (db == null) { db = new DB(); request.setAttribute("db", db); } Session sess = db.getSession(); Book book = (Book) sess.load( Book.class, num_id ); String title = book.getTitle(); String type = book.getType(); int qty = book.getQty(); title = Escape.html(title); title = Escape.javaScript(title); out.println( "{" + "id:" + id + ", " + "title:" + '"' + title + '"' + ", " + "type:" + '"' + type + '"' + ", " + "qty:" + qty + " }" ); } catch (HibernateException x) { out.println(x.getCause()); } finally { out.close(); } } // ... }

Cart Handling

Selecting a book on the home page allows us to "add it to our cart," and then redirects to the cart page to show us our accumulated choices. This is a very primitive form of cart control — we can only add, one at a time, and the cart display is only of the internal data structure.

index.jsp
<!-- at bottom of document --> <% if (display.getValue().equals("anon")) {%> <button id="addtocart">Add To Cart</button> <% } %>

cart.js
$(function() { $("#addtocart").click(function() { $.get( "AddToCart", { id: $("#sel").val() }, function() { location.replace("cart.jsp") // non-stacking // location = "cart.jsp" // alternative, stacking } ) }) })

servlet.AddToCart
package servlet; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import java.util.Map; public class AddToCart extends HttpServlet { protected void processRequest( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { String id = request.getParameter("id"); int num_id = Integer.parseInt(id); Map<Integer,Integer> cart = (Map<Integer,Integer>) request.getSession().getAttribute("cart"); Integer current = cart.get(num_id); if (current == null) { cart.put(num_id, 1); } else { cart.put(num_id, 1 + current); } } finally { out.close(); } } // ... }

cart.jsp
<%@ page contentType="text/html" pageEncoding="UTF-8"%> <jsp:useBean id="cart" scope="session" class="java.util.HashMap" /> <jsp:useBean id="nav" scope="session" class="util.Nav" /> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <% String page_title = "Cart Contents"; %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title><%=page_title%></title> <link rel="stylesheet" href="css/common.css" type="text/css" /> </head> <body> <% nav.setChoice(util.Nav.Choices.ANON); %> <%@ include file="nav.jsp" %> <h1><%=page_title%></h1> <%= cart %> </body> </html>

Admin Authentication

The Admin link, i.e., admin.jsp is basically the same as index.jsp with some configuration with modifications to permit additions and deletions. Authentication goes like this:
  1. Read the status of the authadmin bean. If true, display the contents of admin.jsp directly. If false, forward to the login.jsp page, offering a form for password entry. The forwarding makes it that the user only ever "sees" admin.jsp as the URL.
  2. The login form submission activates an AJAX call to the Validate servlet which uses the information to attempt to match the password with the known "secret" password.
  3. If the entered password matches the secret one, Validate sets authadmin's status to true and generates a "SUCCESS" output; if not the server generates a failure message and leaves authadmin alone.
  4. The JavaScript activator, in login.jsp alerts the user if the output is other than "SUCCESS". If the validation is successful, a reload operation is called, this time bypassing the login form.
The forwarding/AJAX combination is, in my opinion, a "perfect fit" for validation because it makes the login action appear as a temporary impediment to the goal and when the validation is complete there is no effectively trace left.

admin.jsp
<jsp:useBean id="authadmin" scope="session" class="util.AuthInfo" /> <% if (!authadmin.getStatus()) { %> <jsp:forward page="login.jsp" /> <% } %> <!-- the bulk is almost exactly the same as index.jsp with these changes --> <!-- use this JavaScript file instead of "js/cart.js" --> <script type="text/javascript" src="js/delete.js"></script> <!-- set this choice --> <% nav.setChoice(util.Nav.Choices.ADMIN); %> <!-- show a delete button --> &nbsp; <button id="delete">Delete</button> <!-- omit the add-to-cart button -->

login.jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%> <jsp:useBean id="nav" scope="session" class="util.Nav" /> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Login</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="css/common.css" type="text/css" /> <script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/jquery.form.js"></script> <script type="text/javascript" src="js/errors.js"></script> <script type="text/javascript" src="js/login.js"></script> </head> <body> <% nav.setChoice(util.Nav.Choices.LOGIN); %> <%@ include file="nav.jsp" %> <h1>Login</h1> <form id="auth" action="Validate" autocomplete="off"> Password: <input name="pwd" type="password" /> <input type="submit" value="Enter" /> </form> </body> </html>

login.js
$(function() { $('#auth').ajaxForm({ success: function(data) { if (data != "SUCCESS") { alert(data) } else { location.reload(); } } }) })

servlet.Validate
package servlet; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import util.AuthInfo; public class Validate extends HttpServlet { protected void processRequest( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { String pwd = request.getParameter("pwd").trim(); if (!"foo".equals(pwd)) { out.println("Invalid"); return; } AuthInfo authadmin = (AuthInfo) request.getSession().getAttribute("authadmin"); if (authadmin == null) { authadmin = new AuthInfo(); request.getSession().setAttribute("authadmin", authadmin); } authadmin.setStatus(true); out.print("SUCCESS"); } finally { out.close(); } } // ... }

logout.jsp
<jsp:useBean id="authadmin" scope="session" class="util.AuthInfo" /> <% authadmin.setStatus(false); response.sendRedirect("."); %>

Delete

Although the delete option is only available to the admin-authenticated user, we still need to protect the DeleteBook servlet from being used by direct manipulation of the browser location. To do so, we make an initial test of authentication:
AtomicBoolean auth
   = (AtomicBoolean) request.getSession().getAttribute("authadmin");
if (!auth.get()) {
   out.print("Not Validated");
   return;
}
The same step is done in the AddBook servlet (see next section).

admin.jsp
<!-- the delete button is available to admin.jsp --> &nbsp; <button id="delete">Delete</button>

delete.js
$(function() { $("#delete").click(function() { if (!confirm("Are you sure ?")) { return } $.get( "DeleteBook", { id: $("#sel").val() }, function(data) { if (data != "SUCCESS") alert(data) else location.reload(); } ) }) })

servlet.DeleteBook
package servlet; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.hibernate.*; import util.AuthInfo; import models.*; public class DeleteBook extends HttpServlet { protected void processRequest( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { AuthInfo authadmin = (AuthInfo) request.getSession().getAttribute("authadmin"); if (authadmin == null || !authadmin.getStatus()) { out.print("Not Validated"); return; } String id = request.getParameter("id"); if (id == null) { return; } int num_id = Integer.parseInt(id); DB db = (DB) request.getSession().getAttribute("db"); if (db == null) { throw new Exception("unintended usage: db undefined"); } Session sess = db.getSession(); sess.getTransaction().begin(); Book book = (Book) sess.load( Book.class, num_id ); sess.delete(book); sess.flush(); sess.getTransaction().commit(); out.print("SUCCESS"); } catch (HibernateException x) { out.print(x.getCause()); } catch(Exception x) { out.print(x); } finally { out.close(); } } // ... }

Add

Add makes usage of the jQuery Form plugin. It also introduces some new jQuery feature whereby we can reset the form components through the form id as follows — if you know CSS, this notation makes perfect sense:
$("#theform input[name=title]").val("")
$("#theform input[name=qty]").val("")
$("#theform select[name=type]").val("paper")
Note the "authentication protection" employed in the add.jsp script:
<% if (!authadmin.get()) return; %>
This is only a "strong suggestion" that the unauthenticated user should not try to add a book; the actual protection against adding is effected within the AddBook servlet.

add.jsp
<%@ page contentType="text/html" pageEncoding="UTF-8"%> <jsp:useBean id="authadmin" scope="session" class="util.AuthInfo" /> <% if (!authadmin.getStatus()) { %> <jsp:forward page="login.jsp" /> <% } %> <jsp:useBean id="db" scope="session" class="models.DB" /> <jsp:useBean id="nav" scope="session" class="util.Nav" /> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>Add</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="css/common.css" type="text/css" /> <script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/jquery.form.js"></script> <script type="text/javascript" src="js/errors.js"></script> <script type="text/javascript" src="js/add.js"></script> </head> <body> <% nav.setChoice(util.Nav.Choices.ADMIN); %> <%@ include file="nav.jsp" %> <h1>Add</h1> <form id="theform" method="post" action="AddBook"> <table cellpadding="5px"> <tr> <td>title:</td> <td><input type="text" name="title" size="50" /></td> </tr> <tr><td>type:</td> <td> <select name="type"> <option value="paper">paper</option> <option value="cloth">cloth</option> </select> </td> </tr> <tr> <td>qty:</td> <td><input type="text" name="qty" size="5" /></td> </tr> </table> <input type="submit" value="Add" /> </form> </body> </html>

add.js
$(function() { $('#theform').ajaxForm(function(data) { if (data != "SUCCESS") { alert(data) } else { alert("success"); $("#theform input[name=title]").val("") $("#theform input[name=qty]").val("") $("#theform select[name=type]").val("paper") } }) })

servlet.AddBook
package servlet; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.hibernate.*; import util.AuthInfo; import models.*; public class AddBook extends HttpServlet { protected void processRequest( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { AuthInfo authadmin = (AuthInfo) request.getSession().getAttribute("authadmin"); if (authadmin == null || !authadmin.getStatus()) { out.print("Not Validated"); return; } String title = request.getParameter("title"); String type = request.getParameter("type"); String qty = request.getParameter("qty"); if (qty == null || title == null || type == null) { return; } title = title.trim(); qty = qty.trim(); if (title.equals("")) { throw new Exception("title cannot be empty"); } int num_qty; try { num_qty = Integer.parseInt(qty); } catch(Exception x) { throw new Exception("quantity field is invalid"); } DB db = (DB) request.getSession().getAttribute("db"); if (db == null) { throw new Exception("unintended usage: db undefined"); } Book book = new Book(title, type, num_qty); Session sess = db.getSession(); sess.getTransaction().begin(); sess.save(book); sess.flush(); sess.getTransaction().commit(); out.print("SUCCESS"); } catch (HibernateException x) { out.print(x.getCause()); } catch (Exception x) { out.print(x.getMessage()); } finally { out.close(); } } // ... }


© Robert M. Kline