UserSession.java

822 lines | 28.112 kB Blame History Raw Download
/*
 * RUBiS
 * Copyright (C) 2002, 2003, 2004 French National Institute For Research In Computer
 * Science And Control (INRIA).
 * Contact: jmob@objectweb.org
 * 
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation; either version 2.1 of the License, or any later
 * version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 *
 * Initial developer(s): Emmanuel Cecchet, Julie Marguerite
 * Contributor(s): Jeremy Philippe
 */
package edu.rice.rubis.client;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Random;
import java.util.Vector;

/**
 * RUBiS user session emulator. This class plays a random user session emulating
 * a Web browser.
 * 
 * @author <a href="mailto:cecchet@rice.edu">Emmanuel Cecchet </a> and <a
 *         href="mailto:julie.marguerite@inrialpes.fr">Julie Marguerite </a>
 * @version 1.0
 */
public class UserSession extends Thread
{
  private RUBiSProperties rubis         = null;        // access to
  // rubis.properties file
  private URLGenerator    urlGen        = null;        // URL generator
  // corresponding to the
  // version to be used
  // (PHP, EJB or Servlets)
  private TransitionTable transition    = null;        // transition table user
  // for this session
  private String          lastHTMLReply = null;        // last HTML reply
  // received from
  private Random          rand          = new Random(); // random number
  // generator
  private int             userId;                      // User id for the
  // current session
  private String          username      = null;        // User name for the
  // current session
  private String          password      = null;        // User password for the
  // current session
  private URL             lastURL       = null;        // Last accessed URL
  private int             lastItemId    = -1;          // This is to deal with
  // back because the
  // itemId cannot be
  // retrieved from the
  // current page
  private int             lastUserId    = -1;          // This is to deal with
  // back because the
  // itemId cannot be
  // retrieved from the
  // current page
  private Stats           stats;                       // Statistics to collect
  // errors, time, ...
  private int             debugLevel    = 0;           // 0 = no debug message,

  // 1 = just error
  // messages, 2 = error
  // messages+HTML pages, 3
  // = everything!

  /**
   * Creates a new <code>UserSession</code> instance.
   * 
   * @param threadId a thread identifier
   * @param URLGen the URLGenerator to use
   * @param RUBiS rubis.properties
   * @param statistics where to collect statistics
   */
  public UserSession(String threadId, URLGenerator URLGen,
      RUBiSProperties RUBiS, Stats statistics)
  {
    super(threadId);
    urlGen = URLGen;
    rubis = RUBiS;
    stats = statistics;
    debugLevel = rubis.getMonitoringDebug(); // debugging level: 0 = no debug
    // message, 1 = just error
    // messages, 2 = error
    // messages+HTML pages, 3 =
    // everything!

    transition = new TransitionTable(rubis.getNbOfColumns(), rubis
        .getNbOfRows(), statistics, rubis.useTPCWThinkTime());
    if (!transition.ReadExcelTextFile(rubis.getTransitionTable()))
      Runtime.getRuntime().exit(1);
  }

