001    package hirondelle.web4j; 
002    
003    import static hirondelle.web4j.util.Consts.NEW_LINE;
004    import hirondelle.web4j.action.Action;
005    import hirondelle.web4j.action.ResponsePage;
006    import hirondelle.web4j.database.ConnectionSource;
007    import hirondelle.web4j.database.DAOException;
008    import hirondelle.web4j.database.DbConfig;
009    import hirondelle.web4j.model.AppException;
010    import hirondelle.web4j.model.BadRequestException;
011    import hirondelle.web4j.model.DateTime;
012    import hirondelle.web4j.model.Id;
013    import hirondelle.web4j.readconfig.Config;
014    import hirondelle.web4j.readconfig.ConfigReader;
015    import hirondelle.web4j.request.RequestParser;
016    import hirondelle.web4j.request.RequestParserImpl;
017    import hirondelle.web4j.security.ApplicationFirewall;
018    import hirondelle.web4j.security.ApplicationFirewallImpl;
019    import hirondelle.web4j.security.FetchIdentifierOwner;
020    import hirondelle.web4j.security.UntrustedProxyForUserId;
021    import hirondelle.web4j.util.Stopwatch;
022    import hirondelle.web4j.util.Util;
023    import hirondelle.web4j.util.WebUtil;
024    import hirondelle.web4j.webmaster.TroubleTicket;
025    
026    import java.io.IOException;
027    import java.util.Enumeration;
028    import java.util.Iterator;
029    import java.util.LinkedHashMap;
030    import java.util.LinkedHashSet;
031    import java.util.Locale;
032    import java.util.Map;
033    import java.util.Set;
034    import java.util.TimeZone;
035    import java.util.logging.Level;
036    import java.util.logging.Logger;
037    
038    import javax.servlet.RequestDispatcher;
039    import javax.servlet.ServletConfig;
040    import javax.servlet.ServletContext;
041    import javax.servlet.ServletException;
042    import javax.servlet.http.HttpServlet;
043    import javax.servlet.http.HttpServletRequest;
044    import javax.servlet.http.HttpServletResponse;
045    import javax.servlet.http.HttpSession;
046    import javax.servlet.jsp.JspFactory;
047    
048    /**
049      Single point of entry for serving dynamic pages.
050     
051      <P>The application can serve content both directly (by simple, direct reference to 
052      a JSP's URL), and indirectly, through this <tt>Controller</tt>.
053     
054     <P>Like almost all servlets, this class is safe for multi-threaded environments. 
055     
056      <P>Validates user input and request parameters, interacts with a datastore, 
057      and places problem domain model objects in scope for eventual rendering by a JSP. 
058      Performs either a forward or a redirect, according to the instructions of the 
059      {@link Action}.
060     
061      <P>Emails are sent to the webmaster when :
062     <ul>
063      <li>an unexpected problem occurs (the email will include extensive diagnostic 
064      information, including a stack trace)
065      <li>servlet response times degrade to below a configured level
066     </ul>
067     
068     <P>This class is in a distinct package for two reasons :
069     <ul>
070     <li>to make it easier to find, since it is at the very top of the hierarchy
071     <li>to force the <tt>Controller</tt> to use only the public aspects of 
072      the <tt>ui</tt> package. This ensures it remains at a high level of abstraction.
073     </ul>
074     
075      <P>There are key-names defined in this class (see below). Their names need to be 
076      long-winded (<tt>web4j_key_for_...</tt>), unfortunately, in order to 
077      avoid conflict with other tools, including your application. 
078    */
079    public class Controller extends HttpServlet {
080    
081      /**
082       Name and version number of the WEB4J API. 
083       
084       <P>Value: {@value}.
085       <P>Upon startup, this item is logged at <tt>CONFIG</tt> level. (This item is  
086       is simply a hard-coded field in this class. It is not configured in <tt>web.xml</tt>.) 
087      */
088      public static final String WEB4J_VERSION = "WEB4J/4.10.0";
089      
090      /**
091       Key name for the application's character encoding, placed in application scope
092       as a <tt>String</tt> upon startup. This character encoding (charset) is set 
093       as an HTTP header for every reponse.
094       
095       <P>Key name: {@value}.
096       <P>Configured in <tt>web.xml</tt>. The value <tt>UTF-8</tt> is highly recommended.
097      */
098      public static final String CHARACTER_ENCODING = "web4j_key_for_character_encoding";
099    
100      /**
101       Key name for the webmaster email address, placed in application scope
102       as a <tt>String</tt> upon startup.
103       
104       <P>Key name: {@value}.
105       <P>Configured in <tt>web.xml</tt>.
106      */
107      public static final String WEBMASTER = "web4j_key_for_webmaster";
108      
109      /**
110       Key name for the default {@link Locale}, placed in application scope
111       as a <tt>Locale</tt> upon startup.
112       
113       <P>Key name: {@value}.
114       <P>The application programmer is encouraged to use this key for any 
115       <tt>Locale</tt> stored in <em>session</em> scope : the <em>default</em> implementation 
116       of {@link hirondelle.web4j.request.LocaleSource} will always search for this 
117       key in increasingly larges scopes. Thus, the default mechanism will 
118       automatically use the user-specific <tt>Locale</tt> as an override to 
119       the default one.
120       
121       <P>Configured in <tt>web.xml</tt>.
122      */
123      public static final String LOCALE = "web4j_key_for_locale";
124      
125      /**
126       Key name for the default {@link TimeZone}, placed in application scope
127       as a <tt>TimeZone</tt> upon startup.
128       
129       <P>Key name: {@value}.
130       <P>The application programmer is encouraged to use this key for any 
131       <tt>TimeZone</tt> stored in <em>session</em> scope : the <em>default</em> implementation 
132       of {@link hirondelle.web4j.request.TimeZoneSource} will always search for this 
133       key in increasingly larges scopes. Thus, the default mechanism will 
134       automatically use the user-specific <tt>TimeZone</tt> as an override to 
135       the default one.
136       
137       <P>Configured in <tt>web.xml</tt>.
138      */
139      public static final String TIME_ZONE = "web4j_key_for_time_zone";
140    
141      /**
142       Key name for the most recent {@link TroubleTicket}, placed in application scope when a 
143       problem occurs.
144       <P>Key name: {@value}.
145      */
146      public static final String MOST_RECENT_TROUBLE_TICKET = "web4j_key_for_most_recent_trouble_ticket";
147      
148      /**
149       Key name for the startup time, placed in application scope as a {@link DateTime} upon startup.
150       <P>Key name: {@value}.
151      */
152      public static final String START_TIME = "web4j_key_for_start_time";
153      
154      /**
155       Key name for the URI for the current request, placed in request scope as a <tt>String</tt>.
156       
157       <P>Key name: {@value}.
158       <P>Somewhat bizarrely, the servlet API does not allow direct access to this item.
159      */
160      public static final String CURRENT_URI = "web4j_key_for_current_uri";
161      
162      /**
163       Perform operations to be executed only upon startup of 
164       this application, and not during its regular operation. 
165       
166       <P>Operations include :
167       <ul>
168       <li>log version and configuration information
169       <li>distribute configuration information in <tt>web.xml</tt> to the various
170       parts of WEB4J
171       <li>place an {@link ApplicationInfo} object into application scope
172       <li>place the configured character encoding into application scope, for use in JSPs
173       <li>call {@link StartupTasks#startApplication(ServletConfig, String)}, to 
174       allow the application to perform its own startup tasks
175       <li>perform various validations
176       </ul>
177       
178       <P>One or more of the application's databases may not be running when 
179       the web application starts. Upon startup, this Controller first queries each database 
180       for simple name and version information. If that query fails, then the database is 
181       assumed to be "down", and the app's implementation of {@link StartupTasks} 
182       (which usually fetches code tables from the database) is not called. 
183       
184       <P>The web app, however, will not terminate. Instead, this Controller will keep 
185       attempting to connect for each incoming request. When all databases are 
186       determined to be healthy, the Controller will perform the database initialization 
187       tasks it usually performs upon startup, and the app will then function normally.
188       
189       <P>If the database subsequently goes down again, then this Controller will not take 
190       any special action. Instead, the container's connection pool should be configured to 
191       attempt to reconnect automatically on the application's behalf. 
192      */
193      @Override public final void init(ServletConfig aConfig) throws ServletException {
194        super.init(aConfig);
195        fServletConfig = aConfig;
196        try {
197          Stopwatch stopwatch = new Stopwatch();
198          stopwatch.start();
199          
200          Map<String, String> configMap = asMap(aConfig);
201          //the Config class stores settings internally as static items
202          //after this point, any class can get its config data just by using Config as a normal object
203          Config.init(configMap);
204          fConfig = new Config();
205          
206          //any use of logging before this line will fail
207          //first load of application-specific classes; configures and begins logging as well
208          BuildImpl.init(configMap);
209        
210          displaySystemProperties();
211          displayConfigInfo(aConfig); //all items, for both app and framework
212          setCharacterEncodingAndPutIntoAppScope(aConfig);
213          putWebmasterEmailAddressIntoAppScope(aConfig);
214          putDefaultLocaleIntoAppScope(aConfig);
215          putDefaultTimeZoneIntoAppScope(aConfig);
216          putStartTimeIntoAppScope(aConfig);
217          fLogger.fine("System properties and first app-scope items completed " + stopwatch + " after start.");
218          
219          /* 
220           Implementation Note
221           There are strong order dependencies here: ConfigReader is used later in the 
222           init of SqlStatement, for example.
223          */
224          ConfigReader.init(aConfig.getServletContext());
225          WebUtil.init(aConfig);
226          
227          //This will be the first loading of application-specific classes.
228          //This will cause static fields to be initialized.
229          ApplicationInfo appInfo = BuildImpl.forApplicationInfo();
230          displayVersionInfo(aConfig, appInfo);
231          placeAppInfoIntoAppScope(aConfig, appInfo);
232      
233          TroubleTicket.init(aConfig);
234          
235          fLogger.config("Calling ConnectionSource.init(ServletConfig).");
236          ConnectionSource connSource = BuildImpl.forConnectionSource();
237          connSource.init(configMap);
238          fLogger.fine("Init of internal classes, ConnectionSource completed " + stopwatch + " after start.");
239          Config.checkDbNamesInSettings(BuildImpl.forConnectionSource().getDatabaseNames());
240      
241          tryDatabaseInitAndStartupTasks(connSource);
242          fLogger.fine("Database init and startup tasks " + stopwatch + " after start.");
243          
244          CheckModelObjects checkModelObjects = new CheckModelObjects();
245          checkModelObjects.performChecks();
246          stopwatch.stop();
247          fLogger.fine("Cross-Site Scripting scan completed " + stopwatch + " after start.");
248          
249          fLogger.config("*** SUCCESS : STARTUP COMPLETED SUCCESSFULLY for " + appInfo + ". Total startup time : " + stopwatch );
250        }
251        catch (AppException ex) {
252          throw new ServletException(ex);
253        } 
254      }
255    
256      /** Log the name and version of the application. */
257      @Override public void destroy() {
258        ApplicationInfo appInfo = BuildImpl.forApplicationInfo();
259        fLogger.config("Shutting Down Controller for " + appInfo.getName() + "/" + appInfo.getVersion());
260      }
261    
262      /** Call {@link #processRequest}.  */
263      @Override public final void doGet(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
264        logClasses(aRequest, aResponse);
265        processRequest(aRequest, aResponse);
266      }
267    
268      /** Call {@link #processRequest}.  */
269      @Override public final void doPost(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
270        logClasses(aRequest, aResponse);
271        processRequest(aRequest, aResponse);
272      }
273    
274      /** Call {@link #processRequest}.  PUT can be called by <tt>XmlHttpRequest</tt>. */
275      @Override public final void doPut(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
276        logClasses(aRequest, aResponse);
277        processRequest(aRequest, aResponse);
278      }
279      
280      /** Call {@link #processRequest}.  DELETE can be called by <tt>XmlHttpRequest</tt>. */
281      @Override public final void doDelete(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
282        logClasses(aRequest, aResponse);
283        processRequest(aRequest, aResponse);
284      }
285    
286      /**
287       Handle all HTTP requests for <tt>GET</tt>, <tt>POST</tt>, <tt>PUT</tt>, and <tt>DELETE</tt> requests.
288       All of these HTTP methods will funnel through this Java method; any other methods will be handled by the container.
289       If a subclass needs to know the underlying HTTP method, then it must call {@link HttpServletRequest#getMethod()}.  
290       
291       <P>This method can be overridden, if desired. The great majority of applications will not need 
292       to override this method. 
293       
294       <P>Operations include :
295       <ul>
296       <li>set the request character encoding (using the value configured in <tt>web.xml</tt>)
297       <li>set the <tt>charset</tt> HTTP header for the response (using the value configured in <tt>web.xml</tt>)
298       <li>react to a successful user login, using the configured implementation of {@link hirondelle.web4j.security.LoginTasks}
299       <li>get an instance of {@link RequestParser}
300       <li>get its {@link Action}, and execute it 
301       <li>check for an ownership constraint (see {@link UntrustedProxyForUserId})
302       <li>perform either a forward or a redirect to the Action's  {@link hirondelle.web4j.action.ResponsePage}
303       <li>if an unexpected problem occurs, create a {@link TroubleTicket}, log it, and 
304       email it to the webmaster email address configured in <tt>web.xml</tt>
305       <li>if the response time exceeds a configured threshold, build a 
306       {@link TroubleTicket}, log it, and email it to the webmaster address configured in <tt>web.xml</tt>
307       </ul>
308      */
309      protected void processRequest(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException {
310        Stopwatch stopwatch = new Stopwatch();
311        stopwatch.start();
312        
313        aRequest.setCharacterEncoding(fConfig.getCharacterEncoding()); 
314        aResponse.setCharacterEncoding(fConfig.getCharacterEncoding());
315        
316        addCurrentUriToRequest(aRequest, aResponse);
317        RequestParser requestParser = RequestParser.getInstance(aRequest, aResponse);
318        try {
319          LoginTasksHelper loginHelper = new LoginTasksHelper();
320          loginHelper.reactToNewLogins(aRequest);
321          Action action = requestParser.getWebAction();
322          ApplicationFirewall appFirewall = BuildImpl.forApplicationFirewall();
323          appFirewall.doHardValidation(action, requestParser);
324          logAttributesForAllScopes(aRequest);
325          recheckBadDatabasesAndFinishStartup();
326          ResponsePage responsePage = checkOwnershipThenExecuteAction(action, requestParser);
327          if ( responsePage.hasBinaryData() ) {
328            fLogger.fine("Serving binary data. Controller not performing a forward or redirect.");
329          }
330          else {
331            if ( responsePage.getIsRedirect() ) {
332              redirect(responsePage, aResponse);
333            }
334            else {
335              forward(responsePage, aRequest, aResponse);
336            }
337          }
338          stopwatch.stop();
339          if ( stopwatch.toValue() >= fConfig.getPoorPerformanceThreshold() ) {
340            logAndEmailPerformanceProblem(stopwatch.toString(), aRequest);
341          }
342        }
343        catch (BadRequestException ex){
344          //NOTE : sendError() commits the response.
345          if( Util.textHasContent(ex.getErrorMessage()) ){
346            aResponse.sendError(ex.getStatusCode(), ex.getErrorMessage());      
347          }
348          else {
349            aResponse.sendError(ex.getStatusCode());      
350          }
351        }
352        catch (Throwable ex) {
353          //Includes AppException, Bugs, or rare conditions, for example datastore failure
354          logAndEmailSeriousProblem(ex, aRequest);
355          aResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
356        }
357      }
358    
359      /**
360       Change the {@link ResponsePage} according to {@link Locale}.
361       
362       <P>This overridable default implementation does nothing, and returns <tt>null</tt>.
363       If the return value of this method is <tt>null</tt>, then the nominal <tt>ResponsePage</tt>
364       will be used without alteration. If the return value of this method is not <tt>null</tt>,
365       then it will be used to override the nominal <tt>ResponsePage</tt>.
366       
367       <P>This method is intended for applications that use different JSPs for different Locales.
368       For example, if the nominal response is a forward to <tt>Blah_en.jsp</tt>, and the "real"
369       response should be <tt>Blah_fr.jsp</tt>, then this method can be overridden to return the 
370       appropriate {@link ResponsePage}. <span class="highlight">This method is called only for 
371       forward operations. If it is overridden, then its return value must also correspond to a forward 
372       operation.</span>
373       
374       <P><span class="highlight">This style of implementing translation is not recommended.</span>
375       Instead, please use the services of the <tt>hirondelle.web4j.ui.translate</tt> package. 
376      */
377      protected ResponsePage swapResponsePage(ResponsePage aResponsePage, Locale aLocale){
378        return null; //does nothing
379      }
380      
381      /**
382       Inform the webmaster of an unexpected problem with the deployed application.
383       
384       <P>Typically called when an unexpected <tt>Exception</tt> occurs in 
385       {@link #processRequest}. Uses {@link TroubleTicket#mailToRecipients()}.
386       
387        <P>Also, stores the trouble ticket in application scope, for possible 
388        later examination. 
389      */
390      protected final void logAndEmailSeriousProblem (Throwable aException, HttpServletRequest aRequest) {
391        TroubleTicket troubleTicket = new TroubleTicket(aException, aRequest);
392        fLogger.severe("TOP LEVEL CATCHING Throwable");
393        fLogger.severe( troubleTicket.toString() ); 
394        log("SERIOUS PROBLEM OCCURRED.");
395        log( troubleTicket.toString() );
396        fServletConfig.getServletContext().setAttribute(MOST_RECENT_TROUBLE_TICKET, troubleTicket);
397        try {
398          troubleTicket.mailToRecipients();
399        }
400        catch (AppException exception){
401          fLogger.severe("Unable to send email: " + exception.getMessage());
402        }
403      }
404    
405      /**
406       Inform the webmaster of a performance problem.
407       
408       <P>Called only when the response time of a request is above the threshold 
409       value configured in <tt>web.xml</tt>.
410       
411       <P>Builds a <tt>Throwable</tt> with a description of the problem, then creates and 
412       emails a {@link TroubleTicket} to the webmaster.
413       
414       @param aMilliseconds response time of a request in milliseconds
415      */
416      protected final void logAndEmailPerformanceProblem(String aMilliseconds, HttpServletRequest aRequest)  {
417        String message = 
418          "Response time of web application exceeds configured performance threshold." + NEW_LINE + 
419          "Time : " + aMilliseconds
420        ;
421        Throwable ex = new Throwable(message);
422        TroubleTicket troubleTicket = new TroubleTicket(ex, aRequest);
423        fLogger.severe("Poor response time : " + aMilliseconds);
424        fLogger.severe( troubleTicket.toString() ); 
425        log("Poor response time : " + aMilliseconds + " milliseconds");
426        log( troubleTicket.toString() );
427        try {
428          troubleTicket.mailToRecipients();
429        }
430        catch(AppException exception){
431          fLogger.severe("Unable to send email: " + exception.getMessage());
432        }
433      }
434    
435      // PRIVATE 
436    
437      /**
438       Mutable field. Must be accessed in a thread-safe way after init finished.
439       Assumes that all databases are initially down; each is removed from this set, when it 
440       has been detected as being up. Possibly empty.
441      */
442      private Set<String> fBadDatabases = new LinkedHashSet<String>();
443      
444      /** Mutable field. Must be accessed in a thread-safe way after init finished. */
445      private StartupTasks fStartupTasks;
446      
447      /**
448       The config must be saved. It is not accessible from a request, or from the context. 
449       It may be needed after startup, should no db connections be initially available.  
450      */
451      private static ServletConfig fServletConfig;
452      private Config fConfig; 
453      
454    //  /** Item configured in web.xml.  */
455    //  private static final InitParam fPoorPerformanceThreshold = new InitParam(
456    //    "PoorPerformanceThreshold", "20"
457    //  ); 
458    //  /**
459    //   If any request takes longer than this many nanoseconds to be processed, then 
460    //   an email is sent to the webmaster. The web.xml states this configured time in 
461    //   seconds, but nanoseconds is used by this class to perform the comparison.
462    //  */
463    //  private static long fPOOR_PERFORMANCE_THRESHOLD;
464    //  
465    //  /** Item configured in web.xml.  */
466    //  private static final InitParam fCharacterEncoding = new InitParam(
467    //    "CharacterEncoding", "UTF-8"
468    //  ); 
469    //  /**
470    //   Character encoding for this application. 
471    //   
472    //   <P>The Controller will assume that every request will have this 
473    //   character encoding. In addition, this value will be placed in an 
474    //   application scope attribute named {@link Controller#CHARACTER_ENCODING}; 
475    //  */
476    //  private static String fCHARACTER_ENCODING;
477    //  
478    //  /** Item configured in web.xml.  */
479    //  private static final InitParam fDefaultLocale = new InitParam(
480    //    "DefaultLocale", "en"
481    //  );
482    //  /**
483    //   Default Locale for this application. 
484    //   
485    //   <P>Placed in an app scope attribute named {@link Controller#LOCALE} (as a 
486    //   Locale object, not as a String). 
487    //  */
488    //  private static String fDEFAULT_LOCALE;
489    //  
490    //  private static final InitParam fDefaultTimeZone = new InitParam(
491    //    "DefaultUserTimeZone", "GMT"
492    //  );
493    //  /**
494    //   Default TimeZone for this application. 
495    //   
496    //   <P>Placed in an app scope attribute named {@link Controller#TIME_ZONE} (as a 
497    //   TimeZone object, not as a String). 
498    //  */
499    //  private static String fDEFAULT_TIME_ZONE;
500    //
501    //  /** Item configured in web.xml.  */
502    //  private static final InitParam fWebmaster = new InitParam("Webmaster");
503    //  
504    //  /**
505    //  Webmaster email address for this application. 
506    //   
507    //   <P>This value will be placed in an application scope attribute 
508    //   named {@link Controller#WEBMASTER}; 
509    //  */
510    //  private static String fWEBMASTER;
511      
512      private static final boolean DO_NOT_CREATE_SESSION = false;
513      
514      private static final String OWNERSHIP_NO_SESSION =  
515        "According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " + 
516        "However, this request has no session, and ownership constraints work only when the user has logged in." 
517      ;
518     
519      private static final String OWNERSHIP_NO_LOGIN = 
520        "According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " + 
521        "A session exists, but there is no valid login, and ownership constraints work only when the user has logged in." 
522      ;
523      
524      private static final Logger fLogger = Util.getLogger(Controller.class);
525    
526      private void logClasses(HttpServletRequest aRequest, HttpServletResponse aResponse) {
527        fLogger.finest("Request class :" + aRequest.getClass());
528        fLogger.finest("Response class :" + aResponse.getClass());
529      }
530    
531      private void redirect (
532        ResponsePage aDestinationPage, HttpServletResponse aResponse
533      ) throws IOException {
534        String urlWithSessionID = aResponse.encodeRedirectURL(aDestinationPage.toString());
535        fLogger.fine("REDIRECT: " + Util.quote(urlWithSessionID));
536        aResponse.sendRedirect( urlWithSessionID );
537      }
538    
539      private void forward (
540        ResponsePage aResponsePage, HttpServletRequest aRequest, HttpServletResponse aResponse
541      ) throws ServletException, IOException {
542        ResponsePage responsePage = possiblyAlterForLocale(aResponsePage, aRequest);
543        RequestDispatcher dispatcher = aRequest.getRequestDispatcher(responsePage.toString());
544        fLogger.fine("Forward : " + responsePage);
545        dispatcher.forward(aRequest, aResponse);
546      }
547      
548      private ResponsePage possiblyAlterForLocale(ResponsePage aNominalForward, HttpServletRequest aRequest){
549        Locale locale = BuildImpl.forLocaleSource().get(aRequest);
550        ResponsePage langSpecificForward = swapResponsePage(aNominalForward, locale);
551        if ( langSpecificForward != null && langSpecificForward.getIsRedirect() ){
552          throw new RuntimeException(
553            "A 'forward' ResponsePage has been altered for Locale, but is no longer a forward : " + langSpecificForward
554          );
555        }
556        return (langSpecificForward != null) ?  langSpecificForward : aNominalForward;
557      }
558      
559      private Map<String, String> asMap(ServletConfig aConfig){
560        Map<String, String> result = new LinkedHashMap<String, String>();
561        Enumeration initParamNames = aConfig.getInitParameterNames();
562        while ( initParamNames.hasMoreElements() ){
563          String name = (String)initParamNames.nextElement();
564          String value = aConfig.getInitParameter(name);
565          result.put(name, value);
566        }
567        return result;
568      }
569      
570      private void displaySystemProperties(){
571        String sysProps = Util.logOnePerLine(System.getProperties());
572        fLogger.config("System Properties " + sysProps);
573      }
574      
575      private void displayVersionInfo(ServletConfig aConfig, ApplicationInfo aAppInfo){
576        ServletContext context = aConfig.getServletContext();
577        Map<String, String> info = new LinkedHashMap<String, String>();
578        info.put("Application", aAppInfo.getName() + "/" +  aAppInfo.getVersion());
579        info.put("Server", context.getServerInfo());
580        info.put("Servlet API Version", context.getMajorVersion() + "." +  context.getMinorVersion() );
581        if( JspFactory.getDefaultFactory() != null) {
582          //this item is null when outside the normal runtime environment.
583          info.put("Java Server Page API Version", JspFactory.getDefaultFactory().getEngineInfo().getSpecificationVersion());
584        }
585        info.put("Java Runtime Environment (JRE)", System.getProperty("java.version"));
586        info.put("Operating System", System.getProperty("os.name") + "/" + System.getProperty("os.version") );
587        info.put("WEB4J Version", WEB4J_VERSION);
588        fLogger.config("Versions" + Util.logOnePerLine(info));
589      }
590      
591      private void displayConfigInfo(ServletConfig aConfig){
592        fLogger.config(
593          "Context Name : " + Util.quote(aConfig.getServletContext().getServletContextName()) 
594        );
595        
596        Enumeration ctxParamNames = aConfig.getServletContext().getInitParameterNames();
597        Map<String, String> ctxParams = new LinkedHashMap<String, String>();
598        while ( ctxParamNames.hasMoreElements() ){
599          String name = (String)ctxParamNames.nextElement();
600          String value = aConfig.getServletContext().getInitParameter(name);
601          ctxParams.put(name, value);
602        }
603        fLogger.config( "Context Params : " + Util.logOnePerLine(ctxParams));
604        
605        Enumeration initParamNames = aConfig.getInitParameterNames();
606        Map<String, String> initParams = new LinkedHashMap<String, String>();
607        while ( initParamNames.hasMoreElements() ){
608          String name = (String)initParamNames.nextElement();
609          String value = aConfig.getInitParameter(name);
610          initParams.put(name, value);
611        }
612        fLogger.config( "Servlet Params : " + Util.logOnePerLine(initParams));
613      }
614    
615      private void setCharacterEncodingAndPutIntoAppScope(ServletConfig aConfig){
616        aConfig.getServletContext().setAttribute(CHARACTER_ENCODING, fConfig.getCharacterEncoding());
617      }
618      
619      private void putWebmasterEmailAddressIntoAppScope(ServletConfig aConfig){
620        aConfig.getServletContext().setAttribute(WEBMASTER, fConfig.getWebmaster());
621      }
622    
623      private void putDefaultLocaleIntoAppScope(ServletConfig aConfig){
624        aConfig.getServletContext().setAttribute(LOCALE, fConfig.getDefaultLocale());
625      }
626      
627      private void putDefaultTimeZoneIntoAppScope(ServletConfig aConfig){
628        aConfig.getServletContext().setAttribute(TIME_ZONE, fConfig.getDefaultUserTimeZone());
629      }
630      
631      private void putStartTimeIntoAppScope(ServletConfig aConfig){
632        aConfig.getServletContext().setAttribute(START_TIME, DateTime.now(fConfig.getDefaultUserTimeZone()));
633      }
634      
635      private void placeAppInfoIntoAppScope(ServletConfig aConfig, ApplicationInfo aAppInfo){
636        aConfig.getServletContext().setAttribute(
637          ApplicationInfo.KEY, aAppInfo
638        );
639      }
640    
641      /**
642      Log attributes stored in the various scopes.
643      */
644      private void logAttributesForAllScopes(HttpServletRequest aRequest){
645        //the following style is conservative, and is meant to avoid calls which may be expensive
646        //remember that the level of the HANDLER affects whether the item is emitted as well.
647        if( fLogger.getLevel() != null &&  fLogger.getLevel().equals(Level.FINEST) ) {
648          fLogger.finest("Application Scope Items " + Util.logOnePerLine(getApplicationScopeObjectsForLogging(aRequest)));
649          fLogger.finest("Session Scope Items " + Util.logOnePerLine(getSessionScopeObjectsForLogging(aRequest)));
650          fLogger.finest("Request Parameter Names " + Util.logOnePerLine(getRequestParamNamesForLogging(aRequest)));
651        }
652      }
653    
654      /**
655      Return Map of name-value pairs of items in application scope. 
656     
657      <P>In many cases, the actual data will be quite lengthy. For instance, translation data is often 
658      sizeable. Thus, this should be called only when logging at the highest level. 
659      Logging should only be performed after the {@link ApplicationFirewall} has executed.
660      */
661      private Map<String, Object> getApplicationScopeObjectsForLogging(HttpServletRequest aRequest){
662        Map<String, Object> result = new LinkedHashMap<String, Object>();
663        HttpSession session = aRequest.getSession(DO_NOT_CREATE_SESSION);
664        if ( session != null ){
665          ServletContext appScope = session.getServletContext();
666          Enumeration objNames = appScope.getAttributeNames();
667          while ( objNames.hasMoreElements() ){
668            String name = (String)objNames.nextElement();
669            result.put(name, appScope.getAttribute(name));
670          }
671        }
672        return result;
673      }
674      
675      /**
676       Return a Map of keys and objects for each session attribute.
677       Logging should only be performed after the {@link ApplicationFirewall} has executed.
678      */
679      private Map<String, Object> getSessionScopeObjectsForLogging(HttpServletRequest aRequest){
680        Map<String, Object> result = new LinkedHashMap<String, Object>();
681        HttpSession session = aRequest.getSession(DO_NOT_CREATE_SESSION);
682        if ( session != null ){
683          result.put( "(Session Created) : ", DateTime.forInstant(session.getCreationTime(), fConfig.getDefaultUserTimeZone()));
684          result.put( "(Session Timeout - seconds) : ",  new Integer(session.getMaxInactiveInterval()) );
685          Enumeration objNames = session.getAttributeNames();
686          while ( objNames.hasMoreElements() ){
687            String name = (String)objNames.nextElement();
688            result.put(name, session.getAttribute(name));
689          }
690        }
691        return result;
692      }
693      
694      /**
695       Return a Map of key names, objects for each request scope attribute.
696       Logging should only be performed after the {@link ApplicationFirewall} has executed.
697      */
698      private Map<String, Object> getRequestParamNamesForLogging(HttpServletRequest aRequest) {
699        Map<String, Object> result = new LinkedHashMap<String, Object>();
700        Map input = aRequest.getParameterMap();
701        Iterator iter = input.keySet().iterator();
702        while( iter.hasNext() ) {
703          String key = (String)iter.next();
704            result.put(key, aRequest.getAttribute(key));
705        }
706        return result;
707      }
708      
709      private void addCurrentUriToRequest(HttpServletRequest aRequest, HttpServletResponse aResponse){
710        String currentURI = WebUtil.getOriginalRequestURL(aRequest, aResponse);
711        aRequest.setAttribute(CURRENT_URI, currentURI);
712      }
713    
714       private void tryDatabaseInitAndStartupTasks(ConnectionSource aConnSrc) throws DAOException, AppException {
715        fLogger.config("Trying database init and startup tasks.");
716        fStartupTasks = BuildImpl.forStartupTasks();
717        Set<String> dbNames = aConnSrc.getDatabaseNames();
718        fBadDatabases.addAll(dbNames); //guilty till proven innocent
719        if (aConnSrc.getDatabaseNames().isEmpty()) {
720          fLogger.config("No databases in this application, since ConnectionSource returns an empty Set for database names.");
721          startTasksWithNoDb();
722        }
723        else {
724          fLogger.config("Attempting data layer startup tasks.");
725          Set<String> healthyDbs = DbConfig.initDataLayer(); //reads in .sql
726          for (String healthyDb : healthyDbs){
727            fBadDatabases.remove(healthyDb);
728          }
729          startTasksWithNoDb();
730          //start-tasks for the good databases can be run now; the bad ones run later, when they get healthy
731          for(String dbName : dbNames){
732            if (! fBadDatabases.contains(dbName)){
733              fLogger.config("Startup tasks for database: " + dbName);
734              fStartupTasks.startApplication(fServletConfig, dbName);
735            }
736          }
737          if (! fBadDatabases.isEmpty()){
738            fLogger.config("Databases seen to be down at startup: " + fBadDatabases);
739          }
740        }
741      }
742       
743      private void startTasksWithNoDb() throws AppException{
744        initDefaultImplementations();
745        fLogger.config("Startup tasks not needing a database.");
746        fStartupTasks.startApplication(fServletConfig, ""); //tasks not related to a database at all are done first
747      }
748    
749      /** 
750       Warning - this method is called after startup. Therefore it must be thread-safe.
751       When a database goes from 'bad' to 'good', then this Controller needs to acquire 
752       a lock on an object; in a sense, it temporarily goes back to 'init-mode', which is 
753       single-threaded. That is, it's possible that N callers can detect a 
754       bad-to-good transition quasi-simultaneously; they will need to compete for the lock. 
755       But this only happens when there's a change; it doesn't happen for every 
756       invocation of this method. In practice, this small amount of possible blocking 
757       will be acceptable.  
758      */
759      private void recheckBadDatabasesAndFinishStartup() throws AppException{
760        if (! fBadDatabases.isEmpty()){
761          Iterator<String> bad = fBadDatabases.iterator();
762          while(bad.hasNext()){
763            String thisDb = bad.next();
764            boolean healthy = DbConfig.checkHealthOf(thisDb);
765            if (healthy) {
766              synchronized (fBadDatabases) {
767                if(fBadDatabases.contains(thisDb)){ //note the second check, to avoid race conditions
768                  fStartupTasks.startApplication(fServletConfig, thisDb);
769                  bad.remove();
770                }
771              }
772            }
773          }
774        }
775      }
776      
777      /**
778       Must call just before {@link StartupTasks}. 
779       
780       <P>This ensures WEB4J does not mistakenly perform such initialization at a time other than   
781       that available to {@link StartupTasks}. If custom impl's are used, the only place to init them 
782       is in StartupTasks. It is prudent to do the init of default impls at the same place, to ensure 
783       the defaults don't 'cheat', or have any unfair advantage over custom impls.
784      */
785      private void initDefaultImplementations(){
786        fLogger.config("Initializing web4j default implementations.");
787        ApplicationFirewallImpl.init();
788        RequestParserImpl.initWebActionMappings();  
789      }
790      
791      private ResponsePage checkOwnershipThenExecuteAction(Action aAction, RequestParser aRequestParser) throws AppException, BadRequestException {
792        UntrustedProxyForUserId ownershipFirewall = BuildImpl.forOwnershipFirewall();
793        if ( ownershipFirewall.usesUntrustedIdentifier(aRequestParser) ) {
794          fLogger.fine("This request has an ownership constraint.");
795          enforceOwnershipConstraint(aAction, aRequestParser);
796        }
797        else {
798          fLogger.fine("No ownership constraint detected.");
799          if(aAction instanceof FetchIdentifierOwner) {
800            fLogger.warning("Action implements FetchIdentifierOwner, but no ownership constraint is defined in web.xml for this specific operation.");
801          }
802        }
803        return aAction.execute();
804      }
805    
806      private void enforceOwnershipConstraint(Action aAction, RequestParser aRequestParser) throws AppException, BadRequestException {
807        if (aAction instanceof FetchIdentifierOwner ) {
808          FetchIdentifierOwner constraint = (FetchIdentifierOwner)aAction;
809          Id owner = constraint.fetchOwner();
810          String ownerText = (owner == null ? null : owner.getRawString());
811          HttpSession session = aRequestParser.getRequest().getSession(DO_NOT_CREATE_SESSION);
812          if( session == null ) {
813            ownershipConstraintNotImplementedCorrectly(OWNERSHIP_NO_SESSION);
814          }
815          if( aRequestParser.getRequest().getUserPrincipal() == null ) {
816            ownershipConstraintNotImplementedCorrectly(OWNERSHIP_NO_LOGIN);
817          }
818          String loggedInUserName = aRequestParser.getRequest().getUserPrincipal().getName();
819          if ( ! loggedInUserName.equals(ownerText) ) {
820            fLogger.severe(
821              "Violation of an ownership constraint! " + 
822              "The currently logged in user-name ('" + loggedInUserName + "') does not match the name of the data-owner ('" + ownerText + "')." 
823            );
824            throw new BadRequestException(HttpServletResponse.SC_BAD_REQUEST, "Ownership Constraint has been violated.");
825          }
826        }
827        else {
828          ownershipConstraintNotImplementedCorrectly(
829            "According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " + 
830            "Such constraints require the Action to implement the FetchIdentifierOwner interface, but this Action doesn't implement that interface." 
831          );
832        }
833        fLogger.fine("Ownership constraint has been validated.");
834      }
835      
836      private void ownershipConstraintNotImplementedCorrectly(String aMessage){
837        fLogger.severe(aMessage + " Please see the User Guide for more information on Ownership Constraints.");
838        throw new RuntimeException("Ownership Constraint not implemented correctly.");
839      }
840    }