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 }