package org.exolab.castor.xml.util;

import java.util.StringTokenizer;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.core.util.AbstractProperties;
import org.castor.core.util.Messages;
import org.castor.xml.XMLProperties;
import org.exolab.castor.xml.OutputFormat;
import org.exolab.castor.xml.Serializer;
import org.exolab.castor.xml.XMLSerializerFactory;
import org.xml.sax.Parser;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;

/**
 * A couple of routines to manipulate XMLParser instances. Mostly extracted from 'old'
 * LocalConfiguration class.
 * 
 * @author Joachim Grueneis, jgrueneis_at_gmail_dot_com
 * @version $Id$
 * @since 1.1.3
 */
public class XMLParserUtils {

  /**
   * Logger to be used.
   */
  static final Log LOG = LogFactory.getFactory().getInstance(XMLParserUtils.class);

  /**
   * To set validation feature of XMLReader.
   */
  private static final String VALIDATION = "http://xml.org/sax/features/validation";

  /**
   * To set namespaces feature of XMLReader.
   */
  private static final String NAMESPACES = "http://xml.org/sax/features/namespaces";

  /**
   * Sets features on XML reader instance.
   * 
   * @param properties the Properties to read parser features from
   * @param defaultFeatures any default features to use
   * @param validation Whether to enable validation or not.
   * @param namespaces Whether to enable namespace support for not.
   * @param xmlReader The XMLReader instance to configure.
   */
  public static void setFeaturesOnXmlReader(final String parserFeatures,
      final String parserFeaturesToDisable, final boolean validation, final boolean namespaces,
      final XMLReader xmlReader) {
    try {
      xmlReader.setFeature(VALIDATION, validation);
      xmlReader.setFeature(NAMESPACES, namespaces);
      enableFeatures(parserFeatures, xmlReader);
      disableFeatures(parserFeaturesToDisable, xmlReader);
    } catch (SAXException except) {
      LOG.error(Messages.format("conf.configurationError", except));
    }
  }

  /**
   * Enables selected features on the XMLReader instance.
   * 
   * @param features Features to enable
   * @param xmlReader XMLReader instance to be configured.
   * @throws SAXNotRecognizedException If the feature is not recognized by the XMLReader.
   * @throws SAXNotSupportedException If the feature is not supported by the XMLReader.
   */
  private static void enableFeatures(final String features, final XMLReader xmlReader)
      throws SAXNotRecognizedException, SAXNotSupportedException {
    StringTokenizer token;
    if (features != null) {
      token = new StringTokenizer(features, ", ");
      while (token.hasMoreTokens()) {
        xmlReader.setFeature(token.nextToken(), true);
      }
    }
  }

  /**
   * Disables selected features on the XMLReader instance.
   * 
   * @param features Features to disable
   * @param xmlReader XMLReader instance to be configured.
   * @throws SAXNotRecognizedException If the feature is not recognized by the XMLReader.
   * @throws SAXNotSupportedException If the feature is not supported by the XMLReader.
   */
  private static void disableFeatures(final String features, final XMLReader xmlReader)
      throws SAXNotRecognizedException, SAXNotSupportedException {
    StringTokenizer token;
    if (features != null) {
      token = new StringTokenizer(features, ", ");
      while (token.hasMoreTokens()) {
        xmlReader.setFeature(token.nextToken(), false);
      }
    }
  }

  /**
   * To get a SAXParser instance which is then used to get either parser or XMLReader.
   * 
   * @param validation validation flag to set into parser factory
   * @param namespaces namespace flag to set into parser factory
   * @return the SAXParser for further use
   */
  public static SAXParser getSAXParser(final boolean validation, final boolean namespaces) {
    SAXParser saxParser = null;

    SAXParserFactory factory = SAXParserFactory.newInstance();
    factory.setNamespaceAware(namespaces);
    factory.setValidating(validation);
    try {
      saxParser = factory.newSAXParser();
      if (LOG.isDebugEnabled()) {
        LOG.debug("Successfully instantiated a JAXP SAXParser instance.");
      }
    } catch (ParserConfigurationException pcx) {
      LOG.error(Messages.format("conf.configurationError", pcx));
    } catch (org.xml.sax.SAXException sx) {
      LOG.error(Messages.format("conf.configurationError", sx));
    }
    return saxParser;
  }

  /**
   * To get a SAXParser instance from a given {@link SAXParserFactory}, which is then used to obtain
   * either {@link Parser} or {@link XMLReader}.
   * 
   * @return the {@link SAXParser} for further use
   */
  public static SAXParser getSAXParser(SAXParserFactory saxParserFactory) {
    SAXParser saxParser = null;

    try {
      saxParser = saxParserFactory.newSAXParser();
      if (LOG.isDebugEnabled()) {
        LOG.debug("Successfully instantiated a JAXP SAXParser instance.");
      }
    } catch (ParserConfigurationException pcx) {
      LOG.error(Messages.format("conf.configurationError", pcx));
    } catch (org.xml.sax.SAXException sx) {
      LOG.error(Messages.format("conf.configurationError", sx));
    }
    return saxParser;
  }

