package org.drools.xml;

/*
 * Copyright 2005 JBoss Inc
 * 
 * Licensed 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.
 */

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

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

import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * <code>RuleSet</code> loader.
 *
 * Note you can override the default entity resolver by setting the System property of:
 *  <code>org.drools.io.EntityResolve</code> to your own custom entity resolver.
 *  This can be done using -Dorg.drools.io.EntityResolver=YourClassHere on the command line, for instance.
 *
 * @author <a href="mailto:bob@werken.com">bob mcwhirter </a>
 */
public class ExtensibleXmlParser extends DefaultHandler {
    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------
    public static final String  ENTITY_RESOLVER_PROPERTY_NAME = "org.drools.io.EntityResolver";

    /** Namespace URI for the general tags. */
    public static final String  RULES_NAMESPACE_URI           = "http://drools.org/rules";

    private static final String JAXP_SCHEMA_LANGUAGE          = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";

    private static final String W3C_XML_SCHEMA                = "http://www.w3.org/2001/XMLSchema";

    // ----------------------------------------------------------------------
    // Instance members
    // ----------------------------------------------------------------------
    /** SAX parser. */
    private SAXParser           parser;

    /** isValidating */
    private boolean             isValidating                  = true;

    /** Locator for errors. */
    private Locator             locator;

    // private Map repo;

    /** Stack of configurations. */
    private LinkedList          configurationStack;

    /** Current configuration text. */
    private StringBuilder       characters;

    private SemanticModules     modules;

    private boolean             lastWasEndElement;

    private LinkedList          parents;

    private Object              peer;

    private Object              current;

    private Object              data;

    private final MessageFormat message                       = new MessageFormat( "({0}: {1}, {2}): {3}" );

    private final Map           namespaces                    = new HashMap();

    private EntityResolver      entityResolver;
    
    private Document            document;
    private DocumentFragment    docFragment;
    
    private ClassLoader         classLoader;

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    /**
     * Construct.
     *
     * <p>
     * Uses the default JAXP SAX parser and the default classpath-based
     * <code>DefaultSemanticModule</code>.
     * </p>
     */
    public ExtensibleXmlParser() {
        // init
        this.configurationStack = new LinkedList();
        this.parents = new LinkedList();

        initEntityResolver();
    }

    public void setSemanticModules(SemanticModules modules) {
        this.modules = modules;
    }

    /**
     * Construct.
     *
     * <p>
     * Uses the default classpath-based <code>DefaultSemanticModule</code>.
     * </p>
     *
     * @param parser
     *            The SAX parser.
     */
    public ExtensibleXmlParser(final SAXParser parser) {
        this();
        this.parser = parser;
    }

    // ----------------------------------------------------------------------
    // Instance methods
    // ----------------------------------------------------------------------

    /**
     * Read a <code>RuleSet</code> from a <code>Reader</code>.
     *
     * @param reader
     *            The reader containing the rule-set.
     *
     * @return The rule-set.
     * @throws ParserConfigurationException 
     */
    public Object read(final Reader reader) throws SAXException,
                                           IOException {
        return read( new InputSource( reader ) );
    }

    /**
     * Read a <code>RuleSet</code> from an <code>InputStream</code>.
     *
     * @param inputStream
     *            The input-stream containing the rule-set.
     *
     * @return The rule-set.
     * @throws ParserConfigurationException 
     */
    public Object read(final InputStream inputStream) throws SAXException,
                                                     IOException {
        return read( new InputSource( inputStream ) );
    }

