0

I need to transform some XML using XLST 1.0 in Visual Studio 2013.

I have the following XML:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <MessageTemplates>
    <MessageTemplate>
      <Segment name="Uno" cardinality="first">
        <value>something</value>
      </Segment>
      <Segment name="Dos" cardinality="second">
        <value>something</value>
      </Segment>
      <Segment name="Tres" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Quatro" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Cinco" cardinality="second">
        <value>something</value>
      </Segment>
      <Segment name="Seis" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Siete" cardinality="first">
        <value>something</value>
      </Segment>
    </MessageTemplate>
  </MessageTemplates>
</root>

The cardinality attribute of the Segment node is ordinal, first being the highest, and third being the lowest. I need to create nested levels, based on cardinality, as follows:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <MessageTemplates>
    <MessageTemplate>
      <Cardinality type="first">
        <Segment name="Uno">
          <value>something</value>
        </Segment>
        <Cardinality type="second">
          <Segment name="Dos">
            <value>something</value>
          </Segment>
          <Cardinality type="third">
            <Segment name="Tres">
              <value>something</value>
            </Segment>
            <Segment name="Quatro">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Segment name="Cinco">
            <value>something</value>
          </Segment>
          <Cardinality type="third">
            <Segment name="Seis">
              <value>something</value>
            </Segment>
          </Cardinality>
        </Cardinality>
        <Segment name="Siete">
          <value>something</value>
        </Segment>
      </Cardinality>
    </MessageTemplate>
  </MessageTemplates>
</root>

I have tried several different ways to transform this file, but all have failed. I've searched SO and read dozens of posts, but haven't found any cases that match what I am trying to do. I have also tried searching for incremental ways to accomplish my goal, such as only processing one Segment at a time with recursive template calls, etc. The closest I have come is with the following XSLT:

<xsl:template match="MessageTemplates/MessageTemplate">
  <MessageTemplate>
    <xsl:copy-of select="@*"/>
    <xsl:call-template name="cardinality"/>
  </MessageTemplate>
</xsl:template>

<xsl:template name="cardinality" match="MessageTemplates/MessageTemplate/Segment">
  <xsl:choose>
    <xsl:when test="position() = 1">
      <Cardinality type="{Segment/@cardinality}">
        <Segment>
          <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
        </Segment>
      </Cardinality>
    </xsl:when>

    <xsl:when test="position() != last() and following-sibling::Segment/@cardinality != @cardinality">
      <Cardinality type="{@cardinality}">
        <Segment>
          <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
        </Segment>
      </Cardinality>
    </xsl:when>

    <xsl:when test="position() = last()">
      <Segment>
        <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
      </Segment>
    </xsl:when>
  </xsl:choose>
</xsl:template>

Which produced the following XML:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <Version>1.0</Version>
  <MessageTemplates>
    <MessageTemplate>
      <Cardinality type="first">
        <Segment>
          <Cardinality type="">
            <Segment name="Uno">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="second">
            <Segment name="Dos">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Tres">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Quatro">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="second">
            <Segment name="Cinco">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Seis">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Segment name="Siete">
            <value>something</value>
          </Segment>
        </Segment>
      </Cardinality>
    </MessageTemplate>
  </MessageTemplates>
</root>

Basically, what I want is to wrap all Segment nodes in a single Cardinality node. Then, if the cardinality value of the next Segment is lower than the cardinality value of the current Segment, I want to wrap all following Segment nodes in a Cardinality node, as long as the cardinality value is the same. I want this to happen for each cardinality level. Finally, I want to move the cardinality value of the Segment to the type attribute of the Cardinality node. The order of the Segment nodes must be maintained.

Any help would be greatly appreciated.

6
  • You expect a computer to understand that "second" is a child of "first" and "third" is a child of "second"? Commented Apr 21, 2016 at 20:34
  • Those values are just for example, but I do see your point. The order that the unique cardinality values appear in the XML will ultimately dictate the nesting order. Commented Apr 21, 2016 at 20:54
  • That's going to take some work. BTW, why aren't Uno and Siete siblings in the result? They both have the same cardinality of "first". Commented Apr 21, 2016 at 21:05
  • Yeah, I just noticed that myself, and was looking for the 'Edit' button. LOL Commented Apr 21, 2016 at 21:07
  • Which XSLT 1.0 processor will you be using? You could probably take advantage of some extensions, if your processor supports them. Commented Apr 21, 2016 at 21:09

