1
votes

I have some XML that looks like this:

<root>
  <message name="peter">
    <field type="integer" name="pa" />
    <group name="foo">
      <field type="integer" name="action" />
      <field type="integer" name="id" />
      <field type="integer" name="value" />
    </group>
  </message>
  <message name="wendy">
    <field type="string" name="wa" />
    <group name="foo">
      <field type="integer" name="action" />
      <field type="integer" name="id" />
      <field type="integer" name="value" />
    </group>
  </message>
</root>

I have some XSL that I'm using to generate Java code from this XML. Previously I've been making a key, then generating a Java class for each group.

<xsl:key name="groupsByName" match="//group" use="@name"/>
....
<xsl:for-each select="//group[generate-id(.) = generate-id(key('groupsByName',@name)[1])]">
  <xsl:call-template name="class-for-group"/>
</xsl:for-each>

All was well. Now, I've discovered that some messages have groups using the same name as groups present elsewhere, but missing one of the fields. To continue the example XML from above:

  <message name="nana">
    <field type="string" name="na" />
    <group name="foo">
      <field type="integer" name="id" />
      <field type="integer" name="value" />
    </group>
  </message>

A group named "foo" is present, but it's missing the field with name "action".

What I'd like to do is to generate a Java class for each unique subtree. Is this possible? I can't work out what the xsl:key for that would look like. The closest idea I've had is

<xsl:key name="groupsKey" match="//group" use="concat(@name,count(*))"/>

which works for the case in the example above, but is hardly elegant. If there were instead two groups named "foo" with the same number (but different types) of fields, it would fail, so it's not actually a solution.

To be clear, the ideal key (or whatever alternative) would end up calling the template only once for the "peter" and "wendy" cases above, once for the "nana" case and again once for this case:

  <message name="hook">
    <field type="string" name="ha" />
    <group name="foo">
      <field type="string" name="favourite_breakfast" />
      <field type="integer" name="id" />
      <field type="integer" name="value" />
    </group>
  </message>

...because the fields within the group are different to those in the other cases. My key above doesn't cover this case. Is there a way to do so?

1
So, what is the question? In case you are asking for someone to provide a "more ellegant" solution, then you must define "ellegant". - Dimitre Novatchev
@Dimitre, the key I've provided above doesn't actually solve the problem. It's a nasty hack. I'll clarify. - Jon Bright
Also, you must show the exact output that must be produced -- otherwise it is not defined and this clearly isn't a problem... - Dimitre Novatchev
@Jon: Also, it is not clear what exactly is "each unique subtree". - Dimitre Novatchev
@Dimitre, you don't need to worry about the output. I want it to call a template once for each unique subtree. What's in the template is irrelevant? - Jon Bright

1 Answers

1
votes

This transformation fulfills the requirements:

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

 <xsl:key name="kGroupByType" match="group"
  use="@type"/>

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

 <xsl:template match="/">
  <xsl:variable name="vrtfPass1">
   <xsl:apply-templates />
  </xsl:variable>

  <xsl:apply-templates mode="pass2" 
   select="ext:node-set($vrtfPass1)/*"/>
 </xsl:template>

 <xsl:template match="group">
  <xsl:copy>
   <xsl:apply-templates select="@*"/>
   <xsl:call-template name="makeType"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template mode="pass2"
   match="group[generate-id()
               =
                generate-id(key('kGroupByType',@type)[1])
               ]
         ">
  class <xsl:value-of select="concat(@name, '|', @type)"/>

 </xsl:template>

 <xsl:template name="makeType">
  <xsl:attribute name="type">
   <xsl:text>(</xsl:text>
   <xsl:for-each select="*">
     <xsl:value-of select="@type"/>
     <xsl:if test="not(position()=last())">+</xsl:if>
   </xsl:for-each>
   <xsl:text>)</xsl:text>
  </xsl:attribute>
 </xsl:template>
</xsl:stylesheet>

When applied on the provided XML document (with all additions):

<root>
    <message name="peter">
        <field type="integer" name="pa" />
        <group name="foo">
            <field type="integer" name="action" />
            <field type="integer" name="id" />
            <field type="integer" name="value" />
        </group>
    </message>
    <message name="wendy">
        <field type="string" name="wa" />
        <group name="foo">
            <field type="integer" name="action" />
            <field type="integer" name="id" />
            <field type="integer" name="value" />
        </group>
    </message>
    <message name="nana">
        <field type="string" name="na" />
        <group name="foo">
            <field type="integer" name="id" />
            <field type="integer" name="value" />
        </group>
    </message>
    <message name="hook">
        <field type="string" name="ha" />
        <group name="foo">
            <field type="string" name="favourite_breakfast" />
            <field type="integer" name="id" />
            <field type="integer" name="value" />
        </group>
    </message>
</root>

the wanted result is produced:

  class foo|(integer+integer+integer)
  class foo|(integer+integer)
  class foo|(string+integer+integer)

It is left as an exercise to the reader to further adjust this to produce valid names in one's PL, and also to make this work with structures of unlimited nestedness (which I may do in another answer -- however, we need a more precise definition for this more general provlem).