/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2001-2003 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 org.jboss.axis.AxisFault;
import org.jboss.axis.Constants;
import org.jboss.axis.MessageContext;
import org.jboss.axis.description.ElementDesc;
import org.jboss.axis.description.FieldDesc;
import org.jboss.axis.description.OperationDesc;
import org.jboss.axis.description.TypeDesc;
import org.jboss.axis.encoding.SerializationContext;
import org.jboss.axis.encoding.Serializer;
import org.jboss.axis.enums.Style;
import org.jboss.axis.message.SOAPElementAxisImpl;
import org.jboss.axis.soap.SOAPConstants;
import org.jboss.axis.utils.BeanPropertyDescriptor;
import org.jboss.axis.utils.BeanUtils;
import org.jboss.axis.utils.JavaUtils;
import org.jboss.axis.utils.Messages;
import org.jboss.axis.wsdl.fromJava.Types;
import org.jboss.logging.Logger;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;

import javax.xml.namespace.QName;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.List;

/**
 * General purpose serializer/deserializerFactory 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 BeanSerializer implements Serializer
{
   private static Logger log = Logger.getLogger(BeanSerializer.class.getName());

   QName xmlType;
   Class javaType;

   protected BeanPropertyDescriptor[] propertyDescriptor;
   protected TypeDesc typeDesc;


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

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

      if (typeDesc != null)
      {
         propertyDescriptor = typeDesc.getPropertyDescriptors();
      }
      else
      {
         propertyDescriptor = BeanUtils.getPd(javaType, null);
      }
   }

   // Construct BeanSerializer for the indicated class/qname/propertyDesc
   public BeanSerializer(Class javaType, QName xmlType, TypeDesc typeDesc,
                         BeanPropertyDescriptor[] propertyDescriptor)
   {
      this.xmlType = xmlType;
      this.javaType = javaType;
      this.typeDesc = typeDesc;
      this.propertyDescriptor = propertyDescriptor;
   }

   /**
    * Serialize a bean.  Done simply by serializing each bean property.
    *
    * @param name       is the element name
    * @param attributes are the attributes...serialize is free to add more.
    * @param value      is the value
    * @param context    is the SerializationContext
    */
   public void serialize(QName name, Attributes attributes,
                         Object value, SerializationContext context)
           throws IOException
   {

      log.debug("serialize [name=" + name + ",value=" + value + "]");

      MessageContext messageContext = context.getMessageContext();
      OperationDesc operation = messageContext.getOperation();

      // For document style make sure the bean has a qualified name
      // and there is no default namespace
      // TDI 24-June-2004
      if (operation != null && operation.getStyle() == Style.DOCUMENT)
      {
         String uri = name.getNamespaceURI();
         String prefix = name.getPrefix();
         if (prefix.length() == 0 && uri.length() > 0)
         {
            // If this uri was registered as the default namespace, we get the empty prefix
            prefix = context.getPrefixForURI(uri);
            if (prefix.equals("")) prefix = "ns1";
            name = new QName(uri, name.getLocalPart(), prefix);
            context.registerPrefixForURI(prefix, uri);
            context.setNoDefaultNamespace(true);
         }
      }

      // Check for meta-data in the bean that will tell us if any of the
      // properties are actually attributes, add those to the element
      // attribute list
      Attributes beanAttrs = getObjectAttributes(value, attributes, context);

      // Get the encoding style
      String encodingStyle = messageContext.getEncodingStyle();
      boolean isEncoded = Constants.isSOAP_ENC(encodingStyle);

      // check whether we have and xsd:any namespace="##any" type
      boolean suppressElement = !context.getMessageContext().isEncoded() &&
              name.getNamespaceURI().equals("") &&
              name.getLocalPart().equals("any");

      if (!suppressElement)
      {
         context.startElement(name, beanAttrs);
      }

      try
      {
         // Serialize each property
         for (int i = 0; i < propertyDescriptor.length; i++)
         {
            BeanPropertyDescriptor bpDesc = propertyDescriptor[i];

            String propName = bpDesc.getName();
            if (propName.equals("class"))
               continue;

            QName qname = null;
            QName xmlType = null;
            QName itemXmlType = null;
            boolean isOmittable = false;

            // If we have type metadata, check to see what we're doing
            // with this field.  If it's an attribute, skip it.  If it's
            // an element, use whatever qname is in there.  If we can't
            // find any of this info, use the default.

            if (typeDesc != null)
            {
               FieldDesc field = typeDesc.getFieldByName(propName);
               if (field == null)
               {
                  String upperName = propName.substring(0, 1).toUpperCase() + propName.substring(1);
                  field = typeDesc.getFieldByName(upperName);
               }
               
               if (field != null)
               {

                  if (!field.isElement())
                     continue;

                  // This is a very special case where a property wants to be serialized
                  // as bean content. Allowing mixed content.
                  // TDI 22-Jun-2004
                  if (((ElementDesc)field).isAsContent())
                  {
                     Object propValue = bpDesc.get(value);
                     String strval = context.getValueAsString(propValue, xmlType);
                     context.writeSafeString(strval);
                     continue;
                  }

                  itemXmlType = ((ElementDesc)field).getItemXmlType();

                  // If we're SOAP encoded, just use the local part,
                  // not the namespace.  Otherwise use the whole
                  // QName.
                  if (isEncoded)
                  {
                     qname = new QName(field.getXmlName().getLocalPart());
                  }
                  else
                  {
                     qname = field.getXmlName();
                  }
                  isOmittable = field.isMinOccursZero();
                  xmlType = field.getXmlType();
               }
            }

            if (qname == null)
            {
               qname = new QName(propName);
            }

            if (xmlType == null)
            {
               // look up the type QName using the class
               xmlType = context.getQNameForClass(bpDesc.getType());
            }

            // Read the value from the property
            if (bpDesc.isReadable())
            {
               if (!bpDesc.isIndexed())
               {
                  // Normal case: serialize the value
                  Object propValue = bpDesc.get(value);
                  // if meta data says minOccurs=0, then we can skip
                  // it if its value is null and we aren't doing SOAP
                  // encoding.
                  if (propValue == null && isOmittable && !isEncoded)
                     continue;

                  // If this is literal style and the propValue is an Array
                  // serialize each item as if it was the property.
                  // This should probably be handled by a LiteralArraySerializer.
                  // TDI 20-June-2004
                  // com/sun/ts/tests/interop/webservices/jaxrpc/wsi/rpc/literal/marshalltest#MarshallJavaArrayTest
                  if (!isEncoded && propValue != null && JavaUtils.isArrayClass(propValue.getClass())
                          && !Constants.XSD_BASE64.equals(xmlType) && !Constants.XSD_HEXBIN.equals(xmlType))
                  {
                     QName itemType;

                     if (itemXmlType == null)
                     {
                        Class componentType = propValue.getClass().getComponentType();
                        itemType = context.getQNameForClass(componentType);
                     }
                     else
                     {
                        itemType = itemXmlType;
                     }

                     log.debug("Item type is " + itemType);
                     for (int j = 0; j < Array.getLength(propValue); j++)
                     {
                        Object item = Array.get(propValue, j);
                        context.serialize(qname, null, item, itemType, true, new Boolean(false));
                     }
                  }
                  else
                  {
                     context.serialize(qname, null, propValue, xmlType, true, new Boolean(isEncoded));
                  }
               }
               else
               {
                  // Collection of properties: serialize each one
                  int j = 0;
                  while (j >= 0)
                  {
                     Object propValue = null;
                     try
                     {
                        propValue = bpDesc.get(value, j);
                        j++;
                     }
                     catch (Exception e)
                     {
                        j = -1;
                     }
                     if (j >= 0)
                     {
                        context.serialize(qname, null,
                                propValue, xmlType,
                                true, null);
                     }
                  }
               }
            }
         }

         BeanPropertyDescriptor anyDesc = typeDesc == null ? null : typeDesc.getAnyDesc();

         if (anyDesc != null)
         {
            // If we have "extra" content here, it'll be an array
            // of MessageElements.  Serialize each one.
            Object anyVal = anyDesc.get(value);
            if (anyVal != null && anyVal instanceof SOAPElementAxisImpl[])
            {
               SOAPElementAxisImpl[] anyContent = (SOAPElementAxisImpl[])anyVal;
               for (int i = 0; i < anyContent.length; i++)
               {
                  SOAPElementAxisImpl element = anyContent[i];
                  element.output(context);
               }
            }
         }
      }
      catch (InvocationTargetException ite)
      {
         Throwable target = ite.getTargetException();
         log.error(Messages.getMessage("exception00"), target);
         throw new IOException(target.toString());
      }
      catch (Exception e)
      {
         log.error(Messages.getMessage("exception00"), e);
         throw new IOException(e.toString());
      }

      if (!suppressElement)
         context.endElement();
   }


   public String getMechanismType()
   {
      return Constants.AXIS_SAX;
   }

   /**
    * Return XML schema for the specified type, suitable for insertion into
    * the &lt;types&gt; element of a WSDL document, or underneath an
    * &lt;element&gt; or &lt;attribute&gt; declaration.
    *
    * @param javaType the Java Class we're writing out schema for
    * @param types    the Java2WSDL Types object which holds the context
    *                 for the WSDL being generated.
    * @return a type element containing a schema simpleType/complexType
    * @see org.jboss.axis.wsdl.fromJava.Types
    */
   public Element writeSchema(Class javaType, Types types) throws Exception
   {

      // ComplexType representation of bean class
      Element complexType = types.createElement("complexType");

      // See if there is a super class, stop if we hit a stop class
      Element e = null;
      Class superClass = javaType.getSuperclass();
      BeanPropertyDescriptor[] superPd = null;
      List stopClasses = types.getStopClasses();
      if (superClass != null &&
              superClass != java.lang.Object.class &&
              superClass != java.lang.Exception.class &&
              superClass != java.lang.Throwable.class &&
              superClass != java.rmi.RemoteException.class &&
              superClass != org.jboss.axis.AxisFault.class &&
              (stopClasses == null ||
              !(stopClasses.contains(superClass.getName()))))
      {
         // Write out the super class
         String base = types.writeType(superClass);
         Element complexContent = types.createElement("complexContent");
         complexType.appendChild(complexContent);
         Element extension = types.createElement("extension");
         complexContent.appendChild(extension);
         extension.setAttribute("base", base);
         e = extension;
         // Get the property descriptors for the super class
         TypeDesc superTypeDesc = TypeDesc.getTypeDescForClass(superClass);
         if (superTypeDesc != null)
         {
            superPd = superTypeDesc.getPropertyDescriptors();
         }
         else
         {
            superPd = BeanUtils.getPd(superClass, null);
         }
      }
      else
      {
         e = complexType;
      }

      // Add fields under sequence element.
      // Note: In most situations it would be okay
      // to put the fields under an all element.
      // However it is illegal schema to put an
      // element with minOccurs=0 or maxOccurs>1 underneath
      // an all element.  This is the reason why a sequence
      // element is used.
      Element all = types.createElement("sequence");
      e.appendChild(all);

      if (Modifier.isAbstract(javaType.getModifiers()))
      {
         complexType.setAttribute("abstract", "true");
      }

      // Serialize each property
      for (int i = 0; i < propertyDescriptor.length; i++)
      {
         String propName = propertyDescriptor[i].getName();

         // Don't serializer properties named class
         boolean writeProperty = true;
         if (propName.equals("class"))
         {
            writeProperty = false;
         }

         // Don't serialize the property if it is present
         // in the super class property list
         if (superPd != null && writeProperty)
         {
            for (int j = 0; j < superPd.length && writeProperty; j++)
            {
               if (propName.equals(superPd[j].getName()))
               {
                  writeProperty = false;
               }
            }
         }
         if (!writeProperty)
         {
            continue;
         }

         Class fieldType = propertyDescriptor[i].getType();

         // If we have type metadata, check to see what we're doing
         // with this field.  If it's an attribute, skip it.  If it's
         // an element, use whatever qname is in there.  If we can't
         // find any of this info, use the default.

         if (typeDesc != null)
         {
            FieldDesc field = typeDesc.getFieldByName(propName);

            if (field != null)
            {
               QName qname = field.getXmlName();
               QName fieldXmlType = field.getXmlType();
               boolean isAnonymous = fieldXmlType != null && fieldXmlType.getLocalPart().startsWith(">");

               if (qname != null)
               {
                  // FIXME!
                  // Check to see if this is in the right namespace -
                  // if it's not, we need to use an <element ref="">
                  // to represent it!!!

                  // Use the default...
                  propName = qname.getLocalPart();
               }
               if (!field.isElement())
               {
                  writeAttribute(types,
                          propName,
                          fieldType,
                          fieldXmlType,
                          complexType);
               }
               else
               {
                  writeField(types,
                          propName,
                          fieldType,
                          propertyDescriptor[i].isIndexed(),
                          field.isMinOccursZero(),
                          all, isAnonymous);
               }
            }
            else
            {
               writeField(types,
                       propName,
                       fieldType,
                       propertyDescriptor[i].isIndexed(), false, all, false);
            }
         }
         else
         {
            writeField(types,
                    propName,
                    fieldType,
                    propertyDescriptor[i].isIndexed(), false, all, false);
         }
      }

      // done
      return complexType;
   }

   /**
    * write a schema representation of the given Class field and append it to
    * the where Node, recurse on complex types
    *
    * @param fieldName   name of the field
    * @param fieldType   type of the field
    * @param isUnbounded causes maxOccurs="unbounded" if set
    * @param where       location for the generated schema node
    * @throws Exception
    */
   protected void writeField(Types types, String fieldName,
                             Class fieldType,
                             boolean isUnbounded,
                             boolean isOmittable,
                             Element where,
                             boolean isAnonymous) throws Exception
   {
      Element elem;
      if (isAnonymous)
      {
         elem = types.createElementWithAnonymousType(fieldName,
                 fieldType, isOmittable, where.getOwnerDocument());
      }
      else
      {
         String elementType = types.writeType(fieldType);

         if (elementType == null)
         {
            // If writeType returns null, then emit an anytype in such situations.
            QName anyQN = Constants.XSD_ANYTYPE;
            String prefix = types.getNamespaces().getCreatePrefix(anyQN.getNamespaceURI());
            elementType = prefix + ":" + anyQN.getLocalPart();
         }

         elem = types.createElement(fieldName,
                 elementType,
                 types.isNullable(fieldType),
                 isOmittable,
                 where.getOwnerDocument());
      }

      if (isUnbounded)
      {
         elem.setAttribute("maxOccurs", "unbounded");
      }

      where.appendChild(elem);
   }

   /**
    * write aa attribute element and append it to the 'where' Node
    *
    * @param fieldName name of the field
    * @param fieldType type of the field
    * @param where     location for the generated schema node
    * @throws Exception
    */
   protected void writeAttribute(Types types,
                                 String fieldName,
                                 Class fieldType,
                                 QName fieldXmlType,
                                 Element where) throws Exception
   {

      // Attribute must be a simple type.
      if (!types.isAcceptableAsAttribute(fieldType))
      {
         throw new AxisFault(Messages.getMessage("AttrNotSimpleType00",
                 fieldName,
                 fieldType.getName()));
      }
      Element elem = types.createAttributeElement(fieldName,
              fieldType, fieldXmlType,
              false,
              where.getOwnerDocument());
      where.appendChild(elem);
   }

   /**
    * Check for meta-data in the bean that will tell us if any of the
    * properties are actually attributes, add those to the element
    * attribute list
    *
    * @param value the object we are serializing
    * @return attributes for this element, null if none
    */
   protected Attributes getObjectAttributes(Object value,
                                            Attributes attributes,
                                            SerializationContext context)
   {

      if (typeDesc == null || !typeDesc.hasAttributes())
         return attributes;

      AttributesImpl attrs;
      if (attributes == null)
      {
         attrs = new AttributesImpl();
      }
      else if (attributes instanceof AttributesImpl)
      {
         attrs = (AttributesImpl)attributes;
      }
      else
      {
         attrs = new AttributesImpl(attributes);
      }

      try
      {
         // Find each property that is an attribute
         // and add it to our attribute list
         for (int i = 0; i < propertyDescriptor.length; i++)
         {
            String propName = propertyDescriptor[i].getName();
            if (propName.equals("class"))
               continue;

            FieldDesc field = typeDesc.getFieldByName(propName);
            // skip it if its not an attribute
            if (field == null || field.isElement())
               continue;

            QName qname = field.getXmlName();
            if (qname == null)
            {
               qname = new QName("", propName);
            }

            if (propertyDescriptor[i].isReadable() &&
                    !propertyDescriptor[i].isIndexed())
            {
               // add to our attributes
               Object propValue = propertyDescriptor[i].get(value);
               // If the property value does not exist, don't serialize
               // the attribute.  In the future, the decision to serializer
               // the attribute may be more sophisticated.  For example, don't
               // serialize if the attribute matches the default value.
               if (propValue != null)
               {
                  setAttributeProperty(propValue, qname, attrs, context);
               }
            }
         }
      }
      catch (Exception e)
      {
         // no attributes
         return attrs;
      }

      return attrs;
   }

   private void setAttributeProperty(Object propValue,
                                     QName qname,
                                     AttributesImpl attrs,
                                     SerializationContext context) throws Exception
   {
      String propString = context.getValueAsString(propValue, null);
      String namespace = qname.getNamespaceURI();
      String localName = qname.getLocalPart();

      // Force SOAP Envelope namespace on known header element attributes
      // com/sun/ts/tests/jaxrpc/ee/wsi/document/literal/headertest uses a bean with mustUnderstand property
      SOAPConstants soapConstants = context.getMessageContext().getSOAPConstants();
      QName parentElementQName = (QName)context.getElementStack().peek();
      QName headerQName = soapConstants.getHeaderQName();
      if (headerQName.equals(parentElementQName))
      {
         if (localName.equals("mustUnderstand") || localName.equals("role"))
         {
            namespace = soapConstants.getEnvelopeURI();
         }
      }

      attrs.addAttribute(namespace,
              localName,
              context.attributeQName2String(qname),
              "CDATA",
              propString);
   }
}