2 Answers 2

1

Here is a recursive approach. It does produce the required output, at least for the given example. I'm not really happy with it. It is not really reliable, nor fast, nor maintainable, but at least it gives you the basic idea. (If there is no better one)

<xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" />


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

    <xsl:template match="Segment/@cardinality"  />

    <xsl:template match="MessageTemplate">
        <xsl:copy>
            <Cardinality type="first">
                <xsl:apply-templates select="Segment[1]" mode="nested" >
                    <xsl:with-param name="currentcardinality" select="'first'" />
                </xsl:apply-templates>
            </Cardinality>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="comapreNext">
        <xsl:variable name="this" select="@cardinality" />
        <xsl:variable name="next" select="following-sibling::Segment[1]/@cardinality" />
        <xsl:choose>
            <xsl:when test="$this= $next" >
                <xsl:text>eq</xsl:text>
            </xsl:when>
            <xsl:when test="($this='first' and ($next = 'second' or $next = 'third') ) or
                                ($this='second' and ( $next = 'third') )" >
                <xsl:text>lt</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:text>gt</xsl:text>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template match="Segment"  mode="nested">
        <xsl:param name="currentcardinality"/>
        <xsl:variable name="this" select="." />
        <xsl:variable name="next">
            <xsl:call-template name="comapreNext"/>
        </xsl:variable>
        <xsl:variable name="next_le" select="$next='lt' or $next = 'eq'" />
        <xsl:choose>
            <xsl:when test="@cardinality = $currentcardinality  ">
                <!-- copy Segment without cardinality -->
                <xsl:apply-templates select="."  />
                <xsl:apply-templates select="following-sibling::Segment[1][$next_le]" mode="nested" >
                    <xsl:with-param name="currentcardinality" select="@cardinality" />
                </xsl:apply-templates>
            </xsl:when>
            <xsl:otherwise>
                <Cardinality type="{@cardinality}" >
                    <xsl:apply-templates select="."  />
                    <xsl:apply-templates select="following-sibling::Segment[1][$next_le]" mode="nested" >
                        <xsl:with-param name="currentcardinality" select="@cardinality" />
                    </xsl:apply-templates>
                    <xsl:if test="@cardinality = 'second'  ">
                        <!-- find same cardinality but not next -->
                        <xsl:apply-templates select="(following-sibling::Segment[position() != 1][not(@cardinality ='third')])[1][@cardinality = $this/@cardinality]" mode="nested" >
                            <xsl:with-param name="currentcardinality" select="@cardinality" />
                        </xsl:apply-templates>
                    </xsl:if>
                </Cardinality>
            </xsl:otherwise>
        </xsl:choose>
        <xsl:if test="@cardinality = 'first'  ">
            <!-- find same cardinality but not next -->
            <xsl:apply-templates select="(following-sibling::Segment[position() != 1])[@cardinality = $this/@cardinality][1]" mode="nested" >
                <xsl:with-param name="currentcardinality" select="@cardinality" />
            </xsl:apply-templates>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

Which generate the following output:

<MessageTemplates>
  <MessageTemplate>
  <Cardinality type="first">
    <Segment name="Uno">
      <value>something</value>
    </Segment>
    <Cardinality type="second">
      <Segment name="Dos">
        <value>something</value>
      </Segment>
      <Cardinality type="third">
        <Segment name="Tres">
          <value>something</value>
        </Segment>
        <Segment name="Quatro">
          <value>something</value>
        </Segment>
      </Cardinality>
      <Segment name="Cinco">
        <value>something</value>
      </Segment>
      <Cardinality type="third">
        <Segment name="Seis">
          <value>something</value>
        </Segment>
      </Cardinality>
    </Cardinality>
    <Segment name="Siete">
      <value>something</value>
    </Segment>
  </Cardinality>
 </MessageTemplate>
</MessageTemplates>
Sign up to request clarification or add additional context in comments.

