/*
 * 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.metadata.wsdl.xsd;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import org.apache.xerces.impl.dv.xs.SchemaDVFactoryImpl;
import org.apache.xerces.impl.xs.XMLSchemaLoader;
import org.apache.xerces.util.XMLGrammarPoolImpl;
import org.apache.xerces.xni.parser.XMLEntityResolver;
import org.apache.xerces.xni.parser.XMLErrorHandler;
import org.apache.xerces.xs.StringList;
import org.apache.xerces.xs.XSComplexTypeDefinition;
import org.apache.xerces.xs.XSConstants;
import org.apache.xerces.xs.XSElementDeclaration;
import org.apache.xerces.xs.XSLoader;
import org.apache.xerces.xs.XSModel;
import org.apache.xerces.xs.XSModelGroup;
import org.apache.xerces.xs.XSNamedMap;
import org.apache.xerces.xs.XSObjectList;
import org.apache.xerces.xs.XSParticle;
import org.apache.xerces.xs.XSSimpleTypeDefinition;
import org.apache.xerces.xs.XSTerm;
import org.apache.xerces.xs.XSTypeDefinition;
import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSComplexTypeDefinition;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSElementDeclaration;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSErrorHandler;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSSimpleTypeDefinition;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSStringList;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSTypeDefinition;
import org.jboss.wsf.spi.management.ServerConfig;
import org.jboss.wsf.spi.management.ServerConfigFactory;
import org.jboss.wsf.spi.SPIProvider;
import org.jboss.wsf.spi.SPIProviderResolver;

/**
 *  Util class that deals with XML Schema
 *  @author <mailto:Anil.Saldhana@jboss.org>Anil Saldhana
 *  @since  Apr 12, 2005
 */
public class SchemaUtils
{
   private static SchemaUtils ourInstance = new SchemaUtils();

   protected static String xsNS = Constants.NS_SCHEMA_XSD;

   private static Map<Class, QName> toolsTypeMappingOverride = new HashMap<Class, QName>();

   static
   {
      toolsTypeMappingOverride.put(byte[].class, Constants.TYPE_LITERAL_BASE64BINARY);
   }

   public static SchemaUtils getInstance()
   {
      return ourInstance;
   }

   private SchemaUtils()
   {
   }

   /**
    * Checks if the XS Particle is of array type
    * @param xsparticle
    * @return
    */

   public boolean isArrayType(XSParticle xsparticle)
   {
      //Determine if it is an arrayType
      int maxOccurs = xsparticle.getMaxOccurs();
      return xsparticle.getMaxOccursUnbounded() || maxOccurs > 1;
   }

   public static boolean isWrapperArrayType(XSTypeDefinition xst)
   {
      return unwrapArrayType(xst) != null;
   }

   public static XSElementDeclaration unwrapArrayType(XSTypeDefinition xst)
   {
      if (xst instanceof XSComplexTypeDefinition == false)
         return null;

      XSComplexTypeDefinition xc = (XSComplexTypeDefinition)xst;
      if (xc.getContentType() == XSComplexTypeDefinition.CONTENTTYPE_EMPTY)
         return null;

      XSParticle xsp = xc.getParticle();
      if (xsp == null)
         return null;

      XSTerm xsterm = xsp.getTerm();
      if (xsterm instanceof XSModelGroup == false)
         return null;

      XSModelGroup xm = (XSModelGroup)xsterm;
      XSObjectList xo = xm.getParticles();
      if (xo.getLength() != 1)
         return null;

      XSParticle xp = (XSParticle)xo.item(0);
      XSTerm term = xp.getTerm();
      if ((xp.getMaxOccursUnbounded() || xp.getMaxOccurs() > 1) == false || term instanceof XSElementDeclaration == false)
         return null;

      return (XSElementDeclaration)term;
   }

   /**
    * Generate a QName for a simple type
    * @param simple
    * @return
    */
   public static QName handleSimpleType(XSSimpleTypeDefinition simple)
   {
      if (simple == null)
         throw new IllegalArgumentException("XSSimpleTypeDefinition passed is null");

      //Check if the type of SimpleType is a plain xsd type
      if (Constants.NS_SCHEMA_XSD.equals(simple.getNamespace()))
         return createQNameFromXSSimpleType(simple);

      switch (simple.getVariety())
      {
         case XSSimpleTypeDefinition.VARIETY_LIST:
            return handleSimpleType(simple.getItemType());
         case XSSimpleTypeDefinition.VARIETY_UNION:
            XSObjectList list = simple.getMemberTypes();
            if (list.getLength() > 0)
               return handleSimpleType((XSSimpleTypeDefinition)list.item(0));
            throw new WSException("Empty union type not expected");
         case XSSimpleTypeDefinition.VARIETY_ABSENT:
            throw new WSException("Absent variety is not supported in simple types");
      }

      XSTypeDefinition base = simple.getBaseType();
      while (!Constants.NS_SCHEMA_XSD.equals(base.getNamespace()))
         base = base.getBaseType();

      if (!(base instanceof XSSimpleTypeDefinition))
         throw new WSException("Expected base type to be a simple type");

      return new QName(base.getNamespace(), base.getName());
   }

