/*
 * $Id$
 *
 * @COPYRIGHT@
 */
package com.ncube.oda.web;

import java.io.*;
import java.net.*;
import java.util.*;

import com.ncube.oda.util.*;

import org.apache.avalon.excalibur.pool.*;
import org.apache.avalon.framework.activity.*;
import org.apache.avalon.framework.component.*;
import org.apache.avalon.framework.parameters.*;

import org.apache.cocoon.*;
import org.apache.cocoon.components.parser.*;
import org.apache.cocoon.components.parser.Parser;
import org.apache.cocoon.components.xpath.*;
import org.apache.cocoon.environment.*;
import org.apache.cocoon.transformation.AbstractTransformer;
import org.apache.cocoon.xml.*;
import org.apache.cocoon.xml.dom.*;

import org.w3c.dom.*;
import org.xml.sax.*;

/**
 * <para>
 * Adaptation from the default Cocoon XIncludeTransformer. Supports source handlers and
 * URI's.
 *
 * @author <link url="mailto:oda_eng@ncube.com">ODA Development Team</link>
 * @version $Revision$  
 */
public class XIncludeTransformer extends AbstractTransformer implements Composable, Recyclable, Disposable {

    private SourceResolver resolver;

    /** XPath Processor for processing xpointer() fragments */
    private XPathProcessor processor;

    protected ComponentManager manager;

    public static final String XMLBASE_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
    public static final String XMLBASE_ATTRIBUTE = "base";

    public static final String XINCLUDE_NAMESPACE_URI = "http://www.w3.org/2001/XInclude";
    public static final String XINCLUDE_INCLUDE_ELEMENT = "include";
    public static final String XINCLUDE_INCLUDE_ELEMENT_HREF_ATTRIBUTE = "href";
    public static final String XINCLUDE_INCLUDE_ELEMENT_PARSE_ATTRIBUTE = "parse";

    protected URI defaultBaseURI;

    /** The current XMLBase URI. We start with an empty "dummy" URL. **/
    protected URI currentBaseURI;

    /** This is a stack of xml:base attributes which belong to our ancestors **/
    protected Stack uriStack;

    /** namespace uri of the last element which had an xml:base attribute **/
    protected int currentDepth;
    protected Stack depthStack;

    public void setup(SourceResolver resolver, Map objectModel,
                      String source, Parameters parameters)
    throws ProcessingException, SAXException, IOException {
        this.resolver = resolver;
        currentDepth = 0;
        defaultBaseURI = null;
        currentBaseURI = null;
        depthStack = new Stack();
        uriStack = new Stack();
    }

    public void compose(ComponentManager manager) throws ComponentException {
        this.manager = manager;
        try {
            this.processor = (XPathProcessor)this.manager.lookup(XPathProcessor.ROLE);
        } catch (ComponentException e) {
            getLogger().error("cannot obtain XPathProcessor", e);
            throw e;
        }
    }

    public void startElement(String uri, String name, String raw, Attributes attr) throws SAXException {
        String value;
        if ((value = attr.getValue(XMLBASE_NAMESPACE_URI, XMLBASE_ATTRIBUTE)) != null) {
            try {
                startXMLBaseAttribute(uri, name, value);
            } catch (ProcessingException e) {
                getLogger().debug("XincludeTransformer", e);
                throw new SAXException(e);
            }
        } else {
            ++currentDepth;
        }

        if (XINCLUDE_NAMESPACE_URI.equals(uri) && XINCLUDE_INCLUDE_ELEMENT.equals(name)) {
            String href = attr.getValue("",XINCLUDE_INCLUDE_ELEMENT_HREF_ATTRIBUTE);
            String parse = attr.getValue("",XINCLUDE_INCLUDE_ELEMENT_PARSE_ATTRIBUTE);

            if (null == parse) {
                parse = "xml";
            }

            try {
                processXIncludeElement(href, parse);
            } catch (ProcessingException e) {
                getLogger().debug("XincludeTransformer", e);
                throw new SAXException(e);
            } catch (IOException e) {
                getLogger().debug("XincludeTransformer", e);
                throw new SAXException(e);
            }
            return;
        }
        super.startElement(uri, name, raw, attr);
    }

    public void endElement(String uri, String name, String raw) throws SAXException {
        if (--currentDepth < 0) {
            endXMLBaseAttribute();
        }

        // don't include xinclude:include element in SAX stream
        //
        if (XINCLUDE_NAMESPACE_URI.equals(uri) && XINCLUDE_INCLUDE_ELEMENT.equals(name)) {
            return;
        }
        super.endElement(uri, name, raw);
    }

