I have an XML packet received from a third-party web server:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SomeResponse xmlns="http://someurl">
<SomeResult>
.....
</SomeResult>
</SomeResponse>
</soap:Body>
</soap:Envelope>
To be cross-platform capable, this XML is loaded into Delphi's IXMLDocument
:
XmlDoc.LoadFromXML(XmlString);
I'm using a solution to find an XML node using XPath. The solution works in other cases, however when the XML document contains namespace prefixes, it fails.
I'm trying to access path:
/soap:Envelope/soap:Body/SomeResponse/SomeResult
From the linked answer:
function selectNode(xnRoot: IXmlNode; const nodePath: WideString): IXmlNode;
var
intfSelect : IDomNodeSelect;
dnResult : IDomNode;
intfDocAccess : IXmlDocumentAccess;
doc: TXmlDocument;
begin
Result := nil;
if not Assigned(xnRoot) or not Supports(xnRoot.DOMNode, IDomNodeSelect, intfSelect) then
Exit;
dnResult := intfSelect.selectNode(nodePath);
if Assigned(dnResult) then
begin
if Supports(xnRoot.OwnerDocument, IXmlDocumentAccess, intfDocAccess) then
doc := intfDocAccess.DocumentObject
else
doc := nil;
Result := TXmlNode.Create(dnResult, nil, doc);
end;
end;
It fails at dnResult := intfSelect.selectNode(nodePath);
with EOleException
: Reference to undeclared namespace prefix: 'soap'
How do I make this work when the node names have a namespace prefix?
As others have pointed out, different vendors handle namespaces differently. Here is an example using MSXML (the windows default) DOMVendor: (which I DO realise is not exactly what the OP was asking, but I felt it was worth documenting)
XML:
Selection code (for completeness)
Actual setting of XML search namespaces:
Couple of things worth noting: once you add namespaces into the mix at all, Xpath seems to insist on them for everything. Note that I added a 'some' namespace for the search criteria, because the SomResult inherited it from its parent, and I have yet to get XPath to implicitly handle default namespaces.
The OmniXML solution:
I can absolutely confirm the OmniXML XPath does NOT support namespaces per se.
BUT:
since it treats the nodenames as literals, 'soap:Envelope' will work in a query PROVIDED the name in the xml document IS soap:Envelope. So in the OP example, the OmniXML search path '/soap:Envelope/soap:Body/SomeResponse/SomeResult' would work.
Note that you can absolutely NOT rely on inherited or default namespaces, OmniXML matches on the literal nodename.
You could fairly easily implement a loop to either remove or normalize all namespace tags in your document without too much effort.
When I tried this a couple of years ago, I found namespace lookup in XPath was different between xml providers.
If I remember correctly, the Msxml lets you just use the namespace prefixes as they are defined in the xml file.
The ADOM 4 provider requires that you resolve namespace prefixes used in your XPath query to the actual namespaces, independent of the namespace mapping used in the xml file. There is a method pointer for that purpose, OnOx4XPathLookupNamespaceURI. Then you can have a name lookup function like this:
Using this lookup function, and the selectNode function (which looks like something I may have once posted in a Delphi forum, taken from https://github.com/Midiar/adomxmldom/blob/master/xmldocxpath.pas), I could do the following test (using your xml in a string constant):
I had to modify you XPath query a little for the default namespace.
One solution could be to remove all namespaces before you start processing your XML:
(
TXMLHelper
is a helper class that I have with some useful XML handling functions)Do not try to include namespaces in your XPath query. If all you want is the text of the SomeResult node, then you can use '//SomeResult' as query. For some reason the default xml implementation (msxml) barfs on the default namespace
xmlns="http://someurl"
on theSomeResponse
parentnode. However, using OmniXML as the DOMVendor (= Crossplatform and valid from XE7 - thanks to @gabr) this works: