package at.gv.egovernment.moa.util;

import java.util.List;
import java.util.Map;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.traversal.NodeIterator;

import org.jaxen.JaxenException;
import org.jaxen.NamespaceContext;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.dom.DOMXPath;
import org.jaxen.dom.DocumentNavigator;

/**
 * Utility methods to evaluate XPath expressions on DOM nodes.
 * 
 * @author Patrick Peck
 * @version $Id$
 */
public class XPathUtils {

  /**
   * The XPath expression selecting all nodes under a given root (including the
   * root node itself).
   */
  public static final String ALL_NODES_XPATH =
    "(.//. | .//@* | .//namespace::*)";

  /** The <code>DocumentNavigator</code> to use for navigating the document. */
  private static DocumentNavigator documentNavigator =
    DocumentNavigator.getInstance();
  /** The default namespace prefix to namespace URI mappings. */
  private static NamespaceContext NS_CONTEXT;

  static {
    SimpleNamespaceContext ctx = new SimpleNamespaceContext();
    ctx.addNamespace(Constants.MOA_PREFIX, Constants.MOA_NS_URI);
    ctx.addNamespace(Constants.MOA_CONFIG_PREFIX, Constants.MOA_CONFIG_NS_URI);
    ctx.addNamespace(
      Constants.MOA_ID_CONFIG_PREFIX,
      Constants.MOA_ID_CONFIG_NS_URI);
    ctx.addNamespace(Constants.SL10_PREFIX, Constants.SL10_NS_URI);
    ctx.addNamespace(Constants.SL11_PREFIX, Constants.SL11_NS_URI);
    ctx.addNamespace(Constants.ECDSA_PREFIX, Constants.ECDSA_NS_URI);
    ctx.addNamespace(Constants.PD_PREFIX, Constants.PD_NS_URI);
    ctx.addNamespace(Constants.SAML_PREFIX, Constants.SAML_NS_URI);
    ctx.addNamespace(Constants.SAMLP_PREFIX, Constants.SAMLP_NS_URI);
    ctx.addNamespace(Constants.DSIG_PREFIX, Constants.DSIG_NS_URI);
    ctx.addNamespace(Constants.XSLT_PREFIX, Constants.XSLT_NS_URI);
    ctx.addNamespace(Constants.XSI_PREFIX, Constants.XSI_NS_URI);
    ctx.addNamespace(
      Constants.DSIG_FILTER2_PREFIX,
      Constants.DSIG_FILTER2_NS_URI);
    ctx.addNamespace(Constants.DSIG_EC_PREFIX, Constants.DSIG_EC_NS_URI);
    NS_CONTEXT = ctx;
  }

