Package com.canoo.webtest.engine.xpath

Source Code of com.canoo.webtest.engine.xpath.XPathHelper

// Copyright � 2002-2007 Canoo Engineering AG, Switzerland.
package com.canoo.webtest.engine.xpath;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.Map.Entry;

import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFunctionException;

import org.apache.log4j.Logger;
import org.apache.xml.dtm.DTM;
import org.apache.xpath.ExtensionsProvider;
import org.apache.xpath.XPathContext;
import org.apache.xpath.functions.FuncExtFunction;
import org.apache.xpath.functions.Function;
import org.apache.xpath.functions.WrongNumberArgsException;
import org.apache.xpath.jaxp.JAXPVariableStack;
import org.apache.xpath.objects.XObject;
import org.apache.xpath.objects.XObjectFactory;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.NodeIterator;

import com.canoo.webtest.engine.StepFailedException;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.xpath.XPathUtils;
import com.gargoylesoftware.htmlunit.xml.XmlPage;

/**
* Helper class for a central XPath creation allowing to share variable,
* function and namespace contexts.
* TODO: cleanup to use plainly javax.xml.xpath.*
* @author Marc Guillemot
*/
public class XPathHelper
{
  private static final Logger LOG = Logger.getLogger(XPathHelper.class);
  private SimpleXPathVariableResolver fVariableContext = new SimpleXPathVariableResolver();
  private SimpleXPathFunctionResolver fFunctionContext = new SimpleXPathFunctionResolver();
  private SimpleNamespaceContext fNamespaceContext = new SimpleNamespaceContext();

  // hack for a problem in HtmlUnit-2.4 (see XPathHelperTest#upperCaseHtmlTags for details)
  final ThreadLocal<Boolean> htmlUnitXPathUtil_XPathProcessingFlag_;

  private static Map<QName, Object> sGlobalVariables = Collections
      .synchronizedMap(new HashMap<QName, Object>());
  private static Map<QName, Class<? extends Function>> sGlobalFunctions = Collections
      .synchronizedMap(new HashMap<QName, Class<? extends Function>>());
  private static Map<String, String> sGlobalNamespaces = Collections
      .synchronizedMap(new HashMap<String, String>());

  static
  {
    registerWebTestGoodies();
  }

  /**
   * Registers a global variable. Global variables are added to the list of
   * variable of a webtest at the webtest start.
   * @param namespaceURI the namespace URI of the function to be registered (<code>null</code>
   *            if none).
   * @param localName the non-prefixed local portion of the function name to
   *            be registered
   * @param value the variable to be registered
   */
  public static void registerGlobalVariable(final String namespaceURI,
      final String localName, final Object value)
  {
    sGlobalVariables.put(new QName(namespaceURI, localName), value);
  }

  /**
   * Registers WebTest XPath extras.
   */
  private static void registerWebTestGoodies()
  {
    final String namespaceURI = "http://webtest.canoo.com";
    registerGlobalNamespace("wt", namespaceURI);
    registerGlobalFunction(namespaceURI, "matches", MatchesFunction.class);
    registerGlobalFunction(namespaceURI, "cleanText", CleanTextFunction.class);
  }

  /**
   * Gets the registered global variables.
   * @return a synchronized map of (MemberKey, variable value)
   */
  public static Map<QName, Object> getGlobalVariables()
  {
    return sGlobalVariables;
  }

  /**
   * Gets the registered global functions.
   * @return a synchronized map of (MemberKey, function)
   */
  public static Map<QName, Class<? extends Function>> getGlobalFunctions()
  {
    return sGlobalFunctions;
  }

  /**
   * Gets the registered global namespaces.
   * @return a synchronized map of (prefix, namespaceURI)
   */
  public static Map<String, String> getGlobalNamespaces()
  {
    return sGlobalNamespaces;
  }

  /**
   * Registers a global function. Global functions are added to the list of
   * functions of a webtest at the webtest start.
   * @param namespaceURI the namespace URI of the function to be registered (<code>null</code>
   *            if none).
   * @param localName the non-prefixed local portion of the function name to
   *            be registered
   * @param function the function to be registered
   */
  public static void registerGlobalFunction(final String namespaceURI,
      final String localName, final Class<? extends Function> function)
  {
    sGlobalFunctions.put(new QName(namespaceURI, localName), function);
  }

