/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2001-2002 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Axis" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.jboss.axis.encoding.ser;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;

import javax.xml.namespace.QName;

import org.jboss.axis.Constants;
import org.jboss.axis.description.ElementDesc;
import org.jboss.axis.description.FieldDesc;
import org.jboss.axis.description.TypeDesc;
import org.jboss.axis.encoding.DeserializationContext;
import org.jboss.axis.encoding.Deserializer;
import org.jboss.axis.encoding.DeserializerImpl;
import org.jboss.axis.encoding.TypeMapping;
import org.jboss.axis.message.SOAPElementAxisImpl;
import org.jboss.axis.message.SOAPHandler;
import org.jboss.axis.soap.SOAPConstants;
import org.jboss.axis.utils.BeanPropertyDescriptor;
import org.jboss.axis.utils.JavaUtils;
import org.jboss.axis.utils.Messages;
import org.jboss.logging.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * General purpose deserializer for an arbitrary java bean.
 *
 * @author Sam Ruby <rubys@us.ibm.com>
 * @author Rich Scheuerle <scheu@us.ibm.com>
 * @author Tom Jordahl <tomj@macromedia.com>
 */
public class BeanDeserializer extends DeserializerImpl
{
   /** @since 4.0.2 - here to avoid automated check errors */
   private static final long serialVersionUID = -4168128353189545112L;
   private static Logger log = Logger.getLogger(BeanDeserializer.class.getName());

   QName xmlType;
   Class javaType;
   protected Map propertyMap;
   protected QName prevQName;

   /**
    * Type metadata about this class for XML deserialization
    */
   protected TypeDesc typeDesc = null;

   // This counter is updated to deal with deserialize collection properties
   protected int collectionIndex = -1;

   protected SimpleDeserializer cacheStringDSer = null;
   protected QName cacheXMLType = null;
   protected String lastFieldName = null;

   // This is the constructor we find in startElement if the bean
   // does not have a default constructor
   protected DeferredBeanConstruction deferredConstruction;

   // Construct BeanSerializer for the indicated class/qname
   public BeanDeserializer(Class javaType, QName xmlType)
   {
      this(javaType, xmlType, TypeDesc.getTypeDescForClass(javaType));
   }

   // Construct BeanDeserializer for the indicated class/qname and meta Data
   public BeanDeserializer(Class javaType, QName xmlType, TypeDesc typeDesc)
   {
      this(javaType, xmlType, typeDesc, BeanDeserializerFactory.getProperties(javaType, typeDesc));
   }

   // Construct BeanDeserializer for the indicated class/qname and meta Data
   public BeanDeserializer(Class javaType, QName xmlType, TypeDesc typeDesc, Map propertyMap)
   {
      this.xmlType = xmlType;
      this.javaType = javaType;
      this.typeDesc = typeDesc;
      this.propertyMap = propertyMap;

      // create a value
      try
      {
         value = javaType.newInstance();
      }
      catch (Exception e)
      {
         // Don't process the exception at this point.
         // This is defered until the call to startElement
         // which will throw the exception.
      }
   }