  /**
   * Return a <code>NodeIterator</code> over the nodes matching the XPath
   * expression.
   * 
   * All namespace URIs and prefixes declared in the <code>Constants</code>
   * interface are used for resolving namespaces.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param exp The XPath expression to evaluate.
   * @return An iterator over the resulting nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static NodeIterator selectNodeIterator(Node contextNode, String exp)
    throws XPathException {

    return selectNodeIterator(contextNode, NS_CONTEXT, exp);
  }

  /**
   * Return a <code>NodeIterator</code> over the nodes matching the XPath
   * expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param namespaceElement An element from which to build the
   * namespace mapping for evaluating the XPath expression
   * @param exp The XPath expression to evaluate.
   * @return An iterator over the resulting nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static NodeIterator selectNodeIterator(
    Node contextNode,
    Element namespaceElement,
    String exp)
    throws XPathException {

    try {
      SimpleNamespaceContext ctx = new SimpleNamespaceContext();
      ctx.addElementNamespaces(documentNavigator, namespaceElement);
      return selectNodeIterator(contextNode, ctx, exp);
    } catch (JaxenException e) {
      MessageProvider msg = MessageProvider.getInstance();
      String message = msg.getMessage("xpath.00", new Object[] { exp });
      throw new XPathException(message, e);
    }
  }

  /**
   * Return a <code>NodeIterator</code> over the nodes matching the XPath
   * expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param namespaceMapping A namespace prefix to namespace URI mapping
   * (<code>String</code> to <code>String</code>) for evaluating the XPath 
   * expression.
   * @param exp The XPath expression to evaluate.
   * @return An iterator over the resulting nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static NodeIterator selectNodeIterator(
    Node contextNode,
    Map namespaceMapping,
    String exp)
    throws XPathException {

    SimpleNamespaceContext ctx = new SimpleNamespaceContext(namespaceMapping);

    return selectNodeIterator(contextNode, ctx, exp);
  }

  /**
   * Return a <code>NodeIterator</code> over the nodes matching the XPath
   * expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param nsContext The <code>NamespaceContext</code> for resolving namespace
   * prefixes to namespace URIs for evaluating the XPath expression.
   * @param exp The XPath expression to evaluate.
   * @return An iterator over the resulting nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  private static NodeIterator selectNodeIterator(
    Node contextNode,
    NamespaceContext nsContext,
    String exp)
    throws XPathException {

    try {
      DOMXPath xpath = new DOMXPath(exp);
      List nodes;

      xpath.setNamespaceContext(nsContext);
      nodes = xpath.selectNodes(contextNode);
      return new NodeIteratorAdapter(nodes.listIterator());
    } catch (JaxenException e) {
      MessageProvider msg = MessageProvider.getInstance();
      String message = msg.getMessage("xpath.00", new Object[] { exp });
      throw new XPathException(message, e);
    }
  }

  /**
   * Return a <code>NodeList</code> of all the nodes matching the XPath
   * expression.
   * 
   * All namespace URIs and prefixes declared in the <code>Constants</code>
   * interface are used for resolving namespaces.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param exp The XPath expression to evaluate.
   * @return A <code>NodeList</code> containing the matching nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static NodeList selectNodeList(Node contextNode, String exp)
    throws XPathException {

    return selectNodeList(contextNode, NS_CONTEXT, exp);
  }

  /**
   * Return a <code>NodeList</code> of all the nodes matching the XPath
   * expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param namespaceElement An element from which to build the
   * namespace mapping for evaluating the XPath expression
   * @param exp The XPath expression to evaluate.
   * @return A <code>NodeList</code> containing the matching nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static NodeList selectNodeList(
    Node contextNode,
    Element namespaceElement,
    String exp)
    throws XPathException {

    try {
      SimpleNamespaceContext ctx = new SimpleNamespaceContext();

      ctx.addElementNamespaces(documentNavigator, namespaceElement);
      return selectNodeList(contextNode, ctx, exp);
    } catch (JaxenException e) {
      MessageProvider msg = MessageProvider.getInstance();
      String message = msg.getMessage("xpath.00", new Object[] { exp });
      throw new XPathException(message, e);
    }
  }

  /**
   * Return a <code>NodeList</code> of all the nodes matching the XPath
   * expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param namespaceMapping A namespace prefix to namespace URI mapping
   * (<code>String</code> to <code>String</code>) for evaluating the XPath 
   * expression.
   * @param exp The XPath expression to evaluate.
   * @return A <code>NodeList</code> containing the matching nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static NodeList selectNodeList(
    Node contextNode,
    Map namespaceMapping,
    String exp)
    throws XPathException {

    SimpleNamespaceContext ctx = new SimpleNamespaceContext(namespaceMapping);

    return selectNodeList(contextNode, ctx, exp);
  }

  /**
   * Return a <code>NodeList</code> of all the nodes matching the XPath
   * expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param nsContext The <code>NamespaceContext</code> for resolving namespace
   * prefixes to namespace URIs for evaluating the XPath expression.
   * @param exp The XPath expression to evaluate.
   * @return A <code>NodeList</code> containing the matching nodes.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  private static NodeList selectNodeList(
    Node contextNode,
    NamespaceContext nsContext,
    String exp)
    throws XPathException {

    try {
      DOMXPath xpath = new DOMXPath(exp);
      List nodes;

      xpath.setNamespaceContext(nsContext);
      nodes = xpath.selectNodes(contextNode);
      return new NodeListAdapter(nodes);
    } catch (JaxenException e) {
      MessageProvider msg = MessageProvider.getInstance();
      String message = msg.getMessage("xpath.00", new Object[] { exp });
      throw new XPathException(message, e);
    }
  }

  /**
   * Select the first node matching an XPath expression.
   * 
   * All namespace URIs and prefixes declared in the <code>Constants</code>
   * interface are used for resolving namespaces.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param exp The XPath expression to evaluate.
   * @return Node The first node matching the XPath expression, or
   * <code>null</code>, if no node matched.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static Node selectSingleNode(Node contextNode, String exp)
    throws XPathException {

    return selectSingleNode(contextNode, NS_CONTEXT, exp);
  }

  /**
   * Select the first node matching an XPath expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param namespaceElement An element from which to build the
   * namespace mapping for evaluating the XPath expression
   * @param exp The XPath expression to evaluate.
   * @return Node The first node matching the XPath expression, or
   * <code>null</code>, if no node matched.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static Node selectSingleNode(
    Node contextNode,
    Element namespaceElement,
    String exp)
    throws XPathException {

    try {
      SimpleNamespaceContext ctx = new SimpleNamespaceContext();
      ctx.addElementNamespaces(documentNavigator, namespaceElement);

      return selectSingleNode(contextNode, ctx, exp);
    } catch (JaxenException e) {
      MessageProvider msg = MessageProvider.getInstance();
      String message = msg.getMessage("xpath.00", new Object[] { exp });
      throw new XPathException(message, e);
    }
  }

  /**
   * Select the first node matching an XPath expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param namespaceMapping A namespace prefix to namespace URI mapping
   * (<code>String</code> to <code>String</code>) for evaluating the XPath 
   * expression.
   * @param exp The XPath expression to evaluate.
   * @return Node The first node matching the XPath expression, or
   * <code>null</code>, if no node matched.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  public static Node selectSingleNode(
    Node contextNode,
    Map namespaceMapping,
    String exp)
    throws XPathException {

    SimpleNamespaceContext ctx = new SimpleNamespaceContext(namespaceMapping);

    return selectSingleNode(contextNode, ctx, exp);
  }

  /**
   * Select the first node matching an XPath expression.
   * 
   * @param contextNode The root node from which to evaluate the XPath
   * expression.
   * @param nsContext The <code>NamespaceContext</code> for resolving namespace
   * prefixes to namespace URIs for evaluating the XPath expression.
   * @param exp The XPath expression to evaluate.
   * @return Node The first node matching the XPath expression, or
   * <code>null</code>, if no node matched.
   * @throws XPathException An error occurred evaluating the XPath expression.
   */
  private static Node selectSingleNode(
    Node contextNode,
    NamespaceContext nsContext,
    String exp)
    throws XPathException {

    try {
      DOMXPath xpath = new DOMXPath(exp);
      xpath.setNamespaceContext(nsContext);
      return (Node) xpath.selectSingleNode(contextNode);
    } catch (JaxenException e) {
      MessageProvider msg = MessageProvider.getInstance();
      String message = msg.getMessage("xpath.00", new Object[] { exp });
      throw new XPathException(message, e);
    }
  }

