Frage

Say you have a chunk of XML which has multiple namespace prefixes defined and some of them are actually the same namespace just with different prefixes. Using XSLT is there a not too complicated way to merge these prefixes so that you end up with just one prefix for each namespace? For example picking the shortest one?


Example

<soapenv:Envelope
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:f="http://api.example.com/Service"
    xmlns:foo="http://api.example.com/Service">

   <soapenv:Body>

      <foo:serviceResponse>
         <f:profile id="1">Alice</f:profile>
         <f:profile id="2">Bob</f:profile>
      </foo:serviceResponse>

   </soapenv:Body>
</soapenv:Envelope>

Should be turned into for example this:

<soap:Envelope
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:f="http://api.example.com/Service">

   <soap:Body>

      <f:serviceResponse>
         <f:profile id="1">Alice</f:profile>
         <f:profile id="2">Bob</f:profile>
      </f:serviceResponse>

   </soap:Body>
</soap:Envelope>
War es hilfreich?

Lösung 4

This is what I ended up using. Not sure how it compares to the other answers or if it has the same feature-set, but it worked for my case and I don't remember how I got it anymore. If I adjusted one of the answers here or if I found it somewhere else. In either case, it worked for me. Please comment if there are anything that's missing or better in the others and upvote them :)

<stylesheet version="2.0" xmlns="http://www.w3.org/1999/XSL/Transform">

    <!--
        Pulls namespace declarations up towards root node.
    -->

    <template match="@* | text() | processing-instruction() | comment()">
        <copy />
    </template>

    <template match="*">
        <copy copy-namespaces="no">
            <for-each-group group-by="local-name()" select="descendant-or-self::*/namespace::*">
                <copy-of select="." />
            </for-each-group>
            <apply-templates select="@* , node()"/>
        </copy>
    </template>

</stylesheet>

Note: Don't think this actually merges prefixes (don't remember anymore and unable to test it at the moment), but I used it in the end of ESB processes before returning SOAP messages and the namespaces were cleaned up quite a lot so it does do something at least :p

Andere Tipps

You could try along the lines of

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:key name="namespaces" match="dec" use="@ns"/>

<xsl:variable name="prefs">
  <xsl:for-each-group select="/*/namespace::*" group-by="string()">
    <xsl:for-each select="current-group()">
      <xsl:sort select="string-length(local-name())"/>
      <xsl:if test="position() eq 1">
        <dec ns="{string()}"><xsl:value-of select="local-name()"/></dec>
      </xsl:if>
    </xsl:for-each>
  </xsl:for-each-group>
</xsl:variable>

<xsl:template match="@*">
  <xsl:attribute name="{if (key('namespaces', namespace-uri(), $prefs)) then concat(key('namespaces', namespace-uri(), $prefs), ':') else ''}{local-name()}" namespace="{namespace-uri()}" select="."/>
</xsl:template>

<xsl:template match="*">
  <xsl:element name="{if (key('namespaces', namespace-uri(), $prefs)) then concat(key('namespaces', namespace-uri(), $prefs), ':') else ''}{local-name()}" namespace="{namespace-uri()}">
    <xsl:apply-templates select="@* , node()"/>
  </xsl:element>
</xsl:template>

</xsl:stylesheet>

but that only checks for the namespace declarations on the root element (while they are alllowed on any element in your document) and more importantly attribute values in XML schemas or SOAP messages can be of type QName (e.g. xs:integer) and that way the resulting document might no longer be valid (e.g. you have two prefixes x and xs declared, the algorithm eliminates the xs but the attribute value is not fixed to say x:integer). So be warned if you want to use that XSLT to fix SOAP/XML schema stuff which might use qualified names as element or attribute values (and not only for element or attribute names which the XSLT fixes/simplifies).

There is not much control you have over that - it mostly depends on your processor's whim. For example, applying the identity transform template to your input will result in:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:f="http://api.example.com/Service" xmlns:foo="http://api.example.com/Service">
  <soapenv:Body>
    <foo:serviceResponse>
      <foo:profile id="1">Alice</foo:profile>
      <foo:profile id="2">Bob</foo:profile>
    </foo:serviceResponse>
  </soapenv:Body>
