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