/**
 * Redistribution and use of this software and associated documentation ("Software"), with or
 * without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
 * must also contain a copy of this document.
 *
 * 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 name "Exolab" must not be used to endorse or promote products derived from this Software
 * without prior written permission of Intalio, Inc. For written permission, please contact
 * info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
 * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
 * Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``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 INTALIO, INC. 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.
 *
 * Copyright 1999-2002 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id$
 */

package org.exolab.castor.xml.schema.reader;

import java.io.IOException;
import java.io.Reader;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.net.URIException;
import org.exolab.castor.net.URILocation;
import org.exolab.castor.net.URIResolver;
import org.exolab.castor.util.NestedIOException;
import org.exolab.castor.xml.XMLException;
import org.exolab.castor.xml.schema.Schema;
import org.exolab.castor.xml.schema.SchemaContext;
import org.exolab.castor.xml.schema.SchemaContextImpl;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Parser;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * A class for reading XML Schemas.
 * 
 * @author <a href="mailto:kvisco@intalio.com">Keith Visco</a>
 * @version $Revision$ $Date: 2004-10-05 14:27:10 -0600 (Tue, 05 Oct 2004) $
 **/
@SuppressWarnings("deprecation")
public class SchemaReader {

  /**
   * The {@link Log} instance to use.
   */
  private static final Log LOG = LogFactory.getLog(SchemaReader.class);

  /**
   * The Castor XML Context... mother of all.
   */
  private SchemaContext _schemaContext;

  /**
   * XML Parser instance
   */
  private Parser _parser = null;

  /**
   * SAX InputSource to Schema
   */
  private InputSource _source = null;

  /**
   * SAX EntityResolver
   */
  private EntityResolver _resolver = null;

  /**
   * SAX ErrorHandler
   */
  private ErrorHandler _errorHandler = null;

  /**
   * The resolver to be used for resolving href
   */
  private URIResolver _uriResolver;

  /**
   * A flag that indicates that included schemas should be cached instead of being inlined [which is
   * the default behavior as specified by the XML Schema Specification].
   * 
   */
  private boolean _cacheIncludedSchemas = false;

  private Schema _schema = null;

  private boolean _validate = true;

  /**
   * Old fashion style to create a SchemaReader instance.
   * 
   * @throws IOException if no Parser is available
   */
  private void init() throws IOException {
    // get default parser from Configuration
    _schemaContext = new SchemaContextImpl();

    Parser parser = _schemaContext.getParser();

    if (parser == null) {
      String message = "fatal error: unable to create SAX parser.";
      LOG.warn(message);
      throw new IOException(message);
    }

    _parser = parser;
  }

  /**
   * Creates a new SchemaReader for the given InputSource
   * 
   * @param source the InputSource to read the Schema from.
   */
  public SchemaReader(InputSource source) throws IOException {
    init();

    if (source == null)
      throw new IllegalArgumentException("InputSource cannot be null");

    _source = source;

  }

  /**
   * Creates a new SchemaReader for the given Reader
   * 
   * @param reader the Reader to read the Schema from.
   * @param filename for reporting errors.
   **/
  public SchemaReader(Reader reader, String filename) throws IOException {
    init();

    if (reader == null) {
      String err = "The argument 'reader' must not be null.";
      throw new IllegalArgumentException(err);
    }

    _source = new InputSource(reader);
    if (filename == null)
      filename = reader.toString();
    _source.setPublicId(filename);

  }

  /**
   * Creates a new SchemaReader for the given URL
   * 
   * @param url the URL string
   **/
  public SchemaReader(String url) throws IOException {
    init();
    if (url == null) {
      String err = "The argument 'url' must not be null.";
      throw new IllegalArgumentException(err);
    }
    _source = new InputSource(url);

  }

  /**
   * New style how to create a SchemaReader instance, requiring that {@link SchemaContext} and
   * InputSource are set before calling {@link read}.
   */
  public SchemaReader() {
    super();
  }

  /**
   * To set the {@link SchemaContext} to be used. Also resets the parser as it depends of the
   * {@link SchemaContext}.
   * 
   * @param schemaContext the {@link SchemaContext} to be used
   */
  public void setSchemaContext(final SchemaContext schemaContext) {
    this._schemaContext = schemaContext;

    Parser p = _schemaContext.getParser();
    if (p != null) {
      _parser = p;
    }
  }

  /**
   * A different way to create a SchemaReader by using an empty constructor and setting the
   * InputSource afterwards.
   * 
   * @param inputSource the InputSource to read the schema from
   */
  public void setInputSource(final InputSource inputSource) {
    if (inputSource == null) {
      String message = "InputSource must not be null";
      LOG.warn(message);
      throw new IllegalArgumentException(message);
    }
    _source = inputSource;
  }

