package hirondelle.web4j; 

import static hirondelle.web4j.util.Consts.NEW_LINE;
import hirondelle.web4j.action.Action;
import hirondelle.web4j.action.ResponsePage;
import hirondelle.web4j.database.ConnectionSource;
import hirondelle.web4j.database.DAOException;
import hirondelle.web4j.database.DbConfig;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.BadRequestException;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.readconfig.Config;
import hirondelle.web4j.readconfig.ConfigReader;
import hirondelle.web4j.request.RequestParser;
import hirondelle.web4j.request.RequestParserImpl;
import hirondelle.web4j.security.ApplicationFirewall;
import hirondelle.web4j.security.ApplicationFirewallImpl;
import hirondelle.web4j.security.FetchIdentifierOwner;
import hirondelle.web4j.security.UntrustedProxyForUserId;
import hirondelle.web4j.util.Stopwatch;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.WebUtil;
import hirondelle.web4j.webmaster.TroubleTicket;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.jsp.JspFactory;

/**
  Single point of entry for serving dynamic pages.
 
  <P>The application can serve content both directly (by simple, direct reference to 
  a JSP's URL), and indirectly, through this <tt>Controller</tt>.
 
 <P>Like almost all servlets, this class is safe for multi-threaded environments. 
 
  <P>Validates user input and request parameters, interacts with a datastore, 
  and places problem domain model objects in scope for eventual rendering by a JSP. 
  Performs either a forward or a redirect, according to the instructions of the 
  {@link Action}.
 
  <P>Emails are sent to the webmaster when :
 <ul>
  <li>an unexpected problem occurs (the email will include extensive diagnostic 
  information, including a stack trace)
  <li>servlet response times degrade to below a configured level
 </ul>
 
 <P>This class is in a distinct package for two reasons :
 <ul>
 <li>to make it easier to find, since it is at the very top of the hierarchy
 <li>to force the <tt>Controller</tt> to use only the public aspects of 
  the <tt>ui</tt> package. This ensures it remains at a high level of abstraction.
 </ul>
 
  <P>There are key-names defined in this class (see below). Their names need to be 
  long-winded (<tt>web4j_key_for_...</tt>), unfortunately, in order to 
  avoid conflict with other tools, including your application. 
*/
public class Controller extends HttpServlet {

  /**
   Name and version number of the WEB4J API. 
   
   <P>Value: {@value}.
   <P>Upon startup, this item is logged at <tt>CONFIG</tt> level. (This item is  
   is simply a hard-coded field in this class. It is not configured in <tt>web.xml</tt>.) 
  */
  public static final String WEB4J_VERSION = "WEB4J/4.10.0";
  
  /**
   Key name for the application's character encoding, placed in application scope
   as a <tt>String</tt> upon startup. This character encoding (charset) is set 
   as an HTTP header for every reponse.
   
   <P>Key name: {@value}.
   <P>Configured in <tt>web.xml</tt>. The value <tt>UTF-8</tt> is highly recommended.
  */
  public static final String CHARACTER_ENCODING = "web4j_key_for_character_encoding";

  /**
   Key name for the webmaster email address, placed in application scope
   as a <tt>String</tt> upon startup.
   
   <P>Key name: {@value}.
   <P>Configured in <tt>web.xml</tt>.
  */
  public static final String WEBMASTER = "web4j_key_for_webmaster";
  
  /**
   Key name for the default {@link Locale}, placed in application scope
   as a <tt>Locale</tt> upon startup.
   
   <P>Key name: {@value}.
   <P>The application programmer is encouraged to use this key for any 
   <tt>Locale</tt> stored in <em>session</em> scope : the <em>default</em> implementation 
   of {@link hirondelle.web4j.request.LocaleSource} will always search for this 
   key in increasingly larges scopes. Thus, the default mechanism will 
   automatically use the user-specific <tt>Locale</tt> as an override to 
   the default one.
   
   <P>Configured in <tt>web.xml</tt>.
  */
  public static final String LOCALE = "web4j_key_for_locale";
  
  /**
   Key name for the default {@link TimeZone}, placed in application scope
   as a <tt>TimeZone</tt> upon startup.
   
   <P>Key name: {@value}.
   <P>The application programmer is encouraged to use this key for any 
   <tt>TimeZone</tt> stored in <em>session</em> scope : the <em>default</em> implementation 
   of {@link hirondelle.web4j.request.TimeZoneSource} will always search for this 
   key in increasingly larges scopes. Thus, the default mechanism will 
   automatically use the user-specific <tt>TimeZone</tt> as an override to 
   the default one.
   
   <P>Configured in <tt>web.xml</tt>.
  */
  public static final String TIME_ZONE = "web4j_key_for_time_zone";