    /**
     * Read a <code>RuleSet</code> from an <code>InputSource</code>.
     *
     * @param in
     *            The rule-set input-source.
     *
     * @return The rule-set.
     * @throws ParserConfigurationException 
     */
    public Object read(final InputSource in) throws SAXException,
                                            IOException {
        if ( this.docFragment == null ) {
            try {
                this.document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            } catch ( ParserConfigurationException e ) {
                throw new RuntimeException( "Unable to create new DOM Document" );
            }
            this.docFragment = this.document.createDocumentFragment();
        }
        
        SAXParser localParser = null;
        if ( this.parser == null ) {
            final SAXParserFactory factory = SAXParserFactory.newInstance();
            factory.setNamespaceAware( true );

            final String isValidatingString = System.getProperty( "drools.schema.validating" );
            if ( System.getProperty( "drools.schema.validating" ) != null ) {
                this.isValidating = Boolean.getBoolean( "drools.schema.validating" );
            }

            if ( this.isValidating == true ) {
                factory.setValidating( true );
                try {
                    localParser = factory.newSAXParser();
                } catch ( final ParserConfigurationException e ) {
                    throw new RuntimeException( e.getMessage() );
                }

                try {
                    localParser.setProperty( ExtensibleXmlParser.JAXP_SCHEMA_LANGUAGE,
                                             ExtensibleXmlParser.W3C_XML_SCHEMA );
                } catch ( final SAXNotRecognizedException e ) {
                    boolean hideWarnings = Boolean.getBoolean( "drools.schema.hidewarnings" );
                    if ( !hideWarnings ) {
                        System.err.println( "Your SAX parser is not JAXP 1.2 compliant - turning off validation." );
                    }
                    localParser = null;
                }
            }

            if ( localParser == null ) {
                // not jaxp1.2 compliant so turn off validation
                try {
                    this.isValidating = false;
                    factory.setValidating( this.isValidating );
                    localParser = factory.newSAXParser();
                } catch ( final ParserConfigurationException e ) {
                    throw new RuntimeException( e.getMessage() );
                }
            }
        } else {
            localParser = this.parser;
        }

        if ( !localParser.isNamespaceAware() ) {
            throw new RuntimeException( "parser must be namespace-aware" );
        }

        localParser.parse( in,
                           this );

        return this.data;
    }

    public void setData(final Object data) {
        this.data = data;
    }

    public Object getData() {
        return this.data;
    }       

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * @see org.xml.sax.ContentHandler
     */
    public void setDocumentLocator(final Locator locator) {
        this.locator = locator;
    }

    /**
     * Get the <code>Locator</code>.
     *
     * @return The locator.
     */
    public Locator getLocator() {
        return this.locator;
    }

    public void startDocument() {
        this.isValidating = true;
        this.current = null;
        this.peer = null;
        this.lastWasEndElement = false;
        this.parents.clear();
        this.characters = null;
        this.configurationStack.clear();
        this.namespaces.clear();
    }

    private int direction = 0;

    /**
     * @param uri
     * @param localName
     * @param qname
     * @param attrs
     * @throws SAXException
     * @see org.xml.sax.ContentHandler
     *
     * @todo: better way to manage unhandled elements
     */
    public void startElement(final String uri,
                             final String localName,
                             final String qname,
                             final Attributes attrs) throws SAXException {
        if ( direction == 1 ) {
            // going down again, so clear 
            this.peer = null;
        } else {
            direction = 1;
        }

        final Handler handler = getHandler( uri,
                                            localName );

        if ( handler == null ) {
            startElementBuilder( localName,
                                attrs );
            return;
        }

        validate( uri,
                  localName,
                  handler );

        final Object node = handler.start( uri,
                                           localName,
                                           attrs,
                                           this );

        if ( node == null ) {
            this.parents.add( Null.instance );
        } else {
            this.parents.add( node );
        }
    }

    /**
     * @param uri
     * @param localName
     * @param qname
     * @throws SAXException
     * @see org.xml.sax.ContentHandler
     */

    public void endElement(final String uri,
                           final String localName,
                           final String qname) throws SAXException {
        direction = -1;
        final Handler handler = getHandler( uri,
                                            localName );

        if ( handler == null ) {
            if ( this.configurationStack.size() >= 1 ) {
                endElementBuilder();
            }
            return;
        }

        this.current = removeParent();

        this.peer = handler.end( uri,
                                 localName,
                                 this );
    }

    public static class Null {
        public static final Null instance = new Null();
    }

