/**
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.webservice.deployment;

// $Id: TypeMappingDescription.java,v 1.8.2.14 2005/07/04 17:14:46 tdiesler Exp $

import org.jboss.axis.Constants;
import org.jboss.axis.encoding.ser.ArrayDeserializerFactory;
import org.jboss.axis.encoding.ser.ArraySerializerFactory;
import org.jboss.axis.encoding.ser.BeanDeserializerFactory;
import org.jboss.axis.encoding.ser.BeanSerializerFactory;
import org.jboss.axis.encoding.ser.CalendarDeserializerFactory;
import org.jboss.axis.encoding.ser.CalendarSerializerFactory;
import org.jboss.axis.encoding.ser.EnumDeserializerFactory;
import org.jboss.axis.encoding.ser.EnumSerializerFactory;
import org.jboss.axis.encoding.ser.QNameDeserializerFactory;
import org.jboss.axis.encoding.ser.QNameSerializerFactory;
import org.jboss.axis.encoding.ser.SimpleDeserializerFactory;
import org.jboss.axis.encoding.ser.SimpleSerializerFactory;
import org.jboss.axis.enums.Use;
import org.jboss.axis.utils.DOM2Utils;
import org.jboss.axis.utils.JavaUtils;
import org.jboss.logging.Logger;
import org.jboss.util.Classes;
import org.jboss.webservice.encoding.ser.MetaDataBeanDeserializerFactory;
import org.jboss.webservice.encoding.ser.MetaDataBeanSerializerFactory;
import org.jboss.webservice.metadata.jaxrpcmapping.JavaXmlTypeMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.VariableMapping;
import org.jboss.util.xml.DOMUtils;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.Calendar;

/**
 * Abstracts an Axis service description
 *
 * @author thomas.diesler@jboss.org
 * @since 08-June-2004
 */
public class TypeMappingDescription
{
   // provide logging
   private final Logger log = Logger.getLogger(TypeMappingDescription.class);

   private QName typeQName;
   private QName anonymousQName;
   private String javaType;
   private String serFactoryName;
   private String desFactoryName;
   private String encodingURI;
   private BeanXMLMetaData metaData;

   /**
    * By default, type mappings are generated from the information in the WSDL and the JAXRPC.
    * <p/>
    * 1. All types from jaxrpc-mapping should be registered with Axis even when they do not appear in any of the operation
    * parameter of return types. They might be subtypes contained in top level types.
    * <p/>
    * 2. Do not register a type twice when we have more than on service per deployment.
    * <p/>
    * This flag indicates, wheter a type has user defined information comming from ws4ee-deployment.xml
    * A user defined type can alway override an automatically generated type.
    */
   private boolean userDefined;

   public TypeMappingDescription(QName qname, QName anonymousQName, String javaType, Use use, JavaXmlTypeMapping jaxrpcTypeMapping)
   {
      if (qname == null)
         throw new IllegalArgumentException("TypeMapping qname cannot be null");
      if (javaType == null)
         throw new IllegalArgumentException("TypeMapping javaType cannot be null");
      if (use == null)
         throw new IllegalArgumentException("TypeMapping use cannot be null");

      this.javaType = javaType;
      this.typeQName = qname;
      this.anonymousQName = anonymousQName;

      if (jaxrpcTypeMapping != null)
         initMetaDataFromJavaXMLTypeMapping(qname, jaxrpcTypeMapping);

      if (Use.LITERAL.equals(use))
         encodingURI = Constants.URI_LITERAL_ENC;
      else if (Use.ENCODED.equals(use))
         encodingURI = Constants.URI_SOAP11_ENC;
      else
         throw new IllegalArgumentException("Unsupported use: " + use);

   }

