XML XSLT Tutorial

< appboard | 2.4 | builder | data sources | web service

Included with the distribution under ${application.home}/stylesheets/ are some example XSL files that correspond to a variety of sample XML files included under ${application.home}/data/pkg/sample/. You can also use these as guides when you create your own XSL files. To see the result of a transformation (and to help debug custom XSL files), you can use the 'Transform' option when running portal.bat or portal.sh. To do this, in the command line navigate to [INSTALL_HOME]/server/bin/ and run portal Transform to display the possible arguments you can use when doing an XML/XSL transform.

An example of a standard command:

portal Transform -in example.xml -xsl example.xsl -out output.xml

This will take the input XML file and the indicated XSL file, do a transformation, and output the resulting XML to output.xml

Tutorial

Using ${application.home}/data/pkg/sample/hierarchy/hierarchy.xml as an example, this tutorial builds the XSL Transformation in a step-by-step manner. It will be helpful in following the tutorial to have the source XML and the target XML in separate windows or printed out.

1. Review the basic structure of your source XML.

This example consists of a single root node ("rspec") with some descriptive child elements one of which ("computeResource") contains nested information on compute nodes which contain further information including network interfaces. In outline form:
  • rspec
    • aggregate
    • description
    • lifetime
    • computeResource
      • node
        • networkInterface
      • node
        • networkInterface
      • node
        • networkInterface

2. Identify the data elements you want to transform into AppBoard Entities.

In this case, we are interested in nodes and networkInterfaces which we will call Nodes and NICs.

3. Begin the XSLT with the xsl:stylesheet element and an xsl:output element:

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


4. Identify an element that has only a single instance and is the parent of all of the data elements you wish to capture as Entities. In many cases, that would be the root element ("rspec" in this example), but because there is only one "computeResource" instance we can reference it directly as "/rspec/computeResource".


5. Add a match template for that data element that emits the "result" element and definitions for the entities we will capture:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml" version="1.0">
    <xsl:output method="xml" indent="yes" encoding="UTF-8"/>
    <xsl:template match="/rspec/computeResource">
        <result xmlns="http://www.edgeti.com/xml-data">
            <entity name="Nodes">
                <attributes>
                    <attribute primaryKey="true">NodeId</attribute>
                    <attribute>Address</attribute>
                </attributes>
            </entity>
            <entity name="NICs">
                <attributes>
                    <attribute primaryKey="true">NICId</attribute>
                    <attribute>Address</attribute>
                </attributes>
            </entity>
        </result>            
    </xsl:template>
</xsl:stylesheet>
This only works for single instance elements. If there were multiple "computeResource" elements we would start with "/rspec" even if we weren't capturing an Entity corresponding to "computeResource" instances.

6. Add do-nothing templates for the peers of "computeResource" that we plan to ignore:

<xsl:template match="/rspec/aggregate"/>
<xsl:template match="/rspec/description"/>
<xsl:template match="/rspec/lifetime"/>


7. Add a match template for the "node" elements to construct "record" elements for each instance:

<template match="node">
    <record>
        <value name="NodeId"><value-of select="@id"/></value>
        <value name="Address"><value-of select="address"/></value>
    </record>
</template>
Note the use of "@id" to extract an attribute of the "node" element and the use of "address" to extract the value in a child element.

8. Add a match template for the "networkInterface" elements. Note that we use "node/networkInterface" since that is the relative "path" from our "computeResource" element.

<xsl:template match="node/networkInterface">
    <record>
        <value name="NICId"><xsl:value-of select="@id"/></value>
        <value name="Address"><xsl:value-of select="ipAddress"/></value>
        <value name="NodeId"><xsl:value-of select="../@id"/></value>
    </record>
</xsl:template>


9. Place these after the template for "computeResource" and then add xsl:apply-templates elements to reference them to construct children of the "entity" elements in the output:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml" version="1.0">
    <xsl:output method="xml" indent="yes" encoding="UTF-8"/>
    <xsl:template match="/rspec/computeResource">
        <result xmlns="http://www.edgeti.com/xml-data">
            <entity name="Nodes">
                <attributes>
                    <attribute primaryKey="true">NodeId</attribute>
                    <attribute>Address</attribute>
                </attributes>
                <xsl:apply-templates select="node"/>
            </entity>
            <entity name="NICs">
                <attributes>
                    <attribute primaryKey="true">NICId</attribute>
                    <attribute>Address</attribute>
                </attributes>
                <xsl:apply-templates select="node/networkInterface"/>
            </entity>
        </result>            
    </xsl:template>
    <xsl:template match="/rspec/aggregate"/>
    <xsl:template match="/rspec/description"/>
    <xsl:template match="/rspec/lifetime"/>
    <template match="node">
        <record>
            <value name="NodeId"><value-of select="@id"/></value>
            <value name="Address"><value-of select="address"/></value>
        </record>
    </template>
    <xsl:template match="node/networkInterface">
        <record>
            <value name="NICId"><xsl:value-of select="@id"/></value>
            <value name="Address"><xsl:value-of select="ipAddress"/></value>
        </record>
    </xsl:template>
</xsl:stylesheet>
Now we have a working XSL Transform that defines our two entities and constructs all of the records. But, the hierarchy of the data gives us an association that we aren't capturing.

10. Define the association in the "Nodes" entity:

<associations>
    <association fromKey="NodeId" toEntity="NICs" toKey="NodeId"/>
</associations>


11. Add an attribute to "NICs" to store the key:

<entity name="NICs">
    <attributes>
        <attribute primaryKey="true">NICId</attribute>
        <attribute>Address</attribute>
        <attribute>NodeId</attribute>
    </attributes>
</entity>


12. Use "../@id" in an xsl:value-of to add a "NodeId" value to the instances of "NICs".

<xsl:template match="node/networkInterface">
    <record>
        <value name="NICId"><xsl:value-of select="@id"/></value>
        <value name="Address"><xsl:value-of select="ipAddress"/></value>
        <value name="NodeId"><xsl:value-of select="../@id"/></value>
    </record>
</xsl:template>

Resulting XSL:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml" version="1.0">
    <xsl:output method="xml" indent="yes" encoding="UTF-8"/>
    <xsl:template match="/rspec/computeResource">
        <result xmlns="http://www.edgeti.com/xml-data">
            <entity name="Nodes">
                <attributes>
                    <attribute primaryKey="true">NodeId</attribute>
                    <attribute>Address</attribute>
                </attributes>
                <associations>
                    <association fromKey="NodeId" toEntity="NICs" toKey="NodeId"/>
                </associations>
                <xsl:apply-templates select="node"/>
            </entity>
            <entity name="NICs">
                <attributes>
                    <attribute primaryKey="true">NICId</attribute>
                    <attribute>Address</attribute>
                    <attribute>NodeId</attribute>
                </attributes>
                <xsl:apply-templates select="node/networkInterface"/>
            </entity>
        </result>            
    </xsl:template>
    <xsl:template match="/rspec/aggregate"/>
    <xsl:template match="/rspec/description"/>
    <xsl:template match="/rspec/lifetime"/>
    <template match="node">
        <record>
            <value name="NodeId"><value-of select="@id"/></value>
            <value name="Address"><value-of select="address"/></value>
        </record>
    </template>
    <xsl:template match="node/networkInterface">
        <record>
            <value name="NICId"><xsl:value-of select="@id"/></value>
            <value name="Address"><xsl:value-of select="ipAddress"/></value>
            <value name="NodeId"><xsl:value-of select="../@id"/></value>
        </record>
    </xsl:template>
</xsl:stylesheet>