  /**
   * Reads the Schema from the source and returns the Schema object model.
   * 
   * <BR />
   * <B>Note:</B> Subsequent calls to this method will simply return a cached copy of the Schema
   * object. To read a new Schema object, create a new Reader.
   * 
   * @return the new Schema created from the source of this SchemaReader
   **/
  public Schema read() throws IOException {
    if (_schema != null) {
      return _schema;
    }
    if (_parser == null) {
      String message = "Required Parser was not specified";
      LOG.warn(message);
      throw new IllegalStateException(message);
    }
    if (_source == null) {
      String message = "Required Source was not specified";
      LOG.warn(message);
      throw new IllegalStateException(message);
    }
    SchemaUnmarshaller schemaUnmarshaller = null;

    try {
      SchemaUnmarshallerState state = new SchemaUnmarshallerState();
      // TODO[Joachim] state.setConfiguration(_config);
      state.cacheIncludedSchemas = _cacheIncludedSchemas;
      schemaUnmarshaller = new SchemaUnmarshaller(_schemaContext, state);
      if (_uriResolver != null)
        schemaUnmarshaller.setURIResolver(_uriResolver);

      // make sure we mark the URI as processed for cyclic imports/includes
      String uri = _source.getSystemId();
      if (uri != null) {
        URIResolver resolver = schemaUnmarshaller.getURIResolver();
        try {
          URILocation location = resolver.resolve(uri, null);
          if (location != null)
            uri = location.toString();
        } catch (URIException except) {
          throw new NestedIOException(except);
        }
        state.markAsProcessed(uri, schemaUnmarshaller.getSchema());
      }

      Sax2ComponentReader handler = new Sax2ComponentReader(schemaUnmarshaller);
      _parser.setDocumentHandler(handler);

      if (_errorHandler == null)
        _parser.setErrorHandler(handler);
      else
        _parser.setErrorHandler(_errorHandler);

      if (_resolver != null)
        _parser.setEntityResolver(_resolver);
      _parser.parse(_source);
    } catch (XMLException ex) {
      handleException(ex);
    } catch (org.xml.sax.SAXException sx) {
      handleException(sx);
    }

    _schema = schemaUnmarshaller.getSchema();

    if (_validate) {
      try {
        _schema.validate();
      } catch (org.exolab.castor.xml.ValidationException vx) {
        throw new NestedIOException(vx);
      }
    }

    return _schema;

  }

  /**
   * Sets the ErrorHandler.
   * 
   * @param errorHandler
   **/
  public void setErrorHandler(ErrorHandler errorHandler) {
    _errorHandler = errorHandler;
  }

  /**
   * Sets wheter or not to cache the included xml schemas instead of inlining them as specified by
   * the XML Schema specification.
   * 
   * @param cache true to cache the included XML Schemas.
   **/
  public void setCacheIncludedSchemas(boolean cache) {
    _cacheIncludedSchemas = cache;
  }

  /**
   * Sets whether or not post-read validation should occur. By default, validation is enabled. Note
   * that certain read validation cannot be disabled.
   * 
   * @param validate a boolean that when true will force a call to Schema#validate after the schema
   *        is read.
   **/
  public void setValidation(boolean validate) {
    _validate = validate;
  }

  /**
   * Sets the EntityResolver used to resolve SYSTEM Identifier. If the entity resolver is null, the
   * default one will be used.
   * 
   * @param resolver the EntityResolver to use.
   */
  public void setEntityResolver(EntityResolver resolver) {
    _resolver = resolver;
  }

  /**
   * Sets the URIResolver used to resolve hrefs. If the entity resolver is null, the default one
   * will be used.
   * 
   * @param uriresolver the URIResolver to use.
   */
  public void setURIResolver(URIResolver uriresolver) {
    _uriResolver = uriresolver;
  }

  /**
   * Handle an exception which is one of our own XMLExceptions.
   * 
   * @param xmlException the XMLException to handle.
   * @throws IOException
   */
  private void handleException(XMLException xmlException) throws IOException {
    throw new NestedIOException(xmlException);
  }

  /**
   * Handle an exception which is a foreign SAXException.
   * 
   * @param sx The SAXException to handle.
   * @throws IOException
   */
  private void handleException(SAXException sx) throws IOException {
    Exception except = sx.getException();
    if (except == null) {
      except = sx;
    } else if (except instanceof SAXParseException) {
      SAXParseException spe = (SAXParseException) except;
      String filename = spe.getSystemId();
      if (filename == null)
        filename = spe.getPublicId();
      if (filename == null)
        filename = "<filename unavailable>";

      String err = spe.getMessage();

      err += "; " + filename + " [ line: " + spe.getLineNumber();
      err += ", column: " + spe.getColumnNumber() + ']';
      throw new NestedIOException(err, except);
    } else if (except instanceof XMLException) {
      handleException((XMLException) except);
    }

    throw new NestedIOException(except);

  }
}
