/*
*  eXist Open Source Native XML Database
*  Copyright (C) 2001-04 Wolfgang M. Meier (wolfgang@exist-db.org) 
*  and others (see http://exist-db.org)
*
*  This program is free software; you can redistribute it and/or
*  modify it under the terms of the GNU Lesser General Public License
*  as published by the Free Software Foundation; either version 2
*  of the License, or (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU Lesser General Public License for more details.
*
*  You should have received a copy of the GNU Lesser General Public License
*  along with this program; if not, write to the Free Software
*  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* 
*  $Id: XIncludeFilter.java 4773 2006-11-02 14:10:07Z wolfgang_m $
*/
package org.exist.storage.serializers;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URI;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.log4j.Logger;
import org.exist.dom.BinaryDocument;
import org.exist.dom.DocumentImpl;
import org.exist.dom.QName;
import org.exist.dom.XMLUtil;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.security.xacml.AccessContext;
import org.exist.source.DBSource;
import org.exist.source.Source;
import org.exist.source.StringSource;
import org.exist.storage.XQueryPool;
import org.exist.util.serializer.AttrList;
import org.exist.util.serializer.Receiver;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.CompiledXQuery;
import org.exist.xquery.Constants;
import org.exist.xquery.Expression;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQuery;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.NodeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.Type;
import org.xml.sax.SAXException;

/**
 * A filter that listens for XInclude elements in the stream
 * of events generated by the {@link org.exist.storage.serializers.Serializer}.
 * 
 * XInclude elements are expanded at the position where they were found.
 */
public class XIncludeFilter implements Receiver {

	private final static Logger LOG = Logger.getLogger(XIncludeFilter.class);

	public final static String XINCLUDE_NS = "http://www.w3.org/2001/XInclude";
	
	private final static QName HREF_ATTRIB = new QName("href", "");
	
	private Receiver receiver;
	private Serializer serializer;
	private DocumentImpl document = null;
	private HashMap namespaces = new HashMap(10);

	public XIncludeFilter(Serializer serializer, Receiver receiver) {
		this.receiver = receiver;
		this.serializer = serializer;
	}

	public XIncludeFilter(Serializer serializer) {
		this(serializer, null);
	}

	public void setReceiver(Receiver handler) {
		this.receiver = handler;
	}

	public Receiver getReceiver() {
		return receiver;
	}

	public void setDocument(DocumentImpl doc) {
		document = doc;
	}

	/* (non-Javadoc)
	 * @see org.exist.util.serializer.Receiver#characters(java.lang.CharSequence)
	 */
	public void characters(CharSequence seq) throws SAXException {
		receiver.characters(seq);
	}
	
	/* (non-Javadoc)
	 * @see org.exist.util.serializer.Receiver#comment(char[], int, int)
	 */
	public void comment(char[] ch, int start, int length) throws SAXException {
		receiver.comment(ch, start, length);
	}
	
	/**
	 * @see org.xml.sax.ContentHandler#endDocument()
	 */
	public void endDocument() throws SAXException {
		receiver.endDocument();
	}

	/**
	 * @see org.exist.util.serializer.Receiver#endElement(org.exist.dom.QName)
	 */
	public void endElement(QName qname) throws SAXException {
		if( !XINCLUDE_NS.equals( qname.getNamespaceURI() ) )
			receiver.endElement(qname);
	}
	
	public void endPrefixMapping(String prefix) throws SAXException {
		namespaces.remove(prefix);
		receiver.endPrefixMapping(prefix);
	}

	/**
	 * @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
	 */
	public void processingInstruction(String target, String data) throws SAXException {
		receiver.processingInstruction(target, data);
	}

    /**
     * @see org.exist.util.serializer.Receiver#cdataSection(char[], int, int)
     */
    public void cdataSection(char[] ch, int start, int len) throws SAXException {
        receiver.cdataSection(ch, start, len);
    }
    
	/**
	 * @see org.xml.sax.ContentHandler#startDocument()
	 */
	public void startDocument() throws SAXException {
		receiver.startDocument();
	}

	/* (non-Javadoc)
	 * @see org.exist.util.serializer.Receiver#attribute(org.exist.dom.QName, java.lang.String)
	 */
	public void attribute(QName qname, String value) throws SAXException {
		receiver.attribute(qname, value);
	}
	