   /**
    * Set the bean properties that correspond to element attributes.
    * <p/>
    * This method is invoked after startElement when the element requires
    * deserialization (i.e. the element is not an href and the value is not
    * nil.)
    *
    * @param namespace  is the namespace of the element
    * @param localName  is the name of the element
    * @param prefix     is the prefix of the element
    * @param attributes are the attributes on the element...used to get the
    *                   type
    * @param context    is the DeserializationContext
    */
   public void onStartElement(String namespace, String localName, String prefix, Attributes attributes, DeserializationContext context) throws SAXException
   {
      // reset just in case
      lastFieldName = null;

      // Create the bean object if it was not already
      // created in the constructor.
      if (value == null)
      {
         try
         {
            value = javaType.newInstance();
         }
         catch (Exception e)
         {

            // Get the property classes
            ArrayList propTypes = new ArrayList();
            Iterator it = propertyMap.values().iterator();
            while (it.hasNext())
            {
               BeanPropertyDescriptor bpd = (BeanPropertyDescriptor)it.next();
               propTypes.add(bpd.getType());
            }

            log.debug("Looking for deferred ctor: " + propTypes);

            // Get the constructors
            Constructor[] ctors = javaType.getConstructors();

            // Reset the defered construction, this is necessary if the deserializer
            // is beeing reused, which I am not sure about
            deferredConstruction = null;

            // Find the constructor that matches the bean property types
            Constructor bestMatch = null;
            Class[] bestMatchArgs = null;
            for (int i = 0; i < ctors.length; i++)
            {
               Constructor ctor = ctors[i];
               Class[] ctorArgs = ctor.getParameterTypes();
               if (ctorArgs.length > 0)
               {
                  boolean allFound = true;
                  for (int j = 0; j < ctorArgs.length; j++)
                  {
                     allFound &= propTypes.contains(ctorArgs[j]);
                  }

                  if (allFound && (bestMatch == null || ctorArgs.length < bestMatchArgs.length))
                  {
                     bestMatch = ctor;
                     bestMatchArgs = ctorArgs;
                  }
               }
            }

            if (bestMatch != null)
               deferredConstruction = new DeferredBeanConstruction(bestMatch);

            log.debug("Deferred ctor: " + bestMatch);

            // if value still null, then we'll never be able to construct object
            if (value == null && deferredConstruction == null)
            {
               // Failed to create an object.
               throw new SAXException(Messages.getMessage("cantCreateBean00", javaType.getName(), e.toString()));
            }
         }
      }

      // Initialize array properties
      if (typeDesc != null && value != null)
      {
         try
         {
            BeanPropertyDescriptor[] props = typeDesc.getPropertyDescriptors();
            for (int i = 0; i < props.length; i++)
            {
               BeanPropertyDescriptor prop = props[i];
               if (prop.getType().isArray())
               {
                  if (prop.get(value) == null)
                  {
                     Class componentType = prop.getType().getComponentType();
                     Object newArray = Array.newInstance(componentType, 0);
                     prop.set(value, newArray);
                  }
               }
            }
         }
         catch (RuntimeException rte)
         {
            throw rte;
         }
         catch (Exception ex)
         {
            throw new SAXException("Cannot initialize array properties", ex);
         }
      }
      
      // If no type description meta data, there are no attributes, so we are done.
      if (typeDesc == null)
         return;

      // Get the SOAP envelope URI
      SOAPConstants soapConstants = context.getMessageContext().getSOAPConstants();
      String soapenvURI = soapConstants.getEnvelopeURI();

      // loop through the attributes and set bean properties that
      // correspond to attributes
      for (int i = 0; i < attributes.getLength(); i++)
      {
         // If the attribute belongs to the SOAP namespace, we ignore the attr namespace
         // it might still be property of the bean
         QName attrQName = null;
         if (soapenvURI.equals(attributes.getURI(i)))
            attrQName = new QName(attributes.getLocalName(i));
         else attrQName = new QName(attributes.getURI(i), attributes.getLocalName(i));

         String fieldName = typeDesc.getFieldNameForAttribute(attrQName);
         if (fieldName != null)
         {
            // look for the attribute property
            BeanPropertyDescriptor bpd = (BeanPropertyDescriptor)propertyMap.get(fieldName);
            if (bpd != null)
            {
               if (!bpd.isIndexed())
               {
                  // Get the Deserializer for the attribute
                  Deserializer dSer = getDeserializer(null, bpd.getType(), null, context);
                  if (dSer == null)
                     throw new SAXException(Messages.getMessage("unregistered00", bpd.getType().toString()));

                  if (!(dSer instanceof SimpleDeserializer))
                     throw new SAXException(Messages.getMessage("AttrNotSimpleType00", bpd.getName(), bpd.getType().toString()));

                  // Success!  Create an object from the string and set
                  // it in the bean
                  try
                  {
                     dSer.onStartElement(namespace, localName, prefix, attributes, context);
                     Object val = ((SimpleDeserializer)dSer).makeValue(attributes.getValue(i));
                     if (value != null)
                     {
                        bpd.set(value, val);
                     }
                     else if (deferredConstruction != null)
                     {
                        value = deferredConstruction.newBeanInstance(val);
                     }
                  }
                  catch (Exception e)
                  {
                     throw new SAXException(e);
                  }
               }
            }
         }
      } // attribute loop
   }

