package hirondelle.web4j.action;

import static hirondelle.web4j.util.Consts.SPACE;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.database.DynamicSql;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.MessageList;
import hirondelle.web4j.model.MessageListImpl;
import hirondelle.web4j.request.LocaleSource;
import hirondelle.web4j.request.RequestParameter;
import hirondelle.web4j.request.RequestParser;
import hirondelle.web4j.request.TimeZoneSource;
import hirondelle.web4j.security.CsrfFilter;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Args;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.WebUtil;
import java.security.Principal;
import java.util.Collection;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;

/**
 Abstract Base Class (ABC) for implementations of the {@link Action} interface.

 <P>This ABC provides concise methods for common operations, which will make 
 implementations read more clearly, concisely, and at a higher level of abstraction.

 <P>A simple fetch-and-display operation can often be implemented using this class 
 as a base class. However, operations involving user input and/or edits to the 
 datastore should very likely use other abstract base classes, such as 
 {@link ActionTemplateListAndEdit}, {@link ActionTemplateSearch}, and 
 {@link hirondelle.web4j.action.ActionTemplateShowAndApply}.
 
 <P>This class places success/fail messages in session scope, not request scope. 
 This is because such messages often need to survive a redirect operation. 
 For example, when a successful edit to the database occurs, a <em>redirect</em> is 
 usually performed, to avoid problems with browser reloads. 
 The only way a success message can survive a redirect is by being placed 
 in session scope.

<P><em>This class assumes that a session already exists</em>. 
 If a session does not already exist, then calling such methods will result in an error.
 In practice, the user will almost always have already logged in, and this will not 
 be a problem.  As a backup, actions can always explicitly create a session, if needed, 
 by calling 
 <PRE>getRequestParser.getRequest().getSession(true);</PRE>
*/
public abstract class ActionImpl implements Action {
  
  
  /**
   Value {@value} - identifies a {@link hirondelle.web4j.model.MessageList}, placed 
   in session scope, to hold error information for the end user. 
   These errors are used by both WEB4J and the application programmer.
  */
  public static final String ERRORS = "web4j_key_for_errors";
   
  /**
   Value {@value} - identifies a {@link hirondelle.web4j.model.MessageList}, placed 
   in session scope, to hold messages for the end user. These messages are 
   used by both WEB4J and the application programmer. They typically hold success 
   and information messages.
  */
  public static final String MESSAGES = "web4j_key_for_messages";

  /**
  Value {@value} - identifies the user's id, placed in session scope. 
   
  <P>Many applications will benefit from having <i>both</i> the user id <i>and</i> the user login name 
  placed in session scope upon login. The Servlet Container will place the user <i>login name</i> 
  in session scope upon login, but it will not place the corresponding <i>user id</i> 
  (the database's primary key of the user record) in session scope. 
  
  <P>If an application chooses to place the user's underlying database id into session scope under 
  this USER_ID key, then the user's id will be returned by {@link #getUserId()}. 
  */
  public static final String USER_ID = "web4j_key_for_user_id";
  
  /** 
   Value {@value} - generic key for an object placed in scope for a JSP.
   <P>Not mandatory to use this generic key. Provided simply as a convenience. 
  */
  public static final String ITEM_FOR_EDIT = "itemForEdit";
  
  /** 
   Value {@value} - generic key for a collection of objects placed in scope for a JSP. 
   <P>Not mandatory to use this generic key. Provided simply as a convenience. 
  */
  public static final String ITEMS_FOR_LISTING = "itemsForListing";
  
  /** 
  Value {@value} - generic key for a single 'data' object placed in scope for a JSP.
  Usually used with structured data, such as JSON, XML, CSV, and so on. 
  <P>Not mandatory to use this generic key. Provided simply as a convenience. 
  */
  public static final String DATA = "data";

