/*--
  This file is a part of ZetaGrid, a simple and secure Grid Computing
  kernel.

  Copyright (c) 2001-2004 Sebastian Wedeniwski.  All rights reserved.

  Use in source and binary forms, with or without modification,
  are permitted provided that the following conditions are met:

  1. The source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.

  2. The origin of this software must not be misrepresented; you must 
     not claim that you wrote the original software.  If you plan to
     use this software in a product, please contact the author.

  3. Altered source versions must be plainly marked as such, and must
     not be misrepresented as being the original software. The author
     must be informed about these changes.

  4. The name of the author may not be used to endorse or promote 
     products derived from this software without specific prior written 
     permission.

  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
  OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
  GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

  Version 1.9.0, February 8, 2004

  This program is based on the work of:
     S. Wedeniwski
--*/

package zeta.handler.statistic;

import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipInputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import zeta.ZetaServlet;
import zeta.handler.GetHandler;
import zeta.util.DatabaseUtils;
import zeta.util.Parameter;
import zeta.util.StreamUtils;

/**
 *  Base handler to response a GET request for a specific statistic.
**/
public abstract class AbstractHandler implements GetHandler {

  /**
   *  Creates HTML page with the content of a specified statistic.
   *  @param  con  connection to the back-end database
   *  @return HTML page with the content of a specified statistic.
  **/
  abstract public String createPage(Connection con) throws SQLException, ServletException;

  /**
   *  Creates HTML page with the content of a specified statistic.
   *  @param  req  GET request
   *  @param  con  connection to the back-end database
   *  @return HTML page with the content of a specified statistic.
  **/
  protected String createPage(HttpServletRequest req, Connection con) throws SQLException, ServletException {
    InnerPageBuffer innerPageBuffer = null;
    ByteArrayOutputStream out = null;
    try {
      innerPageBuffer = getInnerPageBuffer();
      if (innerPageBuffer == null) {
        return "<tr><td colspan=\"8\"><p>This statistic is not available at moment! Please try again later.</td></tr>";
      } else {
        out = new ByteArrayOutputStream(10*1024);
        StreamUtils.writeData(innerPageBuffer.buffer, out, false, true);
        return out.toString("ISO-8859-1");
      }
    } catch (IOException ioe) {
      throw new ServletException(ioe);
    } finally {
      if (innerPageBuffer != null) {
        innerPageBuffer.close();
      }
    }
  }

  /**
   *  Creates PNG image for a specified name which is defined in the HTML page.
   *  Default is null.
   *  @param  con  connection to the back-end database
   *  @param  imageName name of the image
   *  @return PNG image for a specified name which is defined in the HTML page.
  **/
  public BufferedImage createImage(Connection con, String imageName) throws SQLException, ServletException {
    return null;
  }

  /**
   *  @param servlet  servlet which owns this handler.
   *  @param innerDiffTimePageUpdate duration in milliseconds when the HTML inner page should be new created.
   *  @param diffTimePageUpdate duration in milliseconds when the HTML page should be new created.
   *  @param diffTimeImageUpdate duration in milliseconds when the images should be new created.
  **/
  public AbstractHandler(ZetaServlet servlet, int diffTimeInnerPageUpdate, int diffTimePageUpdate, int diffTimeImageUpdate) {
    this.servlet = servlet;
    this.diffTimeInnerPageUpdate = diffTimeInnerPageUpdate;
    this.diffTimePageUpdate = diffTimePageUpdate;
    this.diffTimeImageUpdate = diffTimeImageUpdate;
  }

  /**
   *  Checks if the image of the web page should be updated.
   *  @param  imageName name of the image
   *  @return <code>true</code> if the image of the web page should be updated.
  **/
  public boolean isImageExpired(String imageName) {
    Long time = (Long)lastImageUpdate.get(imageName);
    long currentTime = System.currentTimeMillis();
    if (time == null || (int)(System.currentTimeMillis() - time.longValue()) >= diffTimeImageUpdate) {
      return true;
    }
    Date d1 = new Date(currentTime);
    Date d2 = new Date(time.longValue());
    return !d1.toString().equals(d2.toString());
  }
  /**
   *  Checks if the web inner page (table) should be updated.
   *  @return <code>true</code> if the web page should be updated.
  **/
  public boolean isInnerPageExpired() {
    return (lastInnerPageUpdate == 0 || (int)(System.currentTimeMillis() - lastInnerPageUpdate) >= diffTimeInnerPageUpdate);
  }

