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 }