Paulo Avelar
Paulo Avelar

Reputation: 2140

I can't select a node value from a namespaced XML

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

Answers (2)

Mathias M&#252;ller
Mathias M&#252;ller

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 ReverseGeocodeResultare 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

  • is only available in XSLT 2.0 unfortunately
  • is not useful because your input XML has more than one default namespace

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

StuartLC
StuartLC

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

Related Questions