  /**
   Key name for the most recent {@link TroubleTicket}, placed in application scope when a 
   problem occurs.
   <P>Key name: {@value}.
  */
  public static final String MOST_RECENT_TROUBLE_TICKET = "web4j_key_for_most_recent_trouble_ticket";
  
  /**
   Key name for the startup time, placed in application scope as a {@link DateTime} upon startup.
   <P>Key name: {@value}.
  */
  public static final String START_TIME = "web4j_key_for_start_time";
  
  /**
   Key name for the URI for the current request, placed in request scope as a <tt>String</tt>.
   
   <P>Key name: {@value}.
   <P>Somewhat bizarrely, the servlet API does not allow direct access to this item.
  */
  public static final String CURRENT_URI = "web4j_key_for_current_uri";
  
  /**
   Perform operations to be executed only upon startup of 
   this application, and not during its regular operation. 
   
   <P>Operations include :
   <ul>
   <li>log version and configuration information
   <li>distribute configuration information in <tt>web.xml</tt> to the various
   parts of WEB4J
   <li>place an {@link ApplicationInfo} object into application scope
   <li>place the configured character encoding into application scope, for use in JSPs
   <li>call {@link StartupTasks#startApplication(ServletConfig, String)}, to 
   allow the application to perform its own startup tasks
   <li>perform various validations
   </ul>
   
   <P>One or more of the application's databases may not be running when 
   the web application starts. Upon startup, this Controller first queries each database 
   for simple name and version information. If that query fails, then the database is 
   assumed to be "down", and the app's implementation of {@link StartupTasks} 
   (which usually fetches code tables from the database) is not called. 
   
   <P>The web app, however, will not terminate. Instead, this Controller will keep 
   attempting to connect for each incoming request. When all databases are 
   determined to be healthy, the Controller will perform the database initialization 
   tasks it usually performs upon startup, and the app will then function normally.
   
   <P>If the database subsequently goes down again, then this Controller will not take 
   any special action. Instead, the container's connection pool should be configured to 
   attempt to reconnect automatically on the application's behalf. 
  */
  @Override public final void init(ServletConfig aConfig) throws ServletException {
    super.init(aConfig);
    fServletConfig = aConfig;
    try {
      Stopwatch stopwatch = new Stopwatch();
      stopwatch.start();
      
      Map<String, String> configMap = asMap(aConfig);
      //the Config class stores settings internally as static items
      //after this point, any class can get its config data just by using Config as a normal object
      Config.init(configMap);
      fConfig = new Config();
      
      //any use of logging before this line will fail
      //first load of application-specific classes; configures and begins logging as well
      BuildImpl.init(configMap);
    
      displaySystemProperties();
      displayConfigInfo(aConfig); //all items, for both app and framework
      setCharacterEncodingAndPutIntoAppScope(aConfig);
      putWebmasterEmailAddressIntoAppScope(aConfig);
      putDefaultLocaleIntoAppScope(aConfig);
      putDefaultTimeZoneIntoAppScope(aConfig);
      putStartTimeIntoAppScope(aConfig);
      fLogger.fine("System properties and first app-scope items completed " + stopwatch + " after start.");
      
      /* 
       Implementation Note
       There are strong order dependencies here: ConfigReader is used later in the 
       init of SqlStatement, for example.
      */
      ConfigReader.init(aConfig.getServletContext());
      WebUtil.init(aConfig);
      
      //This will be the first loading of application-specific classes.
      //This will cause static fields to be initialized.
      ApplicationInfo appInfo = BuildImpl.forApplicationInfo();
      displayVersionInfo(aConfig, appInfo);
      placeAppInfoIntoAppScope(aConfig, appInfo);
  
      TroubleTicket.init(aConfig);
      
      fLogger.config("Calling ConnectionSource.init(ServletConfig).");
      ConnectionSource connSource = BuildImpl.forConnectionSource();
      connSource.init(configMap);
      fLogger.fine("Init of internal classes, ConnectionSource completed " + stopwatch + " after start.");
      Config.checkDbNamesInSettings(BuildImpl.forConnectionSource().getDatabaseNames());
  
      tryDatabaseInitAndStartupTasks(connSource);
      fLogger.fine("Database init and startup tasks " + stopwatch + " after start.");
      
      CheckModelObjects checkModelObjects = new CheckModelObjects();
      checkModelObjects.performChecks();
      stopwatch.stop();
      fLogger.fine("Cross-Site Scripting scan completed " + stopwatch + " after start.");
      
      fLogger.config("*** SUCCESS : STARTUP COMPLETED SUCCESSFULLY for " + appInfo + ". Total startup time : " + stopwatch );
    }
    catch (AppException ex) {
      throw new ServletException(ex);
    } 
  }