  /**
   * To get a SAXParserFactory instance which is then used to obtain an {@link SAXParser} instance.
   * 
   * @param namespaces Whether to provide namespace support.
   * @param validation Whether to produce a validating SAX parser.
   * @return the SAXParserFactory for further use
   */
  public static SAXParserFactory getSAXParserFactory(final boolean validation,
      final boolean namespaces) {
    SAXParserFactory factory = SAXParserFactory.newInstance();
    factory.setNamespaceAware(namespaces);
    factory.setValidating(validation);
    return factory;
  }

  /**
   * Instantiates an {@link XMLReader} instance directly, using {@link Class#forName(String)} to
   * obtain the {@link Class} instance, and uses {@link Class#newInstance()} to create the actual
   * instance.
   * 
   * @param className The class name of the {@link XMLReader} instance to be instantiated.
   * @return An {@link XMLReader} instance.
   */
  public static XMLReader instantiateXMLReader(final String className) {
    XMLReader xmlReader;
    try {
      Class cls;
      cls = Class.forName(className);
      xmlReader = (XMLReader) cls.newInstance();
      if (LOG.isDebugEnabled()) {
        LOG.debug("Successfully instantiated " + className);
      }
    } catch (Exception except) {
      throw new RuntimeException(
          Messages.format("conf.failedInstantiateParser", className, except));
    }
    return xmlReader;
  }

  /**
   * Instantiates an {@link Parser} instance directly, using {@link Class#forName(String)} to obtain
   * the {@link Class} instance, and uses {@link Class#newInstance()} to create the actual instance.
   * 
   * @param className The class name of the {@link Parser} instance to be instantiated.
   * @return An {@link Parser} instance.
   */
  public static Parser instantiateParser(final String className) {
    Parser parser;
    try {
      Class cls;
      cls = Class.forName(className);
      parser = (Parser) cls.newInstance();
      if (LOG.isDebugEnabled()) {
        LOG.debug("Successfully instantiated " + className);
      }
    } catch (Exception except) {
      throw new RuntimeException(
          Messages.format("conf.failedInstantiateParser", className, except));
    }
    return parser;
  }

  public static Parser getParser(final AbstractProperties properties, final String features) {
    Parser parser = null;
    Boolean validation = properties.getBoolean(XMLProperties.PARSER_VALIDATION);
    Boolean namespaces = properties.getBoolean(XMLProperties.NAMESPACES);
    String parserClassName = properties.getString(XMLProperties.PARSER);
    if ((parserClassName == null) || (parserClassName.length() == 0)) {
      SAXParser saxParser =
          XMLParserUtils.getSAXParser(validation.booleanValue(), namespaces.booleanValue());
      if (saxParser != null) {
        try {
          parser = saxParser.getParser();
        } catch (SAXException e) {
          LOG.error(Messages.format("conf.configurationError", e));
        }
      }
    }

    if (parser == null) {
      if ((parserClassName == null) || (parserClassName.length() == 0)
          || (parserClassName.equalsIgnoreCase("xerces"))) {
        parserClassName = "org.apache.xerces.parsers.SAXParser";
      }

      // if a parser class was specified, we try to create it
      parser = XMLParserUtils.instantiateParser(parserClassName);

      if (parser instanceof XMLReader) {
        XMLReader xmlReader = (XMLReader) parser;
        XMLParserUtils.setFeaturesOnXmlReader(
            properties.getString(XMLProperties.PARSER_FEATURES, features),
            properties.getString(XMLProperties.PARSER_FEATURES_DISABLED, ""),
            validation.booleanValue(), namespaces.booleanValue(), xmlReader);
      }
    }
    return parser;
  }

  /**
   * @see org.castor.xml.InternalContext#getSerializer()
   */
  public static Serializer getSerializer(final AbstractProperties properties) {
    Serializer serializer =
        getSerializerFactory(properties.getString(XMLProperties.SERIALIZER_FACTORY))
            .getSerializer();
    serializer.setOutputFormat(getOutputFormat(properties));
    return serializer;
  }

  /**
   * @see org.castor.xml.InternalContext#getOutputFormat()
   */
  public static OutputFormat getOutputFormat(final AbstractProperties properties) {

    boolean indent = properties.getBoolean(XMLProperties.USE_INDENTATION, false);

    String version = properties.getString(XMLProperties.XML_VERSION, "1.0");

    OutputFormat format =
        getSerializerFactory(properties.getString(XMLProperties.SERIALIZER_FACTORY))
            .getOutputFormat();
    format.setMethod(OutputFormat.XML);
    format.setVersion(version);
    format.setIndenting(indent);

    // There is a bad interaction between the indentation and the
    // setPreserveSpace option. The indentated output is strangely indented.
    if (!indent) {
      format.setPreserveSpace(true);
    }

    return format;
  } // -- getOutputFormat

  /**
   * Returns the currently configured XMLSerializerFactory instance.
   * 
   * @param serializerFactoryName the class name of the serializer factory
   * @return XMLSerializerFactory to use by Castor
   */
  public static XMLSerializerFactory getSerializerFactory(final String serializerFactoryName) {
    XMLSerializerFactory serializerFactory;

    try {
      serializerFactory = (XMLSerializerFactory) Class.forName(serializerFactoryName).newInstance();
    } catch (Exception except) {
      throw new RuntimeException(Messages.format("conf.failedInstantiateSerializerFactory",
          serializerFactoryName, except));
    }
    return serializerFactory;
  }


}