  /**
   * Return the value of a DOM element whose location is given by an XPath
   * expression.
   * 
   * @param root The root element from which to evaluate the XPath.
   * @param xpath The XPath expression pointing to the element whose value
   * to return.
   * @param def The default value to return, if no element can be found using
   * the given <code>xpath</code>.
   * @return The element value, if it can be located using the
   * <code>xpath</code>. Otherwise, <code>def</code> is returned.
   */
  public static String getElementValue(
    Element root,
    String xpath,
    String def) {

    Element elem = (Element) XPathUtils.selectSingleNode(root, xpath);
    return elem != null ? DOMUtils.getText(elem) : def;
  }

  /**
   * Return the value of a DOM attribute whose location is given by an XPath
   * expression.
   * 
   * @param root The root element from which to evaluate the XPath.
   * @param xpath The XPath expression pointing to the attribute whose value to
   * return.
   * @param def The default value to return, if no attribute can be found using
   * the given <code>xpath</code>.
   * @return The element value, if it can be located using the
   * <code>xpath</code>. Otherwise, <code>def</code> is returned.
   */
  public static String getAttributeValue(
    Element root,
    String xpath,
    String def) {

    Attr attr = (Attr) XPathUtils.selectSingleNode(root, xpath);
    return attr != null ? attr.getValue() : def;
  }

}