  /**
   * Registers a global namespace. Global namespaces are added to the list of
   * namespaces of a webtest at the webtest start.
   * @param prefix the namespace prefix to resolve
   * @param namespaceURI the namespace URI
   */
  public static void registerGlobalNamespace(final String prefix,
      final String namespaceURI)
  {
    sGlobalNamespaces.put(prefix, namespaceURI);
  }

  /**
   * Initializes from the global functions, variables and namespaces.
   */
  public XPathHelper()
  {
    // copy global namespaces
    for (final Entry<String, String> entry : getGlobalNamespaces().entrySet())
    {
      getNamespaceContext().addNamespace(entry.getKey(), entry.getValue());
    }

    // copy global functions
    for (final Entry<QName, Class<? extends Function>> entry : getGlobalFunctions().entrySet())
    {
      final QName memberKey = entry.getKey();
      getFunctionContext().registerFunction(memberKey, entry.getValue());
    }

    // copy global variables
    for (final Entry<QName, Object> entry : getGlobalVariables().entrySet())
    {
      final QName memberKey = entry.getKey();
      getVariableContext().setVariableValue(memberKey, entry.getValue());
    }

    Field f;
    try {
      f = XPathUtils.class.getDeclaredField("PROCESS_XPATH_");
      f.setAccessible(true);
      htmlUnitXPathUtil_XPathProcessingFlag_ = (ThreadLocal<Boolean>) f.get(XPathUtils.class);
    }
    catch (final Exception e) {
      throw new RuntimeException("Failed to hack HtmlUnit-2.4 XPathUtils.PROCESS_XPATH_", e);
    }
  }

  /**
   * Gets the function resolver used during XPath evaluation for this webtest.
   * @return the context
   */
  public SimpleXPathFunctionResolver getFunctionContext()
  {
    return fFunctionContext;
  }

  /**
   * Gets the namespace context used for namespace resolution during XPath
   * evaluation for this webtest.
   * @return the context
   */
  public SimpleNamespaceContext getNamespaceContext()
  {
    return fNamespaceContext;
  }

  /**
   * Gets the variable context used for variable resolution (the $foo in an
   * xpath expression) during XPath evaluation for this webtest.
   * @return the context
   */
  public SimpleXPathVariableResolver getVariableContext()
  {
    return fVariableContext;
  }

  /**
   * Gets the document object associated to the page that could be provided to
   * the XPath for computations
   * @param page the page
   * @return the "document"
   */
  protected Object getDocument(final Page page)
  {
    if (page == null)
      return null; // no page, only xpath not refering to the document
              // tree should work
    else if (page instanceof HtmlPage)
      return page;
    else if (page instanceof XmlPage)
    {
      final XmlPage xmlPage = (XmlPage) page;
      if (xmlPage.getXmlDocument() == null) // when content type was xml
                          // but document couldn't be
                          // parsed
        throw new StepFailedException(
            "The xml document couldn't be parsed as it is not well formed");
      return xmlPage.getXmlDocument();
    }
    else
    {
      throw buildInvalidDocumentException(page);
    }
  }

  /**
   * Utility to build exception for invalid page
   * @param page the page
   * @return the exception to throw
   */
  StepFailedException buildInvalidDocumentException(final Page page)
  {
    return new StepFailedException(
        "Current response is not an HTML or XML page but of type "
            + page.getWebResponse().getContentType() + " ("
            + page.getClass().getName() + ")");
  }

  public String stringValueOf(final Page _page, final String _xpath)
      throws XPathExpressionException
  {
    try
    {
      htmlUnitXPathUtil_XPathProcessingFlag_.set(true);
      final XObject result = eval(_xpath, getDocument(_page));
      return result.str();
    }
    catch (final TransformerException e)
    {
      throw handleException(e);
    }
    finally
    {
      htmlUnitXPathUtil_XPathProcessingFlag_.set(false);
    }
  }

  private XPathExpressionException handleException(TransformerException _e)
  {
    final Throwable nestedException = _e.getException();
    if (nestedException instanceof XPathFunctionException)
    {
      return (XPathFunctionException) nestedException;
    }
    else
    {
//      if (true)
//        throw new RuntimeException(_e.getCause());
      LOG.info("XPath error", _e);
      return new XPathExpressionException(_e.getMessage()); // stupid but XPathExpressionException(e).getMessage() is null!
    }
  }

  public List<? extends Object> selectNodes(final Page _page, final String _xpath)
      throws XPathExpressionException
  {
    return (List<? extends Object>) getByXPath(_page, _xpath, false);
  }