   // Add type mapping meta data if we have a jaxrpc mapping
   private void initMetaDataFromJavaXMLTypeMapping(QName qname, JavaXmlTypeMapping jaxrpcTypeMapping)
   {
      String xmlns = "xmlns:" + qname.getPrefix() + "='" + qname.getNamespaceURI() + "'";
      StringBuffer buffer = new StringBuffer("<typeMapping " + xmlns + "><typeDesc>");
      VariableMapping[] varMappings = jaxrpcTypeMapping.getVariableMappings();
      for (int i = 0; i < varMappings.length; i++)
      {
         VariableMapping varMapping = varMappings[i];
         String fieldName = varMapping.getJavaVariableName();

         String xmlName = varMapping.getXmlElementName();
         if (xmlName == null)
            xmlName = varMapping.getXmlAttributeName();
         if (xmlName == null)
            xmlName = varMapping.getXmlWildcard();

         boolean isAttribute = varMapping.getXmlAttributeName() != null;
         String asAttr = (isAttribute ? "asAttr='true' " : " ");

         buffer.append("<elementDesc fieldName='" + fieldName + "' xmlName='" + xmlName + "' " + asAttr + "/>");
      }

      buffer.append("<elementOrder>");
      for (int i = 0; i < varMappings.length; i++)
      {
         VariableMapping varMapping = varMappings[i];
         if (varMapping.getXmlElementName() != null)
            buffer.append("<element name='" + varMapping.getJavaVariableName() + "'/>");
      }
      buffer.append("</elementOrder></typeDesc></typeMapping>");

      try
      {
         DocumentBuilder builder = DOM2Utils.getDocumentBuilder();
         Element root = builder.parse(new InputSource(new StringReader(buffer.toString()))).getDocumentElement();
         metaData = BeanXMLMetaData.parse(DOMUtils.getFirstChildElement(root));
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("Cannot construct meta data from: " + buffer);
      }
   }

   /**
    * Load the javaType with the given class loader.
    */
   public Class loadJavaType(ClassLoader cl)
   {
      Class typeClass = null;
      String componentType = null;
      try
      {
         boolean isArrayType = javaType.endsWith("[]");

         if (isArrayType)
            componentType = javaType.substring(0, javaType.indexOf("["));
         else
            componentType = javaType;


         // Array classes do not exist in the wild. They only exist after loading the component
         // class type, and in 1.4.2 they are not created via the ClassLoader.loadClass call
         // http://developer.java.sun.com/developer/bugParade/bugs/4976356.html
         if (isArrayType)
         {
            if (JavaUtils.isJavaKeyword(componentType))
            {
               String className = JavaUtils.getLoadableClassName(javaType);
               typeClass = ClassLoader.getSystemClassLoader().loadClass(className);
            }
            else
            {
               // load the component first
               Class.forName(componentType, true, cl);

               String className = JavaUtils.getLoadableClassName(javaType);
               typeClass = Class.forName(className, true, cl);
            }
         }

         if (isArrayType == false)
         {
            if (JavaUtils.isJavaKeyword(javaType))
            {
               typeClass = getPrimitiveClass(javaType);
            }
            else
            {
               typeClass = cl.loadClass(javaType);
            }
         }

         // Now we got the class we can assign the serializer/deserializer
         initSerializerForClass(typeClass);

      }
      catch (ClassNotFoundException ignore)
      {
         log.warn("Class not found: " + javaType);
      }
      catch (NoClassDefFoundError ignore)
      {
         log.warn("No class definition for " + javaType);
      }

      return typeClass;
   }

   private Class getPrimitiveClass(String javaType)
   {
      if ("int".equals(javaType))
         return int.class;
      else if ("short".equals(javaType))
         return short.class;
      else if ("boolean".equals(javaType))
         return boolean.class;
      else if ("byte".equals(javaType))
         return byte.class;
      else if ("long".equals(javaType))
         return long.class;
      else if ("double".equals(javaType))
         return double.class;
      else if ("float".equals(javaType))
         return float.class;
      else if ("char".equals(javaType))
         return char.class;

      return null;
   }

