/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ws.core.jaxrpc.binding.jbossxb;

// $Id: SchemaBindingBuilder.java 2385 2007-02-16 14:02:27Z thomas.diesler@jboss.com $

import java.util.ArrayList;
import java.util.Iterator;

import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFactory;

import org.apache.xerces.xs.XSModel;
import org.jboss.logging.Logger;
import org.jboss.util.xml.JBossEntityResolver;
import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.core.utils.JBossWSEntityResolver;
import org.jboss.ws.extensions.xop.jaxrpc.JBossXBContentAdapter;
import org.jboss.ws.metadata.jaxrpcmapping.ExceptionMapping;
import org.jboss.ws.metadata.jaxrpcmapping.JavaWsdlMapping;
import org.jboss.ws.metadata.jaxrpcmapping.JavaXmlTypeMapping;
import org.jboss.ws.metadata.jaxrpcmapping.PackageMapping;
import org.jboss.ws.metadata.jaxrpcmapping.VariableMapping;
import org.jboss.xb.binding.metadata.ClassMetaData;
import org.jboss.xb.binding.metadata.PackageMetaData;
import org.jboss.xb.binding.metadata.PropertyMetaData;
import org.jboss.xb.binding.metadata.ValueMetaData;
import org.jboss.xb.binding.sunday.unmarshalling.AttributeBinding;
import org.jboss.xb.binding.sunday.unmarshalling.CharactersHandler;
import org.jboss.xb.binding.sunday.unmarshalling.DefaultSchemaResolver;
import org.jboss.xb.binding.sunday.unmarshalling.ElementBinding;
import org.jboss.xb.binding.sunday.unmarshalling.ModelGroupBinding;
import org.jboss.xb.binding.sunday.unmarshalling.ParticleBinding;
import org.jboss.xb.binding.sunday.unmarshalling.ParticleHandler;
import org.jboss.xb.binding.sunday.unmarshalling.SchemaBinding;
import org.jboss.xb.binding.sunday.unmarshalling.SimpleTypeBinding;
import org.jboss.xb.binding.sunday.unmarshalling.TermBinding;
import org.jboss.xb.binding.sunday.unmarshalling.TypeBinding;
import org.jboss.xb.binding.sunday.unmarshalling.WildcardBinding;
import org.jboss.xb.binding.sunday.unmarshalling.XsdBinder;
import org.jboss.xb.binding.sunday.unmarshalling.impl.runtime.RtElementHandler;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.xml.sax.Attributes;

/**
 * Create SchemaBinding from XSModel and jaxrpc-mapping.
 *
 * @author Thomas.Diesler@jboss.org
 * @author Alexey.Loubyansky@jboss.org
 * @since 18-Oct-2004
 * @see XSModel
 * @see JavaWsdlMapping
 */
public class SchemaBindingBuilder
{
   // provide logging
   private static final Logger log = Logger.getLogger(SchemaBindingBuilder.class);

   /**
    * Creates and initializes an instance of SchemaBinding
    */
   public SchemaBinding buildSchemaBinding(XSModel model, JavaWsdlMapping wsdlMapping)
   {
      JBossEntityResolver resolver = new JBossWSEntityResolver();
      SchemaBinding schemaBinding = XsdBinder.bind(model, new DefaultSchemaResolver(resolver));

      schemaBinding.setIgnoreLowLine(false);
      schemaBinding.setIgnoreUnresolvedFieldOrClass(false);
      schemaBinding.setUnmarshalListsToArrays(true); // note: default jaxb2.0 is false!
      schemaBinding.setSimpleContentProperty("_value");
      schemaBinding.setUseNoArgCtorIfFound(true);
      schemaBinding.setReplacePropertyRefs(false);
      if (wsdlMapping != null)
      {
         bindSchemaToJava(schemaBinding, wsdlMapping);
      }

      // setup MTOM handler
      JBossXBContentAdapter.register(schemaBinding);

      return schemaBinding;
   }

