1

XML:

<root>
  <item>
    <key>mustBeSECONDKey</key>
    <value>MustBeSECONDValue</value>
  </item>
  <item>
    <key>mustBeFIRSTKey</key>
    <value>MustBeFIRSTValue</value>
  </item>
</root>

XSLT:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <file>
      <xsl:for-each select="root/item">
        <xsl:if test="key[text()='mustBeFIRSTKey']">
          <xsl:element name="someCustomTagName">
            <xsl:value-of select="value" />
          </xsl:element>
        </xsl:if>
      </xsl:for-each>
      <xsl:for-each select="root/item">
        <xsl:if test="key[text()='mustBeSECONDKey']">
          <xsl:element name="anotherNameOfATag">
            <xsl:value-of select="value" />
          </xsl:element>
        </xsl:if>
      </xsl:for-each>
    </file>
  </xsl:template>
</xsl:stylesheet>

Output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<file>
  <someCustomTagName>MustBeFIRSTValue</someCustomTagName>
  <anotherNameOfATag>MustBeSECONDValue</anotherNameOfATag>
</file>

The idea here is that I want to make sure that the tags end up in the output document in an order that I specify. So I'm doing that by basically saying "look through everything and find the first thing, then look through everything and find the second thing." This obviously works.

However - and this would be the crux of this question - I imagine that there must be a more efficient way to accomplish this goal (possibly more than one way). What is it?

And there's a further wrinkle. Suppose that mustBeFIRSTKey has two possible values, MustBeFIRSTValueUno and MustBeFIRSTValueNi. I then want to map these two values to another set of two values, Gazpacho and Sushi, respectively. So

<item>
  <key>mustBeFIRSTKey</key>
  <value>MustBeFIRSTValueNi</value>
</item>

becomes

<mustBeFIRSTKey>Sushi</mustBeFIRSTKey>

Edit: I've discovered my problem was mostly a conceptual one looking at the Java side of things. During the setup of my Transformer I was doing this:

StreamSource xsltSource = new StreamSource(ClassLoader.getSystemResourceAsStream(transformLocation));

I should have been doing this:

StreamSource xsltSource = new StreamSource(ClassLoader.getSystemResource(transformLocation).toString());

That StreamSource constructor sets the systemId, which allows me to use @Ian_Robert's more clever solution from below, as follows:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <!-- I discovered I could merge the $map and $inlineMap variables. -->
  <xsl:variable name="inlineMap" select="document('')//xsl:variable[@name = 'inlineMap']/map">
    <map>
      <key from="mustBeFIRSTKey" to="someCustomTagName" />
      <key from="mustBeSECONDKey" to="anotherNameOfATag">
        <value from="MustBeSECONDValueUno" to="Gazpacho" />
        <value from="MustBeSECONDValueNi" to="Sushi" />
      </key>
    </map>
  </xsl:variable>
  <xsl:key name="valueMap" match="value" use="concat(../@from, '|', @from)" />
  <xsl:template match="root">
    <xsl:variable name="items" select="item" />
    <file>
      <xsl:for-each select="$inlineMap">
        <xsl:for-each select="key">
          <xsl:apply-templates select="$items[key = current()/@from]">
            <xsl:with-param name="elementName" select="@to"/>
          </xsl:apply-templates>
        </xsl:for-each>
      </xsl:for-each>
    </file>
  </xsl:template>
  <xsl:template match="item">
    <xsl:param name="elementName" />
    <xsl:variable name="currentItem" select="." />
    <xsl:element name="{$elementName}">
      <xsl:for-each select="$inlineMap">
        <xsl:variable name="value" select="key('valueMap', concat($currentItem/key, '|', $currentItem/value))" />
        <xsl:choose>
          <xsl:when test="$value">
            <xsl:value-of select="$value/@to" />
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select="$currentItem/value" />
          </xsl:otherwise>
        </xsl:choose>
      </xsl:for-each>
    </xsl:element>
  </xsl:template>
</xsl:stylesheet>

This example strips away the logic for handling values without mappings, because for my problem, there are none.

4

3 回答 3

2

您可以使用一个技巧<xsl:sort>来实现这一目标

<xsl:variable name="sortOrder" select="'|MustBeFIRSTKey|MustBeSECONDKey|--' />

<xsl:template match="/">
  <file>
    <xsl:apply-templates select="root/item">
      <xsl:sort select="string-length(substring-after($sortOrder,
                                                      concat('|', key, '|')))"
                data-type="number"
                order="descending" />
    </xsl:apply-templates>
  </file>
</xsl:template>

<xsl:template match="item">
  <xsl:element name="{key}">
    <xsl:value-of select="value" />
  </xsl:element>
</xsl:template>

这会将 MustBeFIRSTKey 放在第一位,MustBeSECONDKey 放在第二位,然后按原始顺序放置任何其他键值。它的工作原理是根据每个键在sortOrder变量中的位置为每个键生成一个数字排序值。对于 MustBeFIRSTKey,该值将为string-length('MustBeSECONDKey|--')(18),对于 MustBeSECONDKey,该值为string-length('--')(2),对于其他任何值,该值为string-length('')(0)。

要更改标签名称,您可以扩展此方法以使用变量来编码映射:

<xsl:variable name="sortOrder" select="concat(
          '|MustBeFIRSTKey+someCustomTagName',
          '|MustBeSECONDKey+anotherNameOfATag',
          '|--' />

<xsl:template match="/">
  <file>
    <xsl:apply-templates select="root/item">
      <xsl:sort select="string-length(substring-after($sortOrder,
                                                      concat('|', key, '+')))"
                data-type="number"
                order="descending" />
    </xsl:apply-templates>
  </file>
</xsl:template>

