Adding A Shopping Cart Using Ajax - Part 2

The shopping cart we added to our bookstore was inadequate in a number of ways:

The Ajax mechanisms worked fine, but in the "real world," those problems need to be solved. In this part of the lesson, I'll show you how to build a more robust shopping cart application using Ajax concepts that takes advantage of the strengths of a Java web application. There are a few new files as well as file types in this project. When it is complete, the expanded project tree should look like this:

1. Start a new Web Application project. Name the project AjaxBooks3, click through the Frameworks dialog, click Finish. Netbeans will create the project files and folders and open index.jsp in the editor. You can close that file and delete it from the Web pages node.

2. There are a few files that are unchanged from the AjaxBooks2 project: xhr.js, allBooks.xml, showBooks.css. Copy those files from the AjaxBooks2 project into the Web Pages node of the new AjaxBooks3 project.

All of the code for this project is available here as a Netbeans project: AjaxBooks3.zip

3. Right-click the Source Packages node in the Web App node tree and choose New > Java Package. Name the package ajaxbooks. Click Finish.

Java Packages are really just ways for you to bundle and distribute your code. They act as a way for you to distinguish a group of files from other groups and aid in re-using code. For now, don't worry about them - you will learn more about Java Packages in later classes.

4. Right-click the ajaxbooks package and choose New > Java Class. Name the Class Item and click Finish. Note that the class already exists for me because I did the screen shot after building the application.

5. Open the Item.java file and replace the code with the following:

package ajaxbooks;

public class Item
{
    private String isbn,title,price;
    private int quantity;
    
    public Item(String isbn,String title, String price){
        this.isbn = isbn;
        this.title = title;
        this.price = price;
        this.quantity = 1;
    }
    public String getISBN(){
        return isbn;
    }
    
    public String getTitle(){
        return title;
    }
    
    public String getPrice(){
        return price;
    }
    
    public int getQuantity(){
        return quantity;
    }
    
    public void setQuantity(int q){
        this.quantity = q;
    }
}

The Item class represents a single book in the application. It is a template to create Item objects. I plan to create multiple Item objects and store them in the ShoppingCart (which we'll build next). The Item class is pretty straightforward. It provides a way for you to create Item objects by providing a constructor that requires the ISBN, Title, and Price. The class then provides read access to those variables and read/write access to the quantity. Since the quantity may change, but the ISBN, Title, and Price won't, providing read/write access to just the quantity makes sense. I am not asking you to understand the Java code from a syntax standpoint. However, I hope you can read and make sense of what the code does. If this had been written in JavaScript, it would have looked something like this:

   var isbn,title,price, quantity;
    
    
    function Item(String isbn,String title, String price){
        this.isbn = isbn;
        this.title = title;
        this.price = price;
        this.quantity = 1;
		 this.getIsbn = getISBN;
    }
    function getISBN(){
        return isbn;
    }
    
    //and so on...
}

As you'll soon see, working with Java objects is nearly identical to working with JavaScript objects. The clear benefit of using Java is that we are not limited to the client. We can manipulate Java objects on the server. We would have to serialize and de-serialize JavaScript objects to use them on the server. (Serialization is the process of packaging objects for transmission along the network wire.)

6. Right-click the ajaxbooks node and choose New > Java Class. Name the file ShoppingCart and click Finish. Once again, the screen shot was done after I created the application.

7. Open the ShoppingCart.java file and replace it with the following:

package ajaxbooks;

/**
 *
 * @author tduffy
 */
import java.util.*;

public class ShoppingCart
{
    private Vector cart = new Vector();
    private int itemCount = 0;
    private double total = 0.0;
    
    public boolean addItem(Item item){
        boolean retValue = true;
        if(itemCount==0){
            //first Item
            cart.addElement(item);
            itemCount++;
            return true;
        }else{
            //compare ISBN
            String isbn = item.getISBN();
            for(int i=0; i<itemCount; i++){
                Item cartItem = (Item)cart.elementAt(i);
                if(isbn.equals(cartItem.getISBN())){
                    //found a match
                    cartItem.setQuantity(cartItem.getQuantity() + 1);
                    retValue = false;
                    break;
                }
            }
            if(retValue){
                //We've checked the cart, no match
                //add the Item
                cart.addElement(item);
                itemCount++;
                return true;
            }else{
                //The Item exists in the cart already
                return false;
            }
        }
    }
    
    public Item getItem(int index){
        return (Item)cart.elementAt(index);
    }
    
    public void removeItem(int index){
        cart.remove(index);
        itemCount--;
    }
    
    public void clearCart(){
        if(cart.size()>0)
            cart.removeAllElements();
        itemCount = 0;
    }
    