  /**
   * Call the HTTP Server according to the given URL and get the reply
   * 
   * @param url URL to access
   * @return <code>String</code> containing the web server reply (HTML file)
   */
  private String callHTTPServer(URL url)
  {
    String HTMLReply = "";
    BufferedInputStream in = null;
    InputStream urlStream = null;
    int retry = 0;

    try
    {
      while (retry < 5)
      {
        // Open the connexion
        try
        {
          urlStream = url.openStream();
          in = new BufferedInputStream(urlStream, 4096);
          //System.out.println("Thread "+this.getName()+": "+url);
        }
        catch (IOException ioe)
        {
          if (debugLevel > 0)
            System.err.println("Thread " + this.getName()
                + ": Unable to open URL " + url + " (" + ioe.getMessage()
                + ")<br>");
          retry++;
          try
          {
            Thread.sleep(1000L);
          }
          catch (InterruptedException i)
          {
            if (debugLevel > 0)
              System.err.println("Thread " + this.getName()
                  + ": Interrupted in callHTTPServer()<br>");
            return null;
          }
          continue;
        }

        // Get the data
        try
        {
          byte[] buffer = new byte[4096];
          int read;

          while ((read = in.read(buffer, 0, buffer.length)) != -1)
          {
            if (read > 0)
              HTMLReply = HTMLReply + new String(buffer, 0, read);
          }
        }
        catch (IOException ioe)
        {
          if (debugLevel > 0)
            System.err.println("Thread " + this.getName()
                + ": Unable to read from URL " + url + " (" + ioe.getMessage()
                + ")<br>");
          return null;
        }

        // No retry at this point
        break;
      }
    }
    catch (Exception ignore)
    {
    }
    finally
    {
      try
      {
        if (in != null)
          in.close();
      }
      catch (IOException ioe)
      {
        if (debugLevel > 0)
          System.err.println("Thread " + this.getName()
              + ": Unable to close URL " + url + " (" + ioe.getMessage()
              + ")<br>");
      }
      try
      {
        if (urlStream != null)
          urlStream.close();
      }
      catch (IOException ioe)
      {
        if (debugLevel > 0)
          System.err.println("Thread " + this.getName()
              + ": Unable to close URL " + url + " (" + ioe.getMessage()
              + ")<br>");
      }

    }
    if (retry == 5)
      return null;

    // Look for any image to download
    Vector images = new Vector();
    int index = HTMLReply.indexOf("<IMG SRC=\"");
    while (index != -1)
    {
      int startQuote = index + 10; // 10 equals to length of <IMG SRC"
      int endQuote = HTMLReply.indexOf("\"", startQuote + 1);
      images.add(HTMLReply.substring(startQuote, endQuote));
      index = HTMLReply.indexOf("<IMG SRC=\"", endQuote);
    }

    // Download all images
    byte[] buffer = new byte[4096];
    while (images.size() > 0)
    {
      URL imageURL = urlGen.genericHTMLFile((String) images.elementAt(0));
      try
      {
        InputStream imageStream = imageURL.openStream();
        BufferedInputStream inImage = new BufferedInputStream(imageStream, 4096);
        while (inImage.read(buffer, 0, buffer.length) != -1)
          ; // Just download, skip data
        inImage.close();
        imageStream.close();
      }
      catch (IOException ioe)
      {
        if (debugLevel > 0)
          System.err.println("Thread " + this.getName()
              + ": Error while downloading image " + imageURL + " ("
              + ioe.getMessage() + ")<br>");
      }
      images.removeElementAt(0);
    }

    return HTMLReply;
  }

  /**
   * Internal method that returns the min between last_index and x if x is not
   * equal to -1.
   * 
   * @param last_index last_index value
   * @param x value to compare with last_index
   * @return x if (x <last_index and x!=-1) else last_index
   */
  private int isMin(int last_index, int x)
  {
    if (x == -1)
      return last_index;
    if (last_index <= x)
      return last_index;
    else
      return x;
  }