  public Object selectFirst(final Page _page, final String _xpath)
      throws XPathExpressionException
  {
    return getByXPath(_page, _xpath, true);
  }

  protected Object getByXPath(final Page _currentResp, final String _xpath,
      final boolean _onlyFirstResult) throws XPathExpressionException
  {
    try
    {
      htmlUnitXPathUtil_XPathProcessingFlag_.set(true);
      final XObject result = eval(_xpath, getDocument(_currentResp));
      switch (result.getType())
      {
        case XObject.CLASS_BOOLEAN:
          return result.bool();
        case XObject.CLASS_NUMBER:
          return result.num();
        case XObject.CLASS_STRING:
          return result.str();
        case XObject.CLASS_NODESET:
          if (_onlyFirstResult)
            return result.nodeset().nextNode();
          else
            return toList(result.nodeset());
        default:
          throw new RuntimeException("Unexpected result type for >" + _xpath
              + "<: " + result.getType());
      }
    }
    catch (final TransformerException e)
    {
      throw handleException(e);
    }
    finally
    {
      htmlUnitXPathUtil_XPathProcessingFlag_.set(false);
    }
  }

  private List<Node> toList(final NodeIterator _nodeset)
  {
    final List<Node> result = new ArrayList<Node>();
    Node node = _nodeset.nextNode();
    while (node != null)
    {
      result.add(node);
      node = _nodeset.nextNode();
    }
    // TODO Auto-generated method stub
    return result;
  }

  private XObject eval(final String expression, final Object contextItem)
      throws javax.xml.transform.TransformerException
  {
    final PrefixResolver prefixResolver = new PrefixResolver(fNamespaceContext, contextItem);
    org.apache.xpath.XPath xpath = new org.apache.xpath.XPath(expression, null,
        prefixResolver, org.apache.xpath.XPath.SELECT, null, fFunctionContext.getFunctionTable());
    // function resolver
    final XPathContext[] contexts = {null};
    final ExtensionsProvider extProvider = new ExtensionsProvider()
    {
      public boolean elementAvailable(String _ns, String _elemName)
          throws TransformerException
      {
        return false;
      }
      public Object extFunction(final FuncExtFunction _extFunction, Vector _argVec)
          throws TransformerException
      {
        final String ns = _extFunction.getNamespace();
        final String name = _extFunction.getFunctionName();
        final Function func = fFunctionContext.resolveFunction(new QName(ns, name), 0);
        if (func == null)
          throw new RuntimeException("Can't find function " + name + " (namespace: " + ns + ")");

        for (int i=0; i<_argVec.size(); ++i)
        {
          try
          {
            func.setArg(XObjectFactory.create(_argVec.get(i)), i);
          }
          catch (final WrongNumberArgsException e)
          {
            throw new RuntimeException(e);
          }
        }
        return func.execute(contexts[0]);
      }
      public Object extFunction(String _ns, String _funcName, Vector _argVec,
          Object _methodKey) throws TransformerException
      {
        return null;
      }
      public boolean functionAvailable(String _ns, String _funcName)
          throws TransformerException
      {
        return fFunctionContext.resolveFunction(new QName(_ns, _funcName), 0) != null;
      }
    };
    XPathContext xpathSupport = new XPathContext(extProvider);
    contexts[0] = xpathSupport;
    xpathSupport.setVarStack(new JAXPVariableStack(fVariableContext));

    // If item is null, then we will create a a Dummy contextNode
    final XObject xobj;
    if (contextItem instanceof Node)
    {
      xobj = xpath.execute(xpathSupport, (Node) contextItem, prefixResolver);
    }
    else
    {
      xobj = xpath.execute(xpathSupport, DTM.NULL, prefixResolver);
    }

    return xobj;
  }
 
  /**
   * Quotes the provided value, handling quotes and double quotes if needed to
   * @param value the value to quote
   * @return the quoted value usable in XPath expression
   */
  public static String quote(final String value)
  {
    if (!value.contains("'")) {
      return "'" + value + "'";
    }
    else if (!value.contains("\""))  {
      return "\"" + value + "\"";
    }
    else {
      final String[] parts = value.split("'");
      String response = "concat(";
      for (int i=0; i<parts.length-1; ++i)
      {
        response += "'" + parts[i] + "', \"'\", ";
      }
      response += "'" + parts[parts.length-1] + "')";
      return response;
    }
  }
}
TOP

Related Classes of com.canoo.webtest.engine.xpath.XPathHelper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.