   /** Merges JavaWsdlMapping into SchemaBinding
    */
   private void bindSchemaToJava(SchemaBinding schemaBinding, JavaWsdlMapping wsdlMapping)
   {
      if (log.isTraceEnabled())
         log.trace("bindSchemaToJava: " + schemaBinding);

      for (PackageMapping packageMapping : wsdlMapping.getPackageMappings())
      {
         processPackageMapping(schemaBinding, packageMapping);
      }

      for (ExceptionMapping exceptionMapping : wsdlMapping.getExceptionMappings())
      {
         processExceptionMapping(schemaBinding, exceptionMapping);
      }
      
      for (JavaXmlTypeMapping typeMapping : wsdlMapping.getJavaXmlTypeMappings())
      {
         processJavaXmlTypeMapping(schemaBinding, typeMapping);
      }
   }

   private void processPackageMapping(SchemaBinding schemaBinding, PackageMapping packageMapping)
   {
      PackageMetaData packageMetaData = schemaBinding.getPackageMetaData();
      if (packageMetaData == null)
      {
         packageMetaData = new PackageMetaData();
         schemaBinding.setPackageMetaData(packageMetaData);
      }

      if (log.isTraceEnabled())
         log.trace("Bound namespace " + packageMapping.getNamespaceURI() + " to package " + packageMapping.getPackageType());

      packageMetaData.setName(packageMapping.getPackageType());
   }

   private void processJavaXmlTypeMapping(SchemaBinding schemaBinding, JavaXmlTypeMapping typeMapping)
   {
      String javaType = typeMapping.getJavaType();
      if (javaType.endsWith("[]"))
      {
         processArrayType(schemaBinding, typeMapping);
      }
      else
      {
         processNonArrayType(schemaBinding, typeMapping);
      }
   }

   private void processExceptionMapping(SchemaBinding schemaBinding, ExceptionMapping exceptionMapping)
   {
      QName xmlType = exceptionMapping.getWsdlMessage();
      String javaType = exceptionMapping.getExceptionType();
      log.trace("processExceptionMapping: [xmlType=" + xmlType + ",javaType=" + javaType + "]");
      
      if (schemaBinding.getType(xmlType) == null)
      {
         TypeBinding typeBinding = new TypeBinding(xmlType);
         ClassMetaData cmd = new ClassMetaData();
         cmd.setUseNoArgCtor(Boolean.FALSE);
         cmd.setImpl(javaType);
         typeBinding.setClassMetaData(cmd);
         typeBinding.setSimple(false);
         schemaBinding.addType(typeBinding);
      }
   }

   private void processArrayType(SchemaBinding schemaBinding, JavaXmlTypeMapping typeMapping)
   {
      QName xmlType = getXmlType(typeMapping);
      log.trace("Ignore array type: " + xmlType);
   }

   private void processNonArrayType(SchemaBinding schemaBinding, JavaXmlTypeMapping typeMapping)
   {
      QName xmlType = getXmlType(typeMapping);
      String javaType = typeMapping.getJavaType();
      log.trace("processNonArrayType: [xmlType=" + xmlType + ",javaType=" + javaType + "]");

      TypeBinding typeBinding = getTypeBinding(schemaBinding, typeMapping);
      if (typeBinding != null)
      {
         // Set the java type, but skip SimpleTypes
         boolean isSimpleTypeBinding = (typeBinding instanceof SimpleTypeBinding);
         if(isSimpleTypeBinding == false)
         {
         ClassMetaData classMetaData = typeBinding.getClassMetaData();
         if (classMetaData == null)
         {
            classMetaData = new ClassMetaData();
            typeBinding.setClassMetaData(classMetaData);
         }
         classMetaData.setImpl(javaType);

         // exception mapping drives whether we should use the noarg ctor
         JavaWsdlMapping wsdlMapping = typeMapping.getJavaWsdlMapping();
         for (ExceptionMapping aux : wsdlMapping.getExceptionMappings())
         {
            if (javaType.equals(aux.getExceptionType()))
            {
               classMetaData.setUseNoArgCtor(false);
               break;
            }
         }

         if (log.isTraceEnabled())
         {
            QName typeQName = typeBinding.getQName();
            log.trace("Bound: [xmlType=" + typeQName + ",javaType=" + javaType + "]");
            }
         }

         VariableMapping[] variableMappings = typeMapping.getVariableMappings();
         for (VariableMapping varMapping : variableMappings)
         {
            if (varMapping.getXmlElementName() != null)
            {
               processXmlElementName(typeBinding, varMapping);
            }
            else if (varMapping.getXmlAttributeName() != null)
            {
               processXmlAttributeName(typeBinding, varMapping);
            }
            else if (varMapping.getXmlWildcard())
            {
               processWildcard(typeBinding, varMapping);
            }
         }
      }
      else
      {
         log.warn("Cannot obtain type binding for: " + xmlType);
      }
   }

