XSLT: choose template, variable length dt_assoc in

2019-06-14 14:53发布

问题:

I am very very new at XSLT, please forgive my oblivious cluelessness. Also, I am not fluent on Stackoverflow etiquette, I hope this is not excessively long, but the example requires a good bit of context. I've been trying to solve this problem for the past few days, learning quite a lot along the way, but unfortunately, I'm not making any more progress.

I redesigned my templates after seeing [larsh] (https://stackoverflow.com/users/423105/larsh) answer here [1] How can I print a single <div> without closing it in XSLT

but before I can even get that far, need to solve this "xsl:choose" and attributes problem. (Indeed that solution still presents problems in my case, that I don't know how many records of each type there are. I'll need to put an <item type='xx'> open tag before the first record in a group and then close it after the final in the group.)

My input is a DNS record exported from Rackspace API as an xml file. The important part looks like this:

<ns2:recordsList totalEntries="5">
    <ns2:record id="A-2542719" type="A" name="midsummernightstamps.com" data="198.101.155.141" ttl="300" updated="2012-10-10T21:34:52Z" created="2010-02-17T05:02:16Z" />
    <ns2:record id="A-2542719" type="A" name="midsummernightstamps.com" data="198.101.155.141" ttl="300" updated="2012-10-10T21:34:52Z" created="2010-02-17T05:02:16Z" />
    <ns2:record id="A-2542719" type="A" name="midsummernightstamps.com" data="198.101.155.143" ttl="300" updated="2012-10-10T21:34:52Z" created="2010-02-17T05:02:16Z" />       
    <ns2:record id="NS-3093871" type="NS" name="midsummernightstamps.com" data="dns1.stabletransit.com" ttl="300" updated="2012-10-10T21:34:52Z" created="2010-02-17T05:03:16Z" />
    <ns2:record id="NS-3093873" type="NS" name="midsummernightstamps.com" data="dns2.stabletransit.com" ttl="300" updated="2012-10-10T21:34:52Z" created="2010-02-17T05:03:16Z" />
    <ns2:record id="CNAME-6051819" type="CNAME" name="vh1.midsummernightstamps.com" data="ns1.eiotx.net" ttl="300" updated="2012-10-10T21:34:52Z" created="2010-02-17T05:05:09Z" />
    <ns2:record id="CNAME-6052005" type="CNAME" name="www.midsummernightstamps.com" data="virtual.eiotx.net" ttl="300" updated="2012-10-10T21:34:52Z" created="2010-02-17T05:05:09Z" />
</ns2:recordsList>

The output should reflect the following example template, where each record type is a list of records within the item under the structure.

<item key="attributes">
      <dt_assoc>
        <item key="domain">example.com</item>
        <item key="records">
          <dt_assoc>
            <item key="A">
              <dt_array>
                <item key="0">
                  <dt_assoc>
                    <item key="subdomain"></item>
                    <item key="ip_address">123.123.123.2</item>
                  </dt_assoc>
                </item>
                <item key="1">
                  <dt_assoc>
                    <item key="subdomain">*</item>
                    <item key="ip_address">123.123.123.3</item>
                  </dt_assoc>
                </item>
                <item key="2">
                  <dt_assoc>
                    <item key="subdomain">www</item>
                    <item key="ip_address">123.123.123.4</item>
                  </dt_assoc>
                </item>
              </dt_array>
            </item>
            <item key="CNAME">
              <dt_array>
                <item key="0">
                  <dt_assoc>
                    <item key="subdomain">portal</item>
                    <item key="hostname">www.example.com</item>
                  </dt_assoc>
                </item>
              </dt_array>
            </item> etc.

(I thought I had it worked out, but the OpenSRS API kept complaining that my XML was invalid. I finally realized that my templates inserted a new <item key="{@type}"> for every record, where in fact there is only one which is the child dt_array which itself has one element per record.) After much squinting and beard pulling, I have produced the following XSLT, which is not producing at all... I am mystified as to the results I'm getting. (Showing the templates and decision for just two of the record types here.)

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns2="http://docs.rackspacecloud.com/dns/api/v1.0">
  <xsl:output method="xml" indent="yes" doctype-system="OPS.dtd" />
  <xsl:template match="/">
    <OPS_envelope>
      <header>
        <version>0.9</version>
      </header>
      <body>
        <data_block>
          <dt_assoc>
            <xsl:apply-templates />
          </dt_assoc>
        </data_block>
      </body>
    </OPS_envelope>
  </xsl:template>
  <xsl:template match="ns2:domain">
    <item key="protocol">XCP</item>
    <item key="action">create_dns_zone</item>
    <item key="object">DOMAIN</item>
    <item key="attributes">
      <dt_assoc>
        <item key="domain">
          <xsl:value-of select="//@name" />
        </item>
        <item key="records">
          <dt_assoc>
            <xsl:apply-templates match="ns2:domain/ns2:recordsList" />
          </dt_assoc>
        </item>
      </dt_assoc>
    </item>
  </xsl:template>
  <xsl:template name="recordTypes" match="ns2:domain/ns2:recordsList/ns2:record">
    <p>DEBUG TEMPLATE RECORD TYPES</p>
    <item key="{@type}">
      <xsl:choose>
       <xsl:when test="ns2:record[@type='A']">
          <p>Choose A</p>
          <xsl:call-template name="typeA" select="./@type" />
        </xsl:when>
        <xsl:when test="ns2:record[@type='CNAME']">
          <p>Choose CNAME</p>
          <xsl:call-template name="typeCNAME" select="./@type" />
        </xsl:when>        
        <xsl:otherwise> <p>DEBUG OTHERWISE - Nada</p>
        </xsl:otherwise>
      </xsl:choose>
    </item>
  </xsl:template>
  <xsl:template name="typeA" match="ns2:record[@type='A']">
    <p>DEBUG TypeA</p>
    <dt_array>
      <item key="{count(preceding::ns2:record[@type='A'])}">
        <dt_assoc>
          <item key="subdomain">
          <xsl:value-of select="./@name" />.</item>
          <item key="ip_address">
            <xsl:value-of select="./@data" />
          </item>
        </dt_assoc>
      </item>
    </dt_array>
  </xsl:template>

  <xsl:template name="typeCNAME">
    <p>DEBUG TypeCNAME</p>
    <dt_array>
      <item key="{count(preceding::ns2:record[@type='CNAME'])}">
        <dt_assoc>
          <item key="subdomain">
          <xsl:value-of select="./@name" />.</item>
          <item key="hostname">
            <xsl:value-of select="./@data" />
          </item>
          <!--<item key="comment"><xsl:value-of select="./@comment" /></item> -->
        </dt_assoc>
      </item>
    </dt_array>
  </xsl:template>
<!-- additional types removed for clarity -->
      </item>
    </dt_array>
  </xsl:template>
</xsl:stylesheet>

And, following, the output against my sample input, where it will be seen, that my xsl:choose is only selected against "@type='A'", and all the others are ignored. I've tested changing the order of the templates, the order of the input data records, etc, but only the "A" gets anywhere, BUT, it will not print the <item key="xxx"> except in the 'xsl:otherwise' clause. This is very strange, since the count selector is working in the "typeA" template, but then the <item key="{@type}"> does not work when the type is "A"... Why just the "A"!?! I'm obviously way off base here somehow conceptually, which is no surprise, but I'd sure like to see where the core problem really is.

    <?xml version="1.0"?>
<!DOCTYPE OPS_envelope SYSTEM "OPS.dtd">
<OPS_envelope xmlns:ns2="http://docs.rackspacecloud.com/dns/api/v1.0">
  <header>
    <version>0.9</version>
  </header>
  <body>
    <data_block>
      <dt_assoc>
        <item key="protocol">XCP</item>
        <item key="action">create_dns_zone</item>
        <item key="object">DOMAIN</item>
        <item key="attributes">
          <dt_assoc>
            <item key="domain">midsummernightstamps.com</item>
            <item key="records">
              <dt_assoc>
                <p>DEBUG TypeA</p>
                <dt_array>
                  <item key="0">
                    <dt_assoc>
                      <item key="subdomain">midsummernightstamps.com.</item>
                      <item key="ip_address">198.101.155.141</item>
                    </dt_assoc>
                  </item>
                </dt_array>
                <p>DEBUG TypeA</p>
                <dt_array>
                  <item key="1">
                    <dt_assoc>
                      <item key="subdomain">midsummernightstamps.com.</item>
                      <item key="ip_address">198.101.155.141</item>
                    </dt_assoc>
                  </item>
                </dt_array>
                <p>DEBUG TypeA</p>
                <dt_array>
                  <item key="2">
                    <dt_assoc>
                      <item key="subdomain">midsummernightstamps.com.</item>
                      <item key="ip_address">198.101.155.143</item>
                    </dt_assoc>
                  </item>
                </dt_array>
                <p>DEBUG TEMPLATE RECORD TYPES</p>
                <item key="NS">
                  <p>DEBUG OTHERWISE - Nada</p>
                </item>
                <p>DEBUG TEMPLATE RECORD TYPES</p>
                <item key="NS">
                  <p>DEBUG OTHERWISE - Nada</p>
                </item>
                <p>DEBUG TEMPLATE RECORD TYPES</p>
                <item key="CNAME">
                  <p>DEBUG OTHERWISE - Nada</p>
                </item>
                <p>DEBUG TEMPLATE RECORD TYPES</p>
                <item key="CNAME">
                  <p>DEBUG OTHERWISE - Nada</p>
                </item>
              </dt_assoc>
            </item>
          </dt_assoc>
        </item>
      </dt_assoc>
    </data_block>
  </body>
</OPS_envelope>

回答1:

It is good you are learning on the way! The next thing you should probably learn about is a technique called Muenchian Grouping. In your case, it looks like you "group" your record elements by their type attribute. In this case, you define a key like so

 <xsl:key name="types" match="ns2:record" use="@type" />

You then start off by selecting the record element which has the fist occurrence of a particular type attribute in the XML, which forms the basis of the group

<xsl:for-each select="ns2:recordsList/ns2:record[generate-id() = generate-id(key('types', @type)[1])]">

Then, to build the group (i.e. to select all the elements in the group), you can just use the key function itself

<item key="{@type}">
    <xsl:apply-templates select="key('types', @type)" />
</item>

You might also consider combining your templates, to avoid too much code repetition.

Try this XSLT:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns2="http://docs.rackspacecloud.com/dns/api/v1.0">
  <xsl:output method="xml" indent="yes" doctype-system="OPS.dtd" />

  <xsl:key name="types" match="ns2:record" use="@type" />

  <xsl:template match="/">
    <OPS_envelope>
      <header>
        <version>0.9</version>
      </header>
      <body>
        <data_block>
          <dt_assoc>
            <xsl:apply-templates />
          </dt_assoc>
        </data_block>
      </body>
    </OPS_envelope>
  </xsl:template>

  <xsl:template match="ns2:domain">
    <item key="protocol">XCP</item>
    <item key="action">create_dns_zone</item>
    <item key="object">DOMAIN</item>
    <item key="attributes">
      <dt_assoc>
        <item key="domain">
          <xsl:value-of select="@name" />
        </item>
        <item key="records">
          <dt_assoc>
            <xsl:for-each select="ns2:recordsList/ns2:record[generate-id() = generate-id(key('types', @type)[1])]">
                <item key="{@type}">
                    <xsl:apply-templates select="key('types', @type)" />
                </item>
            </xsl:for-each>
          </dt_assoc>
        </item>
      </dt_assoc>
    </item>
  </xsl:template>

  <xsl:template match="ns2:record">
    <dt_array>
      <item key="{position() - 1}">
        <dt_assoc>
          <item key="subdomain">
            <xsl:value-of select="./@name" />
              <xsl:text>.</xsl:text>
          </item>
          <xsl:choose>
              <xsl:when test="@type='A'">
                  <item key="ip_address">
                    <xsl:value-of select="@data" />
                  </item>
              </xsl:when>
              <xsl:otherwise>
                  <item key="hostname">
                    <xsl:value-of select="@data" />
                  </item>
              </xsl:otherwise>
          </xsl:choose>
        </dt_assoc>
      </item>
    </dt_array>
  </xsl:template>
</xsl:stylesheet>

Note that this assumes only one domain element in your XML. If you have multiple domains, and want to group records per domain, you should change your key to this instead

<xsl:key name="types" match="ns2:record" use="concat(@type, ../../@name)" />

Make sure you adjust all the uses of the key accordingly too.



标签: xslt dns