  /** Log the name and version of the application. */
  @Override public void destroy() {
    ApplicationInfo appInfo = BuildImpl.forApplicationInfo();
    fLogger.config("Shutting Down Controller for " + appInfo.getName() + "/" + appInfo.getVersion());
  }

  /** Call {@link #processRequest}.  */
  @Override public final void doGet(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
    logClasses(aRequest, aResponse);
    processRequest(aRequest, aResponse);
  }

  /** Call {@link #processRequest}.  */
  @Override public final void doPost(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
    logClasses(aRequest, aResponse);
    processRequest(aRequest, aResponse);
  }

  /** Call {@link #processRequest}.  PUT can be called by <tt>XmlHttpRequest</tt>. */
  @Override public final void doPut(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
    logClasses(aRequest, aResponse);
    processRequest(aRequest, aResponse);
  }
  
  /** Call {@link #processRequest}.  DELETE can be called by <tt>XmlHttpRequest</tt>. */
  @Override public final void doDelete(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
    logClasses(aRequest, aResponse);
    processRequest(aRequest, aResponse);
  }

  /**
   Handle all HTTP requests for <tt>GET</tt>, <tt>POST</tt>, <tt>PUT</tt>, and <tt>DELETE</tt> requests.
   All of these HTTP methods will funnel through this Java method; any other methods will be handled by the container.
   If a subclass needs to know the underlying HTTP method, then it must call {@link HttpServletRequest#getMethod()}.  
   
   <P>This method can be overridden, if desired. The great majority of applications will not need 
   to override this method. 
   
   <P>Operations include :
   <ul>
   <li>set the request character encoding (using the value configured in <tt>web.xml</tt>)
   <li>set the <tt>charset</tt> HTTP header for the response (using the value configured in <tt>web.xml</tt>)
   <li>react to a successful user login, using the configured implementation of {@link hirondelle.web4j.security.LoginTasks}
   <li>get an instance of {@link RequestParser}
   <li>get its {@link Action}, and execute it 
   <li>check for an ownership constraint (see {@link UntrustedProxyForUserId})
   <li>perform either a forward or a redirect to the Action's  {@link hirondelle.web4j.action.ResponsePage}
   <li>if an unexpected problem occurs, create a {@link TroubleTicket}, log it, and 
   email it to the webmaster email address configured in <tt>web.xml</tt>
   <li>if the response time exceeds a configured threshold, build a 
   {@link TroubleTicket}, log it, and email it to the webmaster address configured in <tt>web.xml</tt>
   </ul>
  */
  protected void processRequest(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    
    aRequest.setCharacterEncoding(fConfig.getCharacterEncoding()); 
    aResponse.setCharacterEncoding(fConfig.getCharacterEncoding());
    
    addCurrentUriToRequest(aRequest, aResponse);
    RequestParser requestParser = RequestParser.getInstance(aRequest, aResponse);
    try {
      LoginTasksHelper loginHelper = new LoginTasksHelper();
      loginHelper.reactToNewLogins(aRequest);
      Action action = requestParser.getWebAction();
      ApplicationFirewall appFirewall = BuildImpl.forApplicationFirewall();
      appFirewall.doHardValidation(action, requestParser);
      logAttributesForAllScopes(aRequest);
      recheckBadDatabasesAndFinishStartup();
      ResponsePage responsePage = checkOwnershipThenExecuteAction(action, requestParser);
      if ( responsePage.hasBinaryData() ) {
        fLogger.fine("Serving binary data. Controller not performing a forward or redirect.");
      }
      else {
        if ( responsePage.getIsRedirect() ) {
          redirect(responsePage, aResponse);
        }
        else {
          forward(responsePage, aRequest, aResponse);
        }
      }
      stopwatch.stop();
      if ( stopwatch.toValue() >= fConfig.getPoorPerformanceThreshold() ) {
        logAndEmailPerformanceProblem(stopwatch.toString(), aRequest);
      }
    }
    catch (BadRequestException ex){
      //NOTE : sendError() commits the response.
      if( Util.textHasContent(ex.getErrorMessage()) ){
        aResponse.sendError(ex.getStatusCode(), ex.getErrorMessage());      
      }
      else {
        aResponse.sendError(ex.getStatusCode());      
      }
    }
    catch (Throwable ex) {
      //Includes AppException, Bugs, or rare conditions, for example datastore failure
      logAndEmailSeriousProblem(ex, aRequest);
      aResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
    }
  }

