/*--
  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:
     H. Haddorp
     S. Wedeniwski
--*/

package zeta;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

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

import zeta.handler.GetHandler;
import zeta.handler.PostHandler;
import zeta.handler.database.ConnectionDriver;
import zeta.handler.statistic.AbstractHandler;
import zeta.util.DatabaseUtils;
import zeta.util.Parameter;
import zeta.util.Properties;
import zeta.util.StreamUtils;

/**
 *  Creates an HTTP servlet that dispatches HTTP GET and HTTP POST requests on the ZetaGrid site
 *  to specified handlers.
**/
public class ZetaServlet extends HttpServlet {

  /**
   *  Initialize lists to handle HTTP GET and HTTP POST requests.
  **/
  public ZetaServlet() {
    getHandlers = new ArrayList(20);
    postHandlers = new ArrayList(5);
  }

  private Hashtable parameters = new Hashtable(100);
  public String getInitParameter(String name) {
    String result = null;
    try {
      result = super.getInitParameter(name);
    } catch (NullPointerException e) {
    }
    if (result == null) {
      synchronized (parameters) {
        File file = new File(ZetaConstant.INIT_PARAMETER_PATH + name);
        if (file.exists()) {
          Long lastModified = new Long(file.lastModified());
          Object[] obj = (Object[])parameters.get(name);
          if (obj == null) {
            obj = new Object[2];
            parameters.put(name, obj);
          }
          if (!lastModified.equals(obj[0])) {
            obj[0] = lastModified;
            try {
              obj[1] = new String(StreamUtils.getFile(file.getAbsolutePath(), false, false));
            } catch (IOException ioe) {
              obj[1] = null;
            }
          }
          result = (String)obj[1];
        }
        try {
          Properties properties = new Properties();
          result = properties.get(name);
          if (properties.get("server_id", 1) != 1) {
            Object[] obj = new Object[2];
            obj[0] = new Long(System.currentTimeMillis());
            obj[1] = result;
            parameters.put(name, obj);
          }
        } catch (Exception e) {
        }
      }
    }
    return result;
  }

  public Enumeration getInitParameterNames() {
    if (hasSeparateFiles()) {
      File file = new File(ZetaConstant.INIT_PARAMETER_PATH + '.');
      String[] list = file.list();
      if (list != null) {
        for (int i = 0; i < list.length; ++i) {
          parameters.put(list[i], new Object[2]);
        }
      }
      return parameters.keys();
    } else {
      return super.getInitParameterNames();
    }
  }

  public int getInitParameter(String name, int defaultValue) {
    String value = getInitParameter(name);
    if (value != null) {
      try {
        return Integer.parseInt(value);
      } catch (NumberFormatException nfe) {
      }
    }
    return defaultValue;
  }

  public boolean hasSeparateFiles() {
    return (parameters.size() > 0);
  }

