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 }