Reputation: 3079
Question: What is the correct way to register the default NS for XPath context?
I've gone through numerous posts (mostly non-C++ and Google search) about registering NS, but I cannot find anything for the default namespace and because of that, I cannot search by XPath.
Given the following XML document, the XPath search will only work if the default namespace is not specified (which I can't omit).
<EDSCrate xmlns="http://aviation-ia.com/aeec/SupportFiles/827"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<CrateData id="blah">...</CrateData>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
</EDSCrate>
I tried registering the default NS as "empty" before searching by XPath but it still doesn't work.
xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
xmlXPathRegisterNs(xpathCtx, (const xmlChar*) "", (const xmlChar*)"http://aviation-ia.com/aeec/SupportFiles/827");
//xmlXPathRegisterNs(xpathCtx, NULL, (const xmlChar*)"http://aviation-ia.com/aeec/SupportFiles/827");
xmlXPathRegisterNs(xpathCtx, (const xmlChar*) "xsi", (const xmlChar*)"http://www.w3.org/2001/XMLSchema-instance");
xmlXPathRegisterNs(xpathCtx, (const xmlChar*) "ds", (const xmlChar*)"http://www.w3.org/2000/09/xmldsig#");
The code works if xmlns=
is removed from the root node.
Searching for the following simple XPath will return nothing, example;
/EDSCrate
/EDSCrate/CrateData
I looked through the libxml2 documentation, there is a deprecated way to register global NS at the document level via xmlNewGlobalNs
but that doesn't apply to XPath context...
Then looking at the xmlXPathContext
definition, there seems to be a namespaces
and nsHash
table in it... am I supposed to brute-force register an NS in there?
Update:
Hum... looks like there is an open bug for it? https://gitlab.gnome.org/GNOME/libxml2/-/issues/585
Does anyone know a workaround?
Upvotes: 0
Views: 101
Reputation: 3079
After taking in Remy's comment, I devised a working solution allowing me to search via XPath on the default namespace "as-if" I don't need to specify the custom prefix.
The idea is to, as Remy said, append the prefix in every XPath query, but I am doing it behind the scenes, this requires me to create a special function to patch up the incoming xpaths.
IMPORTANT, the code below has not been tested, it was originally written using something in-house, but I've converted the definition to standard C++ as best I can.
I created a wrapper class to deal with the libxml2
's API and within my wrapper, I can specify a defaultPrefix
and an XPath namespace registration callback.
// callback function to register namespaces for xpath
typedef void (*XPathRegisterNSCallback) (xmlXPathContextPtr ctxt);
XPathRegisterNSCallback xpathRegNSCallback;
// variable to hold the prefix for default namespace
std::string defaultPrefix;
// shared xpath context and object during the search
xmlXPathContextPtr xpathCtx;
xmlXPathObjectPtr xpathObj;
const char* patch_xpath_ns(const char* xpath)
{
// no default namespace prefix defined, so return as-is
if (!defaultPrefix) return xpath;
std::string modified = "";
vector<string> parts;
// split it somehow, I didn't look up how to do it
// parts.split(xpath, "/");
for (std::list<std::string>::const_iterator i = parts.begin(); i != parts.end(); ++i)
{
std::string part = *i;
if (!!part && strchr(part, ':') == NULL)
{
std:string tmp;
sprintf(tmp, "/%s:%s", defaultPrefix, part);
modified.append(tmp);
}
else
{
std:string tmp;
sprintf(tmp, "/%s", part);
modified.append(tmp);
}
}
return modified;
}
xmlNodeSetPtr find_nodes(const char* xpath)
{
// clear the shared xpath context before the search
__clear_xpath();
const char* patched = patch_xpath_ns(XPath);
// now do your regular XPath search
xpathCtx = xmlXPathNewContext(doc);
// run the user defined namespace registration callback
// apply it to the xpathCtx so it becomes NS aware
// this includes the default namespace
if (xpathRegNSCallback) xpathRegNSCallback(xpathCtx);
xpathObj = xmlXPathEvalExpression(BAD_CAST patched, xpathCtx);
// do what you gotta do
}
int main()
{
defaultPrefix = "sf";
xpathRegNSCallback = [](xmlXPathContextPtr ctxt) {
xmlXPathRegisterNs(ctxt, BAD_CAST defaultPrefix, BAD_CAST "http://aviation-ia.com/aeec/SupportFiles/827");
xmlXPathRegisterNs(ctxt, BAD_CAST "xsi", BAD_CAST "http://www.w3.org/2001/XMLSchema-instance");
xmlXPathRegisterNs(ctxt, BAD_CAST "ds", BAD_CAST "http://www.w3.org/2000/09/xmldsig#");
xmlXPathRegisterNs(ctxt, BAD_CAST "xades", BAD_CAST "http://uri.etsi.org/01903/v1.1.1#");
};
...
}
Then when you pass in the XPath that does not contain any prefix, it will patch it and auto-prefix it for you, in the end, you will get something like this when passing //EDSCrate/CrateData
after being patched, example:
//sf:EDSCrate/sf:CrateData
//sf:EDSCrate/sf:CrateData
//sf:AssemblyData
//sf:AssemblyData
//sf:AssemblyItem
//sf:AssemblyItem
//sf:EDSCrate/sf:CrateData
//sf:EDSCrate/sf:CrateData/sf:Description
If I have the free time later, perhaps I could create a GitHub snippet and a working example, but for now, this is what I'd like to share, it is working, and searching through XPath is a breeze now.
I want to special thanks to Remy Lebeau for answering 2 of my recent posts regarding libxml2
, it was a tremendous help to point me in the right direction to find my error and solution.
Upvotes: 0
Reputation: 597896
libxml2's documentation says you can't pass an empty prefix to xmlXPathRegisterNs()
.
XPath 1.0 (the version supported by libxml2) has no concept of a "default" namespace. Every namespace has to be declared with a prefix within the XPath context so the XPath expression can refer to them. As explained in this answer, you can give namespaces (including the "default" ns) any prefix you want within the XPath context, you don't have to use the same prefixes that are defined in the XML itself (XPath doesn't care what they are, only what they refer to).
So, simply declare your own prefix for the "default" namespace, eg:
xmlXPathRegisterNs(xpathCtx, (const xmlChar*) "sf", (const xmlChar*)"http://aviation-ia.com/aeec/SupportFiles/827");
And then you can use that prefix in your XPath expression, eg:
/sf:EDSCrate
/sf:EDSCrate/sf:CrateData
/sf:EDSCrate/sf:CrateData/ds:Signature
On a side note: libxml2 has a BAD_CAST
macro for casting string literals to xmlChar*
:
xmlXPathRegisterNs(xpathCtx, BAD_CAST "sf", BAD_CAST "http://aviation-ia.com/aeec/SupportFiles/827");
Upvotes: 1