  /**
   Constructor. 
   
   <P>This constructor will add an attribute named <tt>'Operation'</tt> to the request. Its 
   value is deduced as specified by {@link #getOperation()}. This attribute is intended for JSPs, 
   which can use it to access the <tt>Operation</tt> regardless of its original source.
   
   @param aNominalPage simply one of the possible {@link ResponsePage}s, 
   arbitrarily chosen as a "default". It may be changed after construction 
   by calling {@link #setResponsePage}. Recommended that the "success" page
   be chosen as the nominal page. If not, then selection of any of the 
   possible {@link ResponsePage}s is acceptable.
   
   @param aRequestParser allows parsing of request parameters into higher level java objects.
  */
  protected ActionImpl(ResponsePage aNominalPage, RequestParser aRequestParser) {
    fFinalResponsePage = aNominalPage;
    fRequestParser = aRequestParser;
    fErrors = new AppException();
    fMessages = new MessageListImpl();
    fLocale = BuildImpl.forLocaleSource().get(fRequestParser.getRequest());
    fTimeZone = BuildImpl.forTimeZoneSource().get(fRequestParser.getRequest());
    fOperation = parseOperation();
    addToRequest("Operation", fOperation);
    fLogger.fine("Operation: " + fOperation);
  }

  public abstract ResponsePage execute() throws AppException;

  /** Return the resource which will render the final result.  */
  public final ResponsePage getResponsePage(){
    return fFinalResponsePage;
  }
  
  /** 
   Return the {@link Operation} associated with this <tt>Action</tt>, if any.
   
   <P>The <tt>Operation</tt> is found as follows :
   <ol>
   <li>if there is a request parameter named <tt>'Operation'</tt>, and it has a value, pass its value to 
   {@link Operation#valueFor(String)} 
   <li>if the above style fails, then the 'extension' is examined. For example, a request to <tt>.../MyAction.list?x=1</tt> 
    would result in {@link Operation#List} being added to request scope, since the extension value <tt>list</tt> is known to 
    {@link Operation#valueFor(String)}.
    This style is useful for implementing fine-grained <tt>&lt;security-constraint&gt;</tt>
    items in <tt>web.xml</tt>. See the User Guide for more information.
    <li>if both of the above methods fail, return <tt>null</tt>
   </ol>
   
   <P>When using the 'extension' style, please note that <tt>web.xml</tt> contains related <tt>servlet-mapping</tt> settings.
   Such settings control which HTTP requests (as defined by a <tt>url-pattern</tt>) are passed from the Servlet Container to  
   your application in the first place. Thus, <b>any 'extensions' which your application intends to use must have a corresponding  
   <tt>servlet-mapping</tt> setting in your <tt>web.xml</tt></b>. 
  */
  protected final Operation getOperation(){
    return fOperation;
  }

  /**
  Return the name of the logged in user. 
  
  <P>By definition in the servlet specification, a successfully logged in user 
  will always have a non-<tt>null</tt> return value for 
  {@link javax.servlet.http.HttpServletRequest#getUserPrincipal()}.
  
  <P>If the user is not logged in, this method will always return <tt>null</tt>.
  
  <P>This method returns {@link SafeText}, not a <tt>String</tt>. 
  The user name is often rendered in the view. Since in general the user name
   may contain special characters, it is appropriate to model it as 
  <tt>SafeText</tt>. 
  */
  protected final SafeText getLoggedInUserName(){
    Principal principal = fRequestParser.getRequest().getUserPrincipal();
    return principal == null ? null : new SafeText(principal.getName());
  }
  
  /**
  Return the {@link Id} stored in session scope under the key {@link #USER_ID}.
  If no such item is found, then return <tt>null</tt>.
  
   <P><style class='highlight'>This internal database identifier should never be served to the client, since that 
   would be a grave security risk.</style> The user id should only be used in server-side code, and never
   presented to the user in a JSP.
  */
  protected final Id getUserId(){
    Id result = (Id) getFromSession(USER_ID);
    return result;
  }
  
  /**
   Called by subclasses if the final {@link ResponsePage}
   differs from the nominal one passed to the constructor.
  
   <P>If an implementation calls this method, it is usually 
   called in {@link #execute}.
  */
  protected final void setResponsePage(ResponsePage aNewResponsePage){
    fFinalResponsePage = aNewResponsePage;
  }
  
  /**
   Add a name-object pair to request scope.
  
   <P>If the pair already exists, it is <em>updated</em> with <tt>aObject</tt>.
  
   @param aName satisfies {@link hirondelle.web4j.util.Util#textHasContent(String)}.
   @param aObject if <tt>null</tt> and a corresponding name-object pair exists, then
   the pair is <em>removed</em> from request scope.
  */
  protected final void addToRequest(String aName, Object aObject){
    Args.checkForContent(aName);
    fRequestParser.getRequest().setAttribute(aName, aObject);
  }

