/*
 * Copyright 1999,2004 The Apache Software Foundation.
 * 
 * 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.
 */

package org.apache.taglibs.standard.tag.common.xml;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.Tag;
import javax.servlet.jsp.tagext.TagSupport;

import org.jaxen.Context;
import org.jaxen.ContextSupport;
import org.jaxen.FunctionContext;
import org.jaxen.Navigator;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.UnresolvableException;
import org.jaxen.VariableContext;
import org.jaxen.XPath;
import org.jaxen.XPathFunctionContext;
import org.jaxen.dom.DOMXPath;
import org.jaxen.dom.DocumentNavigator;
import org.jaxen.saxpath.SAXPathException;
import org.w3c.dom.Node;

/**
 * <p>Support for tag handlers that evaluate XPath expressions.</p>
 *
 * @author Shawn Bayern
 */
// would ideally be a base class, but some of our user handlers already
// have their own parents
public class XPathUtil {

    //*********************************************************************
    // Constructor

    /**
     * Constructs a new XPathUtil object associated with the given
     * PageContext.
     */
    public XPathUtil(PageContext pc) {
	pageContext = pc;
    }

    //*********************************************************************
    // Support for JSTL variable resolution

    private static final String PAGE_NS_URL
	= "http://java.sun.com/jstl/xpath/page";
    private static final String REQUEST_NS_URL
	= "http://java.sun.com/jstl/xpath/request";
    private static final String SESSION_NS_URL
	= "http://java.sun.com/jstl/xpath/session";
    private static final String APP_NS_URL
	= "http://java.sun.com/jstl/xpath/app";
    private static final String PARAM_NS_URL
	= "http://java.sun.com/jstl/xpath/param";
    private static final String INITPARAM_NS_URL
	= "http://java.sun.com/jstl/xpath/initParam";
    private static final String COOKIE_NS_URL
	= "http://java.sun.com/jstl/xpath/cookie";
    private static final String HEADER_NS_URL
	= "http://java.sun.com/jstl/xpath/header";

    private static final String PAGE_P = "pageScope";
    private static final String REQUEST_P = "requestScope";
    private static final String SESSION_P = "sessionScope";
    private static final String APP_P = "applicationScope";
    private static final String PARAM_P = "param";
    private static final String INITPARAM_P = "initParam";
    private static final String COOKIE_P = "cookie";
    private static final String HEADER_P = "header";

    protected class JstlVariableContext implements VariableContext {
	// retrieves object using JSTL's custom variable-mapping rules
        public Object getVariableValue(String namespace, String prefix,
                String localName) throws UnresolvableException {
	    // I'd prefer to match on namespace, but this doesn't appear
            // to work in Jaxen
	    if (prefix == null || prefix.equals("")) {
		return notNull(
		    pageContext.findAttribute(localName),
		    prefix,
		    localName);
	    } else if (prefix.equals(PAGE_P)) {
		return notNull(
		    pageContext.getAttribute(localName,PageContext.PAGE_SCOPE),
		    prefix,
		    localName);
	    } else if (prefix.equals(REQUEST_P)) {
		return notNull(
		    pageContext.getAttribute(localName,
			PageContext.REQUEST_SCOPE),
		    prefix,
		    localName);
	    } else if (prefix.equals(SESSION_P)) {
		return notNull(
		    pageContext.getAttribute(localName,
		        PageContext.SESSION_SCOPE),
		    prefix,
		    localName);
	    } else if (prefix.equals(APP_P)) {
		return notNull(
		    pageContext.getAttribute(localName,
		        PageContext.APPLICATION_SCOPE),
		    prefix,
		    localName);
	    } else if (prefix.equals(PARAM_P)) {
		return notNull(
		    pageContext.getRequest().getParameter(localName),
		    prefix,
		    localName);
	    } else if (prefix.equals(INITPARAM_P)) {
		return notNull(
		    pageContext.getServletContext().
		      getInitParameter(localName),
		    prefix,
		    localName);
	    } else if (prefix.equals(HEADER_P)) {
		HttpServletRequest hsr =
		    (HttpServletRequest) pageContext.getRequest();
		return notNull(
		    hsr.getHeader(localName),
		    prefix,
		    localName);
	    } else if (prefix.equals(COOKIE_P)) {
		HttpServletRequest hsr =
		    (HttpServletRequest) pageContext.getRequest();
		Cookie[] c = hsr.getCookies();
		for (int i = 0; i < c.length; i++)
		    if (c[i].getName().equals(localName))
			return c[i].getValue();
		throw new UnresolvableException("$" + prefix + ":" + localName);
	    } else
		throw new UnresolvableException("$" + prefix + ":" + localName);
        }

