XSLT, flattening results of recursive function

341 views Asked by At

I'm trying to remove nodes that are not used directly or indirectly in defined messages in an xml file. The 'linkage' is as defined below.

I'm trying to process a (fix) xml document to pull out the components that match the message components I'm interested in. Document structure is along the lines of

<fix>
    <messages>
        <message name="myMessage1">
            <field name="abc"/>
            <component name="component1"/>
            <component name="component2"/>
        </message>
       <message name="myMessage2">
           <field name="hello"/>
           <component name="component1"/>
       </message>
    </messages>
    <components>
        <component name="component1">
        </component>
        <component name="component2">
            <group name="agroup">
                <field name="afield"/>
                <component name="component3"/>
            </group>
        </component>
        <component name="component3">
        </component>
        <component name="component4">
        </component>
        <component name="component5">
        </component>
        <component name="component6">
            <group name="agroup">
                <field name="afield"/>
                <component name="component3"/>
                <component name="component4"/>
            </group>
        </component>
    </components>
</fix>

So I've managed to identify the groups in the messages I have. I'm also able to recursively able to obtain the components that are referred to by other components. I want to flatten the component nodes returned and turn them into a unique set, which is where I think my approach falls down.

 <xsl:template match="components">
     <nodes>
         <xsl:for-each select="/fix/messages/message/component">
             <xsl:call-template name="findGroups">
                 <xsl:with-param name="groups" select="@name"/>
             </xsl:call-template>
         </xsl:for-each>
     </nodes>
</xsl:template>

<xsl:template name="findGroups">
     <xsl:param name="groups" />
     <print>
         <xsl:value-of select="$groups"></xsl:value-of>
     </print>
     <xsl:for-each select="/fix/components/component[@name=$groups]">
         <here>
             <xsl:call-template name="findGroups">
                 <xsl:with-param name="groups" select="group/component/@name"/>
             </xsl:call-template>
         </here>
    </xsl:for-each>
</xsl:template>

I've seen solutions for problem similar to this using keys or other techniques, but my xslt/xpath isn't at the stage yet to allow me to adapt these, so could someone please provide assistance?

Thanks.

Hi, I should also note there will be more than one message. The aim is to remove the groups (and eventually other fields) that are not referred to in the message tags within the document. Output should then eventually be

<fix>
    <messages>
        <message name="myMessage1">
            <field name="abc"/>
            <component name="component1"/>
            <component name="component2"/>
        </message>
        <message name="myMessage2">
            <field name="hello"/>
            <component name="component1"/>
        </message>
    </messages>
    <components>
        <component name="component1">
        </component>
        <component name="component2">
            <group name="agroup">
                <field name="afield"/>
                <component name="component3"/>
            </group>
        </component>
        <component name="component3"/>
        </component>
    </components>
</fix>

Note - the component4 and other tags that were not ultimately accessible from the message nodes have been removed.

Okay, the above has been answered, I've now run into an additional problem. In reality the xml document has a number of fields at the end, along the lines of

<fix>
    <messages>
        <message/>
    <messages>
    groups>
        <group/>
    <groups>
    <fields>
        <field name="abc"/>
        <field name="bcd"/>
    <fields>
</fix>

I'm trying to retain only the fields with a name that matches either a field in any message or group location, or matches a gropu or component name. I'm able to retain most, but not all and have ended up with a nasty nested choose structure. Could someone please provide some more pointers? Thanks.

e.g. I'm trying to add the following: -

<xsl:key name="c3" match="message/field" use="@name" />
<xsl:key name="c4" match="group/field" use="@name" />
<xsl:key name="c5" match="group" use="@name" />
<xsl:key name="c6" match="component" use="@name" />

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

<xsl:template match="fields/field[not(key('c3', @name))]">
    <xsl:variable name="IsUsed">
        <xsl:apply-templates select="key('c4', @name)"/>
        <!-- <xsl:apply-templates select="key('c5', @name)"/>
        <xsl:apply-templates select="key('c6', @name)"/> -->
    </xsl:variable>
    <xsl:if test="$IsUsed != ''">
        <xsl:call-template name="identity" />
    </xsl:if>
</xsl:template>

<xsl:template match="group/field[not(key('c1', ../../@name))]" mode="IsUsed">
    <xsl:apply-templates select="key('c2', ../../@name)" mode="IsUsed"/>
</xsl:template>

<xsl:template match="group/field[key('c1', ../../@name)]" mode="IsUsed">
    <xsl:text>1</xsl:text>
</xsl:template>

which I think should match fields used in groups that are used messages. However, this is not happening.

1

There are 1 answers

4
Tim C On

First off, I would start off with the identity template

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

On this own, this copies all your nodes as-is. This means the approach you take is rather than trying to work out what you need to copy across, work out what you don't need to copy.

To aid the looking up of components, I would define two keys; one to look up the original "message" components, and one to look up the ones that occur under "group"

<xsl:key name="c1" match="message/component" use="@name" />
<xsl:key name="c2" match="group/component" use="@name" />

Now, you are only worried about the case where a component is not referenced directly by a "message"

<xsl:template match="components/component[not(key('c1', @name))]">

Within this you would start checking, with a recursive template, whether this "component" element are being referenced elsewhere.

  <xsl:variable name="IsUsed">
     <xsl:apply-templates select="key('c2', @name)" mode="IsUsed"/>
  </xsl:variable>

Although you could do one template for this "IsUsed" check, I shall do it in two:

<xsl:template match="group/component[key('c1', ../../@name)]" mode="IsUsed">
   <xsl:text>1</xsl:text>
</xsl:template>

<xsl:template match="group/component[not(key('c1', ../../@name))]" mode="IsUsed">
   <xsl:apply-templates select="key('c2', ../../@name)" mode="IsUsed"/>
</xsl:template>

So, the first template will match when the parent component is being used by a "message", and return "1", if so, otherwise the second template will match, and do a recursive call to keep on matching.

Try this XSLT:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="xml" indent="yes"/>
   <xsl:key name="c1" match="message/component" use="@name" />
   <xsl:key name="c2" match="group/component" use="@name" />

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

   <xsl:template match="components/component[not(key('c1', @name))]">
      <xsl:variable name="IsUsed">
         <xsl:apply-templates select="key('c2', @name)" mode="IsUsed"/>
      </xsl:variable>
      <xsl:if test="$IsUsed != ''">
         <xsl:call-template name="identity" />
      </xsl:if>
   </xsl:template>

   <xsl:template match="group/component[not(key('c1', ../../@name))]" mode="IsUsed">
      <xsl:apply-templates select="key('c2', ../../@name)" mode="IsUsed"/>
   </xsl:template>

   <xsl:template match="group/component[key('c1', ../../@name)]" mode="IsUsed">
      <xsl:text>1</xsl:text>
   </xsl:template>
</xsl:stylesheet>