001 package hirondelle.web4j.action; 002 003 import static hirondelle.web4j.util.Consts.SPACE; 004 import hirondelle.web4j.BuildImpl; 005 import hirondelle.web4j.database.DynamicSql; 006 import hirondelle.web4j.model.AppException; 007 import hirondelle.web4j.model.Id; 008 import hirondelle.web4j.model.MessageList; 009 import hirondelle.web4j.model.MessageListImpl; 010 import hirondelle.web4j.request.LocaleSource; 011 import hirondelle.web4j.request.RequestParameter; 012 import hirondelle.web4j.request.RequestParser; 013 import hirondelle.web4j.request.TimeZoneSource; 014 import hirondelle.web4j.security.CsrfFilter; 015 import hirondelle.web4j.security.SafeText; 016 import hirondelle.web4j.util.Args; 017 import hirondelle.web4j.util.Util; 018 import hirondelle.web4j.util.WebUtil; 019 import java.security.Principal; 020 import java.util.Collection; 021 import java.util.Locale; 022 import java.util.TimeZone; 023 import java.util.logging.Logger; 024 import javax.servlet.ServletException; 025 import javax.servlet.http.HttpSession; 026 027 /** 028 Abstract Base Class (ABC) for implementations of the {@link Action} interface. 029 030 <P>This ABC provides concise methods for common operations, which will make 031 implementations read more clearly, concisely, and at a higher level of abstraction. 032 033 <P>A simple fetch-and-display operation can often be implemented using this class 034 as a base class. However, operations involving user input and/or edits to the 035 datastore should very likely use other abstract base classes, such as 036 {@link ActionTemplateListAndEdit}, {@link ActionTemplateSearch}, and 037 {@link hirondelle.web4j.action.ActionTemplateShowAndApply}. 038 039 <P>This class places success/fail messages in session scope, not request scope. 040 This is because such messages often need to survive a redirect operation. 041 For example, when a successful edit to the database occurs, a <em>redirect</em> is 042 usually performed, to avoid problems with browser reloads. 043 The only way a success message can survive a redirect is by being placed 044 in session scope. 045 046 <P><em>This class assumes that a session already exists</em>. 047 If a session does not already exist, then calling such methods will result in an error. 048 In practice, the user will almost always have already logged in, and this will not 049 be a problem. As a backup, actions can always explicitly create a session, if needed, 050 by calling 051 <PRE>getRequestParser.getRequest().getSession(true);</PRE> 052 */ 053 public abstract class ActionImpl implements Action { 054 055 056 /** 057 Value {@value} - identifies a {@link hirondelle.web4j.model.MessageList}, placed 058 in session scope, to hold error information for the end user. 059 These errors are used by both WEB4J and the application programmer. 060 */ 061 public static final String ERRORS = "web4j_key_for_errors"; 062 063 /** 064 Value {@value} - identifies a {@link hirondelle.web4j.model.MessageList}, placed 065 in session scope, to hold messages for the end user. These messages are 066 used by both WEB4J and the application programmer. They typically hold success 067 and information messages. 068 */ 069 public static final String MESSAGES = "web4j_key_for_messages"; 070 071 /** 072 Value {@value} - identifies the user's id, placed in session scope. 073 074 <P>Many applications will benefit from having <i>both</i> the user id <i>and</i> the user login name 075 placed in session scope upon login. The Servlet Container will place the user <i>login name</i> 076 in session scope upon login, but it will not place the corresponding <i>user id</i> 077 (the database's primary key of the user record) in session scope. 078 079 <P>If an application chooses to place the user's underlying database id into session scope under 080 this USER_ID key, then the user's id will be returned by {@link #getUserId()}. 081 */ 082 public static final String USER_ID = "web4j_key_for_user_id"; 083 084 /** 085 Value {@value} - generic key for an object placed in scope for a JSP. 086 <P>Not mandatory to use this generic key. Provided simply as a convenience. 087 */ 088 public static final String ITEM_FOR_EDIT = "itemForEdit"; 089 090 /** 091 Value {@value} - generic key for a collection of objects placed in scope for a JSP. 092 <P>Not mandatory to use this generic key. Provided simply as a convenience. 093 */ 094 public static final String ITEMS_FOR_LISTING = "itemsForListing"; 095 096 /** 097 Value {@value} - generic key for a single 'data' object placed in scope for a JSP. 098 Usually used with structured data, such as JSON, XML, CSV, and so on. 099 <P>Not mandatory to use this generic key. Provided simply as a convenience. 100 */ 101 public static final String DATA = "data"; 102 103 /** 104 Constructor. 105 106 <P>This constructor will add an attribute named <tt>'Operation'</tt> to the request. Its 107 value is deduced as specified by {@link #getOperation()}. This attribute is intended for JSPs, 108 which can use it to access the <tt>Operation</tt> regardless of its original source. 109 110 @param aNominalPage simply one of the possible {@link ResponsePage}s, 111 arbitrarily chosen as a "default". It may be changed after construction 112 by calling {@link #setResponsePage}. Recommended that the "success" page 113 be chosen as the nominal page. If not, then selection of any of the 114 possible {@link ResponsePage}s is acceptable. 115 116 @param aRequestParser allows parsing of request parameters into higher level java objects. 117 */ 118 protected ActionImpl(ResponsePage aNominalPage, RequestParser aRequestParser) { 119 fFinalResponsePage = aNominalPage; 120 fRequestParser = aRequestParser; 121 fErrors = new AppException(); 122 fMessages = new MessageListImpl(); 123 fLocale = BuildImpl.forLocaleSource().get(fRequestParser.getRequest()); 124 fTimeZone = BuildImpl.forTimeZoneSource().get(fRequestParser.getRequest()); 125 fOperation = parseOperation(); 126 addToRequest("Operation", fOperation); 127 fLogger.fine("Operation: " + fOperation); 128 } 129 130 public abstract ResponsePage execute() throws AppException; 131 132 /** Return the resource which will render the final result. */ 133 public final ResponsePage getResponsePage(){ 134 return fFinalResponsePage; 135 } 136 137 /** 138 Return the {@link Operation} associated with this <tt>Action</tt>, if any. 139 140 <P>The <tt>Operation</tt> is found as follows : 141 <ol> 142 <li>if there is a request parameter named <tt>'Operation'</tt>, and it has a value, pass its value to 143 {@link Operation#valueFor(String)} 144 <li>if the above style fails, then the 'extension' is examined. For example, a request to <tt>.../MyAction.list?x=1</tt> 145 would result in {@link Operation#List} being added to request scope, since the extension value <tt>list</tt> is known to 146 {@link Operation#valueFor(String)}. 147 This style is useful for implementing fine-grained <tt><security-constraint></tt> 148 items in <tt>web.xml</tt>. See the User Guide for more information. 149 <li>if both of the above methods fail, return <tt>null</tt> 150 </ol> 151 152 <P>When using the 'extension' style, please note that <tt>web.xml</tt> contains related <tt>servlet-mapping</tt> settings. 153 Such settings control which HTTP requests (as defined by a <tt>url-pattern</tt>) are passed from the Servlet Container to 154 your application in the first place. Thus, <b>any 'extensions' which your application intends to use must have a corresponding 155 <tt>servlet-mapping</tt> setting in your <tt>web.xml</tt></b>. 156 */ 157 protected final Operation getOperation(){ 158 return fOperation; 159 } 160 161 /** 162 Return the name of the logged in user. 163 164 <P>By definition in the servlet specification, a successfully logged in user 165 will always have a non-<tt>null</tt> return value for 166 {@link javax.servlet.http.HttpServletRequest#getUserPrincipal()}. 167 168 <P>If the user is not logged in, this method will always return <tt>null</tt>. 169 170 <P>This method returns {@link SafeText}, not a <tt>String</tt>. 171 The user name is often rendered in the view. Since in general the user name 172 may contain special characters, it is appropriate to model it as 173 <tt>SafeText</tt>. 174 */ 175 protected final SafeText getLoggedInUserName(){ 176 Principal principal = fRequestParser.getRequest().getUserPrincipal(); 177 return principal == null ? null : new SafeText(principal.getName()); 178 } 179 180 /** 181 Return the {@link Id} stored in session scope under the key {@link #USER_ID}. 182 If no such item is found, then return <tt>null</tt>. 183 184 <P><style class='highlight'>This internal database identifier should never be served to the client, since that 185 would be a grave security risk.</style> The user id should only be used in server-side code, and never 186 presented to the user in a JSP. 187 */ 188 protected final Id getUserId(){ 189 Id result = (Id) getFromSession(USER_ID); 190 return result; 191 } 192 193 /** 194 Called by subclasses if the final {@link ResponsePage} 195 differs from the nominal one passed to the constructor. 196 197 <P>If an implementation calls this method, it is usually 198 called in {@link #execute}. 199 */ 200 protected final void setResponsePage(ResponsePage aNewResponsePage){ 201 fFinalResponsePage = aNewResponsePage; 202 } 203 204 /** 205 Add a name-object pair to request scope. 206 207 <P>If the pair already exists, it is <em>updated</em> with <tt>aObject</tt>. 208 209 @param aName satisfies {@link hirondelle.web4j.util.Util#textHasContent(String)}. 210 @param aObject if <tt>null</tt> and a corresponding name-object pair exists, then 211 the pair is <em>removed</em> from request scope. 212 */ 213 protected final void addToRequest(String aName, Object aObject){ 214 Args.checkForContent(aName); 215 fRequestParser.getRequest().setAttribute(aName, aObject); 216 } 217 218 /** 219 Return the existing session. 220 <P>If a session does not already exist, then an error will result. 221 */ 222 protected final HttpSession getExistingSession(){ 223 HttpSession result = fRequestParser.getRequest().getSession(DO_NOT_CREATE); 224 if ( result == null ) { 225 String MESSAGE = "No session currently exists. Either require user to login, or create a session explicitly."; 226 fLogger.severe(MESSAGE); 227 throw new UnsupportedOperationException(MESSAGE); 228 } 229 return result; 230 } 231 232 /** 233 Add a name-object pair to an existing session. 234 235 <P>If the pair already exists, it is <em>updated</em> with <tt>aObject</tt>. 236 237 @param aName satisfies {@link hirondelle.web4j.util.Util#textHasContent(String)}. 238 @param aObject if <tt>null</tt> and a corresponding name-object pair exists, then 239 the pair is <em>removed</em> from session scope. 240 */ 241 protected final void addToSession(String aName, Object aObject){ 242 Args.checkForContent(aName); 243 getExistingSession().setAttribute(aName, aObject); 244 } 245 246 /** Synonym for <tt>addToSession(aName, null)</tt>. */ 247 protected final void removeFromSession(String aName){ 248 addToSession(aName, null); 249 } 250 251 /** 252 Retrieve an object from an existing session, or <tt>null</tt> if no 253 object is paired with <tt>aName</tt>. 254 255 @param aName satisfies {@link hirondelle.web4j.util.Util#textHasContent(String)}. 256 */ 257 protected final Object getFromSession(String aName){ 258 Args.checkForContent(aName); 259 return getExistingSession().getAttribute(aName); 260 } 261 262 /** 263 Place an object which is in an existing session into request scope 264 as well. 265 266 <P>When serving the last page in a session, some session 267 items may still be needed for rendering the final page. 268 269 <P>For example, a log off page in a mutlilingual application might present a 270 "goodbye" message in the language that the user was using. Since the 271 session is being destroyed, the {@link Locale} stored in the session must be 272 first copied into request scope before the session is killed. 273 274 @param aName identifies an <tt>Object</tt> which is currently in session scope, and satisfies 275 {@link hirondelle.web4j.util.Util#textHasContent(String)}. If no attribute of the given name 276 is found in the current session, then a <tt>null</tt> is added to the request scope 277 under this name. 278 */ 279 protected final void copyFromSessionToRequest(String aName){ 280 addToRequest( aName, getFromSession(aName) ); 281 } 282 283 /** 284 If a session exists, then it is invalidated. 285 This method should be called only when the user is logging out. 286 */ 287 protected final void endSession(){ 288 if ( hasExistingSession() ) { 289 fLogger.fine("Session exists, and will now be ended."); 290 getExistingSession().invalidate(); 291 } 292 else { 293 fLogger.fine("Session does not currently exist, so cannot be ended."); 294 } 295 } 296 297 /** 298 Add a simple {@link hirondelle.web4j.model.AppResponseMessage} describing a 299 failed validation of user input, or a failed datastore operation. 300 <P>One of the <tt>addError</tt> methods must be called when a failure occurs. 301 */ 302 protected final void addError(String aMessage){ 303 fErrors.add(aMessage); 304 placeErrorsInSession(); 305 } 306 307 /** 308 Add a compound {@link hirondelle.web4j.model.AppResponseMessage} describing a 309 failed validation of user input, or a failed datastore operation. 310 <P>One of the <tt>addError</tt> methods must be called when a failure occurs. 311 */ 312 protected final void addError(String aMessage, Object... aParams){ 313 fErrors.add(aMessage, aParams); 314 placeErrorsInSession(); 315 } 316 317 /** 318 Add all the error messages attached to <tt>aEx</tt>. 319 <P>One of the <tt>addError</tt> methods must be called when a failure occurs. 320 */ 321 protected final void addError(AppException aEx){ 322 fErrors.add(aEx); 323 placeErrorsInSession(); 324 } 325 326 /** 327 Return all the errors passed to all <tt>addError</tt> methods. 328 */ 329 protected final MessageList getErrors(){ 330 return fErrors; 331 } 332 333 /** 334 Return <tt>true</tt> only if at least one <tt>addError</tt> method has been called. 335 */ 336 protected final boolean hasErrors(){ 337 return fErrors.isNotEmpty(); 338 } 339 340 /** 341 Add a simple {@link hirondelle.web4j.model.AppResponseMessage}, to be displayed 342 to the end user. 343 */ 344 protected final void addMessage(String aMessage){ 345 fMessages.add(aMessage); 346 placeMessagesInSession(); 347 } 348 349 /** 350 Add a compound {@link hirondelle.web4j.model.AppResponseMessage}, to be displayed 351 to the end user. 352 */ 353 protected final void addMessage(String aMessage, Object... aParams){ 354 fMessages.add(aMessage, aParams); 355 placeMessagesInSession(); 356 } 357 358 /** 359 Return all messages passed to all <tt>addMessage</tt> methods 360 */ 361 protected final MessageList getMessages(){ 362 return fMessages; 363 } 364 365 /** 366 Return the {@link Locale} associated with the underlying request. 367 368 <P>The configured implementation of {@link LocaleSource} defines how 369 <tt>Locale</tt> is looked up. 370 */ 371 protected final Locale getLocale(){ 372 return fLocale; 373 } 374 375 /** 376 Return the {@link TimeZone} associated with the underlying request. 377 378 <P>The configured implementation of {@link TimeZoneSource} defines how 379 <tt>TimeZone</tt> is looked up. 380 */ 381 protected final TimeZone getTimeZone(){ 382 return fTimeZone; 383 } 384 385 /** Return the {@link RequestParser} passed to the constructor. */ 386 protected final RequestParser getRequestParser(){ 387 return fRequestParser; 388 } 389 390 /** 391 Convenience method for retrieving a parameter as a simple <tt>Id</tt>. 392 393 <P>Synonym for <tt>getRequestParser().toId(RequestParameter)</tt>. 394 */ 395 protected final Id getIdParam(RequestParameter aReqParam){ 396 return fRequestParser.toId(aReqParam); 397 } 398 399 /** 400 Convenience method for retrieving a multivalued parameter as a simple {@code Collection<Id>}. 401 402 <P>Synonym for <tt>getRequestParser().toIds(RequestParameter)</tt>. 403 */ 404 protected final Collection<Id> getIdParams(RequestParameter aReqParam){ 405 return fRequestParser.toIds(aReqParam); 406 } 407 408 /** 409 Convenience method for retrieving a parameter as {@link SafeText}. 410 411 <P>Synonym for <tt>getRequestParser().toSafeText(RequestParameter)</tt>. 412 */ 413 protected final SafeText getParam(RequestParameter aReqParam){ 414 return fRequestParser.toSafeText(aReqParam); 415 } 416 417 /** 418 Convenience method for retrieving a parameter as raw text, with no escaped 419 characters. 420 421 <P>This method call is unsafe in the sense that it returns <tt>String</tt> 422 instead of {@link SafeText}. It is usually preferable to use {@link SafeText}, 423 since it protects against Cross-Site Scripting attacks. 424 425 <P>If, however, the caller needs to use a request parameter 426 value <em>to perform a computation</em>, as opposed to presenting user 427 data in markup, then this method is provided as a convenience. 428 */ 429 protected final String getParamUnsafe(RequestParameter aReqParam){ 430 SafeText result = fRequestParser.toSafeText(aReqParam); 431 return result == null ? null : result.getRawString(); 432 } 433 434 /** 435 Return an <tt>ORDER BY</tt> clause for an SQL statement. 436 437 <P>Provided as a convenience for the common task of creating an 438 <tt>ORDER BY</tt> clause from request parameters. 439 440 @param aSortColumn carries a <tt>ResultSet</tt> column identifer, either a 441 numeric column index, or the name of the column itself. 442 @param aOrder carries the value <tt>ASC</tt> or <tt>DESC</tt> (ignores case). 443 @param aDefaultOrderBy default text to be used if the request parameters are not 444 present, or have no content. 445 */ 446 protected final DynamicSql getOrderBy(RequestParameter aSortColumn, RequestParameter aOrder, String aDefaultOrderBy){ 447 String result = aDefaultOrderBy; 448 String column = getRequestParser().getRawParamValue(aSortColumn); 449 String order = getRequestParser().getRawParamValue(aOrder); 450 if ( Util.textHasContent(column) && Util.textHasContent(order) ) { 451 if ( ! "ASC".equalsIgnoreCase(order) && ! "DESC".equalsIgnoreCase(order)) { 452 String message = "Sort Order must take value 'ASC' or 'DESC' (ignoring case). Actual value :" + Util.quote(order); 453 fLogger.severe(message); 454 throw new RuntimeException(message); 455 } 456 result = DynamicSql.ORDER_BY + column + SPACE + order; 457 } 458 return new DynamicSql(result); 459 } 460 461 /** 462 Create a new session (if one doesn't already exist) <b>outside of the usual user login</b>, 463 and add a CSRF token to the new session to defend against Cross-Site Request Forgery (CSRF) attacks. 464 465 <P>This method exists to extend the {@link CsrfFilter}, to allow it to apply to a form/action that does not already have a 466 user logged in. 467 468 <P><b>Warning:</b> you can only call this method in Actions for which the 469 {@link hirondelle.web4j.security.SuppressUnwantedSessions} filter is <i>NOT</i> in effect. 470 471 <P><b>Warning:</b> This method should be used with care when using Tomcat. 472 This method creates an 'anonymous' session, unattached to any user login. 473 Should the user log in afterwards, a robust web application should assign a <i>new</i> 474 session id. (See <a href='http://www.owasp.org/'>OWASP</a> for more information.) 475 The problem is that Tomcat 5 and 6 do <i>not</i> follow this rule, and will retain any existing 476 session id when the user logs in. 477 478 <P><b>This method is needed only when the user has not yet logged in.</b> 479 An excellent example of operations <i>not</i> requiring a login are operations that deal with 480 account management on a typical public web site : 481 <ul> 482 <li>registering users 483 <li>regaining lost passwords 484 </ul> 485 486 <P>For such forms, it's strongly recommended that corresponding Actions call this method. 487 This will allow the {@link CsrfFilter} mechanism to be used to defend such forms against CSRF attack. 488 As a second benefit, it will also allow information messages sent to the end user to survive <i>redirect</i> operations. 489 */ 490 protected final void createSessionAndCsrfToken(){ 491 boolean CREATE_IF_MISSING = true; 492 HttpSession session = getRequestParser().getRequest().getSession(DO_NOT_CREATE); 493 if( session == null ) { 494 fLogger.fine("No session exists. Creating new session, outside of regular login."); 495 session = getRequestParser().getRequest().getSession(CREATE_IF_MISSING); 496 fLogger.fine("Adding CSRF token to the new session, to defend against CSRF attacks."); 497 CsrfFilter csrfFilter = new CsrfFilter(); 498 try { 499 csrfFilter.addCsrfToken(getRequestParser().getRequest()); 500 } 501 catch (ServletException ex){ 502 throw new RuntimeException(ex); 503 } 504 } 505 else { 506 fLogger.fine("Not creating a new session, since one already exists. Assuming the session contains a CSRF token."); 507 } 508 } 509 510 // PRIVATE // 511 512 /* 513 Design Note : 514 This abstract base class (ABC) does not use protected fields. 515 Instead, its fields are private, and subclasses which need to operate on 516 fields do so indirectly, by calling <tt>final</tt> convenience methods 517 such as {@link #addToRequest}. 518 519 This style was chosen because, in this case, it seems to be simpler. 520 Subclasses need only a small number of interactions with these fields. If a 521 a large number of interactions were needed, then changing field scope to 522 protected would become more attractive. 523 524 As well, note how most methods are declared as <tt>final</tt>, except 525 for the <tt>abstract</tt> ones. 526 */ 527 528 private ResponsePage fFinalResponsePage; 529 private final RequestParser fRequestParser; 530 private final Locale fLocale; 531 private final TimeZone fTimeZone; 532 private final Operation fOperation; 533 534 /* Control the creation of sessions. */ 535 private static final boolean DO_NOT_CREATE = false; 536 537 private final MessageList fErrors; 538 private final MessageList fMessages; 539 540 private static final Logger fLogger = Util.getLogger(ActionImpl.class); 541 542 /** 543 Fetch first from request parameter; if not there, use the 'file extension' instead. 544 If still none, return <tt>null</tt>. 545 */ 546 private Operation parseOperation(){ 547 String opValue = getRequestParser().getRequest().getParameter("Operation"); 548 if( ! Util.textHasContent(opValue) ) { 549 opValue = getFileExtension(); 550 } 551 return Operation.valueFor(opValue); 552 } 553 554 private String getFileExtension(){ 555 String uri = getRequestParser().getRequest().getRequestURI(); 556 fLogger.finest("URI : " + uri); 557 return WebUtil.getFileExtension(uri); 558 } 559 560 private boolean hasExistingSession() { 561 return fRequestParser.getRequest().getSession(DO_NOT_CREATE) != null; 562 } 563 564 /** 565 If {@link #getErrors} has content, place it in session scope under the name {@value #ERRORS}. 566 If it is already in the session, then it is updated. 567 */ 568 private void placeErrorsInSession(){ 569 if ( getErrors().isNotEmpty() ) { 570 addToSession(ERRORS, getErrors()); 571 } 572 } 573 574 /** 575 If {@link #getMessages} has content, place it in session scope under the name {@value #MESSAGES}. 576 If it is already in the session, then it is updated. 577 */ 578 private void placeMessagesInSession(){ 579 if ( getMessages().isNotEmpty() ) { 580 addToSession(MESSAGES, getMessages()); 581 } 582 } 583 }