/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.jelly.parser;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

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

import org.apache.commons.collections.ArrayStack;

import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.Tag;
import org.apache.commons.jelly.TagLibrary;
import org.apache.commons.jelly.impl.CompositeTextScriptBlock;
import org.apache.commons.jelly.impl.ExpressionScript;
import org.apache.commons.jelly.impl.StaticTag;
import org.apache.commons.jelly.impl.ScriptBlock;
import org.apache.commons.jelly.impl.StaticTagScript;
import org.apache.commons.jelly.impl.TagFactory;
import org.apache.commons.jelly.impl.TagScript;
import org.apache.commons.jelly.impl.TextScript;
import org.apache.commons.jelly.util.ClassLoaderUtils;
import org.apache.commons.jelly.expression.CompositeExpression;
import org.apache.commons.jelly.expression.ConstantExpression;
import org.apache.commons.jelly.expression.Expression;
import org.apache.commons.jelly.expression.ExpressionFactory;
import org.apache.commons.jelly.expression.jexl.JexlExpressionFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.xml.sax.Attributes;
import org.xml.sax.ErrorHandler;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.AttributesImpl;

/** <p><code>XMLParser</code> parses the XML Jelly format.
 * The SAXParser and XMLReader portions of this code come from Digester.</p>
 *
 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
 * @version $Revision: 1807798 $
 */
public class XMLParser extends DefaultHandler {

    /**
     * Share the Jelly properties across parsers
     */
    private static Properties jellyProperties;


    /** JellyContext which is used to locate tag libraries*/
    private JellyContext context = new JellyContext();

    /** the expression factory used to evaluate tag attributes */
    private ExpressionFactory expressionFactory;

    /** The current script block */
    private ScriptBlock script;

    /** The current, parent tagScript */
    private TagScript tagScript;

    /** The stack of body scripts. */
    private ArrayStack scriptStack = new ArrayStack();

    /** The stack of tagScripts - use ArrayList as it allows null. */
    private ArrayList tagScriptStack = new ArrayList();

    /** The current text buffer where non-custom tags get written */
    private StringBuffer textBuffer;

    /** Do we allow our doctype definitions to call out to external entities? */
    private boolean allowDtdToCallExternalEntities = false;

    /**
     * The class loader to use for instantiating application objects.
     * If not specified, the context class loader, or the class loader
     * used to load XMLParser itself, is used, based on the value of the
     * <code>useContextClassLoader</code> variable.
     */
    protected ClassLoader classLoader = null;

    /**
     * Do we want to use the Context ClassLoader when loading classes
     * for instantiating new objects?  Default is <code>false</code>.
     */
    protected boolean useContextClassLoader = false;

    /**
     * The application-supplied error handler that is notified when parsing
     * warnings, errors, or fatal errors occur.
     */
    protected ErrorHandler errorHandler = null;

    /**
     * The SAXParserFactory that is created the first time we need it.
     */
    protected static SAXParserFactory factory = null;

    /**
     * The SAXParser we will use to parse the input stream.
     */
    protected SAXParser parser = null;

    /**
     * The XMLReader used to parse digester rules.
     */
    protected XMLReader reader = null;

    /**
     * The Locator associated with our parser.
     */
    protected Locator locator = null;

    /**
     * Registered namespaces we are currently processing.  The key is the
     * namespace prefix that was declared in the document.  The value is an
     * ArrayStack of the namespace URIs this prefix has been mapped to --
     * the top Stack element is the most current one.  (This architecture
     * is required because documents can declare nested uses of the same
     * prefix for different Namespace URIs).
     */
    protected Map namespaces = new HashMap();

    /** The Map of the namespace prefix -&gt; URIs defined for the current element */
    private Map elementNamespaces;

    /**
     * The name of the file being parsed that is passed to the TagScript objects
     * for error reporting
     */
    private String fileName;

    /**
     * Do we want to use a validating parser?
     */
    protected boolean validating = false;

    /** Flag to indicate if this object has been configured */
    private boolean configured;