	/* (non-Javadoc)
	 * @see org.exist.util.serializer.Receiver#startElement(org.exist.dom.QName, org.exist.util.serializer.AttrList)
	 */
	public void startElement(QName qname, AttrList attribs) throws SAXException {
		if (qname.getNamespaceURI() != null && qname.getNamespaceURI().equals(XINCLUDE_NS)) {
			if (qname.getLocalName().equals("include")) {
				LOG.debug("processing include ...");
				processXInclude(attribs.getValue(HREF_ATTRIB));
			}
		} else {
			//LOG.debug("start: " + qName);
			receiver.startElement(qname, attribs);
		}
	}
	
	public void documentType(String name, String publicId, String systemId) 
	throws SAXException {
		receiver.documentType(name, publicId, systemId);
	}

    protected void processXInclude(String href) throws SAXException {
        if(href == null)
            throw new SAXException("No href attribute found in XInclude include element");
        // save some settings
        DocumentImpl prevDoc = document;
        boolean createContainerElements = serializer.createContainerElements;
        serializer.createContainerElements = false;

        //The following comments are the basis for possible external documents
        XmldbURI docUri = null;
        //URI externalUri = null;
        try {
            docUri = XmldbURI.xmldbUriFor(href);
            /*
               if(!stylesheetUri.toCollectionPathURI().equals(stylesheetUri)) {
                   externalUri = stylesheetUri.getXmldbURI();
               }
               */
        } catch (URISyntaxException e) {
            //could be an external URI!
            /*
               try {
                   externalUri = new URI(href);
               } catch (URISyntaxException ee) {
               */
            throw new IllegalArgumentException("Stylesheet URI could not be parsed: "+e.getMessage());
            //}
        }

        // parse the href attribute
        LOG.debug("found href=\"" + href + "\"");
        //String xpointer = null;
        //String docName = href;
        String xpointer = docUri.getFragment();
        if(xpointer!=null) {
            try {
                xpointer = XMLUtil.decodeAttrMarkup(URLDecoder.decode(xpointer, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
            }
            // remove the fragment part from the URI for further processing
            URI u = docUri.getURI();
            try {
                u = new URI(u.getScheme(), u.getUserInfo(), u.getHost(), u.getPort(), u.getPath(), u.getQuery(), null);
                docUri = XmldbURI.xmldbUriFor(u);
            } catch (URISyntaxException e) {
                throw new IllegalArgumentException("Stylesheet URI could not be parsed: " + e.getMessage());
            }
        }

        // extract possible parameters in the URI
        Map params = null;
        String paramStr = docUri.getQuery();
        if (paramStr != null) {
            params = processParameters(paramStr);
        }

        // if docName has no collection specified, assume
        // current collection

        // Patch 1520454 start
        if (!docUri.isAbsolute()) {
            String base = document.getCollection().getURI() + "/";
            String child = "./" + docUri.toString();

            URI baseUri = URI.create(base);
            URI childUri = URI.create(child);

            URI uri = baseUri.resolve(childUri);
            docUri = XmldbURI.create(uri);
        }
        // Patch 1520454 end

        // retrieve the document
        DocumentImpl doc = null;
        try {
            doc = (DocumentImpl) serializer.broker.getXMLResource(docUri);
            if(doc != null && !doc.getPermissions().validate(serializer.broker.getUser(), Permission.READ))
                throw new PermissionDeniedException("Permission denied to read xincluded resource");
        } catch (PermissionDeniedException e) {
            LOG.warn("permission denied", e);
            throw new SAXException(e);
        }
        /* if document has not been found and xpointer is
               * null, throw an exception. If xpointer != null
               * we retry below and interpret docName as
               * a collection.
               */
        if (doc == null && xpointer == null)
            throw new SAXException("document " + docUri + " not found");

        /* Check if the document is a stored XQuery */
        boolean xqueryDoc = false;
        if (doc != null && doc.getResourceType() == DocumentImpl.BINARY_FILE) {
            xqueryDoc = "application/xquery".equals(doc.getMetadata().getMimeType());
        }

        if (xpointer == null && !xqueryDoc)
            // no xpointer found - just serialize the doc
            serializer.serializeToReceiver(doc, false);
        else {
            // process the xpointer or the stored XQuery
            try {
                Source source;
                if (xpointer == null)
                    source = new DBSource(serializer.broker, (BinaryDocument) doc, true);
                else {
                    xpointer = checkNamespaces(xpointer);
                    source = new StringSource(xpointer);
                }
                XQuery xquery = serializer.broker.getXQueryService();
                XQueryPool pool = xquery.getXQueryPool();
                XQueryContext context;
                CompiledXQuery compiled = pool.borrowCompiledXQuery(serializer.broker, source);
                if (compiled != null)
                    context = compiled.getContext();
                else
                    context = xquery.newContext(AccessContext.XINCLUDE);
                context.declareNamespaces(namespaces);
                context.declareNamespace("xinclude", XINCLUDE_NS);
                //TODO: change these to putting the XmldbURI in, but we need to warn users!
                context.declareVariable("xinclude:current-doc", document.getFileURI().toString());
                context.declareVariable("xinclude:current-collection", document.getCollection().getURI().toString());
                if (xpointer != null) {
                    if(doc != null)
                        context.setStaticallyKnownDocuments(new XmldbURI[] { doc.getURI() } );
                    else
                        context.setStaticallyKnownDocuments(new XmldbURI[] { docUri });
                }

                // pass parameters as variables
                if (params != null) {
                    for (Iterator i = params.entrySet().iterator(); i.hasNext(); ) {
                        Map.Entry entry = (Map.Entry) i.next();
                        context.declareVariable(entry.getKey().toString(), entry.getValue());
                    }
                }

                if(compiled == null) {
                    try {
                        compiled = xquery.compile(context, source, xpointer != null);
                    } catch (IOException e) {
                        throw new SAXException("I/O error while reading query for xinclude: " + e.getMessage(), e);
                    }
                }
                LOG.info("xpointer query: " + ExpressionDumper.dump((Expression) compiled));
                Sequence seq = xquery.execute(compiled, null);

                if(Type.subTypeOf(seq.getItemType(), Type.NODE)) {
                    if (LOG.isDebugEnabled())
                        LOG.debug("xpointer found: " + seq.getLength());

                    NodeValue node;
                    for (SequenceIterator i = seq.iterate(); i.hasNext();) {
                        node = (NodeValue) i.nextItem();
                        serializer.serializeToReceiver(node, false);
                    }
                } else {
                    String val;
                    for (int i = 0; i < seq.getLength(); i++) {
                        val = seq.itemAt(i).getStringValue();
                        characters(val);
                    }
                }

            } catch (XPathException e) {
                LOG.warn("xpointer error", e);
                throw new SAXException("Error while processing XInclude expression: " + e.getMessage(), e);
            }
        }
        // restore settings
        document = prevDoc;
        serializer.createContainerElements = createContainerElements;
    }

	/**
	 * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
	 */
	public void startPrefixMapping(String prefix, String uri) throws SAXException {
		namespaces.put(prefix, uri);
		receiver.startPrefixMapping(prefix, uri);
	}

	/**
	 * Process xmlns() schema. We process these here, because namespace mappings should
	 * already been known when parsing the xpointer() expression.
	 * 
	 * @param xpointer
	 * @return
	 * @throws XPathException
	 */
	private String checkNamespaces(String xpointer) throws XPathException {
		int p0;
		while((p0 = xpointer.indexOf("xmlns(")) != Constants.STRING_NOT_FOUND) {
			if(p0 < 0)
				return xpointer;
			int p1 = xpointer.indexOf(')', p0 + 6);
			if(p1 < 0)
				throw new XPathException("expected ) for xmlns()");
			String mapping = xpointer.substring(p0 + 6, p1);
			xpointer = xpointer.substring(0, p0) + xpointer.substring(p1 + 1);
			StringTokenizer tok = new StringTokenizer(mapping, "= \t\n");
			if(tok.countTokens() < 2)
				throw new XPathException("expected prefix=namespace mapping in " + mapping);
			String prefix = tok.nextToken();
			String namespaceURI = tok.nextToken();
			namespaces.put(prefix, namespaceURI);
		}
		return xpointer;
	}
    
    protected HashMap processParameters(String args) {
        HashMap parameters = new HashMap();
        String param;
        String value; 
        int start = 0;
        int end = 0;
        int l = args.length();
        while ((start < l) && (end < l)) {
            while ((end < l) && (args.charAt(end++) != '='))
                ;
            if (end == l)
                break;
            param = args.substring(start, end - 1);
            start = end;
            while ((end < l) && (args.charAt(end++) != '&'))
                ;
            if (end == l)
                value = args.substring(start);
            else
                value = args.substring(start, end - 1);
            start = end;
            try {
                param = URLDecoder.decode(param, "UTF-8");
                value = URLDecoder.decode(value, "UTF-8");
                LOG.debug("parameter: " + param + " = " + value);
                parameters.put(param, value);
            } catch (UnsupportedEncodingException e) {
                LOG.warn(e.getMessage(), e);
            }
        }
        return parameters;
    }
}