  /**
   * Extract an itemId from the last HTML reply. If several itemId entries are
   * found, one of them is picked up randomly.
   * 
   * @return an item identifier or -1 on error
   */
  private int extractItemIdFromHTML()
  {
    if (lastHTMLReply == null)
    {
      if (debugLevel > 0)
        System.err.println("Thread " + this.getName()
            + ": There is no previous HTML reply<br>");
      return -1;
    }

    // Count number of itemId
    int count = 0;
    int keyIndex = lastHTMLReply.indexOf("itemId=");
    while (keyIndex != -1)
    {
      count++;
      keyIndex = lastHTMLReply.indexOf("itemId=", keyIndex + 7); // 7 equals to
      // itemId=
    }
    if (count == 0)
    {
      if (lastItemId >= 0)
        return lastItemId;
      if (debugLevel > 0)
        System.err.println("Thread " + this.getName()
            + ": Cannot found item id in last HTML reply<br>");
      if (debugLevel > 1)
        System.err.println("Thread " + this.getName()
            + ": Last HTML reply is: " + lastHTMLReply + "<br>");
      return -1;
    }

    // Choose randomly an item
    count = rand.nextInt(count) + 1;
    keyIndex = -7;
    while (count > 0)
    {
      keyIndex = lastHTMLReply.indexOf("itemId=", keyIndex + 7); // 7 equals to
      // itemId=
      count--;
    }
    int lastIndex = isMin(Integer.MAX_VALUE, lastHTMLReply.indexOf('\"',
        keyIndex + 7));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('?', keyIndex + 7));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('&', keyIndex + 7));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('>', keyIndex + 7));
    Integer foo = new Integer(lastHTMLReply.substring(keyIndex + 7, lastIndex));
    lastItemId = foo.intValue();
    return lastItemId;
  }

  /**
   * Extract a page value from the last HTML reply (used from BrowseCategories
   * like functions)
   * 
   * @return a page value
   */
  private int extractPageFromHTML()
  {
    if (lastHTMLReply == null)
      return 0;

    int firstPageIndex = lastHTMLReply.indexOf("&page=");
    if (firstPageIndex == -1)
      return 0;
    int secondPageIndex = lastHTMLReply.indexOf("&page=", firstPageIndex + 6); // 6
    // equals
    // to
    // &page=
    int chosenIndex = 0;
    if (secondPageIndex == -1)
      chosenIndex = firstPageIndex; // First or last page => go to next or
    // previous page
    else
    { // Choose randomly a page (previous or next)
      if (rand.nextInt(100000) < 50000)
        chosenIndex = firstPageIndex;
      else
        chosenIndex = secondPageIndex;
    }
    int lastIndex = isMin(Integer.MAX_VALUE, lastHTMLReply.indexOf('\"',
        chosenIndex + 6));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('?', chosenIndex + 6));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('&', chosenIndex + 6));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('>', chosenIndex + 6));
    Integer foo = new Integer(lastHTMLReply.substring(chosenIndex + 6,
        lastIndex));
    return foo.intValue();
  }

  /**
   * Extract an int value corresponding to the given key from the last HTML
   * reply. Example :
   * 
   * <pre>
   * 
   *  
   *   
   *    int userId = extractIdFromHTML(&quot;&amp;userId=&quot;)
   *    
   *   
   *  
   * </pre>
   * 
   * @param key the pattern to look for
   * @return the <code>int</code> value or -1 on error.
   */
  private int extractIntFromHTML(String key)
  {
    if (lastHTMLReply == null)
    {
      if (debugLevel > 0)
        System.err.println("Thread " + this.getName()
            + ": There is no previous HTML reply");
      return -1;
    }

    // Look for the key
    int keyIndex = lastHTMLReply.indexOf(key);
    if (keyIndex == -1)
    {
      // Dirty hack here, ugly but convenient
      if ((key.compareTo("userId=") == 0) && (lastUserId >= 0))
        return lastUserId;
      if (debugLevel > 0)
        System.err.println("Thread " + this.getName() + ": Cannot found " + key
            + " in last HTML reply<br>");
      if (debugLevel > 1)
        System.err.println("Thread " + this.getName()
            + ": Last HTML reply is: " + lastHTMLReply + "<br>");
      return -1;
    }
    int lastIndex = isMin(Integer.MAX_VALUE, lastHTMLReply.indexOf('\"',
        keyIndex + key.length()));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('?', keyIndex
        + key.length()));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('&', keyIndex
        + key.length()));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('>', keyIndex
        + key.length()));
    Integer foo = new Integer(lastHTMLReply.substring(keyIndex + key.length(),
        lastIndex));
    // Dirty hack again here, ugly but convenient
    if (key.compareTo("userId=") == 0)
      lastUserId = foo.intValue();
    return foo.intValue();
  }

  /**
   * Extract a float value corresponding to the given key from the last HTML
   * reply. Example :
   * 
   * <pre>
   * 
   *  
   *   
   *    float minBid = extractFloatFromHTML(&quot;name=minBid value=&quot;)
   *    
   *   
   *  
   * </pre>
   * 
   * @param key the pattern to look for
   * @return the <code>float</code> value or -1 on error.
   */
  private float extractFloatFromHTML(String key)
  {
    if (lastHTMLReply == null)
    {
      if (debugLevel > 0)
        System.err.println("Thread " + this.getName()
            + ": There is no previous HTML reply");
      return -1;
    }

    // Look for the key
    int keyIndex = lastHTMLReply.indexOf(key);
    if (keyIndex == -1)
    {
      if (debugLevel > 0)
        System.err.println("Thread " + this.getName() + ": Cannot found " + key
            + " in last HTML reply<br>");
      if (debugLevel > 1)
        System.err.println("Thread " + this.getName()
            + ": Last HTML reply is: " + lastHTMLReply + "<br>");
      return -1;
    }
    int lastIndex = isMin(Integer.MAX_VALUE, lastHTMLReply.indexOf('\"',
        keyIndex + key.length()));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('?', keyIndex
        + key.length()));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('&', keyIndex
        + key.length()));
    lastIndex = isMin(lastIndex, lastHTMLReply.indexOf('>', keyIndex
        + key.length()));
    Float foo = new Float(lastHTMLReply.substring(keyIndex + key.length(),
        lastIndex));
    return foo.floatValue();
  }

  /**
   * Computes the URL to be accessed according to the given state. If any
   * parameter are needed, they are computed from last HTML reply.
   * 
   * @param state current state
   * @return URL corresponding to the state
   */
  public URL computeURLFromState(int state)
  {
    if (lastHTMLReply != null)
    {
      if (lastHTMLReply.indexOf("Sorry") != -1) // Nothing matched the request,
        // we have to go back
        state = transition.backToPreviousState();
    }
    switch (state)
    {
      case -1 :
        // An error occured, reset to home page
        transition.resetToInitialState();
      case 0 :
        // Home Page
        return urlGen.homePage();
      case 1 :
        // Register User Page
        return urlGen.register();
      case 2 :
        // Register the user in the database
        { // Choose a random nb over already known attributed ids
          int i = rubis.getNbOfUsers() + rand.nextInt(1000000) + 1;
          String firstname = "Great" + i;
          String lastname = "User" + i;
          String nickname = "user" + i;
          String email = firstname + "." + lastname + "@rubis.com";
          String password = "password" + i;
          String regionName = (String) rubis.getRegions().elementAt(
              i % rubis.getNbOfRegions());

          return urlGen.registerUser(firstname, lastname, nickname, email,
              password, regionName);
        }
      case 3 :
        // Browse Page
        return urlGen.browse();
      case 4 :
        // Browse Categories
        return urlGen.browseCategories();
      case 5 :
        // Browse items in a category
        { // We randomly pickup a category from the generated data instead of
          // from the HTML page (faster)
          int categoryId = rand.nextInt(rubis.getNbOfCategories());
          String categoryName = (String) rubis.getCategories().elementAt(
              categoryId);
          return urlGen.browseItemsInCategory(categoryId, categoryName,
              extractPageFromHTML(), rubis.getNbOfItemsPerPage());
        }
      case 6 :
        // Browse Regions
        return urlGen.browseRegions();
      case 7 :
        // Browse categories in a region
        String regionName = (String) rubis.getRegions().elementAt(
            rand.nextInt(rubis.getNbOfRegions()));
        return urlGen.browseCategoriesInRegion(regionName);
      case 8 :
        // Browse items in a region for a given category
        { // We randomly pickup a category and a region from the generated data
          // instead of from the HTML page (faster)
          int categoryId = rand.nextInt(rubis.getNbOfCategories());
          String categoryName = (String) rubis.getCategories().elementAt(
              categoryId);
          int regionId = rand.nextInt(rubis.getNbOfRegions());
          return urlGen.browseItemsInRegion(categoryId, categoryName, regionId,
              extractPageFromHTML(), rubis.getNbOfItemsPerPage());
        }
      case 9 :
        // View an item
        {
          int itemId = extractItemIdFromHTML();
          if (itemId == -1)
            return computeURLFromState(transition.backToPreviousState()); // Nothing
          // then go
          // back
          else
            return urlGen.viewItem(itemId);
        }
      case 10 :
        // View user information
        {
	  int userId = extractIntFromHTML("userId=");
          if (userId == -1)
            return computeURLFromState(transition.backToPreviousState()); // Nothing
          // then go
          // back
          else
            return urlGen.viewUserInformation(userId);
        }
      case 11 :
        // View item bid history
        return urlGen.viewBidHistory(extractItemIdFromHTML());
      case 12 :
        // Buy Now Authentication
        return urlGen.buyNowAuth(extractItemIdFromHTML());
      case 13 :
        // Buy Now confirmation page
        return urlGen.buyNow(extractItemIdFromHTML(), username, password);
      case 14 :
        // Store Buy Now in the database
        {
          int maxQty = extractIntFromHTML("name=maxQty value=");
          if (maxQty < 1)
            maxQty = 1;
          int qty = rand.nextInt(maxQty) + 1;
          return urlGen.storeBuyNow(extractItemIdFromHTML(), userId, qty,
              maxQty);
        }
      case 15 :
        // Bid Authentication
        return urlGen.putBidAuth(extractItemIdFromHTML());
      case 16 :
        // Bid confirmation page
        {
          int itemId = extractItemIdFromHTML();
          if (itemId == -1)
	      return computeURLFromState(transition.backToPreviousState()); // Nothing
          // then go
          // back
          else
            return urlGen.putBid(itemId, username, password);
        }
      case 17 :
        // Store Bid in the database
        { /*
           * Generate randomly the bid, maxBid and quantity values, all other
           * values are retrieved from the last HTML reply
           */
          int maxQty = extractIntFromHTML("name=maxQty value=");
          if (maxQty < 1)
            maxQty = 1;
          int qty = rand.nextInt(maxQty) + 1;
          float minBid = extractFloatFromHTML("name=minBid value=");
          float addBid = rand.nextInt(10) + 1;
          float bid = minBid + addBid;
          float maxBid = minBid + addBid * 2;
          return urlGen.storeBid(extractItemIdFromHTML(), userId, minBid, bid,
              maxBid, qty, maxQty);
        }
      case 18 :
        // Comment Authentication page
        return urlGen.putCommentAuth(extractItemIdFromHTML(),
            extractIntFromHTML("to="));
      case 19 :
        // Comment confirmation page
        return urlGen.putComment(extractItemIdFromHTML(),
            extractIntFromHTML("to="), username, password);
      case 20 :
        // Store Comment in the database
        { // Generate a random comment and rating
          String[] staticComment = {
              "This is a very bad comment. Stay away from this seller !!<br>",
              "This is a comment below average. I don't recommend this user !!<br>",
              "This is a neutral comment. It is neither a good or a bad seller !!<br>",
              "This is a comment above average. You can trust this seller even if it is not the best deal !!<br>",
              "This is an excellent comment. You can make really great deals with this seller !!<br>"};
          int[] staticCommentLength = {staticComment[0].length(),
              staticComment[1].length(), staticComment[2].length(),
              staticComment[3].length(), staticComment[4].length()};
          int[] ratingValue = {-5, -3, 0, 3, 5};
          int rating;
          String comment;

          rating = rand.nextInt(5);
          int commentLength = rand.nextInt(rubis.getCommentMaxLength()) + 1;
          comment = "";
          while (staticCommentLength[rating] < commentLength)
          {
            comment = comment + staticComment[rating];
            commentLength -= staticCommentLength[rating];
          }
          comment = staticComment[rating].substring(0, commentLength);

          return urlGen.storeComment(extractItemIdFromHTML(),
              extractIntFromHTML("name=to value="), userId,
              ratingValue[rating], comment);
        }
      case 21 :
        // Sell page
        return urlGen.sell();
      case 22 :
        // Select a category to sell item
        return urlGen.selectCategoryToSellItem(username, password);
      case 23 :
        {
          int categoryId = rand.nextInt(rubis.getNbOfCategories());
          return urlGen.sellItemForm(categoryId, userId);
        }
      case 24 :
        // Store item in the database
        {
          String name;
          String description;
          float initialPrice;
          float reservePrice;
          float buyNow;
          int duration;
          int quantity;
          int categoryId;
          String staticDescription = "This incredible item is exactly what you need !<br>It has a lot of very nice features including "
              + "a coffee option.<br>It comes with a free license for the free RUBiS software, that's really cool. But RUBiS even if it "
              + "is free, is <B>(C) Rice University/INRIA 2001</B>. It is really hard to write an interesting generic description for "
              + "automatically generated items, but who will really read this <br>You can also check some cool software available on "
              + "http://sci-serv.inrialpes.fr. There is a very cool DSM system called SciFS for SCI clusters, but you will need some "
              + "SCI adapters to be able to run it ! Else you can still try CART, the amazing 'Cluster Administration and Reservation "
              + "Tool'. All those software are open source, so don't hesitate ! If you have a SCI Cluster you can also try the Whoops! "
              + "clustered web server. Actually Whoops! stands for something ! Yes, it is a Web cache with tcp Handoff, On the fly "
              + "cOmpression, parallel Pull-based lru for Sci clusters !! Ok, that was a lot of fun but now it is starting to be quite late "
              + "and I'll have to go to bed very soon, so I think if you need more information, just go on <h1>http://sci-serv.inrialpes.fr</h1> "
              + "or you can even try http://www.cs.rice.edu and try to find where Emmanuel Cecchet or Julie Marguerite are and you will "
              + "maybe get fresh news about all that !!<br>";
          int staticDescriptionLength = staticDescription.length();
          int totalItems = rubis.getTotalActiveItems()
              + rubis.getNbOfOldItems();
          int i = totalItems + rand.nextInt(1000000) + 1;

          name = "RUBiS automatically generated item #" + i;
          int descriptionLength = rand
              .nextInt(rubis.getItemDescriptionLength()) + 1;
          description = "";
          while (staticDescriptionLength < descriptionLength)
          {
            description = description + staticDescription;
            descriptionLength -= staticDescriptionLength;
          }
          description = staticDescription.substring(0, descriptionLength);
          initialPrice = rand.nextInt(5000) + 1;
          if (rand.nextInt(totalItems) < rubis.getPercentReservePrice()
              * totalItems / 100)
            reservePrice = rand.nextInt(1000) + initialPrice;
          else
            reservePrice = 0;
          if (rand.nextInt(totalItems) < rubis.getPercentBuyNow() * totalItems
              / 100)
            buyNow = rand.nextInt(1000) + initialPrice + reservePrice;
          else
            buyNow = 0;
          duration = rand.nextInt(7) + 1;
          if (rand.nextInt(totalItems) < rubis.getPercentUniqueItems()
              * totalItems / 100)
            quantity = 1;
          else
            quantity = rand.nextInt(rubis.getMaxItemQty()) + 1;
          categoryId = rand.nextInt(rubis.getNbOfCategories());
          return urlGen.registerItem(name, description, initialPrice,
              reservePrice, buyNow, duration, quantity, userId, categoryId);
        }
      case 25 :
        // About Me authentification
        return urlGen.aboutMe();
      case 26 :
        // About Me information page
        return urlGen.aboutMe(username, password);
      default :
        if (debugLevel > 0)
          System.err.println("Thread " + this.getName()
              + ": This state is not supported (" + state + ")<br>");
        return null;
    }
  }

  /**
   * Emulate a user session using the current transition table.
   */
  public void run()
  {
    int nbOfTransitions = 0;
    int next = 0;
    long time = 0;
    long startSession = 0;
    long endSession = 0;

    while (!ClientEmulator.isEndOfSimulation())
    {
      // Select a random user for this session
      userId = rand.nextInt(rubis.getNbOfUsers());
      username = "user" + (userId + 1);
      password = "password" + (userId + 1);
      nbOfTransitions = rubis.getMaxNbOfTransitions();
      if (debugLevel > 2)
        System.out.println("Thread " + this.getName()
            + ": Starting a new user session for " + username + " ...<br>");
      startSession = System.currentTimeMillis();
      // Start from Home Page
      transition.resetToInitialState();
      next = transition.getCurrentState();
      while (!ClientEmulator.isEndOfSimulation()
          && !transition.isEndOfSession() && (nbOfTransitions > 0))
      {
        // Compute next step and call HTTP server (also measure time spend in
        // server call)
        lastURL = computeURLFromState(next);
        time = System.currentTimeMillis();
        lastHTMLReply = callHTTPServer(lastURL);
        stats.updateTime(next, System.currentTimeMillis() - time);
        if (lastHTMLReply == null)
        {
          if (debugLevel > 0)
            System.out.println("Thread "+this.getName()+": Cannot connect to HTTP server.");
          transition.resetToInitialState();
          next = transition.getCurrentState();
        }

        // If an error occured, reset to Home page
        else if (lastHTMLReply.indexOf("ERROR") != -1)
        {
          if (debugLevel > 0)
            System.out.println("Thread " + this.getName()
                + ": Error returned from access to " + lastURL + "<br>");
          stats.incrementError(next);
          if (debugLevel > 1)
            System.out.println("Thread " + this.getName()
                + ": HTML reply was: " + lastHTMLReply + "<br>");
          transition.resetToInitialState();
          next = transition.getCurrentState();
        }
        else
          next = transition.nextState();
        nbOfTransitions--;
      }
      if ((transition.isEndOfSession()) || (nbOfTransitions == 0))
      {
        if (debugLevel > 2)
          System.out.println("Thread " + this.getName() + ": Session of "
              + username + " successfully ended<br>");
        endSession = System.currentTimeMillis();
        long sessionTime = endSession - startSession;
        stats.addSessionTime(sessionTime);
      }
      else
      {
        if (debugLevel > 2)
          System.out.println("Thread " + this.getName() + ": Session of "
              + username + " aborted<br>");
      }
    }
  }

}