/*
 * 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 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.ui.rpc;

import java.beans.*;
import java.io.*;
import java.lang.ref.*;
import java.lang.reflect.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.log4j.*;
import com.silverwrist.util.*;

public class XmlRpcSerializer
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(XmlRpcSerializer.class);

  private static XmlRpcSerializer self = null;

  private static SimpleTimeZone utc = new SimpleTimeZone(0,"UTC");

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

  private XmlRpcSerializer()
  { // do nothing
  } // end constructor

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  public final String serialize(boolean b)
  {
    return "<boolean>" + (b ? "1" : "0") + "</boolean>";

  } // end serialize

  public final String serialize(byte b)
  {
    return this.serialize((int)b);

  } // end serialize

  public final String serialize(char c)
  {
    return "<string>" + c + "</string>";

  } // end serialize

  public final String serialize(double d)
  {
    return "<double>" + d + "</double>";

  } // end serialize

  public final String serialize(float f)
  {
    return this.serialize((double)f);

  } // end serialize

  public final String serialize(int i)
  {
    return "<int>" + i + "</int>";

  } // end serialize

  public final String serialize(long l)
  {
    return this.serialize((double)l);

  } // end serialize

  public final String serialize(short s)
  {
    return this.serialize((int)s);

  } // end serialize

  public final String serialize(Object obj) throws XmlRpcFault
  {
    if (obj.getClass().isArray())
    { // treat arrays specially
      Class ct = obj.getClass().getComponentType();
      if (ct==Byte.TYPE)
	return this.serialize((byte[])obj);
      else if (ct==Boolean.TYPE)
	return this.serialize((boolean[])obj);
      else if (ct==Character.TYPE)
	return this.serialize((char[])obj);
      else if (ct==Short.TYPE)
	return this.serialize((short[])obj);
      else if (ct==Integer.TYPE)
	return this.serialize((int[])obj);
      else if (ct==Long.TYPE)
	return this.serialize((long[])obj);
      else if (ct==Float.TYPE)
	return this.serialize((float[])obj);
      else if (ct==Double.TYPE)
	return this.serialize((double[])obj);
      else
	return this.serialize((Object[])obj);

    } // end if

    if (obj==null)
      return serializeString(null);

    // self-serializing objects
    if (obj instanceof XmlRpcSelfSerializer)
      return ((XmlRpcSelfSerializer)obj).serializeXmlRpc();

    // types from java.io.*
    if (obj instanceof InputStream)
      return serializeInputStream((InputStream)obj);

    // types from java.sql.*
    if (obj instanceof java.sql.Blob)
    { // serialize the blob as a binary stream
      try
      { // just get its input stream
	return serializeInputStream(((java.sql.Blob)obj).getBinaryStream());

      } // end try
      catch (java.sql.SQLException e)
      { // fault on error here
	throw new XmlRpcFault(XmlRpcFault.INTERNAL_ERROR,"unable to save binary data");

      } // end catch

    } // end if

    // types from java.util.*
    if (obj instanceof Calendar)
      return serializeCalendar((Calendar)obj);
    if (obj instanceof Collection)
      return serializeCollection((Collection)obj);
    if (obj instanceof java.util.Date)
      return serializeDate((java.util.Date)obj);
    if (obj instanceof Enumeration)
      return serializeEnumeration((Enumeration)obj);
    if (obj instanceof Iterator)
      return serializeIterator((Iterator)obj);
    if (obj instanceof Map)
      return serializeMap((Map)obj);

    // types from java.lang.ref.*
    if (obj instanceof Reference)  // for a reference, encode its referent
      return this.serialize(((Reference)obj).get());

    // types from java.lang.*
    if (obj instanceof Boolean)
      return this.serialize(((Boolean)obj).booleanValue());
    if (obj instanceof Character)
      return this.serialize(((Character)obj).charValue());
    if ((obj instanceof Byte) || (obj instanceof Short) || (obj instanceof Integer))
      return this.serialize(((Number)obj).intValue());
    if ((obj instanceof Long) || (obj instanceof Float) || (obj instanceof Double))
      return this.serialize(((Number)obj).doubleValue());
    // String and StringBuffer are handled properly by the fallback mechanism below

    // last-ditch fallback method - use toString, get the string, and encode it
    return serializeString(obj.toString());

  } // end serialize

  public final String serialize(boolean[] ba)
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<ba.length; i++)
      b.append("<value>").append(this.serialize(ba[i])).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serialize(byte[] ba) throws XmlRpcFault
  {
    return this.serializeInputStream(new ByteArrayInputStream(ba));

  } // end serialize

  public final String serialize(char[] ca)
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<ca.length; i++)
      b.append("<value>").append(this.serialize(ca[i])).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serialize(double[] da)
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<da.length; i++)
      b.append("<value>").append(this.serialize(da[i])).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serialize(float[] fa)
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<fa.length; i++)
      b.append("<value>").append(this.serialize((double)(fa[i]))).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serialize(int[] ia)
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<ia.length; i++)
      b.append("<value>").append(this.serialize(ia[i])).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serialize(long[] la)
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<la.length; i++)
      b.append("<value>").append(this.serialize((double)(la[i]))).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serialize(short[] sa)
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<sa.length; i++)
      b.append("<value>").append(this.serialize((int)(sa[i]))).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serialize(Object[] arr) throws XmlRpcFault
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    for (int i=0; i<arr.length; i++)
      b.append("<value>").append(this.serialize(arr[i])).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serialize

  public final String serializeBean(Object obj, BeanInfo binf) throws XmlRpcFault
  {
    PropertyDescriptor[] props = binf.getPropertyDescriptors();
    StringBuffer b = new StringBuffer("<struct>\r\n");
    final Object[] no_params = new Object[0];
    for (int i=0; i<props.length; i++)
    { // get property names and values
      Method mread = props[i].getReadMethod();
      if (mread!=null)
      { // use the method to get the value associated with the name
	try
	{ // append the member data
	  b.append("<member>\r\n<name>").append(StringUtil.encodeHTML(props[i].getName()));
	  b.append("</name>\r\n<value>").append(this.serialize(mread.invoke(obj,no_params)));
	  b.append("</value>\r\n</member>\r\n");

	} // end try
	catch (IllegalAccessException iae)
	{ // unable to access that method
	  throw new XmlRpcFault(XmlRpcFault.INTERNAL_ERROR,iae);

	} // end catch
	catch (InvocationTargetException ite)
	{ // the method we invoked threw an exception
	  throw new XmlRpcFault(XmlRpcFault.INTERNAL_ERROR,ite.getTargetException());

	} // end catch

      } // end if

    } // end for

    b.append("</struct>\r\n");
    return b.toString();

  } // end serializeBean

  public final String serializeBean(Object obj) throws XmlRpcFault
  {
    try
    { // get the BeanInfo via introspection
      return serializeBean(obj,Introspector.getBeanInfo(obj.getClass()));

    } // end try
    catch (IntrospectionException e)
    { // introspection failed
      throw new XmlRpcFault(XmlRpcFault.INTERNAL_ERROR,e);

    } // end catch

  } // end serializeBean

  public final String serializeCalendar(Calendar cal)
  {
    // Create the two string buffers converting the date.
    StringBuffer rc = new StringBuffer("<dateTime.iso8601>");
    StringBuffer conv = new StringBuffer();
    String c;
    
    // Encode the year first.
    conv.append("0000").append(cal.get(Calendar.YEAR));
    c = conv.toString();
    rc.append(c.substring(c.length()-4));

    // Now the month...
    conv.setLength(0);
    conv.append("00").append(cal.get(Calendar.MONTH) - Calendar.JANUARY + 1);
    c = conv.toString();
    rc.append(c.substring(c.length()-2));

    // And the day...
    conv.setLength(0);
    conv.append("00").append(cal.get(Calendar.DAY_OF_MONTH));
    c = conv.toString();
    rc.append(c.substring(c.length()-2)).append('T');

    // And the hour...
    conv.setLength(0);
    conv.append("00").append(cal.get(Calendar.HOUR_OF_DAY));
    c = conv.toString();
    rc.append(c.substring(c.length()-2)).append(':');

    // And the minute...
    conv.setLength(0);
    conv.append("00").append(cal.get(Calendar.MINUTE));
    c = conv.toString();
    rc.append(c.substring(c.length()-2)).append(':');

    // And the second...
    conv.setLength(0);
    conv.append("00").append(cal.get(Calendar.SECOND));
    c = conv.toString();
    rc.append(c.substring(c.length()-2)).append("</dateTime.iso8601>");

    // This is the resulting date/time value.
    return rc.toString();

  } // end serializeCalendar

  public final String serializeCollection(Collection coll) throws XmlRpcFault
  {
    return serializeIterator(coll.iterator());

  } // end serializeCollection

  public final String serializeDate(java.util.Date date) throws XmlRpcFault
  {
    GregorianCalendar cal = new GregorianCalendar(utc);
    cal.setTime(date);
    return serializeCalendar(cal);

  } // end serializeDate

  public final String serializeEnumeration(Enumeration enum) throws XmlRpcFault
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    while (enum.hasMoreElements())
      b.append("<value>").append(this.serialize(enum.nextElement())).append("</value>");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serializeIterator

  public final String serializeInputStream(InputStream stm) throws XmlRpcFault
  {
    String rc = null;

    try
    { // convert the data bytes to encoded bytes first
      ByteArrayOutputStream internal_stm = new ByteArrayOutputStream();
      OutputStream encode_stm = MimeUtility.encode(internal_stm,"base64");
      IOUtil.copy(stm,encode_stm);
      encode_stm.flush();

      // turn our encoded output into an InputStream
      ByteArrayInputStream internal2_stm = new ByteArrayInputStream(internal_stm.toByteArray());
      IOUtil.shutdown(encode_stm);
      IOUtil.shutdown(internal_stm);

      // build a StringWriter containing the converted contents
      StringWriter wr = new StringWriter();
      wr.write("<base64>");
      InputStreamReader rd = new InputStreamReader(internal2_stm,"US-ASCII");
      IOUtil.copy(rd,wr);
      IOUtil.shutdown(rd);
      IOUtil.shutdown(internal2_stm);
      wr.write("</base64>");

      // send out the complete string
      rc = wr.toString();
      IOUtil.shutdown(wr);

    } // end try
    catch (IOException ioe)
    { // this is an internal error
      throw new XmlRpcFault(XmlRpcFault.INTERNAL_ERROR,"unable to save binary data");

    } // end catch
    catch (MessagingException me)
    { // and so is this
      throw new XmlRpcFault(XmlRpcFault.INTERNAL_ERROR,"unable to encode binary data");

    } // end catch

    return rc;

  } // end serializeInputStream

  public final String serializeIterator(Iterator it) throws XmlRpcFault
  {
    StringBuffer b = new StringBuffer("<array>\r\n<data>\r\n");
    while (it.hasNext())
      b.append("<value>").append(this.serialize(it.next())).append("</value>\r\n");
    b.append("</data>\r\n</array>");
    return b.toString();

  } // end serializeIterator

  public final String serializeMap(Map m) throws XmlRpcFault
  {
    StringBuffer b = new StringBuffer("<struct>\r\n");
    Iterator it = m.entrySet().iterator();
    while (it.hasNext())
    { // serialize each entry in turn
      Map.Entry ntry = (Map.Entry)(it.next());
      b.append("<member>\r\n<name>").append(StringUtil.encodeHTML(ntry.getKey().toString()));
      b.append("</name>\r\n<value>").append(this.serialize(ntry.getValue()));
      b.append("</value>\r\n</member>\r\n");

    } // end while

    b.append("</struct>\r\n");
    return b.toString();

  } // end serializeMap

  public final String serializeString(String s)
  {
    if (s==null)
      return "<string></string>";
    return "<string>" + StringUtil.encodeHTML(s) + "</string>";

  } // end serializeString

  /*--------------------------------------------------------------------------------
   * External static operations
   *--------------------------------------------------------------------------------
   */

  public static final synchronized XmlRpcSerializer get()
  {
    if (self==null)
      self = new XmlRpcSerializer();
    return self;

  } // end get

} // end class XmlRpcSerializer
