/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2002-03 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.dynamo.xmlrpc;

import java.io.*;
import java.text.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.w3c.dom.*;
import com.silverwrist.util.*;
import com.silverwrist.util.xml.*;
import com.silverwrist.dynamo.RequestType;
import com.silverwrist.dynamo.Verb;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.iface.*;
import com.silverwrist.dynamo.util.*;

/**
 * Implements the {@link com.silverwrist.dynamo.iface.Request Request} interface for all XML-RPC requests; parses
 * the request body.
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 */
class XmlRpcRequest extends BaseDelegatingServiceProvider implements Request
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static final String EMPTY_STRING = "";

  // Indicates the types of values associated with the request.
  private static final int ITYP_INTEGER = 0;      // integer
  private static final int ITYP_BOOLEAN = 1;      // Boolean
  private static final int ITYP_STRING = 2;       // string
  private static final int ITYP_DOUBLE = 3;       // double
  private static final int ITYP_DATETIME = 4;     // date/time
  private static final int ITYP_BINARY = 5;       // binary (byte array)
  private static final int ITYP_STRUCT = 6;       // struct (Map)
  private static final int ITYP_ARRAY = 7;        // array (List)

  private static final Map MAP_TYPE;     // maps element names to type values

  private static final DateFormat s_iso8601;   // used to format and parse date/time values

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private HttpServletRequest m_req;            // associated HTTP request
  private String m_method;                     // XML-RPC method
  private HashMap m_params = new HashMap();    // parameters
  private Locale[] m_locales;                  // locale list
  private MemoryObjectStore m_attrs;           // scratchpad for attributes

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  /**
   * Creates a new XML-RPC request, parsing the request body to get the actual request data.
   *
   * @param req The original <CODE>HttpServletRequest</CODE> passed to the servlet.
   * @param verb The verb associated with this request.
   * @param services A {@link com.silverwrist.dynamo.iface.ServiceProvider ServiceProvider} object controlling
   *                 access to request services.
   * @param prev The previous {@link com.silverwrist.dynamo.iface.Request Request} object.  May be <CODE>null</CODE>.
   * @exception com.silverwrist.dynamo.except.RequestParseException If the request data is grossly wrong (these
   *            errors are reported as HTTP error codes).
   * @exception com.silverwrist.dynamo.xmlrpc.FaultCode If an error occurs in parsing the request (these errors
   *            are reported as XML-RPC faults).
   */
  XmlRpcRequest(HttpServletRequest req, Verb verb, ServiceProvider services, Request prev)
      throws RequestParseException, FaultCode
  {
    super("XmlRpcRequest",services);
    if (!(Verb.POST.equals(verb)))
      throw new RequestParseException("XML-RPC requests must be POST requests");
    String ctype = req.getContentType();
    if (!(ctype.startsWith("text/xml")))
      throw new RequestParseException("XML-RPC requests must have an XML content type");

    XMLLoader loader = XMLLoader.get();
    Document request_doc = null;
    try
    { // parse the XML request
      request_doc = loader.load(req.getInputStream(),false);

    } // end try
    catch (XMLLoadException e)
    { // parse error in XML body...
      throw new SystemFaultCode(SystemFaultCode.PARSE_ERROR,e);

    } // end catch
    catch (IOException e)
    { // I/O error in XML body...
      throw new SystemFaultCode(SystemFaultCode.PARSE_ERROR,e);

    } // end catch

    try
    { // load the XML body of the request, get the method name and parameters
      Element method_call = loader.getRootElement(request_doc,"methodCall");
      DOMElementHelper method_call_h = new DOMElementHelper(method_call);
      m_method = loader.getSubElementText(method_call_h,"methodName");
      Element params_elt = method_call_h.getSubElement("params");
      if (params_elt!=null)
      { // get all <param/> elements from under <params/> and process them
	List l = loader.getMatchingSubElements(params_elt,"param");
	int ndx = 0;
	Iterator it = l.iterator();
	while (it.hasNext())
	{ // get the <value/> subelement from each <param/> and store it
	  Element elt = (Element)(it.next());
	  Element value_elt = loader.getSubElement(elt,"value");
	  m_params.put(String.valueOf(ndx++),parseValue(value_elt));

	} // end while

      } // end if (parameters present)

    } // end try
    catch (XMLLoadException e)
    { // translate XML exceptions into RequestParseExceptions
      throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,e);

    } // end catch

    m_req = req;
    m_locales = new Locale[1];
    m_locales[0] = Locale.getDefault();
    m_attrs = new MemoryObjectStore(XmlRpcRequest.class.getName(),prev);

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Parses an XML-RPC &lt;value/&gt; element.
   *
   * @param elt The node coresponding to the &lt;value/&gt; element.
   * @return The parsed value.
   * @exception com.silverwrist.dynamo.xmlrpc.FaultCode If an error occurs in parsing the value.
   */
  private static final Object parseValue(Element elt) throws FaultCode
  {
    NodeList nl = elt.getChildNodes();
    Element type_spec = null;
    for (int i=0; i<nl.getLength(); i++)
    { // look through the subelements to find the type-specific element
      Node n = nl.item(i);
      if (n.getNodeType()!=Node.ELEMENT_NODE)
	continue;
      if (type_spec!=null)
	throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,
				  "more than one type-specifying element within a <value/> element");
      type_spec = (Element)n;

    } // end for

    Object rc = null;
    if (type_spec!=null)
    { // figure out what type this element is
      Integer tval = (Integer)(MAP_TYPE.get(type_spec.getTagName()));
      if (tval==null)
	throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"invalid type \"" + type_spec.getTagName()
				                                  + "\"");
      DOMElementHelper h = new DOMElementHelper(type_spec);
      switch (tval.intValue())
      { // based on the type, act on the contents
	case ITYP_INTEGER:
	  try
	  { // parse the integer value
	    rc = new Integer(Integer.parseInt(h.getElementText(),10));

	  } // end try
	  catch (NumberFormatException e)
	  { // value wasn't an integer
	    throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"invalid integer format",e);

	  } // end catch
	  catch (NullPointerException e)
	  { // value was null
	    throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"integer value not specified");

	  } // end catch
	  break;

	case ITYP_BOOLEAN:
	  { // test the Boolean value
	    String s = h.getElementText();
	    if (s==null)
	      throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"boolean value not specified");
	    if (s.equals("1"))
	      rc = Boolean.TRUE;
	    else if (s.equals("0"))
	      rc = Boolean.FALSE;
	    else
	      throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"invalid boolean format");
	      
	  } // end case
	  break;

	case ITYP_STRING:
	  { // get the string value and return it
	    rc = h.getElementText();
	    if (rc==null)
	      rc = EMPTY_STRING;

	  } // end case
	  break;

	case ITYP_DOUBLE:
	  try
	  { // convert to a Double value
	    rc = new Double(h.getElementText());

	  } // end try
	  catch (NumberFormatException e)
	  { // value wasn't an integer
	    throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"invalid double format",e);

	  } // end catch
	  catch (NullPointerException e)
	  { // value was null
	    throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"double value not specified");

	  } // end catch
	  break;

	case ITYP_DATETIME:
	  try
	  { // convert to a Date value
	    rc = s_iso8601.parse(h.getElementText());

	  } // end try
	  catch (java.text.ParseException e)
	  { // date couldn't be parsed
	    throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"invalid ISO 8601 date format",e);

	  } // end catch
	  catch (NullPointerException e)
	  { // no value in there...
	    throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"date value not specified");

	  } // end catch
	  break;
	  
	case ITYP_BINARY:
	  try
	  { // get the string equivalent of the formatted data
	    String s = h.getElementText();
	    if (s==null)
	    { // null string - return null array
	      rc = new byte[0];
	      break;

	    } // end if

	    // get a stream of encoded bytes
	    ByteArrayInputStream encoded_stm = new ByteArrayInputStream(s.getBytes("US-ASCII"));

	    // use the JavaMail MIME decoder to turn it into decoded bytes
	    InputStream decoded_stm = MimeUtility.decode(encoded_stm,"base64");

	    // copy the decoded bytes to a new array
	    ByteArrayOutputStream output = new ByteArrayOutputStream();
	    IOUtils.copy(decoded_stm,output);
	    IOUtils.shutdown(decoded_stm);
	    IOUtils.shutdown(encoded_stm);

	    // retrieve the output array
	    rc = output.toByteArray();
	    IOUtils.shutdown(output);

	  } // end try
	  catch (UnsupportedEncodingException e)
	  { // WTF? shouldn't happen
	    throw new SystemFaultCode(SystemFaultCode.INTERNAL_ERROR,"internal error: unsupported encoding",e);

	  } // end catch
	  catch (IOException e)
	  { // some sort of error copying binary values around
	    throw new SystemFaultCode(SystemFaultCode.INTERNAL_ERROR,"unable to get binary data",e);

	  } // end catch
	  catch (MessagingException e)
	  { // error in the MIME parsing - dump it out
	    throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"invalid binary data format",e);

	  } // end catch
	  break;

	case ITYP_STRUCT:
	  rc = parseStruct(type_spec);
	  break;

	case ITYP_ARRAY:
	  rc = parseArray(type_spec);
	  break;

	default:
	  throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,"invalid type \""
				                                    + type_spec.getTagName() + "\"");
	  
      } // end switch

    } // end if
    else
    { // if there's no type-specifying element, treat it as a String
      DOMElementHelper h = new DOMElementHelper(elt);
      rc = h.getElementText();
      if (rc==null)
	rc = EMPTY_STRING;

    } // end else

    return rc;

  } // end parseValue

  /**
   * Parses an XML-RPC &lt;struct/&gt; element.
   *
   * @param elt The node coresponding to the &lt;struct/&gt; element.
   * @return The parsed value.
   * @exception com.silverwrist.dynamo.xmlrpc.FaultCode If an error occurs in parsing the value.
   */
  private static final Map parseStruct(Element elt) throws FaultCode
  {
    XMLLoader loader = XMLLoader.get();
    HashMap rc = new HashMap();
    try
    { // get all <member/> sub-elements and process them
      List l = loader.getMatchingSubElements(elt,"member");
      Iterator it = l.iterator();
      while (it.hasNext())
      { // get each <member/> and add its <name/> and <value/> to the map
	Element x = (Element)(it.next());
	DOMElementHelper h = new DOMElementHelper(x);
	String name = loader.getSubElementText(h,"name");
	Element val_elt = loader.getSubElement(h,"value");
	rc.put(name,parseValue(val_elt));
      
      } // end while

    } // end try
    catch (XMLLoadException e)
    { // translate load exception
      throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,e);

    } // end catch

    if (rc.isEmpty())
      return Collections.EMPTY_MAP;
    else
      return Collections.unmodifiableMap(rc);

  } // end parseStruct

  /**
   * Parses an XML-RPC &lt;array/&gt; element.
   *
   * @param elt The node coresponding to the &lt;array/&gt; element.
   * @return The parsed value.
   * @exception com.silverwrist.dynamo.xmlrpc.FaultCode If an error occurs in parsing the value.
   */
  private static final List parseArray(Element elt) throws FaultCode
  {
    XMLLoader loader = XMLLoader.get();
    ArrayList rc = null;
    try
    { // get the <data/> sub-element
      Element data_elt = loader.getSubElement(elt,"data");

      // get the values it contains
      List l = loader.getMatchingSubElements(data_elt,"value");
      rc = new ArrayList(l.size());
      Iterator it = l.iterator();
      while (it.hasNext())
      { // parse the <value/> elements and add them to the list
	Element val_elt = (Element)(it.next());
	rc.add(parseValue(val_elt));

      } // end while

    } // end try
    catch (XMLLoadException e)
    { // translate load exception
      throw new SystemFaultCode(SystemFaultCode.INVALID_REQUEST,e);

    } // end catch

    if ((rc==null) || rc.isEmpty())
      return Collections.EMPTY_LIST;
    else
      return Collections.unmodifiableList(rc);
    
  } // end parseArray

  /*--------------------------------------------------------------------------------
   * Implementations from interface ObjectProvider
   *--------------------------------------------------------------------------------
   */

  /**
   * Retrieves an object from this <CODE>ObjectProvider</CODE>.
   *
   * @param namespace The namespace to interpret the name relative to.
   * @param name The name of the object to be retrieved.
   * @return The object reference specified.
   */
  public Object getObject(String namespace, String name)
  {
    return m_attrs.getObject(namespace,name);

  } // end getObject

  /*--------------------------------------------------------------------------------
   * Implementations from interface ObjectStore
   *--------------------------------------------------------------------------------
   */

  /**
   * Sets an object into this <CODE>ObjectStore</CODE>.
   *
   * @param namespace The namespace to interpret the name relative to.
   * @param name The name of the object to be set.
   * @param value The object to set into the <CODE>ObjectStore</CODE>.
   * @return The previous object that was set into the <CODE>ObjectStore</CODE> under this namespace and name, or
   *         <CODE>null</CODE> if there was no such object.
   */
  public Object setObject(String namespace, String name, Object value)
  {
    return m_attrs.setObject(namespace,name,value);

  } // end setObject

  /**
   * Removes an object from this <CODE>ObjectStore</CODE>.
   *
   * @param namespace The namespace to interpret the name relative to.
   * @param name The name of the object to be removed.
   * @return The previous object that was set into the <CODE>ObjectStore</CODE> under this namespace and name, or
   *         <CODE>null</CODE> if there was no such object.
   */
  public Object removeObject(String namespace, String name)
  {
    return m_attrs.removeObject(namespace,name);

  } // end removeObject

  /**
   * Returns a collection of all object namespaces that have been set into this <CODE>ObjectStore</CODE>.
   *
   * @return A <CODE>java.util.Collection</CODE> containing <CODE>String</CODE> objects specifying all the
   *         object namespaces.
   */
  public Collection getNamespaces()
  {
    return m_attrs.getNamespaces();

  } // end getNamespaces

  /**
   * Returns a collection of all object names that have been set into this <CODE>ObjectStore</CODE> under a given
   * namespace.
   *
   * @param namespace The namespace to look for names under.
   * @return A <CODE>java.util.Collection</CODE> containing <CODE>String</CODE> objects specifying all the
   *         object names for this namespace.
   */
  public Collection getNamesForNamespace(String namespace)
  {
    return m_attrs.getNamesForNamespace(namespace);

  } // end getNamesForNamespace

  /*--------------------------------------------------------------------------------
   * Implementations from interface Request
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns a value indicating the type of this request.
   *
   * @return The type of this request.
   * @see com.silverwrist.dynamo.RequestType#XMLRPC
   */
  public RequestType getType()
  {
    return RequestType.XMLRPC;

  } // end getType

  /**
   * Returns the DNS name or IP address of the server to which the request was made.
   * 
   * @return The server name to which the request was name.
   */
  public String getServerName()
  {
    return m_req.getServerName();

  } // end getServerName

  /**
   * Returns the number of the port on which this request was made.
   *
   * @return The port number on which the request was made, or -1 if it could not be determined.
   */
  public int getServerPort()
  {
    return m_req.getServerPort();

  } // end getServerPort

  /**
   * Returns the portion of the request URI that indicates the context of the request. The context path always comes
   * first in a request URI. The path starts with a "/" character but does not end with a "/" character. For servlets
   * in the default (root) context, this method returns "". The servlet container does not decode this string.
   *
   * @return The context path of the request.
   */
  public String getContextPath()
  {
    return m_req.getContextPath();

  } // end getContextPath

  /**
   * Returns the part of this request's URL that calls the endpoint. This includes either the endpoint name or a path
   * to the endpoint, but does not include any extra path information or a query string.  For XML-RPC requests,
   * this will usually be the value "/RPC2".
   *
   * @return The request path of this request.
   */
  public String getRequestPath()
  {
    return m_req.getServletPath();

  } // end getRequestPath

  /**
   * Returns any extra path information associated with the URL the client sent when it made this request. The extra
   * path information follows the request path but precedes the query string. This method returns <CODE>null</CODE>
   * if there was no extra path information.
   *
   * @return The "extra path" of this request.  If there is no "extra path," returns <CODE>null</CODE>.
   */
  public String getExtraPath()
  {
    return null;

  } // end getExtraPath

  /**
   * Returns the query string that is contained in the request URL after the path.  For XML-RPC requests, this is
   * the name of the method that was called.
   *
   * @return The method name associated with this request.
   */
  public String getQueryString()
  {
    return m_method;

  } // end getQueryString

  /**
   * Returns a value indicating the action associated with this request.  For XML-RPC requests, this verb is
   * always {@link com.silverwrist.dynamo.Verb#POST POST}.
   *
   * @return The action associated with this request.
   * @see com.silverwrist.dynamo.Verb
   */
  public Verb getVerb()
  {
    return Verb.POST;

  } // end getVerb

  /**
   * Returns a string representation of the IP address of the client that made this request.
   *
   * @return The address of the client making the request.
   */
  public String getSourceAddress()
  {
    return m_req.getRemoteAddr();

  } // end getSourceAddress

  /**
   * Returns a java.util.Map of the parameters of this request. For XML-RPC requests, these parameters are passed
   * as an array associated with the method call.
   * <P>The keys of the map are <CODE>String</CODE> objects; the values of the map are arrays of the type specified
   * by the return value of {@link #getParametersEntryClass() getParametersEntryClass()}.  For XML-RPC, the keys
   * are printable-string equivalents of integers representing the parameters' position.
   *
   * @return The parameter map, as specified above.  This map is read-only.
   */
  public Map getParameters()
  {
    return Collections.unmodifiableMap(m_params);

  } // end getParameters

  /**
   * Returns a class specifying the fundamental type of objects contained in the arrays acting as the values of
   * the map returned by {@link #getParameters() getParameters()}.  For XML-RPC, this class is <CODE>Object</CODE>,
   * indicating that the contained objects may be of any type, as the parser has already figured out their precise
   * type.
   *
   * @return The class of array elements within the parameter map.
   */
  public Class getParametersEntryClass()
  {
    return Object.class;

  } // end getParametersEntryClass

  /**
   * Returns a java.util.Map of the data items of this request.  Data items are lengthy streams of data associated
   * with a request.  XML-RPC requests have no data items associated with them.
   *
   * @return The data item map, as specified above.  This map is read-only.
   */
  public Map getDataItems()
  {
    return Collections.EMPTY_MAP;

  } // end getDataItems

  /**
   * Returns an array of <CODE>Locale</CODE> objects indicating, in decreasing order starting with the preferred
   * locale, the locales that are acceptable to the client based on an <CODE>Accept-Language</CODE> header or similar.
   * method.  If the client request doesn't provide an <CODE>Accept-Language</CODE> header or similar indication,
   * this method returns an array containing one <CODE>Locale</CODE>, the default locale for the server.
   *
   * @return An array of preferred <CODE>Locale</CODE> objects for the client.
   */
  public Locale[] getLocales()
  {
    return m_locales;

  } // end getLocales

  /*--------------------------------------------------------------------------------
   * Static initializer
   *--------------------------------------------------------------------------------
   */

  static
  { // Initialize the type map.
    HashMap tmp = new HashMap();
    Integer foo = new Integer(ITYP_INTEGER);
    tmp.put("i4",foo);
    tmp.put("int",foo);
    tmp.put("boolean",new Integer(ITYP_BOOLEAN));
    tmp.put("string",new Integer(ITYP_STRING));
    tmp.put("double",new Integer(ITYP_DOUBLE));
    tmp.put("dateTime.iso8601",new Integer(ITYP_DATETIME));
    tmp.put("base64",new Integer(ITYP_BINARY));
    tmp.put("struct",new Integer(ITYP_STRUCT));
    tmp.put("array",new Integer(ITYP_ARRAY));
    MAP_TYPE = Collections.unmodifiableMap(tmp);

    // Initialize the ISO 8601 date formatter.
    SimpleDateFormat iso = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
    iso.setCalendar(new GregorianCalendar(new SimpleTimeZone(0,"UTC")));
    s_iso8601 = iso;
    
  } // end static initializer

} // end class XmlRpcRequest
