/* Copyright 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.xmlbeans.impl.xpathgen; | |
import org.apache.xmlbeans.XmlCursor; | |
import org.apache.xmlbeans.XmlCursor.TokenType; | |
import javax.xml.namespace.QName; | |
import javax.xml.namespace.NamespaceContext; | |
/** | |
* Generates an XPath String that points to a given position in an XML document | |
*/ | |
public class XPathGenerator | |
{ | |
/** | |
* Generates an XPath pointing to the position in the document indicated by <code>node</code>. | |
* <p>If the <code>context</code> parameter is null, the XPath is absolute, otherwise the | |
* XPath will be relative to the position indicated by <code>context</code>.</p> | |
* <p>Note: the cursor position for the <code>node</code> parameter is not preserved</p> | |
* @param node the position in the document that the generated path will point to | |
* @param context the context node; the generated path will be relative to it if not null and if | |
* pointing to an element on the path from the document root to <code>node</code> | |
* @param nsctx a namespace context that will be used to obtain prefixes; a (non-default) | |
* namespace mapping must be available for all required namespace URIs | |
* @return the generated path as a <code>String</code> | |
* @throws XPathGenerationException if the path could not be generated: the cursor is in a bad | |
* position (like over a comment) or no prefix mapping was found for one of the namespace URIs | |
*/ | |
public static String generateXPath(XmlCursor node, XmlCursor context, NamespaceContext nsctx) | |
throws XPathGenerationException | |
{ | |
if (node == null) | |
throw new IllegalArgumentException("Null node"); | |
if (nsctx == null) | |
throw new IllegalArgumentException("Null namespace context"); | |
TokenType tt = node.currentTokenType(); | |
if (context != null && node.isAtSamePositionAs(context)) | |
return "."; | |
switch (tt.intValue()) | |
{ | |
case TokenType.INT_ATTR: | |
QName name = node.getName(); | |
node.toParent(); | |
String pathToParent = generateInternal(node, context, nsctx); | |
return pathToParent + '/' + '@' + qnameToString(name, nsctx); | |
case TokenType.INT_NAMESPACE: | |
name = node.getName(); | |
node.toParent(); | |
pathToParent = generateInternal(node, context, nsctx); | |
String prefix = name.getLocalPart(); | |
if (prefix.length() == 0) | |
return pathToParent + "/@xmlns"; | |
else | |
return pathToParent + "/@xmlns:" + prefix; | |
case TokenType.INT_START: | |
case TokenType.INT_STARTDOC: | |
return generateInternal(node, context, nsctx); | |
case TokenType.INT_TEXT: | |
int nrOfTextTokens = countTextTokens(node); | |
node.toParent(); | |
pathToParent = generateInternal(node, context, nsctx); | |
if (nrOfTextTokens == 0) | |
return pathToParent + "/text()"; | |
else | |
return pathToParent + "/text()[position()=" + nrOfTextTokens + ']'; | |
default: | |
throw new XPathGenerationException("Cannot generate XPath for cursor position: " + | |
tt.toString()); | |
} | |
} | |
private static String generateInternal(XmlCursor node, XmlCursor context, NamespaceContext nsctx) | |
throws XPathGenerationException | |
{ | |
if (node.isStartdoc()) | |
return ""; | |
if (context != null && node.isAtSamePositionAs(context)) | |
return "."; | |
assert node.isStart(); | |
QName name = node.getName(); | |
XmlCursor d = node.newCursor(); | |
if (!node.toParent()) | |
return "/" + name; | |
int elemIndex = 0, i = 1; | |
node.push(); | |
if (!node.toChild(name)) | |
throw new IllegalStateException("Must have at least one child with name: " + name); | |
do | |
{ | |
if (node.isAtSamePositionAs(d)) | |
elemIndex = i; | |
else | |
i++; | |
} while (node.toNextSibling(name)); | |
node.pop(); | |
d.dispose(); | |
String pathToParent = generateInternal(node, context, nsctx); | |
return i == 1 ? pathToParent + '/' + qnameToString(name, nsctx) : | |
pathToParent + '/' + qnameToString(name, nsctx) + '[' + elemIndex + ']'; | |
} | |
private static String qnameToString(QName qname, NamespaceContext ctx) | |
throws XPathGenerationException | |
{ | |
String localName = qname.getLocalPart(); | |
String uri = qname.getNamespaceURI(); | |
if (uri.length() == 0) | |
return localName; | |
String prefix = qname.getPrefix(); | |
if (prefix != null && prefix.length() > 0) | |
{ | |
// Try to use the same prefix if it maps to the right URI | |
String mappedUri = ctx.getNamespaceURI(prefix); | |
if (uri.equals(mappedUri)) | |
return prefix + ':' + localName; | |
} | |
// The prefix is not specified, or it is not mapped to the right URI | |
prefix = ctx.getPrefix(uri); | |
if (prefix == null) | |
throw new XPathGenerationException("Could not obtain a prefix for URI: " + uri); | |
if (prefix.length() == 0) | |
throw new XPathGenerationException("Can not use default prefix in XPath for URI: " + uri); | |
return prefix + ':' + localName; | |
} | |
/** | |
* Computes how many text nodes the | |
* @param c the position in the document | |
* @return how many text nodes occur before the position determined by <code>c</code> | |
*/ | |
private static int countTextTokens(XmlCursor c) | |
{ | |
int k = 0; | |
int l = 0; | |
XmlCursor d = c.newCursor(); | |
c.push(); | |
c.toParent(); | |
TokenType tt = c.toFirstContentToken(); | |
while (!tt.isEnd()) | |
{ | |
if (tt.isText()) | |
{ | |
if (c.comparePosition(d) > 0) | |
// We have moved after the initial position | |
l++; | |
else | |
k++; | |
} | |
else if (tt.isStart()) | |
c.toEndToken(); | |
tt = c.toNextToken(); | |
} | |
c.pop(); | |
return l == 0 ? 0 : k; | |
} | |
public static void main(String[] args) throws org.apache.xmlbeans.XmlException | |
{ | |
String xml = | |
"<root>\n" + | |
"<ns:a xmlns:ns=\"http://a.com\"><b foo=\"value\">text1<c/>text2<c/>text3<c>text</c>text4</b></ns:a>\n" + | |
"</root>"; | |
NamespaceContext ns = new NamespaceContext() { | |
public String getNamespaceURI(String prefix) | |
{ | |
if ("ns".equals(prefix)) | |
return "http://a.com"; | |
else | |
return null; | |
} | |
public String getPrefix(String namespaceUri) | |
{ | |
return null; | |
} | |
public java.util.Iterator getPrefixes(String namespaceUri) | |
{ | |
return null; | |
} | |
}; | |
XmlCursor c = org.apache.xmlbeans.XmlObject.Factory.parse(xml).newCursor(); | |
c.toFirstContentToken(); // on <root> | |
c.toFirstContentToken(); // on <a> | |
c.toFirstChild(); // on <b> | |
c.toFirstChild(); // on <c> | |
c.push(); System.out.println(generateXPath(c, null, ns)); c.pop(); | |
c.toNextSibling(); | |
c.toNextSibling(); // on the last <c> | |
c.push(); System.out.println(generateXPath(c, null, ns)); c.pop(); | |
XmlCursor d = c.newCursor(); | |
d.toParent(); | |
c.push(); System.out.println(generateXPath(c, d, ns)); c.pop(); | |
d.toParent(); | |
c.push(); System.out.println(generateXPath(c, d, ns)); c.pop(); | |
c.toFirstContentToken(); // on text content of the last <c> | |
c.push(); System.out.println(generateXPath(c, d, ns)); c.pop(); | |
c.toParent(); | |
c.toPrevToken(); // on text content before the last <c> | |
c.push(); System.out.println(generateXPath(c, d, ns)); c.pop(); | |
c.toParent(); // on <b> | |
c.push(); System.out.println(generateXPath(c, d, ns)); c.pop(); | |
c.toFirstAttribute(); // on the "foo" attribute | |
c.push(); System.out.println(generateXPath(c, d, ns)); c.pop(); | |
c.toParent(); | |
c.toParent(); | |
c.toNextToken(); // on the "xmlns:ns" attribute | |
c.push(); System.out.println(generateXPath(c, d, ns)); c.pop(); | |
c.push(); System.out.println(generateXPath(c, null, ns)); c.pop(); | |
} | |
} |