001: // Copyright © 2002-2007 Canoo Engineering AG, Switzerland.
002: package com.canoo.webtest.engine.xpath;
003:
004: import java.util.Collections;
005: import java.util.HashMap;
006: import java.util.Iterator;
007: import java.util.Map;
008:
009: import org.jaxen.Function;
010: import org.jaxen.JaxenException;
011: import org.jaxen.NamespaceContext;
012: import org.jaxen.Navigator;
013: import org.jaxen.SimpleFunctionContext;
014: import org.jaxen.SimpleNamespaceContext;
015: import org.jaxen.SimpleVariableContext;
016: import org.jaxen.XPath;
017: import org.jaxen.XPathFunctionContext;
018: import org.jaxen.dom.DOMXPath;
019: import org.w3c.dom.NamedNodeMap;
020: import org.w3c.dom.Node;
021:
022: import com.canoo.webtest.engine.StepFailedException;
023: import com.gargoylesoftware.htmlunit.Page;
024: import com.gargoylesoftware.htmlunit.html.HtmlPage;
025: import com.gargoylesoftware.htmlunit.html.xpath.HtmlUnitXPath;
026: import com.gargoylesoftware.htmlunit.xml.XmlPage;
027:
028: /**
029: * Helper class for a central XPath creation allowing to share variable, function and namespace
030: * contexts.
031: * @author Marc Guillemot
032: */
033: public class XPathHelper {
034: private SimpleVariableContext fVariableContext = new SimpleVariableContext();
035: private SimpleFunctionContext fFunctionContext = new XPathFunctionContext();
036: private SimpleNamespaceContext fNamespaceContext = new SimpleNamespaceContext();
037:
038: /**
039: * A wrapper for a {@link NamespaceContext} allowing to locally add namespaces
040: */
041: private static class SimpleLocalNamespaceContext extends
042: SimpleNamespaceContext {
043: private final NamespaceContext fWrappedContext;
044:
045: SimpleLocalNamespaceContext(final NamespaceContext _wrapped) {
046: fWrappedContext = _wrapped;
047: }
048:
049: public String translateNamespacePrefixToUri(final String prefix) {
050: String resp = super .translateNamespacePrefixToUri(prefix);
051: if (resp == null)
052: return fWrappedContext
053: .translateNamespacePrefixToUri(prefix);
054: else
055: return resp;
056: }
057: }
058:
059: private static Map sGlobalVariables = Collections
060: .synchronizedMap(new HashMap());
061: private static Map sGlobalFunctions = Collections
062: .synchronizedMap(new HashMap());
063: private static Map sGlobalNamespaces = Collections
064: .synchronizedMap(new HashMap());
065:
066: static {
067: registerWebTestGoodies();
068: }
069:
070: /**
071: * Registers a global variable.
072: * Global variables are added to the list of variable of a webtest at the webtest start.
073: * @param namespaceURI the namespace URI of the function to be registered (<code>null</code> if none).
074: * @param localName the non-prefixed local portion of the function name to be registered
075: * @param value the variable to be registered
076: */
077: public static void registerGlobalVariable(
078: final String namespaceURI, final String localName,
079: final Object value) {
080: sGlobalVariables.put(new MemberKey(namespaceURI, localName),
081: value);
082: }
083:
084: /**
085: * Registers WebTest XPath extras.
086: *
087: */
088: private static void registerWebTestGoodies() {
089: final String namespaceURI = "http://webtest.canoo.com";
090: registerGlobalNamespace("wt", namespaceURI);
091: registerGlobalFunction(namespaceURI, "matches",
092: new MatchesFunction());
093: registerGlobalFunction(namespaceURI, "cleanText",
094: new CleanTextFunction());
095: }
096:
097: /**
098: * Gets the registered global variables.
099: * @return a synchronized map of (MemberKey, variable value)
100: */
101: public static Map getGlobalVariables() {
102: return sGlobalVariables;
103: }
104:
105: /**
106: * Gets the registered global functions.
107: * @return a synchronized map of (MemberKey, function)
108: */
109: public static Map getGlobalFunctions() {
110: return sGlobalFunctions;
111: }
112:
113: /**
114: * Gets the registered global namespaces.
115: * @return a synchronized map of (prefix, namespaceURI)
116: */
117: public static Map getGlobalNamespaces() {
118: return sGlobalNamespaces;
119: }
120:
121: /**
122: * Registers a global function.
123: * Global functions are added to the list of functions of a webtest at the webtest start.
124: * @param namespaceURI the namespace URI of the function to be registered (<code>null</code> if none).
125: * @param localName the non-prefixed local portion of the function name to be registered
126: * @param function the function to be registered
127: */
128: public static void registerGlobalFunction(
129: final String namespaceURI, final String localName,
130: final Function function) {
131: sGlobalFunctions.put(new MemberKey(namespaceURI, localName),
132: function);
133: }
134:
135: /**
136: * Registers a global namespace.
137: * Global namespaces are added to the list of namespaces of a webtest at the webtest start.
138: * @param prefix the namespace prefix to resolve
139: * @param namespaceURI the namespace URI
140: */
141: public static void registerGlobalNamespace(final String prefix,
142: final String namespaceURI) {
143: sGlobalNamespaces.put(prefix, namespaceURI);
144: }
145:
146: /**
147: * Initializes from the global functions, variables and namespaces.
148: */
149: public XPathHelper() {
150: // copy global namespaces
151: for (final Iterator iter = getGlobalNamespaces().entrySet()
152: .iterator(); iter.hasNext();) {
153: final Map.Entry entry = (Map.Entry) iter.next();
154: getNamespaceContext().addNamespace((String) entry.getKey(),
155: (String) entry.getValue());
156: }
157:
158: // copy global functions
159: for (final Iterator iter = getGlobalFunctions().entrySet()
160: .iterator(); iter.hasNext();) {
161: final Map.Entry entry = (Map.Entry) iter.next();
162: final MemberKey memberKey = (MemberKey) entry.getKey();
163: getFunctionContext().registerFunction(
164: memberKey.getNamespaceURI(),
165: memberKey.getLocalName(),
166: (Function) entry.getValue());
167: }
168:
169: // copy global variables
170: for (final Iterator iter = getGlobalVariables().entrySet()
171: .iterator(); iter.hasNext();) {
172: final Map.Entry entry = (Map.Entry) iter.next();
173: final MemberKey memberKey = (MemberKey) entry.getKey();
174: getVariableContext().setVariableValue(
175: memberKey.getNamespaceURI(),
176: memberKey.getLocalName(), entry.getValue());
177: }
178: }
179:
180: /**
181: * Gets the namespace context used for function resolution
182: * during XPath evaluation for this webtest.
183: * @return the context
184: */
185: public SimpleFunctionContext getFunctionContext() {
186: return fFunctionContext;
187: }
188:
189: /**
190: * Gets the namespace context used for namespace resolution
191: * during XPath evaluation for this webtest.
192: * @return the context
193: */
194: public SimpleNamespaceContext getNamespaceContext() {
195: return fNamespaceContext;
196: }
197:
198: /**
199: * Gets the variable context used for variable resolution (the $foo in an xpath expression)
200: * during XPath evaluation for this webtest.
201: * @return the context
202: */
203: public SimpleVariableContext getVariableContext() {
204: return fVariableContext;
205: }
206:
207: /**
208: * Gets the xpath for the page, taking care to initialize everything
209: * @param page the current page (may be null)
210: * @param xpathExpr the xpath expression
211: * @return the xpath instance
212: * @throws JaxenException if the creation fails (not valid xpath)
213: */
214: public XPath getXPath(final Page page, final String xpathExpr)
215: throws JaxenException {
216: final XPath xpath = buildXPath(page, xpathExpr);
217: if (!(page instanceof XmlPage)) // a special namespace context is set for XmlPage's XPath to handle namespaces
218: xpath.setNamespaceContext(fNamespaceContext);
219: xpath.setVariableContext(fVariableContext);
220: xpath.setFunctionContext(fFunctionContext);
221: return xpath;
222: }
223:
224: /**
225: * Gets the document object associated to the page that could be provided to the
226: * XPath for computations
227: * @param page the page
228: * @return the "document"
229: */
230: public Object getDocument(final Page page) {
231: if (page == null)
232: return null; // no page, only xpath not refering to the document tree should work
233: else if (page instanceof HtmlPage)
234: return page;
235: else if (page instanceof XmlPage) {
236: final XmlPage xmlPage = (XmlPage) page;
237: if (xmlPage.getXmlDocument() == null) // when content type was xml but document couldn't be parsed
238: throw new StepFailedException(
239: "The xml document couldn't be parsed as it is not well formed");
240: return xmlPage.getXmlDocument();
241: } else {
242: throw buildInvalidDocumentException(page);
243: }
244: }
245:
246: /**
247: * Builds the xpath for the page
248: * @param page the page (may be <code>null</code>)
249: * @param xpathExpr the XPath expression
250: * @return the XPath object
251: * @throws JaxenException if XPath creation fails
252: */
253: protected XPath buildXPath(final Page page, final String xpathExpr)
254: throws JaxenException {
255: if (page == null)
256: return new XPathWithoutDocument(xpathExpr);
257: else if (page instanceof HtmlPage)
258: return new HtmlUnitXPath(xpathExpr);
259: else if (page instanceof XmlPage) {
260: final XmlPage xmlPage = (XmlPage) page;
261: if (xmlPage.getXmlDocument() == null) // when content type was xml but document couldn't be parsed
262: throw new StepFailedException(
263: "The xml document couldn't be parsed as it is not well formed");
264:
265: final XPath xpath = new DOMXPath(xpathExpr);
266:
267: // retrieve namespaces (a simpler way?)
268: final SimpleLocalNamespaceContext nsContext = new SimpleLocalNamespaceContext(
269: getNamespaceContext());
270: xpath.setNamespaceContext(nsContext);
271:
272: final NamedNodeMap docEltAttributes = xmlPage
273: .getXmlDocument().getDocumentElement()
274: .getAttributes();
275: for (int i = 0; i < docEltAttributes.getLength(); ++i) {
276: final Node attrNode = docEltAttributes.item(i);
277: if (attrNode.getNodeName().startsWith("xmlns:")) {
278: final String namespace = attrNode.getNodeName()
279: .substring(6);
280: nsContext.addNamespace(namespace, attrNode
281: .getNodeValue());
282: }
283: }
284: return xpath;
285: } else {
286: throw buildInvalidDocumentException(page);
287: }
288: }
289:
290: /**
291: * Utility to build exception for invalid page
292: * @param page the page
293: * @return the exception to throw
294: */
295: StepFailedException buildInvalidDocumentException(final Page page) {
296: return new StepFailedException(
297: "Current response is not an HTML or XML page but of type "
298: + page.getWebResponse().getContentType() + " ("
299: + page.getClass().getName() + ")");
300: }
301: }
302:
303: /**
304: * Custom XPath to allow evaluation of XPath expressions that don't involve node access
305: * and would throw if a node access is performed
306: */
307: class XPathWithoutDocument extends org.jaxen.dom.DOMXPath {
308: private final NavigatorWithoutDocument fNavigator;
309:
310: XPathWithoutDocument(final String xpathExpr) throws JaxenException {
311: super (xpathExpr);
312: fNavigator = new NavigatorWithoutDocument(xpathExpr);
313: }
314:
315: public Navigator getNavigator() {
316: return fNavigator;
317: }
318: }
319:
320: class NavigatorWithoutDocument extends org.jaxen.dom.DocumentNavigator {
321: private final String fXPathExpr;
322:
323: NavigatorWithoutDocument(final String xpathExpr) {
324: fXPathExpr = xpathExpr;
325: }
326:
327: public Object getDocumentNode(final Object obj) {
328: throw new StepFailedException("No match for xpath expression <"
329: + fXPathExpr + ">");
330: }
331: }
|