//*****************************************************************************
/*
** FILE:   xt_SaxEngine.java
**
** (c) 1997, 1998 Steve Withall.
**
** HISTORY:
**    21Mar98  stevew  Created, based on Microstar's SAXDemo.
**    17May98  stevew  Updated to SAX 1.0.
**    03Jul98  stevew  Renamed from xt_SaxTestbed to xt_SaxEngine, and
**                      restructured to extend xm_XmlEngine.
*/
package xt;

import xm.xm_NodeFactory;
import xm.xm_NodeTypeRegistry;
import xm.xm_ParseException;
import xm.xm_ParseListener;
import xm.xm_XmlEngine;

import xg.xg_Document;
import xg.xg_Element;
import xg.xg_Node;
import xg.xg_PI;
import xg.xg_Text;
import xg.xg_VerificationException;

import xa.xa_CharChecker;
import xa.xa_NodeTypeChoiceList;

import eh.eh_Debug;

import org.xml.sax.AttributeList;
import org.xml.sax.DocumentHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.Parser;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import java.net.URL;

import java.util.Enumeration;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

//*****************************************************************************
/** Engine for processing an XML source using a SAX parser.
 *
 *  Given the name of an XML file, initiate the parsing of it, and return a tree
 *  of xg_Node-derived classes representing it. The root xg_Node will currently
 *  always be an xg_Document, but xg_Node is used so that in future we may be
 *  able to parse partial documents.
 */