    /**
     * when not null, set the default namespace for
     * unprefixed elements via the DefaultNamespaceFilter
     * class
     */
    private String defaultNamespaceURI = null;

    /**
     * The Log to which logging calls will be made.
     */
    private Log log = LogFactory.getLog(XMLParser.class);

    /**
     * Construct a new XMLParser with default properties.
     */
    public XMLParser() {
    }

    /**
     * Construct a new XMLParser, with the boolean
     * allowDtdToCallExternalEntities being passed in. If this is set to false,
     * the XMLParser will be created with:
     * XMLReader spf = XMLReaderFactory.createXMLReader();
     * spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
     * spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
     * spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",false);
     * as given by
     * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#XMLReader
     */
    public XMLParser(boolean allowDtdToCallExternalEntities) {
        this.allowDtdToCallExternalEntities = allowDtdToCallExternalEntities;
    }

    /**
     * Construct a new XMLParser, allowing a SAXParser to be passed in.  This
     * allows XMLParser to be used in environments which are unfriendly to
     * JAXP1.1 (such as WebLogic 6.0).  Thanks for the request to change go to
     * James House (james@interobjective.com).  This may help in places where
     * you are able to load JAXP 1.1 classes yourself.
     */
    public XMLParser(SAXParser parser) {
        this.parser = parser;
    }

    /**
     * Construct a new XMLParser, allowing an XMLReader to be passed in.  This
     * allows XMLParser to be used in environments which are unfriendly to
     * JAXP1.1 (such as WebLogic 6.0).  Note that if you use this option you
     * have to configure namespace and validation support yourself, as these
     * properties only affect the SAXParser and emtpy constructor.
     */
    public XMLParser(XMLReader reader) {
        this.reader = reader;
    }

    /**
     * Parse the content of the specified file using this XMLParser.  Returns
     * the root element from the object stack (if any).
     *
     * @param file File containing the XML data to be parsed
     *
     * @exception IOException if an input/output error occurs
     * @exception SAXException if a parsing exception occurs
     */
    public Script parse(File file) throws IOException, SAXException {
        return parse(file.toURL());
    }

    /**
     * Parse the content of the specified file using this XMLParser.  Returns
     * the root element from the object stack (if any).
     *
     * @param url URL containing the XML data to be parsed
     *
     * @exception IOException if an input/output error occurs
     * @exception SAXException if a parsing exception occurs
     */
    public Script parse(URL url) throws IOException, SAXException {
        ensureConfigured();
        this.fileName = url.toString();

        InputSource source = new InputSource(url.toString());

        getXMLReader().parse(source);
        return script;
    }

    /**
     * Parse the content of the specified input source using this XMLParser.
     * Returns the root element from the object stack (if any).
     *
     * @param input Input source containing the XML data to be parsed
     *
     * @exception IOException if an input/output error occurs
     * @exception SAXException if a parsing exception occurs
     */
    public Script parse(InputSource input) throws IOException, SAXException {
        ensureConfigured();
        this.fileName = input.getSystemId();
        getXMLReader().parse(input);
        return script;
    }

    /**
     * Parse the content of the specified input stream using this XMLParser.
     * Returns the root element from the object stack (if any).
     * (Note: if reading a File or URL, use one of the URL-based
     * parse methods instead.  This method will not be able
     * to resolve any relative paths inside a DTD.)
     *
     * @param input  Input stream containing the XML data to be parsed
     * @return
     * @exception IOException
     *                   if an input/output error occurs
     * @exception SAXException
     *                   if a parsing exception occurs
     */
    public Script parse(InputStream input) throws IOException, SAXException {
        ensureConfigured();
        this.fileName = getCurrentURI();
        getXMLReader().parse(new InputSource(input));
        return script;
    }

    /**
     * Parse the content of the specified reader using this XMLParser.
     * Returns the root element from the object stack (if any).
     * (Note: if reading a File or URL, use one of the URL-based
     * parse methods instead.  This method will not be able
     * to resolve any relative paths inside a DTD.)
     *
     * @param reader Reader containing the XML data to be parsed
     * @return
     * @exception IOException
     *                   if an input/output error occurs
     * @exception SAXException
     *                   if a parsing exception occurs
     */
    public Script parse(Reader reader) throws IOException, SAXException {
        ensureConfigured();
        this.fileName = getCurrentURI();
        getXMLReader().parse(new InputSource(reader));
        return script;
    }