	private Object notNull(Object o, String prefix, String localName)
	        throws UnresolvableException {
            if (o == null) {
                throw new UnresolvableException("$" + (prefix==null?"":prefix+":") + localName);
            }
	    return o;
	}
    }

    //*********************************************************************
    // Support for XPath evaluation

    private PageContext pageContext;
    private static SimpleNamespaceContext nc;
    private static FunctionContext fc;
    private static Navigator dn;
    private static HashMap exprCache;

    /** Initialize globally useful data. */
    private synchronized static void staticInit() {
	if (nc == null) {
	    // register supported namespaces
            nc = new SimpleNamespaceContext();
            SimpleNamespaceContext nc = new SimpleNamespaceContext();
            nc.addNamespace("page", PAGE_NS_URL);
            nc.addNamespace("request", REQUEST_NS_URL);
            nc.addNamespace("session", SESSION_NS_URL);
            nc.addNamespace("application", APP_NS_URL);
            nc.addNamespace("param", PARAM_NS_URL);
            nc.addNamespace("initParam", INITPARAM_NS_URL);
            nc.addNamespace("header", HEADER_NS_URL);
            nc.addNamespace("cookie", COOKIE_NS_URL);

	    // set up the global FunctionContext
	    fc = XPathFunctionContext.getInstance();

	    // set up the global DocumentNavigator
	    dn = DocumentNavigator.getInstance();

            // create a HashMap to cache the expressions
            exprCache = new HashMap();
	}
    }

    /**
     * Returns a String given an XPath expression and a single context
     * Node.
     */
    public String valueOf(Node n, String xpath) throws JspTagException {
	staticInit();
		try {
	        XPath xp = new DOMXPath(xpath);
	        // return xp.valueOf(getLocalContext(n));
	        return xp.stringValueOf(getLocalContext(n));
		} catch (SAXPathException e) {
		    throw new JspTagException("Error evaluating XPath: " + xpath, e);
		}
    }

    /** Evaluates an XPath expression to a boolean value. */
    public boolean booleanValueOf(Node n, String xpath)
	    throws JspTagException {
	staticInit();
		try {
			XPath xp = parse(xpath);
			return xp.booleanValueOf(getLocalContext(n));
		} catch (SAXPathException e) {
		    throw new JspTagException("Error evaluating XPath: " + xpath, e);
		}
    }

    /** Evalutes an XPath expression to a List of nodes. */
    public List selectNodes(Node n, String xpath) throws JspTagException {
	staticInit();
		try {
		    XPath xp = parse(xpath);
		    return xp.selectNodes(getLocalContext(n));
		} catch (SAXPathException e) {
		    throw new JspTagException("Error evaluating XPath: " + xpath, e);
		}
    }

    /** Evaluates an XPath expression to a single node. */
    public Node selectSingleNode(Node n, String xpath)
	    throws SAXPathException {
	staticInit();
	XPath xp = parse(xpath);
	return (Node) xp.selectSingleNode(getLocalContext(n));
    }

    /** Returns a locally appropriate Jaxen context given a node. */
    private Context getLocalContext(Node n) {
	// set up instance-specific contexts
        VariableContext vc = new JstlVariableContext();
        ContextSupport cs = new ContextSupport(nc, fc, vc, dn);
        Context c = new Context(cs);
        List l = new ArrayList(1);
        l.add(n);
        c.setNodeSet(l);
	return c;
    }

    /**
     * Retrieves a parsed version of the textual XPath expression.
     * The parsed version is retrieved from our static cache if we've
     * seen it previously.
     */
    private XPath parse(String xpath) throws SAXPathException {
        XPath cached = (XPath) exprCache.get(xpath);
        if (cached == null) {
          cached = new DOMXPath(xpath);
          exprCache.put(xpath, cached);
	}
        return cached;
    }

    //*********************************************************************
    // Static support for context retrieval from parent <forEach> tag

    public static Node getContext(Tag t) throws JspTagException {
	ForEachTag xt =
	    (ForEachTag) TagSupport.findAncestorWithClass(
		t, ForEachTag.class);
	if (xt == null)
	    return null;
	else
	    return (xt.getContext());
    }

}