    public int getItemCount(){
        return itemCount;
    }
    
    public Vector getCart(){
        return cart;
    }
    
    public double getTotal(){
        //Since we count every item every time
        //We need to reset total to 0 to avoid double counting
        total = 0.0;
        for(int i=0; i<itemCount; i++){
            Item cartItem = (Item)cart.elementAt(i);
            double unitPrice = new Double(cartItem.getPrice()).doubleValue();
            int quantity = cartItem.getQuantity();
            total += unitPrice * quantity;
        }
        return total;
    }
    
    public String getStringTotal(){
        String subVal = Double.toString(getTotal());
        int decimalLength = subVal.length() - (subVal.lastIndexOf('.')+1);
        if(decimalLength>1)
            subVal = subVal.substring(0,subVal.lastIndexOf('.') + 3);
        else
            subVal += "0";
        return subVal;
        
    }
    
    public String getDisplay(){
        if(cart.isEmpty()){
            return "Shopping Cart is Empty";
        }else{
            StringBuffer sb = new StringBuffer(1024);
            sb.append("<table border=\"0\" cellpadding=\"8\" cellspacing=\"1\">");
            sb.append("<tr>\n<th>Quantity</th>\n<th>ISBN</th>\n<th>Title</th>\n<th>Price</th>\n<th>Remove</th>\n</tr>");
            for(int i=0; i<itemCount; i++){
                if(i%2!=0){
                    //even row - remember we're 0 based but have a header!
                    Item item = (Item)cart.elementAt(i);
                    sb.append("<tr>\n");
                    sb.append("<td class=\"evenRow\"><input type=\"text\" size=\"2\" name=\"item" + i + "\" value=\"" +  item.getQuantity() + "\"></input></td>\n");
                    sb.append("<td class=\"evenRow\">" + item.getISBN() + "</td>\n");
                    sb.append("<td class=\"evenRow\">" + item.getTitle() + "</td>\n");
                    sb.append("<td class=\"evenRow\">$" + item.getPrice() + "</td>\n");
                    sb.append("<td class=\"evenRow\"><input type=\"checkbox\" name=\"remove" + i + "\"></input></td>\n");
                    sb.append("</tr>\n");
                }else{
                    //odd row
                    Item item = (Item)cart.elementAt(i);
                    sb.append("<tr>\n");
                    sb.append("<td><input type=\"text\" size=\"2\" name=\"item" + i + "\" value=\"" +  item.getQuantity() + "\"></input></td>\n");
                    sb.append("<td>" + item.getISBN() + "</td>\n");
                    sb.append("<td>" + item.getTitle() + "</td>\n");
                    sb.append("<td>$" + item.getPrice() + "</td>\n");
                    sb.append("<td><input type=\"checkbox\" name=\"remove" + i + "\"></input></td>\n");
                    sb.append("</tr>\n");
                }
            }
            sb.append("</table>\n");
            sb.append("<div id=\"total\">Total: $" + getStringTotal() + "</div>");
            return sb.toString();
        }
    }
    
    public String getCheckOutDisplay(){
        if(cart.isEmpty()){
            return "Shopping Cart is Empty";
        }else{
            StringBuffer sb = new StringBuffer(1024);
            sb.append("<table border=\"0\" cellpadding=\"8\" cellspacing=\"1\">");
            sb.append("<tr>\n<th>Quantity</th>\n<th>ISBN</th>\n<th>Title</th>\n<th>Price</th>\n</tr>");
            for(int i=0; i<itemCount; i++){
                if(i%2!=0){
                    //even row - remember we're 0 based but have a header!
                    Item item = (Item)cart.elementAt(i);
                    sb.append("<tr>\n");
                    sb.append("<td class=\"evenRow\">" + item.getQuantity() + "</td>\n");
                    sb.append("<td class=\"evenRow\">" + item.getISBN() + "</td>\n");
                    sb.append("<td class=\"evenRow\">" + item.getTitle() + "</td>\n");
                    sb.append("<td class=\"evenRow\">$" + item.getPrice() + "</td>\n");
                    sb.append("</tr>\n");
                }else{
                    //odd row
                    Item item = (Item)cart.elementAt(i);
                    sb.append("<tr>\n");
                    sb.append("<td>" + item.getQuantity() + "</td>\n");
                    sb.append("<td>" + item.getISBN() + "</td>\n");
                    sb.append("<td>" + item.getTitle() + "</td>\n");
                    sb.append("<td>$" + item.getPrice() + "</td>\n");
                    sb.append("</tr>\n");
                }
            }
            sb.append("</table>\n");
            sb.append("<div id=\"total\">Total: $" + getStringTotal() + "</div>");
            sb.append("<div id=\"cartLinks\">\n");
            sb.append("<a href=\"manageCart.jsp\">Manage Cart</a> | ");
            sb.append("<a href=\"clearCart.jsp\">Clear Cart</a>");
            sb.append("</div>");
            return sb.toString();
        }
    }
}

