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