public class xt_SaxEngine extends    xm_XmlEngine
                          implements EntityResolver, DocumentHandler
{
    /** The name of the SAX driver class. */
    String          SaxDriverName     = null;

    /** The XML document created by the most recent parse. */
    xg_Document     CurrentDocument   = null;

    /** The node currently being parsed (if we are in the middle of parsing). */
    xg_Node         CurrentNode       = null;

    /** If some character data is parsed which is composed entirely of whitespace,
     *  it is parked here pending the parsing of the next entity. Depending upon
     *  the type of that next node, this whitespace may */
    String          PendingWhitespace = null;

    /** Factory for creating xg_Nodes of the right type. (Note that while its
     *  methods are currently all static, we use a proper factory object since its
     *  implementation may change). */
    xm_NodeFactory  TheNodeFactory    = new xm_NodeFactory();

    /** The name of the attribute in the attribute list whose value is the name
     *  of the SAX driver class, which in turn decides which parser to use. */
    public final static String DriverAttName = "Driver";

    //*****************************************************************************
    /** Constructor.
     */
    public xt_SaxEngine()
    {
        super();
    }

    //*****************************************************************************
    /** Parse the source supplied by the InputSourceReader.
     *
     *  @param      InputSourceReader  The reader from which to read the XML source
     *  @param      OutputDocument     Document into which to place the parse results
     *  @return                        The xg_Node representing the parse results
     *  @exception  xm_ParseException  The XML source contains an error
     *  @exception  IOException        An error occurred reading from InputSourceReader
     */
    public xg_Node parseSource(Reader       InputSourceReader,
                               xg_Document  OutputDocument) throws xm_ParseException,
                                                                   IOException
    {
        long  StartTime = System.currentTimeMillis();  // Start the clock

        // Find the SAX driver name from the EngineAttList (inherited from xm_XmlEngine).
        if (EngineAttList != null)
            SaxDriverName = EngineAttList.getAttributeValue(DriverAttName);
        if (SaxDriverName == null)
            throw new xm_ParseException("No SAX driver specified (add 'Driver' attribute to config file)");

        // Work on the supplied document; create a new one if it's null.
        if (OutputDocument == null)
            CurrentDocument = new xg_Document();
        else
        {
            OutputDocument.reset();
            CurrentDocument = OutputDocument;
        }
        CurrentNode = CurrentDocument;

        // Create and prepare a SAX parser and a SAX source.
        Parser  SaxParser = createParser(SaxDriverName);
        SaxParser.setEntityResolver(this);    // We are both entity resolver and
        SaxParser.setDocumentHandler(this);   //   document handler for this parser
        InputSource  SaxSource = new InputSource(InputSourceReader);

		// Perform the parse.
        try
        {
            SaxParser.parse(SaxSource);
        }
        catch (SAXParseException  InputException)
        {
            throw new xm_ParseException(InputException.getMessage(),
                                        InputException.getLineNumber(),
                                        InputException.getColumnNumber() );
        }
        catch (SAXException  InputException)
        {
            throw new xm_ParseException(InputException.getMessage() );
        }
        catch (Exception  InputException)
        {
            eh_Debug.printStackTrace(5, InputException);
            throw new xm_ParseException(InputException.toString() );
        }

        StatusMessage = new String("Processing using SAX completed successfully "
                                        + getParseStatsText(StartTime));
        return CurrentDocument;
    }

    //*****************************************************************************
    /** Given a SAX driver class name, dynamically create a parser. The driver
     *  class must implement the SAX Parser interface.
     *
     *  @param      InputDriverClassName  The name of the class
     *  @exception  xm_ParseException     Error creating SAX parser
     */
    public static Parser createParser(String InputDriverClassName)
                                                     throws xm_ParseException
    {
        eh_Debug.add(5, "Create SAX parser using driver '" + InputDriverClassName + "'");
        Class  DriverClass = null;
        try
        {
            DriverClass = Class.forName(InputDriverClassName);
        }
        catch (ClassNotFoundException  InputException)
        {
            throw new xm_ParseException("Cannot find SAX driver class '"
                                           + InputDriverClassName + "'");
        }

        // Instantiate the parser.
        Object  ParserObject = null;
        try
        {
            ParserObject = DriverClass.newInstance();
        }
        catch (IllegalAccessException  InputException)
        {
            throw new xm_ParseException("SAX parser class '" + InputDriverClassName
			                              + "' does not have a zero-argument constructor.");
        }
        catch (InstantiationException  InputException)
        {
            throw new xm_ParseException("SAX parser class '" + InputDriverClassName
			                              + "' cannot be instantiated.");
        }
        catch (Exception  InputException)
        {
            throw new xm_ParseException("Error creating SAX parser class '"
                                           + InputDriverClassName
			                               + "': " + InputException);
        }

		// Check the the parser object actually implements the Parser interface.
        if (!(ParserObject instanceof Parser))
        {
            throw new xm_ParseException("Class '" + InputDriverClassName
			                               + "' does not implement org.xml.sax.Parser");
        }

        return (Parser)ParserObject;
    }

    //*****************************************************************************
    /** Set the parse listener which is to be informed of parse events. The main
     *  purpose of this is to be able to show progress to the user; since SAX
     *  doesn't provide any information which allows us to do this, this
     *  implementation does nothing.
     *
     *  @param  InputParseListener  Listener to be informed of certain parse
     *                               events
     */
    public void setParseListener(xm_ParseListener  InputParseListener)
    {
    }

    //*****************************************************************************
    /** Set the name of the SAX driver class.
     *
     *  @param  InputSaxDriverName  The name of the SAX driver class
     */
    public void setDriverName(String  InputSaxDriverName)
    {
        SaxDriverName = InputSaxDriverName;
    }

    //*****************************************************************************
    /** Prepare and return a string describing how much was parsed, and how fast.
     *
     *  This method is private because it uses the current time as the time at which
     *  the parse is deemed to have completed - so if called from outside later,
     *  a misleading result will be given.
     *
     *  @param  InputParseStartTime  The system time at which the parse started
     *  @return                      A string description of the parse statistics
     */
    protected String getParseStatsText(long  InputParseStartTime)
    {
        long    ParseTimeMillis = System.currentTimeMillis() - InputParseStartTime;
        float   ParseTime       = ((float)ParseTimeMillis) / 1000;
//        float   ParseRate       = TheParseManager.getTotalCharCount() / ParseTime;
        String  ParseStatsText  = new String("in " + ParseTime + " seconds");
//        String  ParseStatsText  = new String(TheParseManager.getTotalCharCount()
//                                             + " characters/" + TheParseManager.getLineCount()
//                                             + " lines in " + ParseTime
//                                             + " seconds (" + ParseRate + " characters per second)");
        return ParseStatsText;
    }

    //*****************************************************************************
    // Methods implementing org.xml.sax.EntityResolver
    //*****************************************************************************
    /** Display a message when resolving an entity.
     *
     *  @param      publicId      The public identifier of the external entity
     *                             being referenced, or null if none was supplied
     *  @param      systemId      The system identifier of the external entity
     *                             being referenced
     *  @return                   An InputSource object describing the new input
     *                             source, or null to request that the parser open
     *                             a regular URI connection to the system identifier
     *  @ exception  SAXException  Any SAX exception, possibly wrapping another
     *                              exception
     *  @ exception  IOException   An IO error, possibly when creating a new
     *                              InputStream or Reader for the InputSource
     */
    public InputSource resolveEntity(String publicID, String systemID)
//                     throws SAXException, IOException
    {
        eh_Debug.add(6, "Resolve external entity: public ID = '" + publicID
                                    + "', for system '" + systemID + "'");
        return null;
    }

    //*****************************************************************************
    // Methods implementing org.xml.sax.DocumentHandler, which receive
    // notification of general document events.
    //*****************************************************************************
    /** Receive an object for locating the origin of SAX document events.
     *
     *  @param  InputLocator  An object that can return the location of any SAX
     *                         document event.
     */
    public void setDocumentLocator(Locator  InputLocator)
    {
        eh_Debug.add(4, "setDocumentLocator: Line " + InputLocator.getLineNumber()
                                      + ", column " + InputLocator.getColumnNumber() );
    }

    //*****************************************************************************
    /** Display a message at the start of the document.
     */
    public void startDocument()
//                    throws SAXException
    {
        eh_Debug.add(4, "Start document");
    }

    //*****************************************************************************
    /** Display a message at the end of the document.
     */
    public void endDocument ()
//                    throws SAXException
    {
        eh_Debug.add(4, "End document");
    }

    //*****************************************************************************
    /** A new element has been parsed. Create an element object of a suitable type,
     *  give it its attributes and then add it to its parent.
     *
     *  @param      InputElementName  The name of the new element
     *  @param      InputAttList      The new element's attributes
     *  @exception  SAXException      Any SAX exception, possibly wrapping another
     *                                 exception
     */
    public void startElement(String InputElementName, AttributeList InputAttList)
                                       throws SAXException
    {
        eh_Debug.add(7, "Start element: " + InputElementName);

        // Create new element.
        xg_Element  NewElement = null;
        try
        {
            NewElement = (xg_Element)TheNodeFactory.createNode(InputElementName,
                                                               xa_NodeTypeChoiceList.ELEMENT_TYPE,
                                                               "xg.xg_Element");
        }
        catch (xm_ParseException  InputException)
        {
            throw new SAXParseException(InputException.getMessage(),
                                        null,
                                        null,
                                        InputException.getLineNum(),
                                        InputException.getColumnNum() );
        }

        // Give the new element any whitespace which proceeds it.
        if (PendingWhitespace != null)
            NewElement.setPrecedingWhitespace(PendingWhitespace);

        // Give the new element its attributes.
        if (InputAttList.getLength() == 0)
        {
            eh_Debug.add(7, "Element " + InputElementName + " has no attributes");
        }
        else
        {
            for (int AttIndex = 0; AttIndex < InputAttList.getLength(); AttIndex++)
            {
                NewElement.addAttribute(InputAttList.getName(AttIndex),
                                        InputAttList.getValue(AttIndex) );
                String AttName  = InputAttList.getName(AttIndex);
                String AttType  = InputAttList.getType(AttIndex);
                String AttValue = InputAttList.getValue(AttIndex);
                eh_Debug.add(7, "Attribute " + AttIndex + ": '" + AttName
                                    + "' = '" + AttValue
                                    + "' (Type '" + AttType + "')");
            }
        }

        CurrentNode.addChild(NewElement);  // Add the new element to its parent
        CurrentNode = NewElement;          // The new element is now the current node
    }

    //*****************************************************************************
    /** The end of the current element has been parsed.
     *
     *  @param      InputElementName  The name of the element just completed
     *  @exception  SAXException      Error verifying this element
     */
    public void endElement(String InputElementName) throws SAXException
    {
        eh_Debug.add(7, "End element: " + InputElementName);

        // Verify the element we've just finished.
        try
        {
            if (VerifyFlag)
                CurrentNode.verify();
        }
        catch (xg_VerificationException  InputException)
        {
//            eh_Debug.add(5, "Verification error in element " + InputElementName
//                                 + ": " + InputException.getMessageID() );
            throw new SAXParseException(InputException.getMessageID(),
                                        null,
                                        null,
                                        0,
                                        0);
//                                        InputException.getLineNum(),
//                                        InputException.getColumnNum() );
        }

        CurrentNode = CurrentNode.getParentNode();  // Parent becomes the current node
    }

    //*****************************************************************************
    /** Display a message when characters are found.
     */
    public void characters(char InputChars[], int start, int length)
//                    throws SAXException
    {
        eh_Debug.add(7, "Characters: '"
		                   + escapeCharacters(InputChars, start, length)
		                   + "' (Start = " + start + ", Length = " + length + ")");
        String  CharString = new String(InputChars, start, length);
        if (xa_CharChecker.isWhitespace(CharString) )
            PendingWhitespace = CharString;
        else
        {
            xg_Text  NewText = new xg_Text(CharString);
            CurrentNode.addChild(NewText);
        }
    }

    //*****************************************************************************
    /** Display a message when ignorable whitespace is found.
     */
    public void ignorableWhitespace(char InputChars[], int start, int length)
//                    throws SAXException
    {
        eh_Debug.add(7, "Ignorable whitespace: \"" +
		                       escapeCharacters(InputChars, start, length) + '"');
    }

    //*****************************************************************************
    /** Print a message when a processing instruction is found.
     */
    public void processingInstruction (String target, String data)
//                    throws SAXException
    {
        eh_Debug.add(7, "Processing instruction: " + target + " " + data);
        xg_PI  NewPI = new xg_PI(new String(target + " " + data) );

        // Give the new PI any whitespace which proceeds it.
        if (PendingWhitespace != null)
            NewPI.setPrecedingWhitespace(PendingWhitespace);

        CurrentNode.addChild(NewPI);
    }

    //*****************************************************************************
    // SAX utility routines.
    //*****************************************************************************
    /** If a URL is relative, make it absolute against the current directory.
     */
/*    public static String makeAbsoluteURL(String  InpurUrlString)
                                 throws java.net.MalformedURLException
    {
        eh_Debug.add(5, "makeAbsoluteURL: Create absolute URL for '" + InpurUrlString + "'");
        URL  baseURL;

        String currentDirectory = System.getProperty("user.dir");
        String fileSep          = System.getProperty("file.separator");
        String file             = currentDirectory.replace(fileSep.charAt(0), '/') + '/';

        if (file.charAt(0) != '/')
        {
            file = "/" + file;
        }
        baseURL = new URL("file", null, file);

        URL     AbsoluteUrl       = new URL(baseURL, InpurUrlString);
        String  AbsoluteUrlString = AbsoluteUrl.toString();
        eh_Debug.add(5, "makeAbsoluteURL: Result URL is '" + AbsoluteUrlString + "'");
        return AbsoluteUrlString;
    }
*/
    //*****************************************************************************
    /** Escape special characters for display.
     */
    private static String escapeCharacters(char ch[], int start, int length)
    {
        StringBuffer out = new StringBuffer();

        for (int i = start; i < start+length; i++)
        {
            if (ch[i] >= 0x20 && ch[i] < 0x7f)
	            out.append(ch[i]);
            else
	            out.append("&#" + (int)ch[i] + ';');
        }

        return out.toString();
    }
}

//*****************************************************************************