Filling and Spilling

I l @ ve RuBoard

One of the requirements from the FRD was that if the user quits his session and has items in the cart that should be remembered the next time the user logs in. In e-commerce parlance, this is called "filling and spilling" the cart.

To accomplish this, you need to be able to detect when the user's session has gone away. The Servlet specification refers to this as "session invalidation " and (luckily) offers an easy way to watch for it.

When you give an object session persistence (with useBean or manually with pageContext.setAttribute ), you can take advantage of an interface provided by Tomcat called HttpSessionBindingListener . If an object implements this interface and is bound into a session, it will be notified via this interface when it is first bound and when it is unbound .

Using this interface, you can watch for the unbinding of the Customer object and dump the current cart (if any) to the database when it happens.

First, you need somewhere to dump it to. Add a simple table to the schema (shown in Listing 11.1).

Listing 11.1 init_cart.sql
 # Schema creation for product-related tables drop table if exists SAVED_CART; create table SAVED_CART (        ISBN        char(20) not null references PRODUCT,        CUSTOMER_ID   int not null,        QUANTITY           int not null); 

SAVED_CART is simply a list of products and quantities that a given user has in his cart.

Now, extend Customer to dump to this table (see Listing 11.2).

Listing 11.2 Additions to Customer.java
 import javax.servlet.http.*; . . . public class Customer implements HttpSessionBindingListener {     private Cart cart;     public Cart getCart() {      return cart;     }     public void setCart(Cart c) {      cart = c;     } . . .     public void valueBound(HttpSessionBindingEvent event) {      cat.debug("User bound");     }     public void valueUnbound(HttpSessionBindingEvent event) {      cat.debug("In unbound");      DBConnection dbConn = null;      Cart cart = getCart();      try         {              dbConn = TurbineDB.getConnection();              if (dbConn == null) {                  cat.error("Can't get database connection");              }              PreparedStatement pstmt =                  dbConn.prepareStatement(sql_bundle.getString("cartClear"));              pstmt.setInt(1, getCustomerId());              pstmt.executeUpdate();              pstmt.close();              cat.debug("Deleted Cart");              pstmt =                  dbConn.prepareStatement(sql_bundle.getString("cartAdd"));              if (cart != null) {                 Iterator items = cart.getItems();                 while (items.hasNext()) {                     CartItem item = (CartItem) items.next();                     pstmt.setString(1, item.getProduct().getISBN());                     pstmt.setInt(2, item.getQuantity());                     pstmt.setInt(3, getCustomerId());                     pstmt.executeUpdate();                     cat.debug("Added item " + item.getProduct().getISBN());                }           }  else {                      cat.debug("Cart is null");              }              pstmt.close();         }      catch (Exception e)          {              cat.error("Error during valueUnbound", e);          }      finally          {              try                  {                      TurbineDB.releaseConnection(dbConn);                  }              catch (Exception e)                  {                   cat.error("Error during release connection", e);              }       }     }     public void fillCart() {      DBConnection dbConn = null;      Cart cart = getCart();      try          {              dbConn = TurbineDB.getConnection();              if (dbConn == null) {                  cat.error("Can't get database connection");              }              PreparedStatement pstmt =                  dbConn.prepareStatement(sql_bundle.getString("cartQuery"));              pstmt.setInt(1, getCustomerId());              ResultSet rs = pstmt.executeQuery();              while (rs.next()) {                  Product p = Product.findProduct(rs.getString("ISBN"));                  if (p != null) {                      cart.addItem(p, rs.getInt("QUANTITY"));                  }              }              rs.close();              pstmt.close();              }          catch (Exception e)              {                  cat.error("Error during fillCart", e);              }          finally              {                  try                      {                         TurbineDB.releaseConnection(dbConn);                      }                  catch (Exception e)                      {                           cat.error("Error during release connection", e);                      }              }     } 

The first thing you did is add HttpSessionBindingListener to the Customer class. You also added a property of the class called cart that will hold a pointer to the cart being used by the session.

Why do you need a pointer to the cart? Can't you just ask the session for it? Unfortunately, no. Consider what will happen upon session invalidation. Each session object will be unbound from the session. What happens if the cart is unbound before the customer is? When the customer asks for the cart, it will no longer be bound to the session. Therefore, you will have to keep a private pointer to the cart that will be available even if the session loses track of it. This will require some changes to the places on the site where you log in and create carts.

REFACTORING

Refactoring is that wonderful process by which you decide that you did things wrong the first time and then rewrite the code. There are two schools of thought regarding refactoring.

The "old school" (where "old" refers to the mid-1990s) says that refactoring means that you didn't do your requirements analysis in sufficient detail and, as a result, you're wasting time redoing work that you should have done the first time.

The "new school" (as represented by methodologies such as extreme programming) says that refactoring is an inevitable result of rapid prototyping, that requirements change over time, and that you need to be able to adapt your code to meet the changes.

As you might guess from the number of backtracks we've already taken during development, I tend to follow the new school. Most outstanding developers I've talked to say that they need to write code twice ”once to understand the problem and once to perfect the solution. I've jokingly referred to this process as "programming by successive approximation ."

My personal advice is this: If you're halfway through something and you suddenly realize that there's a better way to do something, don't be afraid to jump on it.

The first thing that the valueUnbound method of the class (which is the one called during invalidation) does is clear out any old cart values from the database. It does so by running the following statement from the SQLProperties file:

 cartClear=DELETE FROM SAVED_CART WHERE CUSTOMER_ID=? 

With the cart empty of any old values, take a look at the current cart and place a row into the database for each item in the cart using this line:

 cartAdd=INSERT INTO SAVED_CART (ISBN, QUANTITY, CUSTOMER_ID) VALUES (?, ?, ?) 

You will also add the fillCart method while you're in here. fillCart works in reverse, taking items out of the database with this line:

 cartQuery=SELECT ISBN, QUANTITY FROM SAVED_CART WHERE CUSTOMER_ID=? 

It then adds the products to the customer's current cart.

OF COOKIES AND COMBINING

When you use automatic login using cookies and cart fill and spill, you can run into some strange side effects. Consider the following scenario.

Joe User normally uses his home computer to log into BFG. As a result, his home system has his identity cookies on it, and he's always logged in when he's on the site.

Monday, Joe puts a copy of the book The Applet and I into his cart but never checks out. Eventually, the session times out and the book spills into his saved cart.

Tuesday, Joe is at work and realizes that he never bought the book. He goes to the BFG site. Now, because this is the first time he has used his work computer to surf BFG, there are no cookies there. Joe isn't forced to log in.

Joe placed a copy of The Applet and I into his cart and proceeds to the checkout pages. During checkout, he is given an opportunity to log in, and he takes it. Now the saved copy is filled into the cart.

What's the right thing to do? Realize that there's already a copy and not add it? Throw out the entire saved cart? Change the order to two copies?

In the application, the code is just going to mindlessly combine the existing cart and the new contents, but that's by no means the only solution ”or the right one ”for every application.

In short, as the developer, you need to keep in mind all the ways that a user's state can change during a session (reassociating a session with a specific user, logging out, performing automatic login with a cookie, and so on) and make sure that you've thought out all the ramifications .

Restoring State Upon Login

So, with this code in place, whenever a customer session is invalidated, the current cart will be spilled to the database. But what about getting the contents back?

Begin with AutoLogin.jsp, shown in Listing 11.3.

Listing 11.3 AutoLogin.jsp Revised
 <%@ page import="com.bfg.customer.Customer" %> <%@ page import="com.bfg.cart.Cart" %> <%@ page import="javax.servlet.http.Cookie" %> <%@ page import="sun.misc.BASE64Decoder" %> <% {   Cart cart = (Cart) pageContext.getAttribute("cart", PageContext.SESSION_SCOPE);   if (cart == null) {       cart = new Cart();       pageContext.setAttribute("cart", cart, PageContext.SESSION_SCOPE);     }     String email = null;     String password = null;     Cookie cook;     Customer customer = (Customer)  pageContext.getAttribute("customer",PageCon text. graphics/ccc.gif SESSION_SCOPE);   if (customer == null) {        Cookie[] cookies = request.getCookies();        BASE64Decoder dec = new BASE64Decoder();        for (int i = 0; i < cookies.length; i++) {          if (cookies[i].getName().equals("bfgUsername")) {              email = new String(dec.decodeBuffer(cookies[i].getValue()));          }          if (cookies[i].getName().equals("bfgPassword")) {              password = new String(dec.decodeBuffer(cookies[i].getValue()));          }        }        if ((email != null) && (password != null)) {         Customer c = Customer.findCustomer(email);         if ((c != null) && (c.getPassword().equals(password))) {             c.setCart(cart);             c.fillCart();             pageContext.setAttribute("customer",c, PageContext.SESSION_SCOPE);            }         } else {          Customer c = new Customer();          c.setCart(cart);          pageContext.setAttribute("customer", c, PageContext.SESSION_SCOPE);        }   } } %> 

The first thing to notice is that all of the useBean tags have been taken out of here. Also, everything is wrapped in a set of brackets, which means that it won't declare any global variables that might interfere with variables declared in the main body of the JSP that includes this file. The end result is to make this file totally transparent to any JSP file that includes it.

Instead of using useBean , use pageContext.getAttribute to look for session variables and then use setAttribute to set them. The first thing that the code does is look for a cart and establish one if it doesn't already exist. A cart will also need to be given to the Customer object soon.

Next, the code looks to see if there's a customer already associated with the session. If there is, the code deduces that the user must have already encountered this code, so it exits. This assumes that you're going to put this at the top of every JSP page, so that even if a customer goes directly to some obscure page in the middle of the site, the code can try to log in that customer.

If there is no customer for the session, the page looks for the cookies. If it finds them, it gets the customer information and then sets the existing cart to the cart that was made or found. In reality, since the cart is created at the same time this code runs, there should never be a time when no customer is associated with the session but a cart still exists.

Next, the code calls fillCart to read in any saved cart data, set the session cart object to the cart object that the page just created, and continue.

If the page doesn't find the customer or the cookie data, it just creates a new customer object, sets the cart, and continues.

While debugging this code, I discovered a fix that needs to be made to the code in the cookie-creation part of the login page. Because a path wasn't set for the cookies, they are returned to pages in the same directory as Login.jsp (the JSP cust subdirectory). This means that the autologin code would work only if the customer came to that page first. So, you need to add the following line to the two cookie creations in Login.jsp:

 cook.setPath("/"); 

This causes it to return for any Web page on the site.

Now, how do you test this? Well, if you log in to make sure that you've got a cookie with the new path using Login.jsp, put some things in the cart, invalidate the session, and then come back to the index page, you should see that the cart still has your items in it.

But how do you invalidate the session? One solution is to wait for the session to time out. But how long will that take? Well, that turns out to be something that you can define in your web.xml file.

Listing 11.4 shows the web.xml file with a session timeout specified.

Listing 11.4 web.xml with Session Timeout
 <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app     PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"     "http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">     <web-app>   <servlet>     <servlet-name>log4j-init</servlet-name>     <servlet-class>com.bfg.services.Log4jInit</servlet-class>     <init-param>       <param-name>log4j-init-file</param-name>       <param-value>WEB-INF/log4j.properties</param-value>     </init-param>     <load-on-startup>1</load-on-startup>   </servlet>   <servlet>     <servlet-name>turbine-init</servlet-name>     <servlet-class>com.bfg.services.TurbineInit</servlet-class>     <init-param>       <param-name>turbine-resource-directory</param-name>       <param-value>c:/tomcat/webapps/bfg/WEB-INF</param-value>     </init-param>     <load-on-startup>1</load-on-startup>   </servlet>    <session-config>      <session-timeout>5</session-timeout>     </session-config>     <resource-ref>       <res-ref-name>mail/session</res-ref-name>       <res-type>javax.mail.Session</res-type>       <res-auth>Container</res-auth>     </resource-ref> </web-app> 

The line to look for is the one between the <session-config> tags. It specifies a session timeout of 5 minutes, which is good for testing but probably is a bit short for reality (30 minutes is a better choice).

 <session-config>   <session-timeout>5</session-timeout> </session-config> 

The alternative to waiting for the session to time out is to manually invalidate the session. You can create a little JSP page to do this, shown in Listing 11.5.

Listing 11.5 Logout.jsp
 <% session.invalidate(); %> <HEAD><TITLE>Session Invalidated</TITLE></HEAD><BODY> <H1>Session Invalidated</H1> </BODY> 

If you go to this page after putting things in the cart, you'll force the session to invalidate. Then you can go back to the index page and automatically be logged back in.

Unfortunately, the only way to tell for sure that the fill and spill happened is to look at the debugging statements in the log:

 2047848 [HttpProcessor[8080][4]] DEBUG com.bfg.customer.Customer  - User bound 2208361 [StandardManager[/bfg]] DEBUG com.bfg.customer.Customer  - In unbound 2208361 [StandardManager[/bfg]] DEBUG com.bfg.customer.Customer  - Deleted Cart 2208361 [StandardManager[/bfg]] DEBUG com.bfg.customer.Customer  - Added item 67 2320959 2208361 [StandardManager[/bfg]] DEBUG com.bfg.customer.Customer  - Added item 67 2321173 
I l @ ve RuBoard


MySQL and JSP Web Applications. Data-Driven Programming Using Tomcat and MySQL
MySQL and JSP Web Applications: Data-Driven Programming Using Tomcat and MySQL
ISBN: 0672323095
EAN: 2147483647
Year: 2002
Pages: 203
Authors: James Turner

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net