  /**
   Change the {@link ResponsePage} according to {@link Locale}.
   
   <P>This overridable default implementation does nothing, and returns <tt>null</tt>.
   If the return value of this method is <tt>null</tt>, then the nominal <tt>ResponsePage</tt>
   will be used without alteration. If the return value of this method is not <tt>null</tt>,
   then it will be used to override the nominal <tt>ResponsePage</tt>.
   
   <P>This method is intended for applications that use different JSPs for different Locales.
   For example, if the nominal response is a forward to <tt>Blah_en.jsp</tt>, and the "real"
   response should be <tt>Blah_fr.jsp</tt>, then this method can be overridden to return the 
   appropriate {@link ResponsePage}. <span class="highlight">This method is called only for 
   forward operations. If it is overridden, then its return value must also correspond to a forward 
   operation.</span>
   
   <P><span class="highlight">This style of implementing translation is not recommended.</span>
   Instead, please use the services of the <tt>hirondelle.web4j.ui.translate</tt> package. 
  */
  protected ResponsePage swapResponsePage(ResponsePage aResponsePage, Locale aLocale){
    return null; //does nothing
  }
  
  /**
   Inform the webmaster of an unexpected problem with the deployed application.
   
   <P>Typically called when an unexpected <tt>Exception</tt> occurs in 
   {@link #processRequest}. Uses {@link TroubleTicket#mailToRecipients()}.
   
    <P>Also, stores the trouble ticket in application scope, for possible 
    later examination. 
  */
  protected final void logAndEmailSeriousProblem (Throwable aException, HttpServletRequest aRequest) {
    TroubleTicket troubleTicket = new TroubleTicket(aException, aRequest);
    fLogger.severe("TOP LEVEL CATCHING Throwable");
    fLogger.severe( troubleTicket.toString() ); 
    log("SERIOUS PROBLEM OCCURRED.");
    log( troubleTicket.toString() );
    fServletConfig.getServletContext().setAttribute(MOST_RECENT_TROUBLE_TICKET, troubleTicket);
    try {
      troubleTicket.mailToRecipients();
    }
    catch (AppException exception){
      fLogger.severe("Unable to send email: " + exception.getMessage());
    }
  }

  /**
   Inform the webmaster of a performance problem.
   
   <P>Called only when the response time of a request is above the threshold 
   value configured in <tt>web.xml</tt>.
   
   <P>Builds a <tt>Throwable</tt> with a description of the problem, then creates and 
   emails a {@link TroubleTicket} to the webmaster.
   
   @param aMilliseconds response time of a request in milliseconds
  */
  protected final void logAndEmailPerformanceProblem(String aMilliseconds, HttpServletRequest aRequest)  {
    String message = 
      "Response time of web application exceeds configured performance threshold." + NEW_LINE + 
      "Time : " + aMilliseconds
    ;
    Throwable ex = new Throwable(message);
    TroubleTicket troubleTicket = new TroubleTicket(ex, aRequest);
    fLogger.severe("Poor response time : " + aMilliseconds);
    fLogger.severe( troubleTicket.toString() ); 
    log("Poor response time : " + aMilliseconds + " milliseconds");
    log( troubleTicket.toString() );
    try {
      troubleTicket.mailToRecipients();
    }
    catch(AppException exception){
      fLogger.severe("Unable to send email: " + exception.getMessage());
    }
  }

  // PRIVATE 

  /**
   Mutable field. Must be accessed in a thread-safe way after init finished.
   Assumes that all databases are initially down; each is removed from this set, when it 
   has been detected as being up. Possibly empty.
  */
  private Set<String> fBadDatabases = new LinkedHashSet<String>();
  
  /** Mutable field. Must be accessed in a thread-safe way after init finished. */
  private StartupTasks fStartupTasks;
  