Don't be fooled by how complex this code looks. Essentially, every shopping cart has the same abilities. Shopping carts must know how to add and remove items and deal with multiple items of the same kind. All shopping carts must be able to calculate their own total. And all shopping carts must be able to display themselves on the screen. That's all the above code does. The good news is that once a ShoppingCart object has been created, you can ask it to do all of those tasks for you!

8. Right-click the Web Pages node and choose New > HTML... Name the file index and click Finish. Replace the code generated with the following:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>Ajax Books</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <script type="text/javascript" src="xhr.js"></script>
    <script type="text/javascript">
        var catReq = new xhr();
        catReq.onreadystatechange=processCatalog;
        function processCatalog(){
            if(catReq.readyState == 4){
                if(catReq.status == 200){
                    var display = '<table cellpadding="8" cellspacing="1">';
                    display += '<tr><th> </th><th>Title</th><th>Description</th><th>Price</th><th colspan="2">Author</th><th> </th></tr>';
                    var records = catReq.responseXML.documentElement.getElementsByTagName('record');
                    var len = records.length;
                    for(var i=0; i<len; i++){
                        if(i%2 == 0)
                            display += '<tr>';
                        else
                            display += '<tr class="evenRow">';
                        var data = records[i].childNodes;
                        var dLen = data.length;
                        for(var j=0; j<dLen; j++){
                            if(data[j].nodeName == '#text')
                                continue;
                            if(data[j].nodeName == 'Category')
                                continue;
                            if(data[j].nodeName == 'Coverpic')
                                display += '<td valign="top"><img src="' + data[j].firstChild.nodeValue + '"></td>';
                            else if(data[j].nodeName == 'Price')
                                display += '<td valign="top">$' + data[j].firstChild.nodeValue + '</td>';
                            else if(data[j].nodeName == 'ISBN'){
                                display += '<td valign="top">';
                                display += '<input type="button" value="Add To Cart" onClick="addToCart(';
                                display += "'" + data[j].firstChild.nodeValue + "'";
                                display += ')" /></td>';
                            }
                            else
                                display += '<td valign="top">' + data[j].firstChild.nodeValue + '</td>';
                        }
                        display += '</tr>';
                    }
                    display += '</table>';
                    document.getElementById('books').innerHTML = display;
                    document.getElementById('filter').innerHTML = 'Category: ' + document.getElementById('category').value;
                }
            }
        }
        
        function getCategory(val){
            return 'filterBooks.jsp?category=' + val;
        }
        
        var cartReq = new xhr();
        cartReq.onreadystatechange = processCart;
        function addToCart(isbn){
            //send request
            sendReq(cartReq,'get','addToCart.jsp?isbn=' + isbn);
        }
        function processCart(){
            //draw cart
            if(cartReq.readyState == 4){
                if(cartReq.status == 200){
                    document.getElementById('cart').style.display = 'block';
                    document.getElementById('cart').innerHTML = cartReq.responseText;
                }
            }
        }
        function displayCart(){
            sendReq(cartReq,'get','displayCart.jsp');
        }
    </script>
    <link type="text/css" rel="stylesheet" href="showBooks.css" />
  </head>
  <body onLoad="sendReq(catReq,'get',getCategory('all'));displayCart();">
    <div id="cart">
        Shopping Cart:
    </div>
      <h2>Ajax Books</h2>
  <form>
    <select name="category" id="category" onChange="sendReq(catReq,'get',getCategory(this.value))">
        <option value="all">Show All Books</option>
        <option value="database">Database</option>
        <option value="web">Web</option>
        <option value="JavaScript">JavaScript</option>
        <option value="graphics">Graphics</option>
        <option value="programming">Programming</option>
        <option value="HTML">HTML</option>
    </select>
        
    </form>
    <div id="filter">
          Category: all
      </div>
    <div id="books">
        
    </div>
    
  </body>
</html>

The only difference from the last project is the call to displayCart() in the <body> element's onLoad event. Otherwise, the code is essentially unchanged.

9. Right-click the Web Pages node and choose New > JSP... Name the file displayCart and choose Finish. Replace the code with the following:

