package hirondelle.web4j.ui.tag;

import java.io.*;
import java.util.logging.*;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.JspContext;

import hirondelle.web4j.util.Util;

/**
 Base class for implementing custom JSP tags.
 
 <P>The custom tag can optionally have a body. The <tt>.tld</tt> entry for these tags must 
 have their <tt>body-content</tt> set to <tt>scriptless</tt>.
 
 <P>Concrete subclasses of this class perform these tasks :
<ul>
 <li>implement <tt>setXXX</tt> methods, one for each tag attribute;
 each <tt>setXXX</tt> should validate its argument.
 <li>optionally override the {@link #crossCheckAttributes} method, to 
 perform validations depending on more than one attribute.
 <li>implement {@link #getEmittedText}, to return the text to be included in markup.
</ul>
*/
public abstract class TagHelper extends SimpleTagSupport {
  
  /**
   <b>Template</b> method which calls {@link #getEmittedText(String)}.
   
   <P>The body of this tag is evaluated, passed to {@link #getEmittedText(String)}, 
   and the result is then written to the JSP output writer. In addition, this method will call
   {@link #crossCheckAttributes()} at the start of processing.
  */
  @Override public final void doTag() throws JspException {
    try {
      crossCheckAttributes();
      getJspContext().getOut().write(getEmittedText(getBody()));
    }
    catch (Throwable ex){
      fLogger.severe("Cannot execute custom tag. " + Util.quote(ex));
      throw new JspException("Cannot execute custom tag.", ex);
    }
  }
  
  /** 
   Return the text this tag will display in the resulting web page.
   
   @param aOriginalBody is the evaluated body of this tag. If there is no body, or 
   if the body is present but empty, then it is <tt>null</tt>.
   @return the text to display in the resulting web page. 
  */
  abstract protected String getEmittedText(String aOriginalBody) throws JspException, IOException;
   
  /**
   Perform validations that apply to more than one attribute.
  
   <P>This default implementation does nothing.
   
   <P>Validations that apply to a single attribute should be performed in its 
   corresponding <tt>setXXX</tt> method.
  
   <P>If a problem is detected, subclasses must emit a <tt>RuntimeException</tt> 
   describing the problem. If all validations apply to only to a single attribute, 
   then this method should not be overridden.
  */
  protected void crossCheckAttributes() {
    //do nothing in this default impl
  }
  
  /** Return the underlying {@link HttpServletRequest}.  */
  protected final HttpServletRequest getRequest(){
    return (HttpServletRequest)getPageContext().getRequest();
  }
  
  /** Return the underlying {@link HttpServletResponse}.  */
  protected final HttpServletResponse getResponse(){
    return (HttpServletResponse)getPageContext().getResponse();
  }
  
  /** Return the underlying {@link PageContext}.  */
  protected final PageContext getPageContext(){
    JspContext jspContext = getJspContext();
    return (PageContext)jspContext;
  }

  /** 
   Return the name of the JSP implementation class. 
   <P>Intended for debugging only. 
  */
  protected final String getPageName(){
    Servlet servlet = (Servlet)getPageContext().getPage();
    return servlet.getClass().getName();
  }
  
  /**
   Verify that an attribute value has content. 
   
   <P>If no content, then log at <tt>SEVERE</tt> and throw an unchecked exception.
  */
  protected final void checkForContent(String aAttributeName, String aAttributeValue){
    if( ! Util.textHasContent(aAttributeValue) ){
      String message = Util.quote(aAttributeName) + " attribute must have a value.";
      fLogger.severe(message);
      throw new IllegalArgumentException(message);
    }
  }

  // PRIVATE //
  
  private static final Logger fLogger = Util.getLogger(TagHelper.class);
  
  /** 
   Return the evaluated body of this tag.  
   
   <P>The body of this tag cannot contain scriptlets or scriptlet expressions.
   If this tag has no body, or has an empty body, then <tt>null</tt> is returned.
  */
  private String getBody() throws IOException, JspException {
    String result = null;
    JspFragment body = getJspBody();
    if( body != null ){
      StringWriter writer = new StringWriter();
      getJspBody().invoke(writer);
      writer.flush();
      result = writer.toString();
    }
    return result;
  }
}