  /**
   The config must be saved. It is not accessible from a request, or from the context. 
   It may be needed after startup, should no db connections be initially available.  
  */
  private static ServletConfig fServletConfig;
  private Config fConfig; 
  
//  /** Item configured in web.xml.  */
//  private static final InitParam fPoorPerformanceThreshold = new InitParam(
//    "PoorPerformanceThreshold", "20"
//  ); 
//  /**
//   If any request takes longer than this many nanoseconds to be processed, then 
//   an email is sent to the webmaster. The web.xml states this configured time in 
//   seconds, but nanoseconds is used by this class to perform the comparison.
//  */
//  private static long fPOOR_PERFORMANCE_THRESHOLD;
//  
//  /** Item configured in web.xml.  */
//  private static final InitParam fCharacterEncoding = new InitParam(
//    "CharacterEncoding", "UTF-8"
//  ); 
//  /**
//   Character encoding for this application. 
//   
//   <P>The Controller will assume that every request will have this 
//   character encoding. In addition, this value will be placed in an 
//   application scope attribute named {@link Controller#CHARACTER_ENCODING}; 
//  */
//  private static String fCHARACTER_ENCODING;
//  
//  /** Item configured in web.xml.  */
//  private static final InitParam fDefaultLocale = new InitParam(
//    "DefaultLocale", "en"
//  );
//  /**
//   Default Locale for this application. 
//   
//   <P>Placed in an app scope attribute named {@link Controller#LOCALE} (as a 
//   Locale object, not as a String). 
//  */
//  private static String fDEFAULT_LOCALE;
//  
//  private static final InitParam fDefaultTimeZone = new InitParam(
//    "DefaultUserTimeZone", "GMT"
//  );
//  /**
//   Default TimeZone for this application. 
//   
//   <P>Placed in an app scope attribute named {@link Controller#TIME_ZONE} (as a 
//   TimeZone object, not as a String). 
//  */
//  private static String fDEFAULT_TIME_ZONE;
//
//  /** Item configured in web.xml.  */
//  private static final InitParam fWebmaster = new InitParam("Webmaster");
//  
//  /**
//  Webmaster email address for this application. 
//   
//   <P>This value will be placed in an application scope attribute 
//   named {@link Controller#WEBMASTER}; 
//  */
//  private static String fWEBMASTER;
  
  private static final boolean DO_NOT_CREATE_SESSION = false;
  
  private static final String OWNERSHIP_NO_SESSION =  
    "According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " + 
    "However, this request has no session, and ownership constraints work only when the user has logged in." 
  ;
 
  private static final String OWNERSHIP_NO_LOGIN = 
    "According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " + 
    "A session exists, but there is no valid login, and ownership constraints work only when the user has logged in." 
  ;
  
  private static final Logger fLogger = Util.getLogger(Controller.class);

  private void logClasses(HttpServletRequest aRequest, HttpServletResponse aResponse) {
    fLogger.finest("Request class :" + aRequest.getClass());
    fLogger.finest("Response class :" + aResponse.getClass());
  }

  private void redirect (
    ResponsePage aDestinationPage, HttpServletResponse aResponse
  ) throws IOException {
    String urlWithSessionID = aResponse.encodeRedirectURL(aDestinationPage.toString());
    fLogger.fine("REDIRECT: " + Util.quote(urlWithSessionID));
    aResponse.sendRedirect( urlWithSessionID );
  }

  private void forward (
    ResponsePage aResponsePage, HttpServletRequest aRequest, HttpServletResponse aResponse
  ) throws ServletException, IOException {
    ResponsePage responsePage = possiblyAlterForLocale(aResponsePage, aRequest);
    RequestDispatcher dispatcher = aRequest.getRequestDispatcher(responsePage.toString());
    fLogger.fine("Forward : " + responsePage);
    dispatcher.forward(aRequest, aResponse);
  }
  
  private ResponsePage possiblyAlterForLocale(ResponsePage aNominalForward, HttpServletRequest aRequest){
    Locale locale = BuildImpl.forLocaleSource().get(aRequest);
    ResponsePage langSpecificForward = swapResponsePage(aNominalForward, locale);
    if ( langSpecificForward != null && langSpecificForward.getIsRedirect() ){
      throw new RuntimeException(
        "A 'forward' ResponsePage has been altered for Locale, but is no longer a forward : " + langSpecificForward
      );
    }
    return (langSpecificForward != null) ?  langSpecificForward : aNominalForward;
  }
  
