XSLT copy all and replace on conditions

2019-08-18 00:15发布

I need an XSLT transform that copies the whole input document to the output document, inserting a specific element with a default value if the input document does not contain that element.

Specifically, I want to transform input XML of this general form

<Begin>
    <tag1>a</tag1>
    <tag2>b</tag2>
</Begin>

to itself, but if the input has no /Begin/tag1 then I want to provide one. That is, I want to transform

<Begin>
    <tag2>c</tag2>
</Begin>

to

<Begin>
    <tag1>x</tag1>
    <tag2>c</tag2>
</Begin>

This is my my current XSLT:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" indent="yes" omit-xml-declaration="yes"/>
  <xsl:strip-space elements="*"/>

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

</xsl:stylesheet>

It copies the whole input document, but it does not provide for supplying a default <tag1> element. I cannot use <xsl:choose> inside the template in that stylesheet, as it will insert a <tag1> into ever element that does not have one, and I cannot place a choose after the template as it's not valid. How can I provide a default <tag1> element only for the <Begin> element?

标签: xml xslt
2条回答
叼着烟拽天下
2楼-- · 2019-08-18 00:47

You are correct that you cannot put an <xsl:choose> element outside a template. XSL operates by transforming nodes of the input document; you can express the details of the transformation only in terms of how specific nodes are transformed -- i.e. via templates.

Moreover, it is usually more natural to express alternatives by providing alternative templates or select expressions than by using logical elements such as <xsl:choose> of <xsl:if> inside templates.

In this case you have two alternatives to consider:

  1. The input document's /Begin element has a tag1 child
  2. The input document's /Begin element does not have a tag1 child

In the first case, you don't need to do anything special -- the identity transform you've already written will do the right thing. All you need, then, is an appropriate template to handle the latter alternative. As long as it has higher priority than the identity transform, it will be used for the Begin element instead of the identity transform, wherever it applies.

For example,

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" indent="yes" omit-xml-declaration="yes"/>
  <xsl:strip-space elements="*"/>

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

  <xsl:template match="Begin[count(tag1) = 0]">
    <xsl:copy>
      <tag1>x</tag1>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Note the added template, which applies (only) to Begin elements that have no <tag1> child. It copies the whole element and its child nodes, but also inserts a <tag1>.

查看更多
做个烂人
3楼-- · 2019-08-18 01:09

You can add a template that matches Begin if it has no tag1 and inserts the tag1. Note here that this works for your very simple example, but if you have a more complex case with a lot of elements or multiple elements to insert, then the solution could become more complex.

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" indent="yes" omit-xml-declaration="yes"/>
  <xsl:strip-space elements="*"/>

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

  <xsl:template match="Begin[not(tag1)]">
    <xsl:copy>
      <xsl:apply-templates select="@*" />
      <tag1>x</tag1>
      <xsl:apply-templates select="node()" />
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
查看更多
登录 后发表回答