2 Comments

I have tested your solution, and it does work for the given example, so I'm accepting this as the answer. However, I found that when I have a file like 'first', 'first', 'third', and 'first', the last 'first' segment is repeated in the output file. (And yes, I am regretting calling my cardinalities 'first', 'second', and 'third'.) Can you suggest which section of the match="Segment" template I should focus on to resolve this?
Try to changes the last if to <xsl:if test="@cardinality = 'first' and not(following-sibling::Segment[1][@cardinality = 'first']) ">
1

Here's something you could use as your starting point.

It uses the Muenchian grouping method to generate a distinct list of cardinalities, in the order in which they appear in the source XML document.

Starting with the first cardinality in the list, each cardinality fetches the matching segments, then recurses to the next cardinality on the list - thus the desired nesting is achieved.

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:strip-space elements="*"/>
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:key name="segment-by-cardinality" match="Segment" use="@cardinality" />

<xsl:variable name="cardinalities">
    <!-- generate a distinct list of cardinalities -->
    <xsl:for-each select="root/MessageTemplates/MessageTemplate/Segment[count(. | key('segment-by-cardinality', @cardinality)[1]) = 1]">
        <Cardinality type="{@cardinality}"/>
    </xsl:for-each>
</xsl:variable>
<xsl:variable name="cardinalities-set" select="exsl:node-set($cardinalities)/Cardinality" />

<xsl:variable name="source-doc" select="/" />

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

<xsl:template match="MessageTemplate">
    <xsl:copy>
        <!-- start with the top-level cardinality -->
        <xsl:apply-templates select="$cardinalities-set[1]"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="Cardinality">
    <xsl:variable name="type" select="@type" />
    <xsl:copy>  
        <xsl:copy-of select="@*"/>
        <!-- switch the context back to the XML source in order to use key -->
        <xsl:for-each select="$source-doc">
            <xsl:apply-templates select="key('segment-by-cardinality', $type)"/>
        </xsl:for-each>
        <!-- proceed to the next cardinality in the list -->
        <xsl:apply-templates select="following-sibling::Cardinality[1]"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet> 

Applied to your example input, the result will be:

<?xml version="1.0" encoding="utf-8"?>
<root>
   <MessageTemplates>
      <MessageTemplate>
         <Cardinality type="first">
            <Segment name="Uno" cardinality="first">
               <value>something</value>
            </Segment>
            <Segment name="Siete" cardinality="first">
               <value>something</value>
            </Segment>
            <Cardinality type="second">
               <Segment name="Dos" cardinality="second">
                  <value>something</value>
               </Segment>
               <Segment name="Cinco" cardinality="second">
                  <value>something</value>
               </Segment>
               <Cardinality type="third">
                  <Segment name="Tres" cardinality="third">
                     <value>something</value>
                  </Segment>
                  <Segment name="Quatro" cardinality="third">
                     <value>something</value>
                  </Segment>
                  <Segment name="Seis" cardinality="third">
                     <value>something</value>
                  </Segment>
               </Cardinality>
            </Cardinality>
         </Cardinality>
      </MessageTemplate>
   </MessageTemplates>
</root>

Note that this does not match your requirement that "The order of the Segment nodes must be maintained". I don't fully understand this requirement. If you have some criteria by which the children of a Cardinality (i.e. its Segments and the next-higher Cardinality) should be sorted, you could do that in another pass. But since the next-higher Cardinality can contain several Segments, some of which may preceed some of the current Segments, and some not, I don't quite see what the "correct" order is.

2 Comments

Thanks @Michael.hor257k. I appreciate the effort. That is similar to my result when I tried Muenchian grouping. However, the XML that I am attempting to transform is used to drive a message generation engine, so the order of the Segments is crucial. The cardinality I have named 'first' in this example is used for the message header and footer. 'second', identifies a segment that can repeat in the message. 'third' can also repeat, but only within 'second'. I know that my example names did not indicate their functionality, but that is the reasoning behind the order requirement.
I am afraid the logic that needs to applied here still escapes me. I don't know how I would do this if I were doing this manually - so I cannot suggest an algorithm either.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.