    private void validate(final String uri,
                          final String localName,
                          final Handler handler) throws SAXParseException {
        boolean validParent = false;
        boolean validPeer = false;
        boolean invalidNesting = false;

        final Set validParents = handler.getValidParents();
        final Set validPeers = handler.getValidPeers();
        boolean allowNesting = handler.allowNesting();

        // get parent
        Object parent;
        if ( this.parents.size() != 0 ) {
            parent = this.parents.getLast();
        } else {
            parent = null;
        }

        // check valid parents
        // null parent means localname is rule-set
        // dont process if elements are the same
        // instead check for allowed nesting
        final Class nodeClass = getHandler( uri,
                                            localName ).generateNodeFor();
        if ( nodeClass != null && !nodeClass.isInstance( parent ) ) {
            Object allowedParent;
            final Iterator it = validParents.iterator();
            while ( !validParent && it.hasNext() ) {
                allowedParent = it.next();
                if ( parent == null && allowedParent == null ) {
                    validParent = true;
                } else if ( allowedParent != null && ((Class) allowedParent).isInstance( parent ) ) {
                    validParent = true;
                }
            }
            if ( !validParent ) {
                throw new SAXParseException( "<" + localName + "> has an invalid parent element [" + parent + "]",
                                             getLocator() );
            }
        }

        // check valid peers
        // null peer means localname is rule-set
        final Object peer = this.peer;

        Object allowedPeer;
        Iterator it = validPeers.iterator();
        while ( !validPeer && it.hasNext() ) {
            allowedPeer = it.next();
            if ( peer == null && allowedPeer == null ) {
                validPeer = true;
            } else if ( allowedPeer != null && ((Class) allowedPeer).isInstance( peer ) ) {
                validPeer = true;
            }
        }
        if ( !validPeer ) {
            throw new SAXParseException( "<" + localName + "> is after an invalid element: " + Handler.class.getName(),
                                         getLocator() );
        }

        if ( nodeClass != null && !allowNesting ) {
            it = this.parents.iterator();
            while ( !invalidNesting && it.hasNext() ) {
                if ( nodeClass.isInstance( it.next() ) ) {
                    invalidNesting = true;
                }
            }
        }
        if ( invalidNesting ) {
            throw new SAXParseException( "<" + localName + ">  may not be nested",
                                         getLocator() );
        }

    }

    /**
     * Start a configuration node.
     *
     * @param name
     *            Tag name.
     * @param attrs
     *            Tag attributes.
     */
    public void startElementBuilder(final String tagName,
                                   final Attributes attrs) {        
        this.characters = new StringBuilder();
                

        final Element element = this.document.createElement( tagName );
        
        //final DefaultConfiguration config = new DefaultConfiguration( tagName );        

        final int numAttrs = attrs.getLength();

        for ( int i = 0; i < numAttrs; ++i ) {
            element.setAttribute( attrs.getLocalName( i ),
                                 attrs.getValue( i ) );
        }

//        // lets add the namespaces as attributes
//        for ( final Iterator iter = this.namespaces.entrySet().iterator(); iter.hasNext(); ) {
//            final Map.Entry entry = (Map.Entry) iter.next();
//            String ns = (String) entry.getKey();
//            final String value = (String) entry.getValue();
//            if ( ns == null || ns.length() == 0 ) {
//                ns = "xmlns";
//            } else {
//                ns = "xmlns:" + ns;
//            }
//            config.setAttribute( ns,
//                                 value );
//        }

        if ( this.configurationStack.isEmpty() ) {
            this.configurationStack.addLast( element );
        } else {
            ((Element) this.configurationStack.getLast()).appendChild( element );
            this.configurationStack.addLast( element );
        }
    }

    Handler getHandler(final String uri,
                       final String localName) {
        SemanticModule module = this.modules.getSemanticModule( uri );
        if ( module != null ) {
            return module.getHandler( localName );
        } else {
            return null;
        }
    }

    /**
     * @param chars
     * @param start
     * @param len
     * @see org.xml.sax.ContentHandler
     */
    public void characters(final char[] chars,
                           final int start,
                           final int len) {
        if ( this.characters != null ) {
            this.characters.append( chars,
                                    start,
                                    len );
        }
    }

    /**
     * End a configuration node.
     *
     * @return The configuration.
     */
    public Element endElementBuilder() {
        final Element element = (Element) this.configurationStack.removeLast();
        if ( this.characters != null ) {
            element.appendChild( this.document.createTextNode( this.characters.toString() ) );
        }

        this.characters = null;

        return element;
    }

    public Object getParent() {
        return this.parents.getLast();
    }

    public Object getParent(int index) {
        ListIterator it = this.parents.listIterator( this.parents.size() );
        int x = 0;
        Object parent = null;
        while ( x++ <= index ) {
            parent = it.previous();
        }
        return parent;
    }

    public Object removeParent() {
        Object parent = this.parents.removeLast();
        while ( parent == null && !this.parents.isEmpty() ) {
            parent = this.parents.removeLast();
        }
        return parent;
    }

    public LinkedList getParents() {
        return this.parents;
    }

    public Object getParent(final Class parent) {
        final ListIterator it = this.parents.listIterator( this.parents.size() );
        Object node = null;
        while ( it.hasPrevious() ) {
            node = it.previous();
            if ( parent.isInstance( node ) ) {
                break;
            }
        }
        return node;
    }