   private void processXmlAttributeName(TypeBinding typeBinding, VariableMapping varMapping)
   {
      String xmlAttrName = varMapping.getXmlAttributeName();
      log.trace("processXmlAttributeName: " + xmlAttrName);

      QName xmlName = new QName(xmlAttrName);
      AttributeBinding attrBinding = typeBinding.getAttribute(xmlName);
      if (attrBinding == null)
      {
         Iterator i = typeBinding.getAttributes().iterator();
         while (i.hasNext())
         {
            AttributeBinding auxBinding = (AttributeBinding)i.next();
            if (auxBinding.getQName().getLocalPart().equals(xmlAttrName))
            {
               if (attrBinding != null)
                  log.warn("Ambiguous binding for attribute: " + xmlAttrName);

               attrBinding = auxBinding;
            }
         }
      }

      if (attrBinding == null)
      {
         // attributeFormDefault="qualified"
         String nsURI = typeBinding.getQName().getNamespaceURI();
         if (Constants.SOAP11_ATTR_MUST_UNDERSTAND.equals(xmlAttrName) || Constants.SOAP11_ATTR_ACTOR.equals(xmlAttrName)
               || Constants.SOAP12_ATTR_ROLE.equals(xmlAttrName))
         {
            nsURI = Constants.NS_SOAP11_ENV;
         }
         QName auxName = new QName(nsURI, xmlAttrName);
         attrBinding = typeBinding.getAttribute(auxName);
      }

      if (attrBinding == null)
      {
         QName typeQName = typeBinding.getQName();
         throw new WSException("Attribute " + xmlName + " found in jaxrpc-mapping but not in the schema: " + typeQName);
      }

      String javaVariableName = varMapping.getJavaVariableName();
      PropertyMetaData prop = new PropertyMetaData();
      prop.setName(javaVariableName);
      attrBinding.setPropertyMetaData(prop);

      if (log.isTraceEnabled())
         log.trace("Bound attribute " + xmlName + " to property " + prop.getName());
   }

   private void processXmlElementName(TypeBinding typeBinding, VariableMapping varMapping)
   {
      QName xmlName = new QName(varMapping.getXmlElementName());
      log.trace("processXmlElementName: " + xmlName);

      ElementBinding element = typeBinding.getElement(xmlName);
      QName typeQName = typeBinding.getQName();
      if (element == null && typeQName != null)
      {
         // elementFormDefault="qualified"
         String nsURI = typeQName.getNamespaceURI();
         QName auxName = new QName(nsURI, varMapping.getXmlElementName());
         element = typeBinding.getElement(auxName);
      }

      if (element == null)
      {
         // <element ref=
         ParticleBinding particle = typeBinding.getParticle();
         if (particle != null)
         {
            TermBinding term = particle.getTerm();
            if (term instanceof ModelGroupBinding)
            {
               Iterator iterator = ((ModelGroupBinding)term).getParticles().iterator();
               element = findLocalPathElement(iterator, new String[] { varMapping.getXmlElementName() }, 0);
            }
         }
      }

      if (element == null)
         throw new WSException("Element " + xmlName + " found in jaxrpc-mapping but not in the schema: " + typeQName);

      String javaVariableName = varMapping.getJavaVariableName();
      if (javaVariableName != null)
      {
         PropertyMetaData prop = new PropertyMetaData();
         prop.setName(javaVariableName);
         element.setPropertyMetaData(prop);

         if (log.isTraceEnabled())
            log.trace("Bound element " + xmlName + " to property " + prop.getName());
      }
   }