  /**
   *  Checks if the web page should be updated.
   *  @return <code>true</code> if the web page should be updated.
  **/
  public boolean isPageExpired() {
    return (lastPageUpdate == 0 || (int)(System.currentTimeMillis() - lastPageUpdate) >= diffTimePageUpdate);
  }

  /**
   *  Returns time difference when a web inner page expired.
   *  @return time difference when a web inner page expired.
  **/
  public int getInnerPageExpiredDiffTime() {
    return diffTimeInnerPageUpdate;
  }

  /**
   *  Returns time difference when a web page expired.
   *  @return time difference when a web page expired.
  **/
  public int getPageExpiredDiffTime() {
    return diffTimePageUpdate;
  }

  /**
   *  Event when a new image was generated.
   *  @param  imageName name of the image
  **/
  final public void imageUpdateCompleted(String imageName) {
    lastImageUpdate.put(imageName, new Long(System.currentTimeMillis()));
  }

  /**
   *  Event when a new web page was generated.
  **/
  final public void pageUpdateCompleted() {
    lastPageUpdate = lastInnerPageUpdate = System.currentTimeMillis();
  }

  /**
   *  Returns the buffer containing the main inner HTML page (table).
   *  The inner page buffer should be closed if it not used.
   *  @return inner page buffer
  **/
  public InnerPageBuffer getInnerPageBuffer() throws ServletException {
    InnerPageBuffer innerPageBuffer = new InnerPageBuffer();
    boolean exists = false;
    if (servlet.hasSeparateFiles()) {
      ZipInputStream zip = null;
      Connection con = null;
      Statement stmt = null;
      try {
        String pathPage = Parameter.getValue(null, "path_page", Parameter.GLOBAL_PARAMETER, null, 3600000);
        if (pathPage == null) {
          con = servlet.getConnection();
          stmt = con.createStatement();
          pathPage = Parameter.getValue(stmt, "path_page", Parameter.GLOBAL_PARAMETER, null, 3600000);
        }
        if (pathPage == null) {
          pathPage = "";
        } else if (pathPage.length() > 0 && pathPage.charAt(pathPage.length()-1) != '/') {
          pathPage += '/';
        }
        File file = new File(pathPage + getClass().getName() + ".zip");
        exists = true;
        if (file.exists() && System.currentTimeMillis()-file.lastModified() < 4*diffTimeInnerPageUpdate+129600000) {  // 36 hours
          zip = new ZipInputStream(new FileInputStream(file));
          if (zip.getNextEntry() != null) {
            timestampOfPage = file.lastModified();
            innerPageBuffer.buffer = new BufferedInputStream(zip);
            innerPageBuffer.resource = new Object[1];
            innerPageBuffer.resource[0] = zip;
            return innerPageBuffer;
          }
        }
      } catch (Throwable e) {
        if (innerPageBuffer != null) {
          innerPageBuffer.close();
        }
      } finally {
        DatabaseUtils.close(stmt);
        DatabaseUtils.close(con);
      }
    } else {
      Connection con = null;
      Statement stmt = null;
      try {
        con = servlet.getConnection();
        stmt = con.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT page,last_update FROM zeta.page WHERE classname='" + getClass().getName() + '\'');
        if (rs.next()) {
          exists = true;
          Timestamp t = rs.getTimestamp(2);
          if (System.currentTimeMillis()-t.getTime() < 4*diffTimeInnerPageUpdate+129600000) {  // 36 hours
            ZipInputStream zip = new ZipInputStream(rs.getBinaryStream(1));
            if (zip.getNextEntry() != null) {
              timestampOfPage = (t == null)? System.currentTimeMillis() : t.getTime();
              innerPageBuffer.buffer = new BufferedInputStream(zip);
              innerPageBuffer.resource = new Object[3];
              innerPageBuffer.resource[0] = rs;
              innerPageBuffer.resource[1] = stmt;
              innerPageBuffer.resource[2] = con;
              return innerPageBuffer;
            }
          }
        }
        rs.close();
        DatabaseUtils.close(stmt);
        timestampOfPage = System.currentTimeMillis();
        if (!exists) {
          try {
            innerPageBuffer.buffer = new BufferedInputStream(new ByteArrayInputStream(createPage(con).getBytes("ISO-8859-1")));
          } finally {
            DatabaseUtils.close(con);
          }
        }
        return innerPageBuffer;
      } catch (Throwable e) {
        if (innerPageBuffer != null) {
          innerPageBuffer.close();
        }
      }
    }   
    return null;
  }

  /**
   *  Returns the buffered page. Update the buffer if the web page is expired.
   *  @param  req  GET request
   *  @return buffered page
  **/
  public String getBufferedPage(HttpServletRequest req) throws ServletException {
    synchronized (pageBuffer) {
      createCompletePage(req);
      String s = pageBuffer;
      if (isPageExpired()) {
        pageBuffer = "";
      }
      return s;
    }
  }

  /**
   *  Handles a GET request <code>resp</code> for the specified statistic.
   *  The response <code>resp</code> contains either the HTML page or an image.
  **/
  final public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String imageName = req.getParameter("image");
    if (imageName != null) {
      // send image
      ByteArrayOutputStream stream = (ByteArrayOutputStream)imageBuffer.get(imageName);
      if (stream == null || isImageExpired(imageName)) {
        if (stream == null) {
          stream = new ByteArrayOutputStream(32*1024);
          imageBuffer.put(imageName, stream);
        }
        boolean ok = false;
        if (servlet.hasSeparateFiles()) {
          Connection con = null;
          Statement stmt = null;
          FileInputStream in = null;
          try {
            String pathPage = Parameter.getValue(null, "path_page", Parameter.GLOBAL_PARAMETER, null, 3600000);
            if (pathPage == null) {
              con = servlet.getConnection();
              stmt = con.createStatement();
              pathPage = Parameter.getValue(stmt, "path_page", Parameter.GLOBAL_PARAMETER, null, 3600000);
            }
            if (pathPage == null) {
              pathPage = "";
            } else if (pathPage.charAt(pathPage.length()-1) != '/') {
              pathPage += '/';
            }
            File file = new File(pathPage + getClass().getName() + '_' + imageName + ".png");
            if (file.exists() && System.currentTimeMillis()-file.lastModified() < 129600000) {  // 36 hours
              in = new FileInputStream(file);
              stream.reset();
              StreamUtils.writeData(in, stream, false, true);
              imageUpdateCompleted(imageName);
              ok = true;
            }
          } catch (Exception e) {
          } finally {
            DatabaseUtils.close(stmt);
            DatabaseUtils.close(con);
            StreamUtils.close(in);
          }
        }
        if (!ok) {
          Connection con = null;
          try {
            con = servlet.getConnection();
            Statement stmt = null;
            try {
              stmt = con.createStatement();
              ResultSet rs = stmt.executeQuery("SELECT image,last_update FROM zeta.image WHERE classname='" + getClass().getName() + "' AND imagename='" + imageName + '\'');
              if (rs.next()) {
                Timestamp t = rs.getTimestamp(2);
                if (System.currentTimeMillis()-t.getTime() < 129600000) { // 36 hours
                  stream.reset();
                  StreamUtils.writeData(rs.getBinaryStream(1), stream, false, true);
                }
              } else {
                BufferedImage image = createImage(con, imageName);
                if (image == null) {
                  throw new ServletException("No image available!");
                }
                stream.reset();
                com.sun.jimi.core.encoder.png.PNGEncoder encoder = new com.sun.jimi.core.encoder.png.PNGEncoder();
                encoder.encodeImage(com.sun.jimi.core.Jimi.createRasterImage(image.getSource()), stream);
                stream.close();
              }
              rs.close();
              imageUpdateCompleted(imageName);
            } catch (com.sun.jimi.core.JimiException ex) {
            } finally {
              DatabaseUtils.close(stmt);
            }
          } catch (Exception e) {
            throw new ServletException(e);
          } finally {
            DatabaseUtils.close(con);
          }
        }
      }
      resp.setContentType("image/png");
      resp.setContentLength(stream.size());
      resp.setHeader("Cache-Control", "max-age=3600"); //1 hour
      stream.writeTo(resp.getOutputStream());
    } else {
      // send page
      try {
        String buffer = getBufferedPage(req);
        resp.setContentType("text/html");
        resp.setContentLength(buffer.length());
        resp.getOutputStream().print(buffer);
      } catch (Exception e) {
        throw new ServletException(e);
      }
    }
  }

  /**
   *  Determines the width of the image in the statistic page.
   *  @param  imageName name of the image
   *  @return the width of the image in the statistic page.
  **/
  public int getImageWidth(String imageName) {
    return imgWidth;
  }

  /**
   *  Determines the height of the image in the statistic page.
   *  @param  imageName name of the image
   *  @return the height of the image in the statistic page.
  **/
  public int getImageHeight(String imageName) {
    return imgHeight;
  }

  /**
   *  Creates HTML page with the content of a specified statistic.
   *  @param  req  GET request
   *  @return HTML page with the content of a specified statistic.
  **/
  private void createCompletePage(HttpServletRequest req) throws ServletException {
    if (isPageExpired()) {
      StringBuffer buffer = new StringBuffer(100*1024);
      String s = servlet.getInitParameter("page.title");
      if (s != null) {
        buffer.append(s);
      }
      appendMenu(buffer);
      s = servlet.getInitParameter("frame.top");
      if (s != null) {
        int idx = s.indexOf("@time@");
        if (idx >= 0) {
          buffer.append(s.substring(0, idx));
          buffer.append(timeFormatter.format((timestampOfPage == 0)? new java.util.Date() : new java.util.Date(timestampOfPage)));
          if (idx+6 < s.length()) {
            buffer.append(s.substring(idx+6));
          }
        } else {
          buffer.append(s);
        }
      }
      Connection con = null;
      String dbError = "The DB2 Server may be down";
      try {
        con = servlet.getConnection();
        dbError = null;
        buffer.append(createPage(req, con));
      } catch (SQLException se) {
        dbError = se.getMessage();
      } catch (ServletException se) {
        dbError = se.getMessage();
      } finally {
        DatabaseUtils.close(con);
      }
      if (dbError != null) {
        buffer.append("<tr><td colspan=\"8\"><p>This statistic is not available at moment (");
        buffer.append(dbError);
        buffer.append(")!<p>Please try again later.</td></tr>");
      }
      s = servlet.getInitParameter("frame.bottom");
      if (s != null) {
        buffer.append(s);
      }
      pageBuffer = buffer.toString();
      pageUpdateCompleted();
    }
  }

  /**
   *  Append a menu on the left side of the HTML page.
   *  @param buffer buffer containing the HTML page.
  **/
  private void appendMenu(StringBuffer buffer) {
    final int l = servlet.getNumberOfStatisticHandlers();
    for (int i = 0; i < l; ++i) {
      Class c = servlet.getStatisticHandlerClass(i);
      if (c == getClass()) {
        // ToDo: not fix
        buffer.append("<tr><td BGCOLOR=\"#FFFFFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
        buffer.append("<td COLSPAN=\"");
        buffer.append((i == 0)? 3 : 5);
        buffer.append("\" BGCOLOR=\"#FFFFFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
        buffer.append("<td BGCOLOR=\"#FFFFFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td></tr>\n");
        buffer.append("<tr><td");
        if (i > 0) {
          buffer.append(" COLSPAN=\"2\"");
        }
        buffer.append(" BGCOLOR=\"#FFFFFF\"><span class=\"nav\"></span></td>\n");
        buffer.append("<td COLSPAN=\"5\" HEIGHT=\"21\" BGCOLOR=\"#FFFFFF\"><span class=\"nav\">");
        buffer.append(servlet.getStatisticHandlerName(c));
        buffer.append("</span></td>\n");
        buffer.append("<td BGCOLOR=\"#FFFFFF\"><span class=\"nav\"></span></td></tr>\n");
        buffer.append("<tr><td BGCOLOR=\"#FFFFFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
        buffer.append("<td COLSPAN=\"");
        buffer.append((i == 0)? 3 : 5);
        buffer.append("\" BGCOLOR=\"#FFFFFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
        buffer.append("<td BGCOLOR=\"#FFFFFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td></tr>\n");
      } else {
        if (i == 0) {
          buffer.append("<tr><td><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
          buffer.append("<td COLSPAN=\"3\" BGCOLOR=\"#CCCCFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
          buffer.append("<td><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td></tr>\n");
        }
        buffer.append("<tr><td");
        if (i > 0) {
          buffer.append(" COLSPAN=\"2\"");
        }
        buffer.append(">&nbsp;</td>\n");
        buffer.append("<td COLSPAN=\"");
        buffer.append((i == 0)? 3 : 2);
        buffer.append("\" HEIGHT=\"21\"><span class=\"nav\"><a href=\"");
        buffer.append(servlet.getHandlerAddress(c));
        buffer.append("\">");
        buffer.append(servlet.getStatisticHandlerName(c));
        buffer.append("</a></span></td>\n");
        buffer.append("<td>&nbsp;</td></tr>\n");
        if (i+1 == l) {
          buffer.append("<tr><td><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
          buffer.append("<td COLSPAN=\"3\" BGCOLOR=\"#CCCCFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
          buffer.append("<td><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td></tr>\n");
        } else if (servlet.getStatisticHandlerClass(i+1) != getClass()) {
          buffer.append("<tr><td COLSPAN=\"2\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
          buffer.append("<td COLSPAN=\"2\" BGCOLOR=\"#CCCCFF\"><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td>\n");
          buffer.append("<td><img SRC=\"/zeta/images/odot.gif\" ALT=\"\" BORDER=0 / height=1 width=1></td></tr>\n");
        }
      }
    }
  }

  /**
   *  Servlet which owns this handler.
  **/
  protected ZetaServlet servlet;

  /**
   *  Format 'MM/dd/yyyy HH:mm:ss' to print current timestamp.
  **/
  private static DateFormat timeFormatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss", Locale.GERMANY);

  /**
   *  Width of the created images.
  **/
  protected static final int imgWidth  = 700;

  /**
   *  Height of the created images.
  **/
  protected static final int imgHeight = 500;

  /**
   *  Buffer containing the created images today.
  **/
  private Map imageBuffer = new HashMap();

  /**
   *  Timestamp when the images are created.
  **/
  private Map lastImageUpdate = new HashMap();

  /**
   *  Buffer containing the HTML page.
  **/
  private String pageBuffer = "";

  /**
   *  Timestamp when the inner page is send to a client.
  **/
  private long lastInnerPageUpdate = 0;

  /**
   *  Timestamp when the page is send to a client.
  **/
  private long lastPageUpdate = 0;

  /**
   *  Duration in milliseconds when the HTML page should be new created.
  **/
  private int diffTimeInnerPageUpdate = 0;

  /**
   *  Duration in milliseconds when the HTML page should be new created.
  **/
  private int diffTimePageUpdate = 0;

  /**
   *  Duration in milliseconds when the images should be new created.
  **/
  private int diffTimeImageUpdate = 0;

  /**
   *  Timestamp when the inner page was generated.
  **/
  private long timestampOfPage = 0;

  class InnerPageBuffer {
    /**
     *  Returns <code>true</code> if and only if the stream contains the specified string <code>search</code>.
     *  Sets the position of the stream where the stream contains the specified string <code>search</code>.
     *  End of the stream will be set if the stream does not contain the specified string <code>search</code>.
     *  @param search string which is searched
     *  @param ignoreCase if <code>true</code>, ignore case when comparing characters.
     *  @return <code>true</code> if and only if the stream contains the specified string <code>search</code>.
    **/
    public boolean skip(String search, boolean ignoreCase) {
      try {
        return StreamUtils.skip(buffer, search, ignoreCase);
      } catch (IOException ioe) {
        return false;
      }
    }

    /**
     *  Returns lines from the input stream where all lines contains the specified string <code>search</code>.
     *  All these lines must be continuously inside the stream.
     *  @param in   input stream
     *  @param search starting string which is searched
     *  @param ignoreCase if <code>true</code>, ignore case when comparing characters.
     *  @return lines from the input stream where all lines contains the specified string <code>search</code>
     *          or <code>null</code> if the specified string is not defined in the stream.
     *  @exception  IOException  if an I/O error occurs.
    **/
    public String[] getLines(String search, boolean ignoreCase) {
      try {
        return StreamUtils.getLines(buffer, search, ignoreCase);
      } catch (IOException ioe) {
        return null;
      }
    }

    /**
     *  Returns the string of the stream which is between the specified strings <code>left</code> and <code>right</code>.
     *  The resulting string does not include the specified strings <code>left</code> and <code>right</code>.
     *  The position of the stream is after the specified ending string <code>right</code>.
     *  End of the stream will be set if the stream does not contain one of the specified strings.
     *  @param search starting string which is searched
     *  @param search ending string which is searched
     *  @param ignoreCase if <code>true</code>, ignore case when comparing characters.
     *  @return the string of the stream which is between the specified strings <code>left</code> and <code>right</code>
     *          or <code>null</code> if the specified strings are not defined in the stream.
    **/
    public String between(String left, String right, boolean ignoreCase) {
      try {
        return StreamUtils.between(buffer, left, right, ignoreCase);
      } catch (IOException ioe) {
        return null;
      }
    }

    /**
     *  Closes all resources.
    **/
    void close() throws ServletException {
      if (buffer != null) {
        StreamUtils.close(buffer);
        buffer = null;
      }
      if (resource != null) {
        for (int i = 0; i < resource.length; ++i) {
          if (resource[i] instanceof InputStream) {
            StreamUtils.close((InputStream)resource[i]);
          } else if (resource[i] instanceof ResultSet) {
            try {
              ((ResultSet)resource[i]).close();
            } catch (SQLException e) {
            }
          } else if (resource[i] instanceof Statement) {
            DatabaseUtils.close((Statement)resource[i]);
          } else if (resource[i] instanceof Connection) {
            DatabaseUtils.close((Connection)resource[i]);
          } else {
            throw new ServletException("Unknow resource!");
          }
        }
        resource = null;
      }
    }

    BufferedInputStream buffer = null;
    Object[] resource = null;
  }
}