</soapenv:Envelope>

when using libxslt, but Saxon will return:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:f="http://api.example.com/Service" xmlns:foo="http://api.example.com/Service">
   <soapenv:Body>
      <foo:serviceResponse>
         <f:profile id="1">Alice</f:profile>
         <f:profile id="2">Bob</f:profile>
      </foo:serviceResponse>
   </soapenv:Body>
</soapenv:Envelope>

You may try to strip the prefixes completely, using something like:

<xsl:template match="*">
    <xsl:element name="{local-name()}" namespace="{namespace-uri()}">
        <xsl:copy-of select="@*"/>
        <xsl:apply-templates/>
    </xsl:element>
</xsl:template>

Here, Saxon will return:

<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
   <Body>
      <serviceResponse xmlns="http://api.example.com/Service">
         <profile id="1">Alice</profile>
         <profile id="2">Bob</profile>
      </serviceResponse>
   </Body>
</Envelope>

while libxslt will generate somewhat random (but uniform) prefixes, e.g.

<?xml version="1.0" encoding="UTF-8"?>
<ns13:Envelope xmlns:ns13="http://schemas.xmlsoap.org/soap/envelope/">
  <ns13:Body>
    <ns14:serviceResponse xmlns:ns14="http://api.example.com/Service">
      <ns14:profile id="1">Alice</ns14:profile>
      <ns14:profile id="2">Bob</ns14:profile>
    </ns14:serviceResponse>
  </ns13:Body>
</ns13:Envelope>

and neither of them is wrong.

Interesting question. This is close to what you need. But did not spend a lot of time on it, so there might be ways to shorten it substantially.

  • The shorter prefix is used for the output XML
  • there are superfluous namespace declarations on elements. This is because it is decided at runtime which prefixes should be discarded and you can only specify a static list of NCNames as the value of exclude-result-prefixes.

The latter could be fixed after the transformation below, with a simple identity transform to exclude the prefixes that are not used anymore. For example, see Dimitre Novatchev's answer here.

Stylesheet

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:f="http://api.example.com/Service"
    xmlns:foo="http://api.example.com/Service">

    <xsl:output method="xml" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="*">
      <xsl:variable name="node" select="."/>
      <xsl:variable name="prefix" select="substring-before(name(), concat(':', local-name()))"/>
      <xsl:variable name="scope">
          <xsl:for-each select="in-scope-prefixes(.)">
              <xsl:element name="{.}">
                  <xsl:value-of select="namespace-uri-for-prefix(.,$node)"/>
              </xsl:element>
          </xsl:for-each>
      </xsl:variable>
      <xsl:choose>
          <xsl:when test="$scope/*[name() != $prefix and namespace-uri-for-prefix($prefix,$node) = . and string-length(./name()) lt string-length($prefix)]">
              <xsl:variable name="match" select="$scope/*[name() != $prefix and namespace-uri-for-prefix($prefix,$node) = .]"/>
              <xsl:element name="{concat($match/name(),':',$node/local-name())}">
                  <xsl:apply-templates select="@*|node()"/>
              </xsl:element>
          </xsl:when>
          <xsl:otherwise>
                <xsl:copy>
                   <xsl:apply-templates select="@*|node()"/>
                </xsl:copy>
          </xsl:otherwise>
      </xsl:choose>

    </xsl:template>

    <xsl:template match="@*|processing-instruction()|text()|comment()">
        <xsl:copy>
         <xsl:apply-templates select="@*|node()"/>
      </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

Output

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
      <f:serviceResponse xmlns:f="http://api.example.com/Service">
         <f:profile xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                    xmlns:foo="http://api.example.com/Service"
                    id="1">Alice</f:profile>
         <f:profile xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                    xmlns:foo="http://api.example.com/Service"
                    id="2">Bob</f:profile>
      </f:serviceResponse>
   </soap:Body>
</soap:Envelope>

Output (after applying something similar to this)

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
      <f:serviceResponse xmlns:f="http://api.example.com/Service">
         <f:profile id="1">Alice</f:profile>
         <f:profile id="2">Bob</f:profile>
      </f:serviceResponse>
   </soap:Body>
</soap:Envelope>
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top