   /**
    * For the basic xsd types, patch prefix and ns uri
    * @param qname
    * @return
    */
   public QName patchXSDQName(QName qname)
   {
      if (qname == null)
         return null;
      return new QName(Constants.NS_SCHEMA_XSD, qname.getLocalPart(), Constants.PREFIX_XSD);
   }

   /**
    * Given a XSTypeDefinition and the QName for the element,
    * create a XSElementDeclaration object
    * @param xmlName Name of the element
    * @param xst Type of the element
    * @return XSElementDeclaration
    */
   public XSElementDeclaration createXSElementDeclaration(QName xmlName, XSTypeDefinition xst)
   {
      String name = xmlName.getLocalPart();
      String ns = xmlName.getNamespaceURI();
      JBossXSElementDeclaration jbel = new JBossXSElementDeclaration(name, ns);
      jbel.setTypeDefinition(xst);
      jbel.setScope(XSConstants.SCOPE_GLOBAL);
      return jbel;
   }

   /**
    * Create a QName given a Simple Type
    * @param xs
    * @return   qname
    */
   public static QName createQNameFromXSSimpleType(XSSimpleTypeDefinition xs)
   {
      String nsuri = xs.getNamespace();
      String localpart = xs.getName();
      if (xsNS.equals(nsuri))
         return new QName(nsuri, localpart, Constants.PREFIX_XSD);
      else return new QName(nsuri, localpart);
   }

   /**
    * Return typemapping override for tools if any
    *
    * @param javaType
    * @return
    */
   public QName getToolsOverrideInTypeMapping(Class javaType)
   {
      return toolsTypeMappingOverride.get(javaType);
   }

   /**
    * Check if a global element exists
    * @param xmlName  QName of the element that needs to be checked
    * @param xsmodel Schema model that is passed
    * @return
    */
   public boolean hasGlobalElement(QName xmlName, XSModel xsmodel)
   {
      if (xmlName == null)
         throw new IllegalArgumentException("xmlName is null");
      if (xsmodel == null)
         throw new IllegalArgumentException("XSModel is null");
      boolean bool = false;
      String name = xmlName.getLocalPart();
      if (name == null)
         throw new IllegalArgumentException("xmlName has a null name");
      String ns = xmlName.getNamespaceURI();
      if (ns == null)
         throw new IllegalArgumentException("xmlName has a null namespace");
      if (xsmodel.getElementDeclaration(name, ns) != null)
         bool = true;
      return bool;
   }

   /**
    * Check if a Complex Type  exists
    * @param xmlType  QName of the Complex Type that needs to be checked
    * @param xsmodel Schema model that is passed
    * @return
    */
   public boolean hasComplexTypeDefinition(QName xmlType, URL xsdLocation)
   {
      if (xsdLocation == null)
         throw new IllegalArgumentException("xsdLocation is null");
      XSModel xsmodel = parseSchema(xsdLocation);
      return this.hasComplexTypeDefinition(xmlType, xsmodel);
   }

   /**
    * Check if a Global Element  exists
    * @param xmlType  QName of the global element that needs to be checked
    * @param xsmodel Schema model that is passed
    * @return
    */
   public boolean hasGlobalElement(QName xmlName, URL xsdLocation)
   {
      if (xmlName == null)
         throw new IllegalArgumentException("xmlName is null");
      if (xsdLocation == null)
         throw new IllegalArgumentException("xsdLocation is null");
      XSModel xsmodel = parseSchema(xsdLocation);
      boolean bool = false;
      String name = xmlName.getLocalPart();
      if (name == null)
         throw new IllegalArgumentException("xmlName has a null name");
      String ns = xmlName.getNamespaceURI();
      if (ns == null)
         throw new IllegalArgumentException("xmlName has a null namespace");
      if (xsmodel.getElementDeclaration(name, ns) != null)
         bool = true;
      return bool;
   }