   private void processWildcard(TypeBinding typeBinding, VariableMapping varMapping)
   {
      log.trace("processWildcard: " + typeBinding.getQName());

      PropertyMetaData prop = null;
      String javaVariableName = varMapping.getJavaVariableName();
      if (javaVariableName != null)
      {
         prop = new PropertyMetaData();
         prop.setName(javaVariableName);
      }

      if (prop == null)
      {
         prop = new PropertyMetaData();
         prop.setName("_any");
      }

      WildcardBinding wildcard = typeBinding.getWildcard();
      wildcard.setUnresolvedElementHandler(new SoapElementHandler());
      wildcard.setUnresolvedCharactersHandler(new SoapCharactersHandler());
      wildcard.setPropertyMetaData(prop);

      if (log.isTraceEnabled())
         log.trace("Bound wildcard of " + typeBinding.getQName() + " to property " + prop.getName());
   }

   private TypeBinding getTypeBinding(SchemaBinding schemaBinding, JavaXmlTypeMapping typeMapping)
   {
      String qnameScope = typeMapping.getQnameScope();
      QName anonymousTypeQName = typeMapping.getAnonymousTypeQName();
      if (anonymousTypeQName != null)
      {
         return getAnonymousTypeBinding(schemaBinding, anonymousTypeQName);
      }

      QName xmlType = typeMapping.getRootTypeQName();

      TypeBinding typeBinding = null;
      if ("complexType".equals(qnameScope) || "simpleType".equals(qnameScope))
      {
         typeBinding = schemaBinding.getType(xmlType);
         if (typeBinding == null)
         {
            log.warn("Type definition not found in schema: " + xmlType);
         }
      }
      else if ("element".equals(qnameScope))
      {
         ElementBinding element = schemaBinding.getElement(xmlType);
         if (element != null)
         {
            typeBinding = element.getType();
         }
         else
         {
            log.warn("Global element not found in schema: " + xmlType);
         }
      }
      else
      {
         throw new WSException("Unexpected qname-scope for " + typeMapping.getJavaType() + ": " + qnameScope);
      }
      return typeBinding;
   }

   public TypeBinding getAnonymousTypeBinding(SchemaBinding schemaBinding, QName typeQName)
   {
      String expression = typeQName.getLocalPart();
      if (log.isTraceEnabled())
         log.trace("Searching for anonymous expression: " + expression);

      ArrayList list = new ArrayList(10);

      for (int i = 0, begin = -1; i < expression.length(); i++)
      {
         if (expression.charAt(i) == '>')
         {
            if (begin != -1)
            {
               list.add(expression.substring(begin, i));
               begin = -1;
            }
         }
         else
         {
            if (begin == -1)
               begin = i;
            else if (i == expression.length() - 1)
               list.add(expression.substring(begin));
         }
      }

      ElementBinding element = findLocalPathElement(schemaBinding.getElements(), ((String[])list.toArray(new String[0])));
      if (element == null)
         element = findLocalPathElementInTypes(schemaBinding.getTypes(), ((String[])list.toArray(new String[0])));

      if (element == null)
         return null;

      return element.getType();
   }

   public void bindParameterToElement(SchemaBinding schemaBinding, QName xmlName, QName xmlType)
   {
      TypeBinding typeBinding;
      boolean isAnonymousType = xmlType.getLocalPart().startsWith(">");
      if (isAnonymousType)
      {
         typeBinding = getAnonymousTypeBinding(schemaBinding, xmlType);
      }
      else
      {
         typeBinding = schemaBinding.getType(xmlType);
      }

      if (typeBinding != null)
      {
         if(!isAnonymousType)
            schemaBinding.addElement(xmlName, typeBinding);
      }
      else if (xmlType.equals(Constants.TYPE_LITERAL_ANYTYPE) == false)
      {
         throw new WSException("Root type " + xmlType + " not found in the schema.");
      }
   }

   private ElementBinding findLocalPathElement(Iterator elements, String[] path)
   {
      while (elements.hasNext())
      {
         ElementBinding element = (ElementBinding)elements.next();
         element = findLocalPathElement(element, path, 0);
         if (element != null)
            return element;
      }

      return null;
   }

   private ElementBinding findLocalPathElementInTypes(Iterator types, String[] path)
   {
      while (types.hasNext())
      {
         TypeBinding type = (TypeBinding)types.next();
         if (type.getQName().getLocalPart().equals(path[0]))
         {
            ParticleBinding particle = type.getParticle();
            if (particle == null)
               continue;

            TermBinding term = particle.getTerm();
            if (!term.isModelGroup())
               continue;

            return findLocalPathElement(((ModelGroupBinding)term).getParticles().iterator(), path, 1);
         }
      }

      return null;
   }