<!-- for keys that have a mapping -->
<xsl:template match="item[substring-after($sortOrder, concat('|', key, '+'))]">
  <xsl:element name="{substring-before(
       substring-after($sortOrder, concat('|', key, '+')), '|')}">
    <xsl:value-of select="value" />
  </xsl:element>
</xsl:template>

<!-- for keys that don't -->
<xsl:template match="item">
  <xsl:element name="{key}">
    <xsl:value-of select="value" />
  </xsl:element>
</xsl:template>

编辑:对于您的第三个问题,如果您想重新映射值和键,那么我认为单独的映射文件可能是最清晰的方法。

映射.xml

<map>
  <key from="MustBeFIRSTKey" to="someCustomTagName">
    <value from="MustBeFIRSTValueUno" to="Gazpacho" />
    <value from="MustBeFIRSTValueNi" to="Sushi" />
  </key>
  <key from="MustBeSECONDKey" to="anotherNameOfATag" />
</map>

样式表

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:variable name="map" select="document('mapping.xml')/map" />

  <!-- define some keys used for looking up entries in the mapping -->
  <xsl:key name="keyMap" match="key" use="@from" />
  <xsl:key name="valMap" match="value" use="concat(../@from, '|', @from)" />

  <xsl:template match="/">
    <xsl:variable name="items" select="root/item" />
    <file>
      <!-- the key function looks up nodes in the "current document", so
           we need this for-each to switch the context to the mapping file
           for the key lookups -->
      <xsl:for-each select="$map">
        <!-- go through the keys in order -->
        <xsl:for-each select="key">
          <!-- process all items with that key... -->
          <xsl:apply-templates select="$items[key = current()/@from]">
            <!-- ... using the mapped tag name -->
            <xsl:with-param name="tagName" select="@to"/>
          </xsl:apply-templates>
        </xsl:for-each>
        <!-- now process any remaining keys that don't have a mapping -->
        <xsl:apply-templates select="$items[not(key('keyMap', key))]" />
      </xsl:for-each>
    </file>
  </xsl:template>

  <xsl:template match="item">
    <!-- tag name defaults to the key text if another name is not passed in -->
    <xsl:param name="tagName" select="key" />
    <xsl:variable name="curItem" select="." />
    <xsl:element name="{$tagName}">
      <!-- again, switch focus to the mapping file for key lookups -->
      <xsl:for-each select="$map">
        <!-- do we have a remapping for the item's *value*? -->
        <xsl:variable name="value"
            select="key('valMap', concat($curItem/key, '|', $curItem/value))" />
        <xsl:choose>
          <xsl:when test="$value"><xsl:value-of select="$value/@to" /></xsl:when>
          <xsl:otherwise><xsl:value-of select="$curItem/value" /></xsl:otherwise>
        </xsl:choose>
      </xsl:for-each>
    </xsl:element>
  </xsl:template>

</xsl:stylesheet>

如果映射文件足够小,您可以通过替换将其内联在样式表中

  <xsl:variable name="map" select="document('mapping.xml')/map" />

  <xsl:variable name="inlineMap">
    <map>
      <key from="..." to="..."/>
      <!-- ... -->
    </map>
  </xsl:variable>

  <xsl:variable name="map"
                select="document('')//xsl:variable[@name='inlineMap']/map" />

这使用了document('')使您能够访问样式表本身的 XML 树的技巧。

最后,请注意keyMap仅用于查找其键在映射文件中没有条目的项目。如果您有所有可能键的映射,则可以不使用该键(以及<xsl:apply-templates select="$items[not(key('keyMap', key))]" />使用它的键)。

于 2013-09-13T18:04:21.787 回答
1

我会先写一个模板

<xsl:template match="item">
          <xsl:element name="{key}">
            <xsl:value-of select="value" />
          </xsl:element>
</xsl:template>

现在你可以使用

  <xsl:template match="/root">
    <file>
      <xsl:apply-templates select="item[key ='MustBeFIRSTKey']"/>
      <xsl:apply-templates select="item[key = 'MustBeSECONDKey']"/>
    </file>
  </xsl:template>

使用 XSLT 2.0,您只需要

  <xsl:template match="/root">
    <file>
      <xsl:apply-templates select="item[key ='MustBeFIRSTKey'], item[key = 'MustBeSECONDKey']"/>
    </file>
  </xsl:template>

当然,对于 XSLT 1.0 和 2.0,您都可以定义一个键

<xsl:key name="k1" match="item" use="key"/>

然后将代码缩短为

  <xsl:template match="/root">
    <file>
      <xsl:apply-templates select="key('k1', 'MustBeFIRSTKey')"/>
      <xsl:apply-templates select="key('k1', 'MustBeSECONDKey')"/>
    </file>
  </xsl:template>

分别

  <xsl:template match="/root">
    <file>
      <xsl:apply-templates select="key('k1', 'MustBeFIRSTKey'), key('k1', 'MustBeSECONDKey')"/>
    </file>
  </xsl:template>

当然,根据您的预期排序,您可以使用xsl:sort而不是拼出顺序。

于 2013-09-13T17:48:35.980 回答
1

我会让这个数据驱动。在单独的 XML 片段(单独的文档或样式表的一部分)中定义项目的顺序:

<order>
  <item>mustbefirst</item>
  <item>mustbesecond</item>
</order>

并使用它来驱动样式表处理:

<xsl:template match="order">
  <xsl:for-each select="item">
    <xsl:variable name="target" select="$root/item[key=current()]">
    <xsl:if test="$target">
      <xsl:element name="$target/key"><xsl:value-of select="$target/value"/></xsl:element>
    </xsl:if>
  </xsl:for-each>
</xsl:template>

其中 $root 绑定到您的“根”元素。

于 2013-09-14T08:08:16.340 回答