   /**
    * Check if a Complex Type  exists
    * @param xmlType  QName of the Complex Type that needs to be checked
    * @param xsmodel Schema model that is passed
    * @return
    */
   public boolean hasComplexTypeDefinition(QName xmlType, XSModel xsmodel)
   {
      if (xmlType == null)
         throw new IllegalArgumentException("xmlType is null");
      if (xsmodel == null)
         throw new IllegalArgumentException("XSModel is null");
      boolean bool = false;
      String name = xmlType.getLocalPart();
      if (name == null)
         throw new IllegalArgumentException("xmlName has a null name");
      String ns = xmlType.getNamespaceURI();
      if (ns == null)
         throw new IllegalArgumentException("xmlName has a null namespace");
      if (xsmodel.getTypeDefinition(name, ns) != null)
         bool = true;
      return bool;
   }

   /**
    * Get formatted string for the qname representing the xstype
    * @param xstype
    * @return
    */
   public String getFormattedString(XSTypeDefinition xstype)
   {
      String ns = xstype.getNamespace();
      String name = xstype.getName();
      if (!ns.equals(xsNS))
         name = getPrefix(ns) + ":" + name;

      return name;
   }

   /**
    * Get a QName for a xs type
    * @param xstype
    * @return
    */
   public QName getQName(XSTypeDefinition xstype)
   {
      String prefix = null;
      String ns = xstype.getNamespace();
      String name = xstype.getName();
      if (!ns.equals(xsNS))
         prefix = Constants.PREFIX_TNS;
      else prefix = Constants.PREFIX_XSD;

      return new QName(ns, name, prefix);
   }

   /**
    * Get a schema basic type
    * TODO: Migrate this off of the Xerces Impl
    * @param localpart
    * @return
    */
   public JBossXSTypeDefinition getSchemaBasicType(String localpart)
   {
      JBossXSTypeDefinition xt = null;
      /**
       * Special case: xs:anyType
       */
      if ("anyType".equals(localpart))
      {
         JBossXSComplexTypeDefinition ct = new JBossXSComplexTypeDefinition(localpart, Constants.NS_SCHEMA_XSD);
         ct.setContentType(XSComplexTypeDefinition.CONTENTTYPE_EMPTY);
         xt = ct;
      }
      else
      {
         XSSimpleTypeDefinition xstype = (new SchemaDVFactoryImpl()).getBuiltInType(localpart);
         xt = new JBossXSSimpleTypeDefinition(xstype);

      }
      return xt;
   }

   /**
    * Get an instance of XSLoader that is capable of
    * parsing schema files
    *
    * @return
    */
   public XSLoader getXSLoader()
   {
      XMLSchemaLoader xsloader = new XMLSchemaLoader();
      JBossXSErrorHandler eh = new JBossXSErrorHandler();
      xsloader.setErrorHandler(eh);
      xsloader.setProperty("http://apache.org/xml/properties/internal/grammar-pool", new XMLGrammarPoolImpl());
      return xsloader;
   }

   /**
    * Get an instance of XSLoader that is capable of
    * parsing schema files
    * @param xeh XML Error handler
    * @param xer XML Entity Resolver
    * @return
    */
   public XSLoader getXSLoader(XMLErrorHandler xeh, XMLEntityResolver xer)
   {
      XMLSchemaLoader xsloader = new XMLSchemaLoader();
      xsloader.setEntityResolver(xer);
      xsloader.setErrorHandler(xeh);
      xsloader.setProperty("http://apache.org/xml/properties/internal/grammar-pool", new XMLGrammarPoolImpl());
      return xsloader;
   }

   /**
    * Given a schema file, generate a schema model
    * @param schemaLoc java.net.URL object to the schema file
    * @return schema model
    */
   public XSModel parseSchema(URL schemaLoc)
   {
      return parseSchema(schemaLoc.toExternalForm());
   }

   /**
    * Given a schema file, generate a schema model
    * @param schemaLoc  string representation to the location of schema
    * @return schema model
    */
   public XSModel parseSchema(String schemaLoc)
   {
      XSLoader xsloader = getXSLoader();
      XSModel xsModel = xsloader.loadURI(schemaLoc);
      if (xsModel == null)
         throw new WSException("Cannot parse schema: " + schemaLoc);
      return xsModel;
   }

   /**
    * Given a list of schema locations, parse and
    * provide a Xerces XSModel
    *
    * @param locations
    * @return
    */
   public XSModel parseSchema(List<String> locations)
   {
      JBossXSStringList slist = new JBossXSStringList(locations);
      XSLoader xsloader = getXSLoader();
      return xsloader.loadURIList(slist);
   }

