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      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    }