  private Map<String, String> asMap(ServletConfig aConfig){
    Map<String, String> result = new LinkedHashMap<String, String>();
    Enumeration initParamNames = aConfig.getInitParameterNames();
    while ( initParamNames.hasMoreElements() ){
      String name = (String)initParamNames.nextElement();
      String value = aConfig.getInitParameter(name);
      result.put(name, value);
    }
    return result;
  }
  
  private void displaySystemProperties(){
    String sysProps = Util.logOnePerLine(System.getProperties());
    fLogger.config("System Properties " + sysProps);
  }
  
  private void displayVersionInfo(ServletConfig aConfig, ApplicationInfo aAppInfo){
    ServletContext context = aConfig.getServletContext();
    Map<String, String> info = new LinkedHashMap<String, String>();
    info.put("Application", aAppInfo.getName() + "/" +  aAppInfo.getVersion());
    info.put("Server", context.getServerInfo());
    info.put("Servlet API Version", context.getMajorVersion() + "." +  context.getMinorVersion() );
    if( JspFactory.getDefaultFactory() != null) {
      //this item is null when outside the normal runtime environment.
      info.put("Java Server Page API Version", JspFactory.getDefaultFactory().getEngineInfo().getSpecificationVersion());
    }
    info.put("Java Runtime Environment (JRE)", System.getProperty("java.version"));
    info.put("Operating System", System.getProperty("os.name") + "/" + System.getProperty("os.version") );
    info.put("WEB4J Version", WEB4J_VERSION);
    fLogger.config("Versions" + Util.logOnePerLine(info));
  }
  
  private void displayConfigInfo(ServletConfig aConfig){
    fLogger.config(
      "Context Name : " + Util.quote(aConfig.getServletContext().getServletContextName()) 
    );
    
    Enumeration ctxParamNames = aConfig.getServletContext().getInitParameterNames();
    Map<String, String> ctxParams = new LinkedHashMap<String, String>();
    while ( ctxParamNames.hasMoreElements() ){
      String name = (String)ctxParamNames.nextElement();
      String value = aConfig.getServletContext().getInitParameter(name);
      ctxParams.put(name, value);
    }
    fLogger.config( "Context Params : " + Util.logOnePerLine(ctxParams));
    
    Enumeration initParamNames = aConfig.getInitParameterNames();
    Map<String, String> initParams = new LinkedHashMap<String, String>();
    while ( initParamNames.hasMoreElements() ){
      String name = (String)initParamNames.nextElement();
      String value = aConfig.getInitParameter(name);
      initParams.put(name, value);
    }
    fLogger.config( "Servlet Params : " + Util.logOnePerLine(initParams));
  }

  private void setCharacterEncodingAndPutIntoAppScope(ServletConfig aConfig){
    aConfig.getServletContext().setAttribute(CHARACTER_ENCODING, fConfig.getCharacterEncoding());
  }
  
  private void putWebmasterEmailAddressIntoAppScope(ServletConfig aConfig){
    aConfig.getServletContext().setAttribute(WEBMASTER, fConfig.getWebmaster());
  }

  private void putDefaultLocaleIntoAppScope(ServletConfig aConfig){
    aConfig.getServletContext().setAttribute(LOCALE, fConfig.getDefaultLocale());
  }
  
  private void putDefaultTimeZoneIntoAppScope(ServletConfig aConfig){
    aConfig.getServletContext().setAttribute(TIME_ZONE, fConfig.getDefaultUserTimeZone());
  }
  
  private void putStartTimeIntoAppScope(ServletConfig aConfig){
    aConfig.getServletContext().setAttribute(START_TIME, DateTime.now(fConfig.getDefaultUserTimeZone()));
  }
  
  private void placeAppInfoIntoAppScope(ServletConfig aConfig, ApplicationInfo aAppInfo){
    aConfig.getServletContext().setAttribute(
      ApplicationInfo.KEY, aAppInfo
    );
  }

  /**
  Log attributes stored in the various scopes.
  */
  private void logAttributesForAllScopes(HttpServletRequest aRequest){
    //the following style is conservative, and is meant to avoid calls which may be expensive
    //remember that the level of the HANDLER affects whether the item is emitted as well.
    if( fLogger.getLevel() != null &&  fLogger.getLevel().equals(Level.FINEST) ) {
      fLogger.finest("Application Scope Items " + Util.logOnePerLine(getApplicationScopeObjectsForLogging(aRequest)));
      fLogger.finest("Session Scope Items " + Util.logOnePerLine(getSessionScopeObjectsForLogging(aRequest)));
      fLogger.finest("Request Parameter Names " + Util.logOnePerLine(getRequestParamNamesForLogging(aRequest)));
    }
  }