   /**
    * Checks if the XSModel is empty
    * @param xsmodel
    * @return  true (if empty) and false (if not empty)
    */
   public boolean isEmptySchema(XSModel xsmodel)
   {
      if (xsmodel == null)
         return true;
      String targetNS = getTargetNamespace(xsmodel);
      if (targetNS == null)
         throw new WSException("Target Namespace of xsmodel is null");
      XSNamedMap tmap = xsmodel.getComponentsByNamespace(XSConstants.TYPE_DEFINITION, targetNS);
      XSNamedMap emap = xsmodel.getComponentsByNamespace(XSConstants.ELEMENT_DECLARATION, targetNS);

      if (tmap != null && tmap.getLength() > 0)
         return false;
      if (emap != null && emap.getLength() > 0)
         return false;

      return true;
   }

   /**
    * Checks if the XSModel is empty given a namespace
    * @param xsmodel Schema Model to check
    * @param namespace namespace to check components for
    * @return  true (if empty) and false (if not empty)
    */
   public boolean isEmptySchema(XSModel xsmodel, String namespace)
   {
      if (xsmodel == null)
         return true;
      if (namespace == null)
         throw new WSException("Target Namespace of xsmodel is null");
      XSNamedMap tmap = xsmodel.getComponentsByNamespace(XSConstants.TYPE_DEFINITION, namespace);
      XSNamedMap emap = xsmodel.getComponentsByNamespace(XSConstants.ELEMENT_DECLARATION, namespace);

      if (tmap != null && tmap.getLength() > 0)
         return false;
      if (emap != null && emap.getLength() > 0)
         return false;

      return true;
   }

   /**
    * Get the schema definitions as a String
    * @param targetNS The Target Namespace
    * @return
    */
   public static String getSchemaDefinitions(String targetNS)
   {
      StringBuilder buffer = new StringBuilder();
      buffer.append(" targetNamespace='" + targetNS + "'");
      buffer.append(" xmlns='" + Constants.NS_SCHEMA_XSD + "'");
      buffer.append(" xmlns:" + Constants.PREFIX_SOAP11_ENC + "='" + Constants.URI_SOAP11_ENC + "'");
      buffer.append(" xmlns:" + Constants.PREFIX_TNS + "='" + targetNS + "'");
      //buffer.append(" xmlns:" + WSDLConstants.PREFIX_XSD + "='" + WSDLConstants.NS_SCHEMA_XSD + "'");
      buffer.append(" xmlns:" + Constants.PREFIX_XSI + "='" + Constants.NS_SCHEMA_XSI + "'");
      buffer.append(">");
      return buffer.toString();
   }

   /** Get the temp file for a given namespace
    */
   public static File getSchemaTempFile(String targetNS) throws IOException
   {
      if (targetNS.length() == 0)
         throw new IllegalArgumentException("Invalid null target namespace");

      String fname = targetNS;
      if (fname.indexOf("://") > 0)
         fname = fname.substring(fname.indexOf("://") + 3);

      File tmpdir = null;
      try
      {
         SPIProvider spiProvider = SPIProviderResolver.getInstance().getProvider();
         ServerConfig serverConfig = spiProvider.getSPI(ServerConfigFactory.class).getServerConfig();File tmpDir = serverConfig.getServerTempDir();
         tmpdir = serverConfig.getServerTempDir();
         tmpdir = new File(tmpdir.getCanonicalPath() + "/jbossws");
         tmpdir.mkdirs();
      }
      catch (Throwable th)
      {
         // ignore if the server config cannot be found
         // this would be the case if we are on the client side
      }

      fname = fname.replace('/', '_');
      fname = fname.replace(':', '_');
      fname = fname.replace('?', '_');
      fname = fname.replace('#', '_');

      File file = File.createTempFile("JBossWS_" + fname, ".xsd", tmpdir);
      return file;
   }

   /**
    * Get the TargetNamespace from the schema model
    */
   public static String getTargetNamespace(XSModel xsmodel)
   {
      if (xsmodel == null)
         throw new IllegalArgumentException("Illegal Null Argument: xsmodel");
      String targetNS = null;
      StringList slist = xsmodel.getNamespaces();
      int len = slist != null ? slist.getLength() : 0;
      for (int i = 0; i < len; i++)
      {
         String ns = slist.item(i);
         if (Constants.NS_SCHEMA_XSD.equals(ns))
            continue;
         targetNS = ns;
         break;
      }
      return targetNS;
   }

   private String getPrefix(String namespace)
   {
      if (Constants.URI_SOAP11_ENC.equals(namespace))
         return Constants.PREFIX_SOAP11_ENC;
      else if (Constants.NS_SOAP11_ENV.equals(namespace) || Constants.NS_SOAP12_ENV.equals(namespace))
         return Constants.PREFIX_ENV;

      // Fall back to target namespace even though this is incorrect
      return Constants.PREFIX_TNS;
   }
}
