Reputation: 2140
I'm having trouble transforming a SOAP response XML into a plain text string. I'm starting with XLST and I've read all I could about. Apparently what I need to accomplish is simple, but all examples are way simpler than my context.
First, I'm reaching a web service (Bing Maps Reverse Geocoding) that returns this XML structure:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<ReverseGeocodeResponse xmlns="http://dev.virtualearth.net/webservices/v1/geocode/contracts">
<ReverseGeocodeResult xmlns:a="http://dev.virtualearth.net/webservices/v1/geocode" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<BrandLogoUri xmlns="http://dev.virtualearth.net/webservices/v1/common">
http://dev.virtualearth.net/Branding/logo_powered_by.png
</BrandLogoUri>
<ResponseSummary xmlns="http://dev.virtualearth.net/webservices/v1/common">
<AuthenticationResultCode>ValidCredentials</AuthenticationResultCode>
<Copyright>(...)</Copyright>
<FaultReason i:nil="true" />
<StatusCode>Success</StatusCode>
<TraceId>(...)</TraceId>
</ResponseSummary>
<a:Results xmlns:b="http://dev.virtualearth.net/webservices/v1/common">
<b:GeocodeResult>
<b:Address>
<b:AddressLine>(...)</b:AddressLine>
<b:AdminDistrict>SP</b:AdminDistrict>
<b:CountryRegion>Brasil</b:CountryRegion>
<b:District />
<b:FormattedAddress>(...)</b:FormattedAddress>
<b:Locality>Campinas</b:Locality>
<b:PostalCode>13069-380</b:PostalCode>
<b:PostalTown />
</b:Address>
<b:BestView>(...)</b:BestView>
<b:Confidence>Medium</b:Confidence>
<b:DisplayName>(...)</b:DisplayName>
<b:EntityType>Address</b:EntityType>
<b:Locations>(...)</b:Locations>
<b:MatchCodes xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<c:string>Good</c:string>
</b:MatchCodes>
</b:GeocodeResult>
<b:GeocodeResult>
(...)
</b:GeocodeResult>
</a:Results>
</ReverseGeocodeResult>
</ReverseGeocodeResponse>
</s:Body>
</s:Envelope>
The node b:GeocodeResult
is repeated about 10 times. The other parts with (...)
are irrelevant (no related nodes).
The only thing I need from this extensive response are the nodes b:Locality
and b:AdminDistrict
.
I'm struggling for the last couple days to get this done.
Here's one of the many approaches:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://dev.virtualearth.net/webservices/v1/common"
xmlns:a="http://dev.virtualearth.net/webservices/v1/geocode"
xmlns:b="http://dev.virtualearth.net/webservices/v1/common"
xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<xsl:template match="/s:Envelope/s:Body/ReverseGeocodeResponse/ReverseGeocodeResult/a:Results/b:GeocodeResult/b:Address">
<xsl:value-of select="b:Locality"/> - <xsl:value-of select="b:AdminDistrict"/>
</xsl:template>
</xsl:stylesheet>
I know this should only return the first b:Locality
and b:AdminDistrict
nodes, and that's perfect. But when I try this, the result is all the text in the XML (no tags at all, just concatenated text). Some variations of this approach return only the ' - ' fragment between the two xsl:value-of
tags.
What am I doing wrong? Can this be related to the infinity of namespaces?
Upvotes: 3
Views: 432
Reputation: 22617
What is happening in your stylesheet
What happens in your original code is this: The one template you have written does not match anything in the input XML. This means that the code inside this template is never executed. Instead, for all nodes in the input XML, the default, built-in templates are applied.
The built-in templates traverse through the tree and do not output anything else than all the text content. That is why you end up with:
But when I try this, the result is all the text in the XML (no tags at all, just concatenated text).
To prevent this, write an empty template that matches all text:
<xsl:template match="text()"/>
Then, you immediately and more clearly see the difference between your template not being applied at all (no output) and of it giving the wrong result (wrong output).
Why is this happening in your stylesheet?
The template does not match anything because your path expression:
/s:Envelope/s:Body/ReverseGeocodeResponse/ReverseGeocodeResult/a:Results/b:GeocodeResult/b:Address"
does not match any node in the input XML. For the path expression above, an XPath processor expects that the ReverseGeocodeResponse
and ReverseGeocodeResult
are in no namespace. But for your input XML, that's not true:
<ReverseGeocodeResponse xmlns="http://dev.virtualearth.net/webservices/v1/geocode/contracts">
<ReverseGeocodeResult xmlns:a="http://dev.virtualearth.net/webservices/v1/geocode">
On the ReverseGeocodeResponse
element, there is a default namespace - that in this case also applies to this element itself. Also, it causes its child element ReverseGeocodeResult
to take on this namespace.
A solution to this
Declare this namespace (http://dev.virtualearth.net/webservices/v1/geocode/contracts
) in your XSLT stylesheet and prefix the two elements that have it. I know you tried to mimick the input XML's default namespace with:
<xsl:stylesheet version="1.0"
xmlns="http://dev.virtualearth.net/webservices/v1/common">
but the effect is different. This defines a default namespace for elements in the XSLT stylesheet. But what you wanted to do is define an default namespace for XPath expressions. That's also possible with xpath-default-namespace
- which
Stylesheet
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:a="http://dev.virtualearth.net/webservices/v1/geocode"
xmlns:b="http://dev.virtualearth.net/webservices/v1/common"
xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays"
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:con="http://dev.virtualearth.net/webservices/v1/geocode/contracts">
<xsl:output method="text"/>
<xsl:template match="/s:Envelope/s:Body/con:ReverseGeocodeResponse/con:ReverseGeocodeResult/a:Results/b:GeocodeResult/b:Address">
<xsl:value-of select="b:Locality"/> - <xsl:value-of select="b:AdminDistrict"/>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
Text Output
Campinas - SP
Upvotes: 3
Reputation: 107237
The jumble
of xml you are seeing is due to the default processing rules of the built-in templates. Typically, if you only want to process specific elements in the document, you'll want to capture the root element, and then use apply-templates
selectively.
Also, the reason why you aren't seeing the expected values is because ReverseGeocodeResponse
and ReverseGeocodeResult
are actually xmlns namespace http://dev.virtualearth.net/webservices/v1/geocode/contracts
- you'll need to adjust your xslt appropriately (I've added alias zz
):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://dev.virtualearth.net/webservices/v1/common"
xmlns:a="http://dev.virtualearth.net/webservices/v1/geocode"
xmlns:b="http://dev.virtualearth.net/webservices/v1/common"
xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:zz="http://dev.virtualearth.net/webservices/v1/geocode/contracts">
<xsl:template match="/">
<xsl:apply-templates select="/s:Envelope/s:Body/zz:ReverseGeocodeResponse/zz:ReverseGeocodeResult/a:Results/b:GeocodeResult/b:Address"/>
</xsl:template>
<xsl:template match="b:Address">
<xsl:value-of select="b:Locality"/> - <xsl:value-of select="b:AdminDistrict"/>
</xsl:template>
</xsl:stylesheet>
Upvotes: 1