0

I'm a beginner in XSLT programming. I've got the task to transform the following xml:

<Test>TestA::test1</Test>
<Test>TestA::test2</Test>
<Test>TestB::test3</Test>
<Test>TestB::test4</Test>

The output xml shall look like this:

<Class id="TestA">
    <Method id="test1"/>
    <Method id="test2"/>
</Class>
<Class id="TestB">
    <Method id="test3"/>
    <Method id="test4"/>
</Class>

The input xml contains the names of CppUnit test cases in C++ style (pattern Class::Method). I've tried a lot of different approaches and read myriad of stackoverflow threds, but couldn't find a solution.

I have to solve the problem using XSLT 1.0.

Thanks in advance, mexl

mexl916
  • 3
  • 3

2 Answers2

1

This is basically a grouping problem, to be solved (in XSLT 1.0) by Muenchian grouping with a (very slight) twist. However, first your input must have a root element - otherwise it's not an XML document:

<root>
    <Test>TestA::test1</Test>
    <Test>TestA::test2</Test>
    <Test>TestB::test3</Test>
    <Test>TestB::test4</Test>
</root>

With that in place, the following stylesheet:

XSLT 1.0

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

<xsl:key name="k" match="Test" use="substring-before(., '::')" />

<xsl:template match="/">
    <output>
        <xsl:for-each select="root/Test[count(. | key('k', substring-before(., '::'))[1]) = 1]">
            <Class id="{substring-before(., '::')}">
                <xsl:for-each select="key('k', substring-before(., '::'))">
                     <Method id="{substring-after(., '::')}"/>
                </xsl:for-each>
            </Class>
        </xsl:for-each>
    </output>
</xsl:template>

</xsl:stylesheet>

will return:

<?xml version="1.0" encoding="UTF-8"?>
<output>
   <Class id="TestA">
      <Method id="test1"/>
      <Method id="test2"/>
   </Class>
   <Class id="TestB">
      <Method id="test3"/>
      <Method id="test4"/>
   </Class>
</output>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Nice answer, and submitted quicker than mine. There is nothing wrong with this answer, but I would recommend, though, looking at using `apply-templates` instead of nested `for-each` As your templates and/or input data change over time, you'll be glad you did. See this discussion for some good points for either approach: http://stackoverflow.com/questions/6342902/for-loops-vs-apply-templates and this blog for a stronger argument against `for-each` http://gregbee.ch/blog/using-xsl-for-each-is-almost-always-wrong – biscuit314 Sep 19 '14 at 14:46
  • The outer for-each could be changed. The inner for-each is required to change the context node and avoids using "//Test" which is bad for performance when the doc is large. Good solutions – ljdelight Sep 19 '14 at 15:09
  • @biscuit314 I've heard the *argument* countless times. I am still waiting for someone to come up with a *reason*. Just saying, not looking for a discussion. – michael.hor257k Sep 19 '14 at 15:17
  • @michael.hor257k - yeah, I try to avoid such debates too. What's good is what works. But I've found this does improve extensibility and maintainability. There are those who exaggerate the benefits. In a very small application there's not much to be gained either way. But I've seen it for myself: if there's a chance of growing or changing over time, having a bunch of tiny single-purpose templates makes life easier. Adjusting `apply-templates` amongst an eco-system of such templates makes adapting to change more natural, less risky, and leads to surprising re-use opportunities. – biscuit314 Sep 19 '14 at 15:33
0

Use a combination of xsl:key / generate-id, substring-before /substring-after, as follows:

Given

<Tests>
  <Test>TestA::test1</Test>
  <Test>TestA::test2</Test>
  <Test>TestB::test3</Test>
  <Test>TestB::test4</Test>
</Tests>

Use this template:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output indent="yes" method="xml" encoding="UTF-8"/>

<xsl:key name="testClassKey" match="Test" use="substring-before(., '::')"/>

<xsl:template match="Test" mode="asMethod">
  <xsl:variable name="methodId" select="substring-after(., '::')" />

  <Method id="{$methodId}" />
</xsl:template>


<xsl:template match="Test">
  <xsl:variable name="thisTest" select="." />
  <xsl:variable name="classId" select="substring-before(., '::')" />

  <Class id="{$classId}">
    <xsl:apply-templates select="//Test[substring-before(., '::') = $classId]" mode="asMethod"/>
  </Class>
</xsl:template>


<xsl:template match="Tests">
  <TestClasses>
    <xsl:apply-templates select="Test[generate-id(.) = generate-id(key('testClassKey', substring-before(., '::'))[1])]" />
  </TestClasses>
</xsl:template>
</xsl:stylesheet>

To get this result:

<?xml version="1.0" encoding="UTF-8"?>
<TestClasses>
  <Class id="TestA">
    <Method id="test1" />
    <Method id="test2" />
  </Class>
  <Class id="TestB">
    <Method id="test3" />
    <Method id="test4" />
  </Class>
</TestClasses>
biscuit314
  • 2,384
  • 2
  • 21
  • 29