   /**
    * Deserializer interface called on each child element encountered in
    * the XML stream.
    *
    * @param namespace  is the namespace of the child element
    * @param localName  is the local name of the child element
    * @param prefix     is the prefix used on the name of the child element
    * @param attributes are the attributes of the child element
    * @param context    is the deserialization context.
    * @return is a Deserializer to use to deserialize a child (must be
    *         a derived class of SOAPHandler) or null if no deserialization should
    *         be performed.
    */
   public SOAPHandler onStartChild(String namespace, String localName, String prefix, Attributes attributes, DeserializationContext context) throws SAXException
   {
      log.debug("onStartChild: " + new QName(namespace, localName));

      BeanPropertyDescriptor propDesc = null;
      FieldDesc fieldDesc = null;

      SOAPConstants soapConstants = context.getMessageContext().getSOAPConstants();
      String encodingStyle = context.getMessageContext().getEncodingStyle();
      boolean isEncoded = Constants.isSOAP_ENC(encodingStyle);

      QName elemQName = new QName(namespace, localName);
      // The collectionIndex needs to be reset for Beans with multiple arrays
      if ((prevQName == null) || (!prevQName.equals(elemQName)))
      {
         collectionIndex = -1;
      }
      prevQName = elemQName;

      // Fastpath nil checks...
      // Commented out. If the bean property is an array (of simple types) that must groe to its propper size
      // we need to process the nil
      // TDI 20-June-2004
      // com/sun/ts/tests/interop/webservices/jaxrpc/wsi/rpc/literal/marshalltest#MarshallJavaArrayTest
      //  if (context.isNil(attributes))
      //      return null;

      if (typeDesc != null)
      {
         // Lookup the name appropriately (assuming an unqualified
         // name for SOAP encoding, using the namespace otherwise)
         String fieldName = typeDesc.getFieldNameForElement(elemQName);
         if (fieldName != null)
         {
            propDesc = (BeanPropertyDescriptor)propertyMap.get(fieldName);
            if (propDesc == null)
            {
               String lowerName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
               propDesc = (BeanPropertyDescriptor)propertyMap.get(lowerName);
            }
            if (propDesc == null)
            {
               String upperName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
               propDesc = (BeanPropertyDescriptor)propertyMap.get(upperName);
            }

            fieldDesc = typeDesc.getFieldByName(fieldName);
            if (fieldDesc == null)
            {
               String lowerName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
               fieldDesc = typeDesc.getFieldByName(lowerName);
            }
         }

         // Hold on to the field name so that getDeserializer() can use it
         lastFieldName = fieldName;
      }
      else
      {
         lastFieldName = null;
      }

      if (propDesc == null)
      {
         // look for a field by this name.
         propDesc = (BeanPropertyDescriptor)propertyMap.get(localName);
      }

      // try and see if this is an xsd:any namespace="##any" element before
      // reporting a problem
      if (propDesc == null)
      {
         // try to put unknown elements into a SOAPElement property, if
         // appropriate
         propDesc = getAnyPropertyDesc();
         if (propDesc != null)
         {
            try
            {
               SOAPElementAxisImpl[] curElements = (SOAPElementAxisImpl[])propDesc.get(value);
               int length = 0;
               if (curElements != null)
               {
                  length = curElements.length;
               }
               SOAPElementAxisImpl[] newElements = new SOAPElementAxisImpl[length + 1];
               if (curElements != null)
               {
                  System.arraycopy(curElements, 0, newElements, 0, length);
               }
               SOAPElementAxisImpl thisEl = context.getCurElement();

               newElements[length] = thisEl;
               propDesc.set(value, newElements);
               // if this is the first pass through the MessageContexts
               // make sure that the correct any element is set,
               // that is the child of the current MessageElement, however
               // on the first pass this child has not been set yet, so
               // defer it to the child SOAPHandler
               if (!localName.equals(thisEl.getName()))
               {
                  return new SOAPHandler(newElements, length);
               }
               return new SOAPHandler();
            }
            catch (Exception e)
            {
               throw new SAXException(e);
            }
         }
      }

      if (propDesc == null)
      {
         // No such field
         throw new SAXException(Messages.getMessage("badElem00", javaType.getName(), localName));
      }

      // Get the child's xsi:type if available
      QName childXMLType = context.getTypeFromXSITypeAttr(namespace, localName, attributes);

      String href = attributes.getValue(soapConstants.getAttrHref());

      // If no xsi:type or href, check the meta-data for the field
      if (childXMLType == null && fieldDesc != null && href == null)
      {
         childXMLType = fieldDesc.getXmlType();
      }

      // The bean property might be the super class of the actual type
      Class propType = propDesc.getType();
      TypeMapping tm = context.getTypeMapping();
      Class childType = tm.getClassForQName(childXMLType);
      if (childType != null && propType.isAssignableFrom(childType))
         propType = childType;

      // Get Deserializer for child, default to using DeserializerImpl
      Deserializer dSer = getDeserializer(childXMLType, propType, href, context);

      // It is an error if the dSer is not found - the only case where we
      // wouldn't have a deserializer at this point is when we're trying
      // to deserialize something we have no clue about (no good xsi:type,
      // no good metadata).
      if (dSer == null)
      {

         // FIXME : Currently this doesn't throw an error solely to enable the
         //     "terra" testcase to pass.  We should, IMO, fix the test (either
         //     to support <xsd:list> or to throw an error when we find such a thing
         //     in the WSDL at WSDL2Java time).  Once that's done, this should be
         //     uncommented and the next two lines deleted.
         //
         //            throw new SAXException(Messages.getMessage("noDeser00",
         //                                                       childXMLType.toString()));

         dSer = new DeserializerImpl();
         return (SOAPHandler)dSer;
      }

      // Register value target
      if (propDesc.isWriteable() || deferredConstruction != null)
      {
         // If this is an indexed property, and the deserializer we found
         // was NOT the ArrayDeserializer, this is a non-SOAP array:
         // <bean>
         //   <field>value1</field>
         //   <field>value2</field>
         // ...
         // In this case, we want to use the collectionIndex and make sure
         // the deserialized value for the child element goes into the
         // right place in the collection.
         if (propDesc.isIndexed() && !(dSer instanceof ArrayDeserializer))
         {
            collectionIndex++;
            dSer.registerValueTarget(getBeanPropertyTarget(propDesc));
         }
         // If this is literal style and the property type is an Array
         // deserialize each item as if it was the property.
         // This should probably be handled by a LiteralArrayDeserializer.
         // TDI 20-June-2004
         // com/sun/ts/tests/interop/webservices/jaxrpc/wsi/rpc/literal/marshalltest#MarshallJavaArrayTest
         else if (!isEncoded && JavaUtils.isArrayClass(propDesc.getType()) && !(dSer instanceof Base64Deserializer) && !(dSer instanceof HexDeserializer))
         {
            collectionIndex++;
            dSer.registerValueTarget(getBeanPropertyTarget(propDesc));
         }
         else
         {
            // If we're here, the element maps to a single field value,
            // whether that be a "basic" type or an array, so use the
            // normal (non-indexed) BeanPropertyTarget form.
            collectionIndex = -1;
            dSer.registerValueTarget(getBeanPropertyTarget(propDesc));
         }
      }

      // Let the framework know that we need this deserializer to complete
      // for the bean to complete.
      addChildDeserializer(dSer);

      return (SOAPHandler)dSer;
   }