<%@page contentType="text/html"%>
<%@page pageEncoding="UTF-8"%>
<%@page import="ajaxbooks.*"%>
<%
ShoppingCart cart = (ShoppingCart)session.getAttribute("cart");
if(cart == null)
    out.print("<h4>Shopping Cart Is Empty</h4>");
else
    out.print(cart.getCheckOutDisplay());
%>

The code above grabs the shopping cart from the session object. If the cart doesn't exist, the empty cart message is displayed. If the cart exists, it is displayed. Because all of the code necessary for the cart to display itself is contained within the ShoppingCart class, you simply need to ask for it to display the cart.

The session object is created automatically for you by Tomcat. The session represents a single user within your application and each user is given their own session object. Tomcat guarantees that each session object is unique. The session object is the ideal place to store the shopping cart for a number of reasons. First is that it is unique for each user. Second is that it is availabale from anywhere in the application. Third is that it exists over a broader scope than does any given page. That is, if a user navigates away from a page, the session object doesn't get destroyed. In Java web applications, you use the getAttribute() and setAttribute() methods to get objects into and out of the session. While I apologize for skimming over this very important topic, that's all you really need to know for now!

One other thing to note before I move on is that I'm using HTML as the data transport language and the responseText property of the XMLHttpRequest object. This allows me to build complex HTML strings for display without having to parse the return object on the client. Since all of the HTML is generated on the server, the process is actually much like asking for an HTML file from the server - only we are replacing only part of the file rather than the whole page. This allows me to use AJAX for what it's good at as well as Java for what it is good at.

You should, at this point, be able to right-click the index.html file and choose Run File. That should start up Tomcat and your browser if necessary and serve up the page. The only visible clue that something is different is the message displaying that the cart is empty.

10. There are only a few more files to create. Right-click the Web Pages node and choose New > JSP... Name the file clearCart and click Finish. Replace the code with the following:

<%@page contentType="text/html"%>
<%@page pageEncoding="UTF-8"%>
<%
   session.setAttribute("cart", null);
%>
<html>
<head>
<script type="text/javascript">
   location.href = 'index.html';
   </script>
</head>
</html>
 

One of the requirements of using the session object is that any page that accesse it must be able to see it. That means JSP files rather than plain HTML. The above code simply sets the cart stored in the session to null and then navigates to the index.html page.

11. Right-click the Web Pages node and choose New > JSP... Name the file manageCart and click Finish. Replace the code with the following:

<%@page contentType="text/html"%>
<%@page pageEncoding="UTF-8"%>
<%@page import="ajaxbooks.*" %>


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Manage Shopping Cart</title>
        <link rel="stylesheet" href="showBooks.css" />
    </head>
    <body>
    <h2>Shopping Cart</h2>
    <form action="updateCart.jsp" method="POST">
        <%
            ShoppingCart cart = (ShoppingCart)session.getAttribute("cart");
            if(cart == null)
                out.print("<h4>Shopping Cart Is Empty</h4>");
            else{
                out.print(cart.getDisplay());
                if(cart.getItemCount()>0){
        %>
                <input type="submit" value="Update Cart" />
        <%
                }
            }
        %>
        
    </form>
    <p>
        <a href="index.html">Continue Shopping</a>
    </p>
    
    
    </body>
</html>

This file sets up a form to handle any updates to the cart pointed at updateCart.jsp (we'll create that file next). Notice that this file uses the traditional rather than the AJAX approach in that it submits the entire page to the server for processing.

12. Right-click the Web Pages node and choose New > JSP... Name the file updateCart and click Finish. Replace the code with the following:

<%@page import="ajaxbooks.*"%>

<%
ShoppingCart cart = (ShoppingCart)session.getAttribute("cart");
if(cart==null){
    response.sendRedirect("manageCart.jsp");
}else{
    //We have the cart, let's update it
    int itemCount = cart.getItemCount();
    int quant = 0;
    for(int i=itemCount-1; i>=0; i--){
        try{
            //User may have erased quantity or entered non-numeric value
            quant = new Integer(request.getParameter("item" + i)).intValue();
        }catch(NumberFormatException nfe){
            quant = 1;
        }
        //Get the next Item
        Item item = cart.getItem(i);
        //Set its quantity
        item.setQuantity(quant);
        if(request.getParameter("remove" + i)!=null){
            //Remove checkbox checked
            cart.removeItem(i);
        }

    }
    //Put the cart back in the session
    session.setAttribute("cart",cart);
    //go look at it!
    response.sendRedirect("manageCart.jsp");
}
%>

This file simply loops through all of the data sent in the post and sets the appropriate values in the cart. If the remove checkbox was checked, the item is removed from the cart. You may now right-click the index.html file and choose Run File. You should be able to interact with the entire application.