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