   /**
    * Initialize the serializer/deserializer factory pair.
    */
   private void initSerializerForClass(Class typeClass)
   {
      boolean isEncoded = Constants.URI_SOAP11_ENC.equals(encodingURI);

      boolean isSimpleType = (typeClass == null
              || Classes.isPrimitive(typeClass)
              || typeClass.getName().startsWith("java.lang")
              || typeClass.getName().startsWith("java.math"));

      if (getSerializerFactoryName() == null)
      {
         String serFactory = null;
         if (isSimpleType)
         {
            serFactory = SimpleSerializerFactory.class.getName();
         }
         else if (JavaUtils.isArrayClass(typeClass))
         {
            if (isEncoded)
            {
               serFactory = ArraySerializerFactory.class.getName();
            }
         }
         else if (JavaUtils.isEnumClass(typeClass))
         {
            serFactory = EnumSerializerFactory.class.getName();
         }
         else if (QName.class.isAssignableFrom(typeClass))
         {
            serFactory = QNameSerializerFactory.class.getName();
         }
         else if (Calendar.class.isAssignableFrom(typeClass))
         {
            serFactory = CalendarSerializerFactory.class.getName();
         }
         else
         {
            if (metaData != null)
            {
               serFactory = MetaDataBeanSerializerFactory.class.getName();
            }
            else
            {
               serFactory = BeanSerializerFactory.class.getName();
            }
         }

         setSerializerFactoryName(serFactory);
      }

      if (getDeserializerFactoryName() == null)
      {
         String desFactory = null;
         if (isSimpleType)
         {
            desFactory = SimpleDeserializerFactory.class.getName();
         }
         else if (JavaUtils.isEnumClass(typeClass))
         {
            desFactory = EnumDeserializerFactory.class.getName();
         }
         else if (JavaUtils.isArrayClass(typeClass))
         {
            if (isEncoded)
            {
               desFactory = ArrayDeserializerFactory.class.getName();
            }
         }
         else if (QName.class.isAssignableFrom(typeClass))
         {
            desFactory = QNameDeserializerFactory.class.getName();
         }
         else if (Calendar.class.isAssignableFrom(typeClass))
         {
            desFactory = CalendarDeserializerFactory.class.getName();
         }
         else
         {
            if (metaData != null)
            {
               desFactory = MetaDataBeanDeserializerFactory.class.getName();
            }
            else
            {
               desFactory = BeanDeserializerFactory.class.getName();
            }
         }

         setDeserializerFactoryName(desFactory);
      }
   }

   public String getJavaType()
   {
      return javaType;
   }

   public QName getTypeQName()
   {
      return typeQName;
   }

   public QName getAnonymousQName()
   {
      return anonymousQName;
   }

   public String getDeserializerFactoryName()
   {
      return desFactoryName;
   }

   public String getSerializerFactoryName()
   {
      return serFactoryName;
   }

   public String getEncodingURI()
   {
      return encodingURI;
   }

   public void setEncodingURI(String encodingURI)
   {
      this.encodingURI = encodingURI;
   }

   public void setTypeQName(QName typeQName)
   {
      this.typeQName = typeQName;
   }

   public void setSerializerFactoryName(String serFactoryName)
   {
      this.serFactoryName = serFactoryName;
   }

   public void setDeserializerFactoryName(String desFactoryName)
   {
      this.desFactoryName = desFactoryName;
   }

   public BeanXMLMetaData getMetaData()
   {
      return metaData;
   }

   public void setMetaData(BeanXMLMetaData metaData)
   {
      this.metaData = metaData;
   }

   public boolean isUserDefined()
   {
      return userDefined;
   }

   public void setUserDefined(boolean userDefined)
   {
      this.userDefined = userDefined;
   }

   public void writeWSDD(PrintWriter out)
   {
      String javaType = getJavaType();
      QName typeQName = getTypeQName();
      String encodingURI = getEncodingURI();

      log.trace("TypeMapping: " + this);

      String typeAttr = WSDDGenerator.getQNameAttrValue(typeQName);

      String typePrefix = typeQName.getPrefix();
      String xmlns = "xmlns:" + typePrefix + "='" + typeQName.getNamespaceURI() + "'";

      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      Class typeClass = loadJavaType(cl);

      // The serializer/derserializer factory might not be known until we load the class
      String serializer = getSerializerFactoryName();
      String deserializer = getDeserializerFactoryName();

      if (isUserDefined())
         out.println("  <!-- User defined type mapping -->");

      if (typeClass == null)
         out.println("  <!-- Class not found, ignore type mapping");
      else if (serializer == null || deserializer == null)
         out.println("  <!-- Serializer/Deserializer not found, ignore type mapping");

      out.println("  <typeMapping");
      out.println("    qname='" + typeAttr + "' " + xmlns);
      out.println("    type='java:" + javaType + "'");
      out.println("    serializer='" + serializer + "'");
      out.println("    deserializer='" + deserializer + "'");
      out.println("    encodingStyle='" + encodingURI + "'>");
      if (metaData != null)
      {
         metaData.serializeAsXML(out);
      }
      out.println("  </typeMapping>");

      if (typeClass == null || serializer == null || deserializer == null)
         out.println("  -->");
   }

   public boolean equals(Object obj)
   {
      if (obj == null) return false;
      return toString().equals(obj.toString());
   }

   public int hashCode()
   {
      return toString().hashCode();
   }

   public String toString()
   {
      return "[type=" + typeQName + ",anonymous=" + anonymousQName + ",java=" + javaType + ",serf=" + serFactoryName + ",desf=" + desFactoryName + ",encoding=" + encodingURI + "]";
   }
}