   /** Check that we have a valid bean instance
    */
   public void onEndElement(String namespace, String localName, DeserializationContext context) throws SAXException
   {
      super.onEndElement(namespace, localName, context);

      if (value == null && deferredConstruction != null)
         throw new SAXException("Could not construct bean using: " + deferredConstruction);
   }

   /**
    * Get the target, its either direct on the value or defered via an constructor
    */
   private BeanPropertyTarget getBeanPropertyTarget(BeanPropertyDescriptor propDesc)
   {
      BeanPropertyTarget target = null;

      if (value != null)
         target = new BeanPropertyTarget(value, propDesc, collectionIndex);
      else if (deferredConstruction != null)
         target = new BeanPropertyTarget(this, deferredConstruction, propDesc);
      else throw new IllegalStateException("Cannot get a valid property target");

      return target;
   }

   /**
    * Get a BeanPropertyDescriptor which indicates where we should
    * put extensibility elements (i.e. XML which falls under the
    * auspices of an &lt;xsd:any&gt; declaration in the schema)
    *
    * @return an appropriate BeanPropertyDescriptor, or null
    */
   public BeanPropertyDescriptor getAnyPropertyDesc()
   {
      if (typeDesc == null)
         return null;

      return typeDesc.getAnyDesc();
   }

   /**
    * Get the Deserializer for the attribute or child element.
    *
    * @param xmlType  QName of the attribute/child element or null if not known.
    * @param javaType Class of the corresponding property
    * @param href     String is the value of the href attribute, which is used
    *                 to determine whether the child element is complete or an
    *                 href to another element.
    * @param context  DeserializationContext
    * @return Deserializer or null if not found.
    */
   protected Deserializer getDeserializer(QName xmlType, Class javaType, String href, DeserializationContext context)
   {

      String encodingStyle = context.getMessageContext().getEncodingStyle();
      boolean isEncoded = Constants.isSOAP_ENC(encodingStyle);

      // See if we have a cached deserializer
      if (cacheStringDSer != null)
      {
         if (String.class.equals(javaType) && href == null && (cacheXMLType == null && xmlType == null || cacheXMLType != null && cacheXMLType.equals(xmlType)))
         {
            cacheStringDSer.reset();
            return cacheStringDSer;
         }
      }

      Deserializer dSer = null;
      TypeMapping tm = context.getTypeMapping();

      if (xmlType != null && href == null)
      {
         // Use the xmlType to get the deserializer.
         dSer = context.getDeserializerForType(xmlType);
      }

      if (dSer == null)
      {

         // If the xmlType is not set, get a default xmlType
         QName defaultXMLType = tm.getTypeQName(javaType);

         // If there is not href, then get the deserializer
         // using the javaType and default XMLType,
         // If there is an href, the create the generic
         // DeserializerImpl and set its default type (the
         // default type is used if the href'd element does
         // not have an xsi:type.
         if (href == null)
         {
            dSer = context.getDeserializer(javaType, defaultXMLType);
         }
         else
         {
            dSer = new DeserializerImpl();
            dSer.setDefaultType(defaultXMLType);
         }
      }

      if (javaType.equals(String.class) && dSer instanceof SimpleDeserializer)
      {
         cacheStringDSer = (SimpleDeserializer)dSer;
         cacheXMLType = xmlType;
      }

      // If this is literal style and the javaType is an Array
      // deserialize each item as if it was the property.
      // This should probably be handled by a LiteralArrayDeserializer.
      // TDI 20-June-2004
      // com/sun/ts/tests/interop/webservices/jaxrpc/wsi/rpc/literal/marshalltest#MarshallJavaArrayTest
      if (dSer == null && !isEncoded && JavaUtils.isArrayClass(javaType) && javaType != Byte[].class && javaType != byte[].class)
      {

         FieldDesc fieldDesc = (lastFieldName != null) ? typeDesc.getFieldByName(lastFieldName) : null;
         Class compType = javaType.getComponentType();
         QName itemXmlType = null;

         if (fieldDesc != null && fieldDesc instanceof ElementDesc)
         {
            itemXmlType = ((ElementDesc)fieldDesc).getItemXmlType();
         }

         // If there is no itemXmlType defined, or no fieldDesc is available use the typemapping 
         // on the component type
         if (itemXmlType == null)
         {
            itemXmlType = tm.getTypeQName(compType);
         }

         log.debug("Using itemXmlType = " + itemXmlType);

         dSer = context.getDeserializer(compType, itemXmlType);
      }

      return dSer;
   }

   /**
    * This will assign text content to the bean property that is an element
    * with the asContent flag set to true. There can only be one, or is there a way to know which?
    * TDI 22-June-2004
    */
   public void characters(char[] p1, int p2, int p3) throws SAXException
   {
      super.characters(p1, p2, p3);

      if (typeDesc != null)
      {
         Iterator it = propertyMap.values().iterator();
         while (it.hasNext())
         {
            BeanPropertyDescriptor bpDesc = (BeanPropertyDescriptor)it.next();
            if (bpDesc.isWriteable())
            {
               String name = bpDesc.getName();
               FieldDesc fieldDesc = typeDesc.getFieldByName(name);
               if (fieldDesc instanceof ElementDesc && ((ElementDesc)fieldDesc).isAsContent())
               {
                  String strContent = new String(p1, p2, p3);
                  try
                  {
                     log.debug("Setting content property: " + name + "=" + strContent);
                     bpDesc.set(value, strContent);
                  }
                  catch (Exception e)
                  {
                     log.warn("Cannot set content property", e);
                  }
               }
            }
         }
      }
   }
}