  /**
   Return the existing session. 
   <P>If a session does not already exist, then an error will result.
  */
  protected final HttpSession getExistingSession(){
    HttpSession result = fRequestParser.getRequest().getSession(DO_NOT_CREATE);
    if ( result == null ) {
      String MESSAGE = "No session currently exists. Either require user to login, or create a session explicitly.";
      fLogger.severe(MESSAGE);
      throw new UnsupportedOperationException(MESSAGE);
    }
    return result;
  }
  
  /**
   Add a name-object pair to an existing session.
  
   <P>If the pair already exists, it is <em>updated</em> with <tt>aObject</tt>.
  
   @param aName satisfies {@link hirondelle.web4j.util.Util#textHasContent(String)}.
   @param aObject if <tt>null</tt> and a corresponding name-object pair exists, then
   the pair is <em>removed</em> from session scope.
  */
  protected final void addToSession(String aName, Object aObject){
    Args.checkForContent(aName);
    getExistingSession().setAttribute(aName, aObject);
  }
  
  /** Synonym for <tt>addToSession(aName, null)</tt>.   */
  protected final void removeFromSession(String aName){
    addToSession(aName, null);
  }
  
  /**
   Retrieve an object from an existing session, or <tt>null</tt> if no 
   object is paired with <tt>aName</tt>. 
  
   @param aName satisfies {@link hirondelle.web4j.util.Util#textHasContent(String)}.
  */
  protected final Object getFromSession(String aName){
    Args.checkForContent(aName);
    return getExistingSession().getAttribute(aName);
  }

  /**
   Place an object which is in an existing session into request scope
   as well.
  
  <P>When serving the last page in a session, some session 
   items may still be needed for rendering the final page.
   
   <P>For example, a log off page in a mutlilingual application might present a 
   "goodbye" message in the language that the user was using. Since the 
   session is being destroyed, the {@link Locale} stored in the session must be 
   first copied into request scope before the session is killed.
  
   @param aName identifies an <tt>Object</tt> which is currently in session scope, and satisfies 
   {@link hirondelle.web4j.util.Util#textHasContent(String)}. If no attribute of the given name 
   is found in the current session, then a <tt>null</tt> is added to the request scope 
   under this name.
  */
  protected final void copyFromSessionToRequest(String aName){
    addToRequest( aName, getFromSession(aName) );
  }

  /** 
   If a session exists, then it is invalidated.
   This method should be called only when the user is logging out.  
  */
  protected final void endSession(){
    if ( hasExistingSession() ) {
      fLogger.fine("Session exists, and will now be ended.");
      getExistingSession().invalidate();
    }
    else {
      fLogger.fine("Session does not currently exist, so cannot be ended.");
    }
  }

  /**
   Add a simple {@link hirondelle.web4j.model.AppResponseMessage} describing a 
   failed validation of user input, or a  failed datastore operation. 
   <P>One of the <tt>addError</tt> methods must be called when a failure occurs.
  */
  protected final void addError(String aMessage){
    fErrors.add(aMessage);
    placeErrorsInSession();
  }
  
  /**
   Add a compound {@link hirondelle.web4j.model.AppResponseMessage} describing a 
   failed validation of user input, or a  failed datastore operation. 
   <P>One of the <tt>addError</tt> methods must be called when a failure occurs.
  */
  protected final void addError(String aMessage, Object... aParams){
    fErrors.add(aMessage, aParams);
    placeErrorsInSession();
  }
  
  /**
   Add all the error messages attached to <tt>aEx</tt>.  
   <P>One of the <tt>addError</tt> methods must be called when a failure occurs.
  */
  protected final void addError(AppException aEx){
    fErrors.add(aEx);
    placeErrorsInSession();
  }
  
  /**
   Return all the errors passed to all <tt>addError</tt> methods.
  */
  protected final MessageList getErrors(){
    return fErrors;
  }
  
  /**
   Return <tt>true</tt> only if at least one <tt>addError</tt> method has been called.
  */
  protected final boolean hasErrors(){
    return fErrors.isNotEmpty();
  }
  
  /**
   Add a simple {@link hirondelle.web4j.model.AppResponseMessage}, to be displayed 
   to the end user.
  */
  protected final void addMessage(String aMessage){
    fMessages.add(aMessage);
    placeMessagesInSession();
  }
  