   private ElementBinding findLocalPathElement(ElementBinding element, String[] path, int pos)
   {
      String name = path[pos];
      if (!name.equals(element.getQName().getLocalPart()))
         return null;

      // End of path
      if (path.length - 1 == pos)
         return element;

      ParticleBinding particle = element.getType().getParticle();
      if (particle == null)
         return null;

      TermBinding term = particle.getTerm();
      if (!term.isModelGroup())
         return null;

      ModelGroupBinding group = (ModelGroupBinding)term;
      Iterator i = group.getParticles().iterator();

      // Increase depth
      return findLocalPathElement(i, path, pos + 1);
   }

   private ElementBinding findLocalPathElement(Iterator particles, String[] path, int pos)
   {
      while (particles.hasNext())
      {
         TermBinding term = ((ParticleBinding)particles.next()).getTerm();
         if (term instanceof ElementBinding)
         {
            ElementBinding element = (ElementBinding)term;
            element = findLocalPathElement(element, path, pos);
            if (element != null)
               return element;

         }
         else if (term instanceof ModelGroupBinding)
         {
            Iterator i = ((ModelGroupBinding)term).getParticles().iterator();
            ElementBinding element = findLocalPathElement(i, path, pos);
            if (element != null)
               return element;
         }
      }

      return null;
   }

   /** Get the <root-type-qname>, fall back to <anonymous-type-qname>
    */
   private QName getXmlType(JavaXmlTypeMapping typeMapping)
   {
      QName xmlType = typeMapping.getRootTypeQName();
      if (xmlType == null && typeMapping.getAnonymousTypeQName() != null)
         xmlType = typeMapping.getAnonymousTypeQName();

      return xmlType;
   }

   // Inner

   public static class SoapCharactersHandler extends CharactersHandler
   {
      public Object unmarshalEmpty(QName qName, TypeBinding typeBinding, NamespaceContext nsCtx, ValueMetaData valueMetaData)
      {
         return "";
      }

      public Object unmarshal(QName qName, TypeBinding typeBinding, NamespaceContext nsCtx, ValueMetaData valueMetaData, String value)
      {
         return value;
      }

      public void setValue(QName qName, ElementBinding element, Object owner, Object value)
      {
         SOAPElement e = (SOAPElement)owner;
         Text textNode = e.getOwnerDocument().createTextNode((String)value);
         e.appendChild(textNode);
      }
   }

   public static class SoapElementHandler extends RtElementHandler implements ParticleHandler
   {
      private SOAPFactory factory;

      public Object startParticle(Object parent, QName elementName, ParticleBinding particle, Attributes attrs, NamespaceContext nsCtx)
      {
         SOAPFactory factory = getFactory();
         SOAPElement element = null;
         try
         {
            String prefix = elementName.getPrefix();
            String ns = elementName.getNamespaceURI();
            if (ns != null && ns.length() > 0)
            {
               prefix = nsCtx.getPrefix(ns);
            }

            element = factory.createElement(elementName.getLocalPart(), prefix, ns);
         }
         catch (SOAPException e)
         {
            throw new IllegalStateException("Failed to create SOAPElement", e);
         }

         if (attrs != null)
         {
            for (int i = 0; i < attrs.getLength(); ++i)
            {
               element.setAttribute(attrs.getLocalName(i), attrs.getValue(i));
            }
         }

         return element;
      }

      public Object endParticle(Object o, QName elementName, ParticleBinding particle)
      {
         return o;
      }

      public void setParent(Object parent, Object o, QName elementName, ParticleBinding particle, ParticleBinding parentParticle)
      {
         if (parent instanceof SOAPElement)
         {
            ((SOAPElement)parent).appendChild((Element)o);
         }
         else
         {
            super.setParent(parent, o, elementName, particle, parentParticle);
         }
      }

      private SOAPFactory getFactory()
      {
         if (factory == null)
         {
            try
            {
               factory = SOAPFactory.newInstance();
            }
            catch (SOAPException e)
            {
               throw new IllegalStateException("Failed to create soap element factory", e);
            }
         }
         return factory;
      }
   }
}