    public void setDocumentLocator(Locator locator) {
        try {
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("XIncludeTransformer: setDocumentLocator called " + locator.getSystemId());
            }
            defaultBaseURI = new URI(locator.getSystemId());
        } catch (Exception e) {
            getLogger().debug("XincludeTransformer", e);
        }
        super.setDocumentLocator(locator);
    }

    protected void startXMLBaseAttribute(String uri, String name, String value) throws ProcessingException {
        String urlLoc = value;
        if (getLogger().isDebugEnabled()) {
            getLogger().debug("XIncludeTransformer: XMLBase = " + urlLoc);
        }

        URI oldURI = currentBaseURI;
        try {
            currentBaseURI = new URI(urlLoc);
        } catch (Exception e) {
            throw new ProcessingException("Could not resolve '" + urlLoc + "'", e);
        }

        // make sure we modify the stack only after we have a valid URI
        //
        uriStack.push(oldURI != null? oldURI : defaultBaseURI);
        depthStack.push(new Integer(currentDepth));
        currentDepth = 0;
    }

    protected void endXMLBaseAttribute() {
        if (getLogger().isDebugEnabled()) {
            getLogger().debug("XIncludeTransformer: XMLBase ended");
        }

        if (uriStack.size() > 0) {
            currentBaseURI = (URI)uriStack.pop();
            currentDepth = ((Integer)depthStack.pop()).intValue();
        } else {
            currentBaseURI = defaultBaseURI;
            currentDepth = 0;
        }
    }

    protected void processXIncludeElement(String href, String parse) throws SAXException, ProcessingException,IOException {
        URI baseURI = currentBaseURI != null? currentBaseURI : defaultBaseURI;
        if (getLogger().isDebugEnabled()) {
            getLogger().debug("Processing XInclude element: href=" + href + ", parse=" + parse);
            getLogger().debug("Base URI: " + baseURI);
        }

        URI uri = new URI(baseURI, href);
        if (getLogger().isDebugEnabled()) {
            getLogger().debug("URL: " + uri);
        }

        Source source = this.resolver.resolve(uri.toString());
        try {
            if (parse.equals("text")) {
                getLogger().debug("Parse type is text");
                processTextInclude(source);
            } else if (parse.equals("xml")) {
                getLogger().debug("Parse type is XML");
                processXMLInclude(source, uri.getFragment());
            }
        } finally {
            source.recycle();
        }
    }

    private void processTextInclude(Source source) throws SAXException, ProcessingException, IOException {
        InputStream input = source.getInputStream();
        try {
            Reader reader = new BufferedReader(new InputStreamReader(input));
            int read;
            char ary[] = new char[1024];
            while ((read = reader.read(ary)) != -1) {
                super.characters(ary, 0, read);
            }
        } finally {
            try {
                input.close();
            } catch (IOException ignore) {
            }
        }
    }

    private void processXMLInclude(Source source, String fragment) throws SAXException, ProcessingException, IOException {
        if (fragment == null || fragment.length() == 0) {
            IncludeXMLConsumer consumer = new IncludeXMLConsumer(this);
            source.toSAX(consumer);
            return;
        }

        Parser parser = null;
        try {
            getLogger().debug("Looking up " + Parser.ROLE);
            parser = (Parser)manager.lookup(Parser.ROLE);

            String xpath;
            if (fragment.startsWith("xpointer(") && fragment.endsWith(")")) {
                xpath = fragment.substring(9, fragment.length()-1);
            } else {
                // convert a bare xpointer into an id() xpath form
                //
                xpath = "id('" + fragment + "')";
            }

            getLogger().debug("XPath is " + xpath);
            Document document = parser.parseDocument(source.getInputSource());
            NodeList list = processor.selectNodeList(document,xpath);
            DOMStreamer streamer = new DOMStreamer(super.contentHandler, super.lexicalHandler);
            int length = list.getLength();
            for (int i=0; i<length; i++) {
                streamer.stream(list.item(i));
            }
        } catch (SAXException e) {
            getLogger().error("Error in processXIncludeElement", e);
            throw e;
        } catch (IOException e) {
            getLogger().error("Error in processXIncludeElement", e);
            throw e;
        } catch (ComponentException e) {
            getLogger().error("Error in processXIncludeElement", e);
            throw new SAXException(e);
        } finally {
            if (parser != null) {
                this.manager.release(parser);
            }
        }
    }

    public void recycle() {
        // Reset all variables to initial state.
        this.resolver = null;
        defaultBaseURI = null;
        currentBaseURI = null;
        uriStack = null;
        depthStack = null;
        super.recycle();
    }

    public void dispose() {
        this.manager.release((Component)this.processor);
    }
}