  /**
   *  Initialize the resources that are held for the life of the servlet.
   *  Setup the database connection driver and the configured handlers for HTTP GET and HTTP POST requests.
  **/
  public void init() throws ServletException {
    try {
      serverId = Integer.parseInt(getInitParameter("server.id"));
      // init DB
      String driver = getInitParameter("database.connection.driver");
      try {
        connectionDriver = new ConnectionDriver(driver,
                                                getInitParameter("database.connection.url"),
                                                getInitParameter("database.connection.username"),
                                                getInitParameter("database.connection.password"),
                                                getInitParameter("database.connection.poolsize", 10),
                                                getInitParameter("database.connection.total.connections", 50),
                                                getInitParameter("database.connection.check.query"));
        poolsize = 0;
      } catch (Throwable t) {
        throw new ServletException(t);
      }
      // generate logs
      normalLogging = true;
      Connection con  = null;
      Statement  stmt = null;
      FileWriter fout = null;
      try {
        con = getConnection();
        stmt = con.createStatement();
        String pathLog = Parameter.getValue(stmt, "path_log", Parameter.GLOBAL_PARAMETER, null, 3600000);
        if (pathLog != null && pathLog.length() > 0) {
          synchronized (stdLogFilename) {
            errorLogFilename = pathLog + "error_servlet.log";
            stdLogFilename = pathLog + "servlet.log";
            if (new File(stdLogFilename).length() >= MAX_LOG_FILESIZE) {
              rollOver(errorLogFilename, MAX_BACKUP_INDEX);
              rollOver(stdLogFilename, MAX_BACKUP_INDEX);
            }
            errorLog = new FileWriter(errorLogFilename, true);
            stdLog = new FileWriter(stdLogFilename, true);
            normalLogging = false;
            fout = new FileWriter(pathLog + "init.log", true);
            fout.write(logFormat.format(new Date())+'\n');
          }
        }
      } catch (Exception e) {
        normalLogging = false;
      } finally {
        StreamUtils.close(fout);
        DatabaseUtils.close(stmt);
        DatabaseUtils.close(con);
      }
      // init configured handlers
      Runtime runtime = Runtime.getRuntime();
      log("\n\n\n===========\ninit config, free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      numberOfStatisticHandlers = 0;
      try {
        List statisticHandlers = new ArrayList(20);
        Enumeration enum = getInitParameterNames();
        while (enum.hasMoreElements()) {
          String name = (String)enum.nextElement();
          addStatisticHandler(name, getInitParameter(name), statisticHandlers);
          log("add statistic handler " + name + ", free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
        }
        final int l = statisticHandlers.size();
        for (int i = 0; i < l; ++i) {
          getHandlers.add(statisticHandlers.get(i));
        }
        log("init " + numberOfStatisticHandlers + " statistic handlers, free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      } catch (Throwable t) {
        throw new ServletException(t);
      }
    } catch (ServletException se) {
      log(se);
      throw se;
    }
  }

  /**
   *  Destroy the resources that are held for the life of the servlet.
  **/
  public void destroy() {
    super.destroy();
  }

  /**
   *  Returns a connection to the back-end database. This connection must be closed.
   *  @return a connection to the back-end database.
  **/
  public Connection getConnection() throws ServletException {
    try {
      String url = getInitParameter("database.connection.url");
      synchronized (ZetaServlet.class) {
        if (url != null && !url.equals(connectionDriver.getURL())) {
          connectionDriver.setURL(url);
          poolsize = 0;
        }
        int sz = connectionDriver.getPoolsize();
        if (sz != poolsize) {
          log("current poolsize " + sz);
          poolsize = sz;
        }
      }
      return DriverManager.getConnection("jdbc:pool:zetagrid");
    } catch (Exception e) {
      throw new ServletException(e);
    }
  }

  /**
   *  Returns the ID of the server.
   *  @return the ID of the server.
  **/
  public int getServerId() {
    return serverId;
  }

  /**
   *  Sets the ID of the server.
  **/
  public void setServerId(int serverId) {
    this.serverId = serverId;
  }

  /**
   *  Returns the number of defined statistic handlers.
   *  @return the number of defined statistic handlers.
  **/
  public int getNumberOfStatisticHandlers() {
    return numberOfStatisticHandlers;
  }

  /**
   *  Returns the i-th defined statistic handler.
   *  @param i  index
   *  @return the i-th defined statistic handler.
  **/
  public Class getStatisticHandlerClass(int i) {
    int idx = -1;
    String handlerStatistic = "zeta.handler.statistic"; // ToDo: not fix
    final int l = getHandlers.size();
    for (int j = 0; j < l; ++j) {
      Object[] obj = (Object[])getHandlers.get(j);
      if (obj[2].getClass().getName().startsWith(handlerStatistic) && ++idx == i) {
        return obj[2].getClass();
      }
    }
    return null;
  }

  /**
   *  Searches the handler name by the specified class of this statistic handler.
   *  @param  handler  class of a statistic handler
   *  @return name of the handler; null if no handler is found.
  **/
  public String getStatisticHandlerName(Class handler) {
    Iterator iter = getHandlers.iterator();
    while (iter.hasNext()) {
      Object[] obj = (Object[])iter.next();
      if (handler == obj[2].getClass()) {
        return (String)obj[1];
      }
    }
    return null;
  }

  /**
   *  Searches the handler object by the specified class of this statistic handler.
   *  @param  handler  class of a statistic handler
   *  @return object of the handler; null if no handler is found.
  **/
  public AbstractHandler getStatisticHandler(Class handler) {
    Iterator iter = getHandlers.iterator();
    while (iter.hasNext()) {
      Object[] obj = (Object[])iter.next();
      if (handler == obj[2].getClass()) {
        return (AbstractHandler)obj[2];
      }
    }
    return null;
  }

  /**
   *  Searches the handler address by the specified class of this GET handler.
   *  @param  handler  class of a statistic handler
   *  @return HTTP address of the handler; null if no handler is found.
  **/
  public String getHandlerAddress(Class handler) {
    Iterator iter = getHandlers.iterator();
    while (iter.hasNext()) {
      Object[] obj = (Object[])iter.next();
      if (handler == obj[2].getClass()) {
        return (hasSeparateFiles())? "/servlet/service" + obj[0] : "/zeta/service" + obj[0];   // ToDo: not fix (use "url-pattern" of "web.xml")
      }
    }
    return null;
  }

  /**
   *  Called by the server to allow this servlet to handle a GET request.
   *  @param  req  contains the request the client has made of the servlet.
   *  @param  resp contains the response the servlet sends to the client.
   *  @exception  IOException  if an I/O error occurs when the servlet handles the GET request.
   *  @exception  javax.servlet.ServletException if the request for the GET could not be handled.
   *  @see javax.servlet.http.HttpServlet#doGet
  **/
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Runtime runtime = Runtime.getRuntime();
    int localId = ++id;
    try {
      GetHandler handler = (GetHandler)findHandler(getHandlers, req.getPathInfo());
      if (handler != null) {
        log("start " + localId + ':' + req.getPathInfo() + ' ' + req.getRemoteHost() + " (" + req.getRemoteAddr() + ") free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
        handler.doGet(req, resp);
        log("stop " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      }
    } catch (IOException ioe) {
      log("exception " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      log(ioe);
      throw ioe;
    } catch (ServletException se) {
      log("exception " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      log(se);
      throw se;
    } catch (Throwable t) {
      log("exception " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      log(t);
      throw new ServletException(t);
    }
  }

  /**
   *  Called by the server to allow this servlet to handle a POST request.
   *  @param  req  contains the request the client has made of the servlet.
   *  @param  resp contains the response the servlet sends to the client.
   *  @exception  IOException  if an I/O error occurs when the servlet handles the POST request.
   *  @exception  javax.servlet.ServletException if the request for the POST could not be handled.
   *  @see javax.servlet.http.HttpServlet#doPost
  **/
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    int localId = ++id;
    Runtime runtime = Runtime.getRuntime();
    try {
      PostHandler handler = (PostHandler)findHandler(postHandlers, req.getPathInfo());
      if (handler != null) {
        log("start " + localId + ':' + req.getPathInfo() + ' ' + req.getRemoteHost() + " (" + req.getRemoteAddr() + ") free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
        handler.doPost(req, resp);
        log("stop " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      }
    } catch (IOException ioe) {
      log("exception " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      log(ioe);
      throw ioe;
    } catch (ServletException se) {
      log("exception " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      log(se);
      throw se;
    } catch (Throwable t) {
      log("exception " + localId + " free:" + runtime.freeMemory() + " total=" + runtime.totalMemory() + " max=" + runtime.maxMemory());
      log(t);
      throw new ServletException(t);
    }
  }

  /**
   *  Adds a statistic handler.
  **/
  public void addStatisticHandler(String name, String parameter, List statisticHandlers) throws Exception {
    String handlerGet = "handler.get.";
    String handlerPost = "handler.post.";
    String handlerStatistic = "zeta.handler.statistic."; // ToDo: not fix
    int idx1 = name.indexOf(' ');
    int idx2 = name.indexOf(' ', idx1+1);
    if (idx1 <= 0) {
      idx1 = name.length();
    }
    if (name.startsWith(handlerGet)) {
      Object[] handler = { "/" + name.substring(handlerGet.length(), idx1), (idx1 == name.length())? "" : name.substring(idx2+1), getInstance(Class.forName(parameter)) };
      if (parameter.startsWith(handlerStatistic) && idx1 > 0 && idx1+1 < idx2) {
        ++numberOfStatisticHandlers;
        int posHandlerStatistic = Integer.parseInt(name.substring(idx1+1, idx2));
        if (statisticHandlers == null) {
          getHandlers.add(handler);
        } else {
          while (posHandlerStatistic >= statisticHandlers.size()) {
            statisticHandlers.add(null);
          }
          statisticHandlers.set(posHandlerStatistic, handler);
        }
      } else {
        getHandlers.add(handler);
      }
    } else if (name.startsWith(handlerPost)) {
      Object[] handler = { "/" + name.substring(handlerPost.length(), idx1), (idx1 == name.length())? "" : name.substring(idx2+1), getInstance(Class.forName(parameter)) };
      postHandlers.add(handler);
    }
  }

  public void log(String msg) {
    if (normalLogging) {
      super.log(msg);
    } else {
      synchronized (stdLogFilename) {
        if (new File(stdLogFilename).length() >= MAX_LOG_FILESIZE) {
          StreamUtils.close(stdLog);
          stdLog = null;
          StreamUtils.close(errorLog);
          errorLog = null;
          rollOver(stdLogFilename, MAX_BACKUP_INDEX);
          rollOver(errorLogFilename, MAX_BACKUP_INDEX);
          initLogFile();
        }
        if (stdLog != null) {
          try {
            stdLog.write(logFormat.format(new Date()) + ' ' + msg + '\n');
          } catch (Exception e) {
            super.log(msg);
          }
        } else {
          initLogFile();
          if (stdLog != null) {
            try {
              stdLog.write(logFormat.format(new Date()) + ' ' + msg + '\n');
            } catch (Exception e) {
              super.log(msg);
            }
          } else {
            super.log(msg);
          }
        }
      }
    }
  }

  public void log(Throwable t) {
    if (!normalLogging) {
      synchronized (stdLogFilename) {
        if (new File(errorLogFilename).length() >= MAX_LOG_FILESIZE) {
          StreamUtils.close(stdLog);
          stdLog = null;
          StreamUtils.close(errorLog);
          errorLog = null;
          rollOver(stdLogFilename, MAX_BACKUP_INDEX);
          rollOver(errorLogFilename, MAX_BACKUP_INDEX);
          initLogFile();
        }
        if (errorLog != null) {
          try {
            errorLog.write(logFormat.format(new Date())+'\n');
            t.printStackTrace(new PrintWriter(errorLog));
            errorLog.write("\n");
          } catch (Exception e) {
          }
        } else {
          initLogFile();
          if (errorLog != null) {
            try {
              errorLog.write(logFormat.format(new Date())+'\n');
              t.printStackTrace(new PrintWriter(errorLog));
              errorLog.write("\n");
            } catch (Exception e) {
            }
          }
        }
      }
    }
  }

  private void initLogFile() {
    Connection con  = null;
    Statement  stmt = null;
    try {
      if (errorLogFilename == null || errorLogFilename.length() == 0) {
        con = getConnection();
        stmt = con.createStatement();
        String pathLog = Parameter.getValue(stmt, "path_log", Parameter.GLOBAL_PARAMETER, null, 3600000);
        if (pathLog != null && pathLog.length() > 0) {
          stdLogFilename = pathLog + "servlet.log";
          errorLogFilename = pathLog + "error_servlet.log";
        }
      }
      if (errorLogFilename != null && errorLogFilename.length() > 0 && errorLog == null) {
        errorLog = new FileWriter(errorLogFilename, true);
      }
      if (stdLogFilename != null && stdLogFilename.length() > 0 && stdLog == null) {
        stdLog = new FileWriter(stdLogFilename, true);
      }
    } catch (IOException e) {
      normalLogging = true;
    } catch (Exception e) {
    } finally {
      DatabaseUtils.close(stmt);
      DatabaseUtils.close(con);
    }
  }

  /**
   *  Implements the usual roll over behaviour of log files.
  **/
  private void rollOver(String filename, int maxBackupIndex) {
    if (maxBackupIndex > 0) {
      File file = new File(filename + '.' + maxBackupIndex);
      if (file.exists()) {
        file.delete();
      }
      for (int i = maxBackupIndex-1; i >= 1; --i) {
        File f = new File(filename + '.' + i);
        if (f.exists()) {
          f.renameTo(file);
        }
        file = f;
      }
      new File(filename).renameTo(file);
    }
  }

  /**
   *  Searches the handler by the specified name.
   *  @param  handlers  container with the handler
   *  @param  name  name of the handler which is searched
   *  @return instance of the handler which name starts with the specified name; null if no handler is found.
  **/
  private Object findHandler(List handlers, String name) {
    if (name != null) {
      Iterator iter = handlers.iterator();
      while (iter.hasNext()) {
        Object[] obj = (Object[])iter.next();
        if (name.equals((String)obj[0])) {
          return obj[2];
        }
      }
    }
    return null;
  }

  /**
   *  Builds a new instance of the specified handler class 
   *  @param  handlerClass  class of the handler
  **/
  private Object getInstance(Class handlerClass) throws Exception, Error {
    try {
      Constructor c = handlerClass.getDeclaredConstructor(new Class[] { ZetaServlet.class });
      return c.newInstance(new Object[] { this });
    } catch(NoSuchMethodException e) {
      return handlerClass.newInstance();
    }
  }

  /**
   *  Map to handle HTTP GET requests.
  **/
  private List getHandlers;

  /**
   *  Map to handle HTTP POST requests.
  **/
  private List postHandlers;

  /**
   *  Number of handlers which handle HTTP GET requests for statistics.
  **/
  private int numberOfStatisticHandlers = 0;

  /**
   *  ID of the server.
  **/
  private int serverId = 0;

  /**
   *  ID for logging.
  **/
  private int id = 0;

  private int poolsize = 0;
  private ConnectionDriver connectionDriver = null;

  private String stdLogFilename = "";
  private Writer stdLog = null;
  private String errorLogFilename = "";
  private Writer errorLog = null;
  private SimpleDateFormat logFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss", Locale.GERMANY);
  private boolean normalLogging = true;
  private final static long MAX_LOG_FILESIZE = 5*1024*1024;
  private final static int MAX_BACKUP_INDEX = 3;
}