    public Object getPeer() {
        return this.peer;
    }

    public Object getCurrent() {
        return this.current;
    }

    public InputSource resolveEntity(final String publicId,
                                     final String systemId) throws SAXException {
        try {
            final InputSource inputSource = resolveSchema( publicId,
                                                           systemId );
            if ( inputSource != null ) {
                return inputSource;
            }
            if ( this.entityResolver != null ) {
                return this.entityResolver.resolveEntity( publicId,
                                                          systemId );
            }
        } catch ( final IOException ioe ) {
        }
        return null;
    }

    public void startPrefixMapping(final String prefix,
                                   final String uri) throws SAXException {
        super.startPrefixMapping( prefix,
                                  uri );
        this.namespaces.put( prefix,
                             uri );
    }

    public void endPrefixMapping(final String prefix) throws SAXException {
        super.endPrefixMapping( prefix );
        this.namespaces.remove( prefix );
    }

    private void print(final SAXParseException x) {
        final String msg = this.message.format( new Object[]{x.getSystemId(), new Integer( x.getLineNumber() ), new Integer( x.getColumnNumber() ), x.getMessage()} );
        System.out.println( msg );
    }

    public void warning(final SAXParseException x) {
        print( x );
    }

    public void error(final SAXParseException x) {
        print( x );
    }

    public void fatalError(final SAXParseException x) throws SAXParseException {
        print( x );
        throw x;
    }

    private InputSource resolveSchema(final String publicId,
                                      final String systemId) throws SAXException,
                                                            IOException {
        // Schema files must end with xsd
        if ( !systemId.toLowerCase().endsWith( "xsd" ) ) {
            return null;
        }

        // try the actual location given by systemId
        try {
            final URL url = new URL( systemId );
            return new InputSource( url.openStream() );
        } catch ( final Exception e ) {
        }

        // Try and get the index for the filename, else return null
        String xsd;
        int index = systemId.lastIndexOf( "/" );
        if ( index == -1 ) {
            index = systemId.lastIndexOf( "\\" );
        }
        if ( index != -1 ) {
            xsd = systemId.substring( index + 1 );
        } else {
            xsd = systemId;
        }

        ClassLoader cl = Thread.currentThread().getContextClassLoader();

        if ( cl == null ) {
            cl = ExtensibleXmlParser.class.getClassLoader();
        }

        // Try looking in META-INF
        {
            final InputStream is = cl.getResourceAsStream( "META-INF/" + xsd );
            if ( is != null ) {
                return new InputSource( is );
            }
        }

        // Try looking in /META-INF
        {
            final InputStream is = cl.getResourceAsStream( "/META-INF/" + xsd );
            if ( is != null ) {
                return new InputSource( is );
            }
        }

        // Try looking at root of classpath
        {
            final InputStream is = cl.getResourceAsStream( "/" + xsd );
            if ( is != null ) {
                return new InputSource( is );
            }
        }

        // Try current working directory
        {
            final File file = new File( xsd );
            if ( file.exists() ) {
                return new InputSource( new BufferedInputStream( new FileInputStream( file ) ) );
            }
        }

        cl = ClassLoader.getSystemClassLoader();

        // Try looking in META-INF
        {
            final InputStream is = cl.getResourceAsStream( "META-INF/" + xsd );
            if ( is != null ) {
                return new InputSource( is );
            }
        }

        // Try looking in /META-INF
        {
            final InputStream is = cl.getResourceAsStream( "/META-INF/" + xsd );
            if ( is != null ) {
                return new InputSource( is );
            }
        }

        // Try looking at root of classpath
        {
            final InputStream is = cl.getResourceAsStream( "/" + xsd );
            if ( is != null ) {
                return new InputSource( is );
            }
        }

        return null;
    }

    /**
     * Intializes EntityResolver that is configured via system property ENTITY_RESOLVER_PROPERTY_NAME.
     */
    private void initEntityResolver() {
        final String entityResolveClazzName = System.getProperty( ExtensibleXmlParser.ENTITY_RESOLVER_PROPERTY_NAME );
        if ( entityResolveClazzName != null && entityResolveClazzName.length() > 0 ) {
            try {
                final Class entityResolverClazz = Thread.currentThread().getContextClassLoader().loadClass( entityResolveClazzName );
                this.entityResolver = (EntityResolver) entityResolverClazz.newInstance();
            } catch ( final Exception ignoreIt ) {
            }
        }
    }

}