  /**
  Return Map of name-value pairs of items in application scope. 
 
  <P>In many cases, the actual data will be quite lengthy. For instance, translation data is often 
  sizeable. Thus, this should be called only when logging at the highest level. 
  Logging should only be performed after the {@link ApplicationFirewall} has executed.
  */
  private Map<String, Object> getApplicationScopeObjectsForLogging(HttpServletRequest aRequest){
    Map<String, Object> result = new LinkedHashMap<String, Object>();
    HttpSession session = aRequest.getSession(DO_NOT_CREATE_SESSION);
    if ( session != null ){
      ServletContext appScope = session.getServletContext();
      Enumeration objNames = appScope.getAttributeNames();
      while ( objNames.hasMoreElements() ){
        String name = (String)objNames.nextElement();
        result.put(name, appScope.getAttribute(name));
      }
    }
    return result;
  }
  
  /**
   Return a Map of keys and objects for each session attribute.
   Logging should only be performed after the {@link ApplicationFirewall} has executed.
  */
  private Map<String, Object> getSessionScopeObjectsForLogging(HttpServletRequest aRequest){
    Map<String, Object> result = new LinkedHashMap<String, Object>();
    HttpSession session = aRequest.getSession(DO_NOT_CREATE_SESSION);
    if ( session != null ){
      result.put( "(Session Created) : ", DateTime.forInstant(session.getCreationTime(), fConfig.getDefaultUserTimeZone()));
      result.put( "(Session Timeout - seconds) : ",  new Integer(session.getMaxInactiveInterval()) );
      Enumeration objNames = session.getAttributeNames();
      while ( objNames.hasMoreElements() ){
        String name = (String)objNames.nextElement();
        result.put(name, session.getAttribute(name));
      }
    }
    return result;
  }
  
  /**
   Return a Map of key names, objects for each request scope attribute.
   Logging should only be performed after the {@link ApplicationFirewall} has executed.
  */
  private Map<String, Object> getRequestParamNamesForLogging(HttpServletRequest aRequest) {
    Map<String, Object> result = new LinkedHashMap<String, Object>();
    Map input = aRequest.getParameterMap();
    Iterator iter = input.keySet().iterator();
    while( iter.hasNext() ) {
      String key = (String)iter.next();
        result.put(key, aRequest.getAttribute(key));
    }
    return result;
  }
  
  private void addCurrentUriToRequest(HttpServletRequest aRequest, HttpServletResponse aResponse){
    String currentURI = WebUtil.getOriginalRequestURL(aRequest, aResponse);
    aRequest.setAttribute(CURRENT_URI, currentURI);
  }

   private void tryDatabaseInitAndStartupTasks(ConnectionSource aConnSrc) throws DAOException, AppException {
    fLogger.config("Trying database init and startup tasks.");
    fStartupTasks = BuildImpl.forStartupTasks();
    Set<String> dbNames = aConnSrc.getDatabaseNames();
    fBadDatabases.addAll(dbNames); //guilty till proven innocent
    if (aConnSrc.getDatabaseNames().isEmpty()) {
      fLogger.config("No databases in this application, since ConnectionSource returns an empty Set for database names.");
      startTasksWithNoDb();
    }
    else {
      fLogger.config("Attempting data layer startup tasks.");
      Set<String> healthyDbs = DbConfig.initDataLayer(); //reads in .sql
      for (String healthyDb : healthyDbs){
        fBadDatabases.remove(healthyDb);
      }
      startTasksWithNoDb();
      //start-tasks for the good databases can be run now; the bad ones run later, when they get healthy
      for(String dbName : dbNames){
        if (! fBadDatabases.contains(dbName)){
          fLogger.config("Startup tasks for database: " + dbName);
          fStartupTasks.startApplication(fServletConfig, dbName);
        }
      }
      if (! fBadDatabases.isEmpty()){
        fLogger.config("Databases seen to be down at startup: " + fBadDatabases);
      }
    }
  }
   
  private void startTasksWithNoDb() throws AppException{
    initDefaultImplementations();
    fLogger.config("Startup tasks not needing a database.");
    fStartupTasks.startApplication(fServletConfig, ""); //tasks not related to a database at all are done first
  }