  /**
   Add a compound {@link hirondelle.web4j.model.AppResponseMessage}, to be displayed 
   to the end user.
  */
  protected final void addMessage(String aMessage, Object... aParams){
    fMessages.add(aMessage, aParams);
    placeMessagesInSession();
  }
  
  /**
   Return all messages passed to all <tt>addMessage</tt> methods
  */
  protected final MessageList getMessages(){
    return fMessages;
  }

  /**
   Return the {@link Locale} associated with the underlying request. 
   
   <P>The configured implementation of {@link LocaleSource} defines how 
   <tt>Locale</tt> is looked up.  
  */
  protected final Locale getLocale(){
    return fLocale;
  }
  
  /**
   Return the {@link TimeZone} associated with the underlying request. 
   
   <P>The configured implementation of {@link TimeZoneSource} defines how 
   <tt>TimeZone</tt> is looked up.  
  */
  protected final TimeZone getTimeZone(){
    return fTimeZone;
  }
  
  /** Return the {@link RequestParser} passed to the constructor. */
  protected final RequestParser getRequestParser(){
    return fRequestParser;
  }
  
  /**
   Convenience method for retrieving a parameter as a simple <tt>Id</tt>.
   
   <P>Synonym for <tt>getRequestParser().toId(RequestParameter)</tt>.
  */
  protected final Id getIdParam(RequestParameter aReqParam){
    return fRequestParser.toId(aReqParam);
  }
  
  /**
  Convenience method for retrieving a multivalued parameter as a simple {@code Collection<Id>}.
  
  <P>Synonym for <tt>getRequestParser().toIds(RequestParameter)</tt>.
 */
  protected final Collection<Id> getIdParams(RequestParameter aReqParam){
    return fRequestParser.toIds(aReqParam);
  }
  
  /**
   Convenience method for retrieving a parameter as {@link SafeText}.
   
   <P>Synonym for <tt>getRequestParser().toSafeText(RequestParameter)</tt>.
  */
  protected final SafeText getParam(RequestParameter aReqParam){
    return fRequestParser.toSafeText(aReqParam);
  }
  
  /**
   Convenience method for retrieving a parameter as raw text, with no escaped 
   characters.
   
   <P>This method call is unsafe in the sense that it returns <tt>String</tt> 
   instead of {@link SafeText}.  It is usually preferable to use {@link SafeText}, 
   since it protects against Cross-Site Scripting attacks.
   
   <P>If, however, the caller needs to use a request parameter 
   value <em>to perform a computation</em>, as opposed to presenting user 
   data in markup, then this method is provided as a convenience.
  */
  protected final String getParamUnsafe(RequestParameter aReqParam){
    SafeText result = fRequestParser.toSafeText(aReqParam);
    return result == null ? null : result.getRawString();
  }
  
  /**
   Return an <tt>ORDER BY</tt> clause for an SQL statement.
   
   <P>Provided as a convenience for the common task of creating an 
   <tt>ORDER BY</tt> clause from request parameters.
    
   @param aSortColumn carries a <tt>ResultSet</tt> column identifer, either a 
   numeric column index, or the name of the column itself.
   @param aOrder carries the value <tt>ASC</tt> or <tt>DESC</tt> (ignores case).
   @param aDefaultOrderBy default text to be used if the request parameters are not 
   present, or have no content.
  */
  protected final DynamicSql getOrderBy(RequestParameter aSortColumn, RequestParameter aOrder, String aDefaultOrderBy){
    String result = aDefaultOrderBy;
    String column = getRequestParser().getRawParamValue(aSortColumn);
    String order = getRequestParser().getRawParamValue(aOrder); 
    if ( Util.textHasContent(column) && Util.textHasContent(order) ) {
      if ( ! "ASC".equalsIgnoreCase(order) && ! "DESC".equalsIgnoreCase(order)) {
        String message = "Sort Order must take value 'ASC' or 'DESC' (ignoring case). Actual value :" + Util.quote(order);
        fLogger.severe(message);
        throw new RuntimeException(message);
      }
      result = DynamicSql.ORDER_BY + column + SPACE + order;      
    }
    return new DynamicSql(result);
  }
  
