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 }