  /** 
   Warning - this method is called after startup. Therefore it must be thread-safe.
   When a database goes from 'bad' to 'good', then this Controller needs to acquire 
   a lock on an object; in a sense, it temporarily goes back to 'init-mode', which is 
   single-threaded. That is, it's possible that N callers can detect a 
   bad-to-good transition quasi-simultaneously; they will need to compete for the lock. 
   But this only happens when there's a change; it doesn't happen for every 
   invocation of this method. In practice, this small amount of possible blocking 
   will be acceptable.  
  */
  private void recheckBadDatabasesAndFinishStartup() throws AppException{
    if (! fBadDatabases.isEmpty()){
      Iterator<String> bad = fBadDatabases.iterator();
      while(bad.hasNext()){
        String thisDb = bad.next();
        boolean healthy = DbConfig.checkHealthOf(thisDb);
        if (healthy) {
          synchronized (fBadDatabases) {
            if(fBadDatabases.contains(thisDb)){ //note the second check, to avoid race conditions
              fStartupTasks.startApplication(fServletConfig, thisDb);
              bad.remove();
            }
          }
        }
      }
    }
  }
  
  /**
   Must call just before {@link StartupTasks}. 
   
   <P>This ensures WEB4J does not mistakenly perform such initialization at a time other than   
   that available to {@link StartupTasks}. If custom impl's are used, the only place to init them 
   is in StartupTasks. It is prudent to do the init of default impls at the same place, to ensure 
   the defaults don't 'cheat', or have any unfair advantage over custom impls.
  */
  private void initDefaultImplementations(){
    fLogger.config("Initializing web4j default implementations.");
    ApplicationFirewallImpl.init();
    RequestParserImpl.initWebActionMappings();  
  }
  
  private ResponsePage checkOwnershipThenExecuteAction(Action aAction, RequestParser aRequestParser) throws AppException, BadRequestException {
    UntrustedProxyForUserId ownershipFirewall = BuildImpl.forOwnershipFirewall();
    if ( ownershipFirewall.usesUntrustedIdentifier(aRequestParser) ) {
      fLogger.fine("This request has an ownership constraint.");
      enforceOwnershipConstraint(aAction, aRequestParser);
    }
    else {
      fLogger.fine("No ownership constraint detected.");
      if(aAction instanceof FetchIdentifierOwner) {
        fLogger.warning("Action implements FetchIdentifierOwner, but no ownership constraint is defined in web.xml for this specific operation.");
      }
    }
    return aAction.execute();
  }

  private void enforceOwnershipConstraint(Action aAction, RequestParser aRequestParser) throws AppException, BadRequestException {
    if (aAction instanceof FetchIdentifierOwner ) {
      FetchIdentifierOwner constraint = (FetchIdentifierOwner)aAction;
      Id owner = constraint.fetchOwner();
      String ownerText = (owner == null ? null : owner.getRawString());
      HttpSession session = aRequestParser.getRequest().getSession(DO_NOT_CREATE_SESSION);
      if( session == null ) {
        ownershipConstraintNotImplementedCorrectly(OWNERSHIP_NO_SESSION);
      }
      if( aRequestParser.getRequest().getUserPrincipal() == null ) {
        ownershipConstraintNotImplementedCorrectly(OWNERSHIP_NO_LOGIN);
      }
      String loggedInUserName = aRequestParser.getRequest().getUserPrincipal().getName();
      if ( ! loggedInUserName.equals(ownerText) ) {
        fLogger.severe(
          "Violation of an ownership constraint! " + 
          "The currently logged in user-name ('" + loggedInUserName + "') does not match the name of the data-owner ('" + ownerText + "')." 
        );
        throw new BadRequestException(HttpServletResponse.SC_BAD_REQUEST, "Ownership Constraint has been violated.");
      }
    }
    else {
      ownershipConstraintNotImplementedCorrectly(
        "According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " + 
        "Such constraints require the Action to implement the FetchIdentifierOwner interface, but this Action doesn't implement that interface." 
      );
    }
    fLogger.fine("Ownership constraint has been validated.");
  }
  
  private void ownershipConstraintNotImplementedCorrectly(String aMessage){
    fLogger.severe(aMessage + " Please see the User Guide for more information on Ownership Constraints.");
    throw new RuntimeException("Ownership Constraint not implemented correctly.");
  }
}