  /**
   Create a new session  (if one doesn't already exist) <b>outside of the usual user login</b>, 
   and add a CSRF token to the new session to defend against Cross-Site Request Forgery (CSRF) attacks.
   
   <P>This method exists to extend the {@link CsrfFilter}, to allow it to apply to a form/action that does not already have a 
   user logged in. 
   
   <P><b>Warning:</b> you can only call this method in Actions for which the 
   {@link hirondelle.web4j.security.SuppressUnwantedSessions} filter is <i>NOT</i> in effect.
   
   <P><b>Warning:</b> This method should be used with care when using Tomcat.
   This method creates an 'anonymous' session, unattached to any user login.
   Should the user log in afterwards, a robust web application should assign a <i>new</i>
   session id. (See <a href='http://www.owasp.org/'>OWASP</a> for more information.)
   The problem is that Tomcat 5 and 6 do <i>not</i> follow this rule, and will retain any existing 
   session id when the user logs in. 
   
   <P><b>This method is needed only when the user has not yet logged in.</b>
   An excellent example of operations <i>not</i> requiring a login are operations that deal with 
   account management on a typical public web site :
   <ul>
    <li>registering users
    <li>regaining lost passwords
   </ul>
   
   <P>For such forms, it's strongly recommended that corresponding Actions call this method.
   This will allow the {@link CsrfFilter} mechanism to be used to defend such forms against CSRF attack.  
   As a second benefit, it will also allow information messages sent to the end user to survive <i>redirect</i> operations.
  */
  protected final void createSessionAndCsrfToken(){
    boolean CREATE_IF_MISSING = true;
    HttpSession session = getRequestParser().getRequest().getSession(DO_NOT_CREATE);
    if( session == null ) {
      fLogger.fine("No session exists. Creating new session, outside of regular login.");
      session = getRequestParser().getRequest().getSession(CREATE_IF_MISSING);
      fLogger.fine("Adding CSRF token to the new session, to defend against CSRF attacks.");
      CsrfFilter csrfFilter = new CsrfFilter();
      try { 
        csrfFilter.addCsrfToken(getRequestParser().getRequest());
      }
      catch (ServletException ex){
        throw new RuntimeException(ex);
      }
    }
    else {
      fLogger.fine("Not creating a new session, since one already exists. Assuming the session contains a CSRF token.");
    }
  }
  
  // PRIVATE //
  
  /* 
   Design Note :
   This abstract base class (ABC) does not use protected fields.
   Instead, its fields are private, and subclasses which need to operate on 
   fields do so indirectly, by calling <tt>final</tt> convenience methods
   such as {@link #addToRequest}.
  
   This style was chosen because, in this case, it seems to be simpler. 
   Subclasses need only a small number of interactions with these fields. If a 
   a large number of interactions were needed, then changing field scope to 
   protected would become more attractive.
  
   As well, note how most methods are declared as <tt>final</tt>, except 
   for the <tt>abstract</tt> ones.
  */

  private ResponsePage fFinalResponsePage;
  private final RequestParser fRequestParser;
  private final Locale fLocale;
  private final TimeZone fTimeZone;
  private final Operation fOperation; 

  /* Control the creation of sessions.  */
  private static final boolean DO_NOT_CREATE = false;
  
  private final MessageList fErrors;
  private final MessageList fMessages;
  
  private static final Logger fLogger = Util.getLogger(ActionImpl.class);
  
  /**
   Fetch first from request parameter; if not there, use the 'file extension' instead.
   If still none, return <tt>null</tt>.
  */
  private Operation parseOperation(){
    String opValue = getRequestParser().getRequest().getParameter("Operation");
    if( ! Util.textHasContent(opValue) ) {
      opValue = getFileExtension();
    }
    return Operation.valueFor(opValue);
  }
  
  private String getFileExtension(){
    String uri = getRequestParser().getRequest().getRequestURI();
    fLogger.finest("URI : " + uri);
    return WebUtil.getFileExtension(uri);
  }
  
  private boolean hasExistingSession() {
    return fRequestParser.getRequest().getSession(DO_NOT_CREATE) != null;
  }
  
  /**
   If {@link #getErrors} has content, place it in session scope under the name {@value #ERRORS}.
   If it is already in the session, then it is updated.
  */
  private void placeErrorsInSession(){
    if ( getErrors().isNotEmpty() ) {
      addToSession(ERRORS, getErrors());
    }
  }
   
  /**
   If {@link #getMessages} has content, place it in session scope under the name {@value #MESSAGES}.
   If it is already in the session, then it is updated.
  */
  private void placeMessagesInSession(){
    if ( getMessages().isNotEmpty() ) {
      addToSession(MESSAGES, getMessages());
    }
  }
}
