package hirondelle.web4j.ui.tag;

import java.util.*;
import java.util.logging.*;
import java.security.Principal;
import hirondelle.web4j.util.Util;
import static hirondelle.web4j.util.Consts.PASSES;
import static hirondelle.web4j.util.Consts.FAILS;
import static hirondelle.web4j.util.Consts.EMPTY_STRING;

/**
 Toggle display according to the user's role.
 
 <P><span class="highlight">It's important to note that the <em>sole</em> use of this 
 tag does <em>not</em> robustly enforce security constraints.</span>
 This tag is meant as a "cosmetic convenience" for removing items from JSPs (usually a link). 
 The problem is that a hacker can always construct any given URI manually and send it to the server. 
 Such malicious requests can only be handled robustly by a <tt>security-constraint</tt>
 defined in <tt>web.xml</tt>.   
 
 <P>Example:
 <PRE>
 &lt;w:show ifRole="webmaster,translator"&gt;
   show tag content only if the user is logged in, 
   and has at least 1 of the specified roles
 &lt;/w:show&gt;
 </PRE>
 
 Example with role specified by negation:
 <PRE> 
 &lt;w:show ifRoleNot="read-only"&gt;
   show tag content only if the user is logged in, 
   and has none of the specified roles
 &lt;/w:show&gt;
 </PRE>

 Example with logic attached not to role, but simply whether or not the user has logged in:
 <PRE>
 &lt;w:show ifLoggedIn="true"&gt;
   show tag content only if the user is logged in 
 &lt;/w:show&gt;
 
 &lt;w:show ifLoggedIn="false"&gt;
   show tag content only if the user is not logged in 
 &lt;/w:show&gt;</pre>
 
 The above styles are all mutually exclusive. You can specify only 1 attribute at a time with this tag.
 
 <P>The body of this class is either echoed as is, or is suppressed entirely.
 
 <P>By definition (in the servlet specification), a user is logged in when <tt>request.getUserPrincipal()</tt> 
 returns a value having content. When a user is logged in, the container can assign 
 1 or more roles to the user. Roles are only assigned after a successful login.
*/
public final class ShowForRole extends TagHelper {

  /** Optional, comma-delimited list of accepted roles. */
  public void setIfRole(String aRoles){
    fAcceptedRoles = getRoles(aRoles);
  }
  
  /** Optional, comma-delimited list of denied roles.  */
  public void setIfRoleNot(String aRoles){
    fDeniedRoles = getRoles(aRoles); 
  }
  
  /** 
   Optional, simple flag indicating if user is or is not logged in.
   @param aFlag - see {@link Util#parseBoolean(String)} for the list of accepted values. 
  */
  public void setIfLoggedIn(String aFlag){
    fIfLoggedIn = Util.parseBoolean(aFlag); //null if the flag is null
  }
  
  /**
   One and only one of the {@link #setIfRole}, {@link #setIfRoleNot}, or 
   {@link #setIfLoggedIn(String)} attributes must be set.
  */
  protected void crossCheckAttributes() {
    int numAttrsSpecified = 0;
    if (isModeAcceptingRoles()) ++numAttrsSpecified;
    if (isModeDenyingRoles())  ++numAttrsSpecified;
    if (isModeLogin()) ++numAttrsSpecified;
    
    if(numAttrsSpecified != 1) {
      String message = "Please specify 1 (and only 1) attribute for this tag: ifRole, ifRoleNot, or ifLoggedIn. Page Name: " + getPageName();
      fLogger.severe(message);
      throw new IllegalArgumentException(message);
    }
  }
  
  /** See class comment.  */
  @Override protected String getEmittedText(String aOriginalBody) {
    boolean showBody = false;
    Principal user = getRequest().getUserPrincipal();
    boolean isCurrentlyLoggedIn = (user != null);
    
    if (isModeAcceptingRoles() && isCurrentlyLoggedIn){
      showBody = hasOneOrMoreOfThe(fAcceptedRoles);
    }
    else if (isModeDenyingRoles() && isCurrentlyLoggedIn){
      showBody = ! hasOneOrMoreOfThe(fDeniedRoles);
    }
    else if (isModeLogin()){
      showBody = fIfLoggedIn ? isCurrentlyLoggedIn : ! isCurrentlyLoggedIn; 
    }
    return showBody ? aOriginalBody : EMPTY_STRING;
  }
  
  // PRIVATE
  private List<String> fAcceptedRoles = new ArrayList<String>();
  private List<String> fDeniedRoles = new ArrayList<String>();
  /** The null-ity of this field is needed to indicate the 'unspecified' state. */
  private Boolean fIfLoggedIn; 
  
  private static final String DELIMITER = ",";
  private static final Logger fLogger = Util.getLogger(ShowForRole.class);
  
  private List<String> getRoles(String aRawRoles){
    List<String> result = new ArrayList<String>();
    StringTokenizer parser = new StringTokenizer( aRawRoles, DELIMITER);
    while ( parser.hasMoreTokens() ) {
      result.add( parser.nextToken().trim() );
    }
    return result;
  }
  
  private boolean isModeAcceptingRoles(){
    return ! fAcceptedRoles.isEmpty();
  }
  
  private boolean isModeDenyingRoles(){
    return ! fDeniedRoles.isEmpty();
  }
  
  private boolean isModeLogin(){
    return fIfLoggedIn != null;
  }

  private boolean hasOneOrMoreOfThe(List<String> aRoles){
    boolean result = FAILS;
    for (String role: aRoles){
      if ( getRequest().isUserInRole(role) ) {
        result = PASSES;
        break;
      }
    }
    return result;
  }
}