    /**
     * Parse the content of the specified URI using this XMLParser.
     * Returns the root element from the object stack (if any).
     *
     * @param uri URI containing the XML data to be parsed
     *
     * @exception IOException if an input/output error occurs
     * @exception SAXException if a parsing exception occurs
     */
    public Script parse(String uri) throws IOException, SAXException {
        ensureConfigured();
        this.fileName = uri;
        getXMLReader().parse(uri);
        return script;
    }

    /**
     * Return the currently mapped namespace URI for the specified prefix,
     * if any; otherwise return <code>null</code>.  These mappings come and
     * go dynamically as the document is parsed.
     *
     * @param prefix Prefix to look up
     */
    public String findNamespaceURI(String prefix) {
        ArrayStack stack = (ArrayStack) namespaces.get(prefix);
        if (stack == null) {
            return (null);
        }
        try {
            return ((String) stack.peek());
        }
        catch (EmptyStackException e) {
            return (null);
        }
    }

    // Properties
    //-------------------------------------------------------------------------
    public JellyContext getContext() {
        return context;
    }

    public void setContext(JellyContext context) {
        this.context = context;
    }

    /**
     * Set the jelly namespace to use for unprefixed elements.
     * Will be overridden by an explicit namespace in the
     * XML document.
     *
     * @param namespace jelly namespace to use (e.g. 'jelly:core')
     */
    public void setDefaultNamespaceURI(String namespace) {
        this.defaultNamespaceURI = namespace;
    }

    /**
     * Return the class loader to be used for instantiating application objects
     * when required.  This is determined based upon the following rules:
     * <ul>
     * <li>The class loader set by <code>setClassLoader()</code>, if any</li>
     * <li>The thread context class loader, if it exists and the
     *     <code>useContextClassLoader</code> property is set to true</li>
     * <li>The class loader used to load the XMLParser class itself.
     * </ul>
     */
    public ClassLoader getClassLoader() {
        return ClassLoaderUtils.getClassLoader(classLoader, useContextClassLoader, getClass());
    }

    /**
     * Set the class loader to be used for instantiating application objects
     * when required.
     *
     * @param classLoader The new class loader to use, or <code>null</code>
     *  to revert to the standard rules
     */
    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * Return the boolean as to whether the context classloader should be used.
     */
    public boolean getUseContextClassLoader() {
        return useContextClassLoader;
    }

    /**
     * Determine whether to use the Context ClassLoader (the one found by
     * calling <code>Thread.currentThread().getContextClassLoader()</code>)
     * to resolve/load classes.  If not
     * using Context ClassLoader, then the class-loading defaults to
     * using the calling-class' ClassLoader.
     *
     * @param use determines whether to use JellyContext ClassLoader.
     */
    public void setUseContextClassLoader(boolean use) {
        useContextClassLoader = use;
    }

    /**
     * Return the error handler for this XMLParser.
     */
    public ErrorHandler getErrorHandler() {
        return (this.errorHandler);
    }

