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>&lt;security-constraint&gt;</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    }