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