    /**
     * Set the error handler for this XMLParser.
     *
     * @param errorHandler The new error handler
     */
    public void setErrorHandler(ErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    /**
     * Return the current Logger associated with this instance of the XMLParser
     */
    public Log getLogger() {
        return log;
    }

    /**
     * Set the current logger for this XMLParser.
     */
    public void setLogger(Log log) {
        this.log = log;
    }

    /** @return the expression factory used to evaluate tag attributes */
    public ExpressionFactory getExpressionFactory() {
        if (expressionFactory == null) {
            expressionFactory = createExpressionFactory();
        }
        return expressionFactory;
    }

    /** Sets the expression factory used to evaluate tag attributes */
    public void setExpressionFactory(ExpressionFactory expressionFactory) {
        this.expressionFactory = expressionFactory;
    }

    /**
     * Return the SAXParser we will use to parse the input stream.  If there
     * is a problem creating the parser, return <code>null</code>.
     */
    public SAXParser getParser() {
        // Return the parser we already created (if any)
        if (parser != null) {
            return (parser);
        }
        // Create and return a new parser
        synchronized (this) {
            try {
                if (factory == null) {
                    factory = SAXParserFactory.newInstance();
                }
                factory.setNamespaceAware(true);
                factory.setValidating(validating);
                parser = factory.newSAXParser();
                return (parser);
            }
            catch (Exception e) {
                log.error("XMLParser.getParser: ", e);
                return (null);
            }
        }
    }

    /**
     * By setting the reader in the constructor, you can bypass JAXP and
     * be able to use digester in Weblogic 6.0.
     *
     * @deprecated Use getXMLReader() instead, which can throw a
     *  SAXException if the reader cannot be instantiated
     */
    public XMLReader getReader() {
        try {
            return (getXMLReader());
        }
        catch (SAXException e) {
            log.error("Cannot get XMLReader", e);
            return (null);
        }
    }

    /**
     * Return the XMLReader to be used for parsing the input document.
     *
     * @exception SAXException if no XMLReader can be instantiated
     */
    public synchronized XMLReader getXMLReader() throws SAXException {
        if (reader == null) {
            reader = getParser().getXMLReader();
            if (!allowDtdToCallExternalEntities) {
                reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
                reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            }
            if (this.defaultNamespaceURI != null) {
                reader = new DefaultNamespaceFilter(this.defaultNamespaceURI,reader);
            }
        }
        //set up the parse
        reader.setContentHandler(this);
        reader.setDTDHandler(this);
        //reader.setEntityResolver(this);
        reader.setErrorHandler(this);

        return reader;
    }

    /**
     * Return the validating parser flag.
     */
    public boolean getValidating() {
        return (this.validating);
    }

    /**
     * Set the validating parser flag.  This must be called before
     * <code>parse()</code> is called the first time.
     *
     * @param validating The new validating parser flag.
     */
    public void setValidating(boolean validating) {
        this.validating = validating;
    }


    /**
     * Returns the script that has just been created if this class is used
     * as a SAX ContentHandler and passed into some XML processor or parser.
     *
     * @return the ScriptBlock created if SAX events are piped into this class,
     * which must include a startDocument() and endDocument()
     */
    public ScriptBlock getScript() {
        return script;
    }


    // ContentHandler interface
    //-------------------------------------------------------------------------
    /**
     * Process notification of the beginning of the document being reached.
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void startDocument() throws SAXException {
        script = new ScriptBlock();
        textBuffer = new StringBuffer();
        tagScript = null;
        scriptStack.clear();
        tagScriptStack.clear();
    }

    /**
     * Process notification of the end of the document being reached.
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void endDocument() throws SAXException {
        textBuffer = null;
    }

    /**
     * Process notification of the start of an XML element being reached.
     *
     * @param namespaceURI The Namespace URI, or the empty string if the
     *   element has no Namespace URI or if Namespace processing is not
     *   being performed.
     * @param localName The local name (without prefix), or the empty
     *   string if Namespace processing is not being performed.
     * @param qName The qualified name (with prefix), or the empty
     *   string if qualified names are not available.\
     * @param list The attributes attached to the element. If there are
     *   no attributes, it shall be an empty Attributes object.
     * @exception SAXException if a parsing error is to be reported
     */
    public void startElement(
        String namespaceURI,
        String localName,
        String qName,
        Attributes list)
        throws SAXException {

        try {
            // add check to ensure namespace URI is "" for no namespace
            if ( namespaceURI == null ) {
                namespaceURI = "";
            }

            // if this is a tag then create a script to run it
            // otherwise pass the text to the current body
            TagScript newTagScript = createTag(namespaceURI, localName, list);
            if (newTagScript == null) {
                newTagScript = createStaticTag(namespaceURI, localName, qName, list);
            }
            tagScript = newTagScript;
            tagScriptStack.add(tagScript);
            if (tagScript != null) {
                // set the line number details
                if ( locator != null ) {
                    tagScript.setLocator(locator);
                }
                // sets the file name element names
                tagScript.setFileName(fileName);
                tagScript.setElementName(qName);
                tagScript.setLocalName(localName);

                if (textBuffer.length() > 0) {
                    addTextScript(textBuffer.toString());
                    textBuffer.setLength(0);
                }
                script.addScript(tagScript);
                // start a new body
                scriptStack.push(script);
                script = new ScriptBlock();
                tagScript.setTagBody(script);
            }
            else {
                // XXXX: might wanna handle empty elements later...
                textBuffer.append("<");
                textBuffer.append(qName);
                int size = list.getLength();
                for (int i = 0; i < size; i++) {
                    textBuffer.append(" ");
                    textBuffer.append(list.getQName(i));
                    textBuffer.append("=");
                    textBuffer.append("\"");
                    textBuffer.append(list.getValue(i));
                    textBuffer.append("\"");
                }
                textBuffer.append(">");
            }
        }
        catch (SAXException e) {
            throw e;
        }
        catch (Exception e) {
            log.error( "Caught exception: " + e, e );
            throw new SAXException( "Runtime Exception: " + e, e );
        }
    }

    /**
     * Process notification of character data received from the body of
     * an XML element.
     *
     * @param buffer The characters from the XML document
     * @param start Starting offset into the buffer
     * @param length Number of characters from the buffer
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void characters(char buffer[], int start, int length)
        throws SAXException {
        textBuffer.append(buffer, start, length);
    }

    /**
     * Process notification of the end of an XML element being reached.
     *
     * @param namespaceURI The Namespace URI, or the empty string if the
     *   element has no Namespace URI or if Namespace processing is not
     *   being performed.
     * @param localName The local name (without prefix), or the empty
     *   string if Namespace processing is not being performed.
     * @param qName The qualified XML 1.0 name (with prefix), or the
     *   empty string if qualified names are not available.
     * @exception SAXException if a parsing error is to be reported
     */
    public void endElement(String namespaceURI, String localName, String qName)
        throws SAXException {
        try {
            tagScript = (TagScript) tagScriptStack.remove(tagScriptStack.size() - 1);
            if (tagScript != null) {
                if (textBuffer.length() > 0) {
                    addTextScript(textBuffer.toString());
                    textBuffer.setLength(0);
                }
                script = (ScriptBlock) scriptStack.pop();
            }
            else {
                textBuffer.append("</");
                textBuffer.append(qName);
                textBuffer.append(">");
            }

            // now lets set the parent tag variable
            if ( tagScriptStack.isEmpty() ) {
                tagScript = null;
            }
            else {
                tagScript = (TagScript) tagScriptStack.get(tagScriptStack.size() - 1);
            }
        } catch (Exception e) {
            log.error( "Caught exception: " + e, e );
            throw new SAXException( "Runtime Exception: " + e, e );
        }
    }

    /**
     * Process notification that a namespace prefix is coming in to scope.
     *
     * @param prefix Prefix that is being declared
     * @param namespaceURI Corresponding namespace URI being mapped to
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void startPrefixMapping(String prefix, String namespaceURI)
        throws SAXException {
        // Register this prefix mapping
        ArrayStack stack = (ArrayStack) namespaces.get(prefix);
        if (stack == null) {
            stack = new ArrayStack();
            namespaces.put(prefix, stack);
        }
        stack.push(namespaceURI);

        if ( elementNamespaces == null ) {
            elementNamespaces = new HashMap();
        }
        elementNamespaces.put(prefix, namespaceURI);
    }

    /**
     * Process notification that a namespace prefix is going out of scope.
     *
     * @param prefix Prefix that is going out of scope
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void endPrefixMapping(String prefix) throws SAXException {
        // Deregister this prefix mapping
        ArrayStack stack = (ArrayStack) namespaces.get(prefix);
        if (stack == null) {
            return;
        }
        try {
            stack.pop();
            if (stack.empty()) {
                namespaces.remove(prefix);
            }
        }
        catch (EmptyStackException e) {
            throw createSAXException("endPrefixMapping popped too many times");
        }
    }

    /**
     * Process notification of ignorable whitespace received from the body of
     * an XML element.
     *
     * @param buffer The characters from the XML document
     * @param start Starting offset into the buffer
     * @param len Number of characters from the buffer
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void ignorableWhitespace(char buffer[], int start, int len)
        throws SAXException {
        ; // No processing required
    }

    /**
     * Process notification of a processing instruction that was encountered.
     *
     * @param target The processing instruction target
     * @param data The processing instruction data (if any)
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void processingInstruction(String target, String data)
        throws SAXException {
        ; // No processing is required
    }

    /**
     * Set the document locator associated with our parser.
     *
     * @param locator The new locator
     */
    public void setDocumentLocator(Locator locator) {
        this.locator = locator;
    }

    /**
     * Process notification of a skipped entity.
     *
     * @param name Name of the skipped entity
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void skippedEntity(String name) throws SAXException {
        ; // No processing required
    }


    // DTDHandler interface
    //-------------------------------------------------------------------------

    /**
     * Receive notification of a notation declaration event.
     *
     * @param name The notation name
     * @param publicId The public identifier (if any)
     * @param systemId The system identifier (if any)
     */
    public void notationDecl(String name, String publicId, String systemId) {
    }

    /**
     * Receive notification of an unparsed entity declaration event.
     *
     * @param name The unparsed entity name
     * @param publicId The public identifier (if any)
     * @param systemId The system identifier (if any)
     * @param notation The name of the associated notation
     */
    public void unparsedEntityDecl(
        String name,
        String publicId,
        String systemId,
        String notation) {
    }


    // ErrorHandler interface
    //-------------------------------------------------------------------------

    /**
     * Forward notification of a parsing error to the application supplied
     * error handler, if any, otherwise throw a SAXException with the error.
     *
     * @param exception The error information
     *
     * @exception SAXException if a parsing exception occurs
     */
    public void error(SAXParseException exception) throws SAXException {
        log.error(
            "Parse Error at line "
                + exception.getLineNumber()
                + " column "
                + exception.getColumnNumber()
                + ": "
                + exception.getMessage(),
            exception);
        if (errorHandler != null) {
            errorHandler.error(exception);
        } else {
            throw exception;
        }
    }

    /**
     * Forward notification of a fatal parsing error to the application
     * supplied error handler, if any, otherwise throw a SAXException with the error.
     *
     * @param exception The fatal error information
     *
     * @exception SAXException if a parsing exception occurs
     */
    public void fatalError(SAXParseException exception) throws SAXException {
        log.error(
            "Parse Fatal Error at line "
                + exception.getLineNumber()
                + " column "
                + exception.getColumnNumber()
                + ": "
                + exception.getMessage(),
            exception);
        if (errorHandler != null) {
            errorHandler.fatalError(exception);
        } else {
            throw exception;
        }
    }

    /**
     * Forward notification of a parse warning to the application supplied
     * error handler (if any).  Unlike XMLParser.error(SAXParseException) and
     * XMLParser.fatalError(SAXParseException), this implementation will
     * NOT throw a SAXException by default if no error handler is supplied.
     *
     * @param exception The warning information
     *
     * @exception SAXException if a parsing exception occurs
     */
    public void warning(SAXParseException exception) throws SAXException {
        log.error(
            "Parse Warning at line "
                + exception.getLineNumber()
                + " column "
                + exception.getColumnNumber()
                + ": "
                + exception.getMessage(),
            exception);
        if (errorHandler != null) {
            errorHandler.warning(exception);
        }
    }

    // Implementation methods
    //-------------------------------------------------------------------------
    /**
     * If this object has not been configured then register the default
     * namespaces
     */
    private void ensureConfigured() {
        if (!configured) {
            configure();
            configured = true;
        }
    }

    /**
     * This method is called only once before parsing occurs
     * which allows tag libraries to be registered and so forth
     */
    protected void configure() {
        // load the properties file of libraries available
        Properties properties = getJellyProperties();
        for (Iterator iter = properties.entrySet().iterator(); iter.hasNext();) {
            Map.Entry entry = (Map.Entry) iter.next();
            String uri = (String) entry.getKey();
            String className = (String) entry.getValue();
            String libraryURI = "jelly:" + uri;

            // don't overload any Mock Tags already
            if ( ! context.isTagLibraryRegistered(libraryURI) ) {
                context.registerTagLibrary(libraryURI, className);
            }
        }
    }


    /**
     * A helper method which loads the static Jelly properties once on startup
     */
    protected synchronized Properties getJellyProperties() {
        if (jellyProperties == null) {
            jellyProperties = new Properties();

            InputStream in = null;
            URL url =
                getClassLoader().getResource("org/apache/commons/jelly/jelly.properties");
            if (url != null) {
                log.debug("Loading Jelly default tag libraries from: " + url);
                try {
                    in = url.openStream();
                    jellyProperties .load(in);
                }
                catch (IOException e) {
                    log.error("Could not load jelly properties from: " + url + ". Reason: " + e, e);
                }
                finally {
                    try {
                        in.close();
                    }
                    catch (Exception e) {
                        if (log.isDebugEnabled()) log.debug("error closing jelly.properties", e);
                    }
                }
            }
        }
        return jellyProperties;
    }

    /**
     * Factory method to create new Tag script for the given namespaceURI and name or
     * return null if this is not a custom Tag.
     */
    protected TagScript createTag(
        String namespaceURI,
        String localName,
        Attributes list)
        throws SAXException {
        try {
            // use the URI to load a taglib
            TagLibrary taglib = context.getTagLibrary(namespaceURI);
            if (taglib == null) {
                if (namespaceURI != null && namespaceURI.startsWith("jelly:")) {
                    String uri = namespaceURI.substring(6);
                    // try to find the class on the claspath
                    try {
                        Class taglibClass = getClassLoader().loadClass(uri);
                        taglib = (TagLibrary) taglibClass.newInstance();
                        context.registerTagLibrary(namespaceURI, taglib);
                    }
                    catch (ClassNotFoundException e) {
                        throw createSAXException("Could not load class: " + uri + " so taglib instantiation failed", e);
                    }
                    catch (IllegalAccessException e) {
                        throw createSAXException("Constructor for class is not accessible: " + uri + " so taglib instantiation failed",e);
                    }
                    catch (InstantiationException e) {
                        throw createSAXException("Class could not be instantiated: " + uri + " so taglib instantiation failed",e);
                    }
                    catch (ClassCastException e) {
                        throw createSAXException("Class is not a TagLibrary: " + uri + " so taglib instantiation failed",e);
                    }
                }
            }
            if (taglib != null) {
                TagScript script = taglib.createTagScript(localName, list);
                if ( script != null ) {
                    configureTagScript(script);

                    // clone the attributes to keep them around after this parse
                    script.setSaxAttributes(new AttributesImpl(list));

                    // now iterate through through the expressions
                    int size = list.getLength();
                    for (int i = 0; i < size; i++) {
                        String attributeName = list.getLocalName(i);
                        String attributeValue = list.getValue(i);
                        Expression expression =
                            taglib.createExpression(
                                getExpressionFactory(),
                                script,
                                attributeName,
                                attributeValue);
                        if (expression == null) {
                            expression = createConstantExpression(localName, attributeName, attributeValue);
                        }
                        script.addAttribute(attributeName, expression);
                    }
                }
                return script;
            }
            return null;
        }
        catch (Exception e) {
            log.warn(
                "Could not create taglib or URI: " + namespaceURI + " tag name: " + localName,
                e);
            throw createSAXException(e);
        }
    }


    /**
     * Factory method to create a static Tag that represents some static content.
     */
    protected TagScript createStaticTag(
        final String namespaceURI,
        final String localName,
        final String qName,
        Attributes list)
        throws SAXException {
        try {
            StaticTag tag = new StaticTag( namespaceURI, localName, qName );
            StaticTagScript script = new StaticTagScript(
                new TagFactory() {
                    public Tag createTag(String name, Attributes attributes) {
                        return new StaticTag( namespaceURI, localName, qName );
                    }
                }
            );
            configureTagScript(script);

            // now iterate through through the expressions
            int size = list.getLength();
            for (int i = 0; i < size; i++) {
                String attributeValue = list.getValue(i);
                Expression expression = CompositeExpression.parse(
                        attributeValue, getExpressionFactory()
                    );
                String attrQName = list.getQName(i);
                script.addAttribute(attrQName, expression);
            }
            return script;
        }
        catch (Exception e) {
            log.warn(
                "Could not create static tag for URI: "
                    + namespaceURI
                    + " tag name: "
                    + localName,
                e);
            throw createSAXException(e);
        }
    }


    /**
     * Configure a newly created TagScript instance before any Expressions are created
     *
     * @param aTagScript
     */
    protected void configureTagScript(TagScript aTagScript) {
        // set parent relationship...
        aTagScript.setParent(this.tagScript);

        // set the namespace Map
        if ( elementNamespaces != null ) {
            aTagScript.setTagNamespacesMap( elementNamespaces );
            elementNamespaces = null;
        }
    }

    /**
     * Adds the text to the current script block parsing any embedded
     * expressions inot ExpressionScript objects.
     */
    protected void addTextScript(String text) throws JellyException {
        Expression expression =
            CompositeExpression.parse(text, getExpressionFactory());

        addExpressionScript(script, expression);
    }


    /**
     * Adds the given Expression object to the current Script.
     */
    protected void addExpressionScript(ScriptBlock script, Expression expression) {
        if ( expression instanceof ConstantExpression ) {
            ConstantExpression constantExpression
                = (ConstantExpression) expression;
            Object value = constantExpression.getValue();
            if ( value != null ) {
                script.addScript(new TextScript( value.toString() ));
            }
        }
        else
        if ( expression instanceof CompositeExpression ) {
            CompositeTextScriptBlock newBlock = new CompositeTextScriptBlock();
            script.addScript(newBlock);

            CompositeExpression compositeExpression
                = (CompositeExpression) expression;
            Iterator iter = compositeExpression.getExpressions().iterator();
            while (iter.hasNext()) {
                addExpressionScript( newBlock, (Expression) iter.next() );
            }
        }
        else {
            script.addScript(new ExpressionScript(expression));
        }
    }

    protected Expression createConstantExpression(
        String tagName,
        String attributeName,
        String attributeValue)  {
        return new ConstantExpression(attributeValue);
    }

    protected ExpressionFactory createExpressionFactory() {
        return new JexlExpressionFactory();
    }

    /**
     * @return the current context URI as a String or null if there is no
     * current context defined on the JellyContext
     */
    protected String getCurrentURI() {
        URL url = this.getContext().getCurrentURL();
        return (url != null) ? url.toString() : null;
    }

    /**
     * Create a SAX exception which also understands about the location in
     * the file where the exception occurs
     *
     * @return the new exception
     */
    protected SAXException createSAXException(String message, Exception e) {
        log.warn("Underlying exception: " + e);
        e.printStackTrace();
        if (locator != null) {
            String error =
                "Error at ("
                    + locator.getLineNumber()
                    + ", "
                    + locator.getColumnNumber()
                    + "): "
                    + message;
            if (e != null) {
                return new SAXParseException(error, locator, e);
            }
            else {
                return new SAXParseException(error, locator);
            }
        }
        log.error("No Locator!");
        if (e != null) {
            return new SAXException(message, e);
        }
        else {
            return new SAXException(message);
        }
    }

    /**
     * Create a SAX exception which also understands about the location in
     * the digester file where the exception occurs
     *
     * @return the new exception
     */
    protected SAXException createSAXException(Exception e) {
        return createSAXException(e.getMessage(), e);
    }
    /**
     * Create a SAX exception which also understands about the location in
     * the digester file where the exception occurs
     *
     * @return the new exception
     */
    protected SAXException createSAXException(String message) {
        return createSAXException(message, null);
    }
}
