user982124
user982124

Reputation: 4610

Transform Dynamic XML using XSLT

I'm new to XSLT and have managed to work with XML where the nodes are repeated for each row. I've been given some XML to import that has different elements for each patient record, for example:

<?xml version="1.0" encoding="utf-8"?>
<data>
    <patient>
        <link_id>123</link_id>
        <diagnoses>
            <diabetes_type2>
                <diabetes_type2_active>True</diabetes_type2_active>
                <diabetes_type2_description>diabetes mellitus</diabetes_type2_description>
                <diabetes_type2_diagnosis_date>06051999</diabetes_type2_diagnosis_date>
            </diabetes_type2>
        </diagnoses>
    </patient>
    <patient>
        <link_id>456</link_id>
        <diagnoses>
            <chd>
                <chd_active>True</chd_active>
                <chd_description>ischaemic heart disease</chd_description>
                <chd_diagnosis_date>05071997</chd_diagnosis_date>
            </chd>
            <coad>
                <coad_active>True</coad_active>
                <coad_description>chronic obstructive airways disease</coad_description>
                <coad_diagnosis_date>28011986</coad_diagnosis_date>
            </coad>
            <depression>
                <depression_active>True</depression_active>
                <depression_description>depression</depression_description>
                <depression_diagnosis_date>28011986</depression_diagnosis_date>
            </depression>
            <myocardial_infarction>
                <myocardial_infarction_active>True</myocardial_infarction_active>
                <myocardial_infarction_description>myocardial infarction</myocardial_infarction_description>
                <myocardial_infarction_diagnosis_date>05071997</myocardial_infarction_diagnosis_date>
            </myocardial_infarction>
            <osteoarthritis>
                <osteoarthritis_active>True</osteoarthritis_active>
                <osteoarthritis_description>osteoarthritis of the knee</osteoarthritis_description>
                <osteoarthritis_diagnosis_date>28011986</osteoarthritis_diagnosis_date>
            </osteoarthritis>
            <stroke>
                <stroke_active>True</stroke_active>
                <stroke_description>cerebrovascular accident</stroke_description>
                <stroke_diagnosis_date>01011996</stroke_diagnosis_date>
            </stroke>
        </diagnoses>
    </patient>
</data>

I need to import the diagnoses values but I don't want to hard code all the hundreds of possible values that could appear. I was hoping there was a way I could dynamically reference these regardless of their element name. I would typically use something like this:

    <xsl:for-each select="./data/patient/diagnoses">
            <ROW MODID="" RECORDID="">
                <COL>
                    <DATA>
                        <xsl:value-of select="../../link_id"/>
                    </DATA>
                </COL>
                <COL>
                    <DATA>
                        <xsl:value-of select="./type"/>
                    </DATA>
                </COL>
                <COL>
                    <DATA>
                        <xsl:value-of select="./description"/>
                    </DATA>
                </COL>
                <COL>
                    <DATA>
                        <xsl:value-of select="./active"/>
                    </DATA>
                </COL>
                <COL>
                    <DATA>
                        <xsl:value-of select="./diagnosis_date"/>
                    </DATA>
                </COL>
            </ROW>
        </xsl:for-each>

but not sure how to modify this for the dynamic elements that I'm now working with.

Upvotes: 1

Views: 697

Answers (3)

Sean B. Durkin
Sean B. Durkin

Reputation: 12729

How about .... (This is a better and simpler solution that the one accepted by the OP!)

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output omit-xml-declaration="yes" indent="yes" encoding="UTF-8" />
<xsl:strip-space elements="*" />  

<xsl:template match="data">
  <TABLE>
    <HEADER>
      <COL><DATA>Link</DATA></COL>
      <COL><DATA>Type</DATA></COL>
      <COL><DATA>active</DATA></COL>
      <COL><DATA>description</DATA></COL>
      <COL><DATA>diagnosis_date</DATA></COL>
    </HEADER>
    <xsl:apply-templates select="patient/diagnoses/*" />
  </TABLE>
</xsl:template>

<xsl:template match="diagnoses/*">
  <ROW>
    <COL><DATA><xsl:value-of select="../../link_id" /></DATA></COL>
    <COL><DATA><xsl:value-of select="local-name()" /></DATA></COL>
    <xsl:apply-templates />    
  </ROW>
</xsl:template>

<xsl:template match="*">
  <COL><DATA><xsl:value-of select="." /></DATA></COL>
</xsl:template>  

</xsl:stylesheet>

When applied to your provided input document, this transform yields output ...

<table>
    <header>
        <col>
            <data>Link</data>
        </col>
        <col>
            <data>Type</data>
        </col>
        <col>
            <data>active</data>
        </col>
        <col>
            <data>description</data>
        </col>
        <col>
            <data>diagnosis_date</data>
        </col>
    </header>
    <row>
        <col>
            <data>123</data>
        </col>
        <col>
            <data>diabetes_type2</data>
        </col>
        <col>
            <data>True</data>
        </col>
        <col>
            <data>diabetes mellitus</data>
        </col>
        <col>
            <data>06051999</data>
        </col>
    </row>
    <row>
        <col>
            <data>456</data>
        </col>
        <col>
            <data>chd</data>
        </col>
        <col>
            <data>True</data>
        </col>
        <col>
            <data>ischaemic heart disease</data>
        </col>
        <col>
            <data>05071997</data>
        </col>
    </row>
    <row>
        <col>
            <data>456</data>
        </col>
        <col>
            <data>coad</data>
        </col>
        <col>
            <data>True</data>
        </col>
        <col>
            <data>chronic obstructive airways disease</data>
        </col>
        <col>
            <data>28011986</data>
        </col>
    </row>
    <row>
        <col>
            <data>456</data>
        </col>
        <col>
            <data>depression</data>
        </col>
        <col>
            <data>True</data>
        </col>
        <col>
            <data>depression</data>
        </col>
        <col>
            <data>28011986</data>
        </col>
    </row>
    <row>
        <col>
            <data>456</data>
        </col>
        <col>
            <data>myocardial_infarction</data>
        </col>
        <col>
            <data>True</data>
        </col>
        <col>
            <data>myocardial infarction</data>
        </col>
        <col>
            <data>05071997</data>
        </col>
    </row>
    <row>
        <col>
            <data>456</data>
        </col>
        <col>
            <data>osteoarthritis</data>
        </col>
        <col>
            <data>True</data>
        </col>
        <col>
            <data>osteoarthritis of the knee</data>
        </col>
        <col>
            <data>28011986</data>
        </col>
    </row>
    <row>
        <col>
            <data>456</data>
        </col>
        <col>
            <data>stroke</data>
        </col>
        <col>
            <data>True</data>
        </col>
        <col>
            <data>cerebrovascular accident</data>
        </col>
        <col>
            <data>01011996</data>
        </col>
    </row>
</table>

Alternative version

In the above I have assumed that the active, description and diagnosis_date elements are fixed and in fixed order. If the input document can have a variable range of properties (like /data/patient/diagnoses/coad/coad_myproperty) or the ~_active, ~_description, ~_diagnosis_date properties are not in fixed order, then try this alternative (more dynamic) version ...

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output omit-xml-declaration="yes" indent="yes" encoding="UTF-8" />
<xsl:strip-space elements="*" />  

<xsl:variable name="fields" select="
  distinct-values(
    /data/patient/diagnoses/*/*
      [starts-with( local-name(), concat( local-name(..),'_'))]
      /substring(   local-name(), string-length( local-name(..))+2))"
  />

<xsl:template match="data">
  <TABLE>
    <HEADER>
      <COL><DATA>Link</DATA></COL>
      <COL><DATA>Type</DATA></COL>
      <COL><DATA>active</DATA></COL>
      <xsl:for-each select="$fields">
        <COL><DATA><xsl:value-of select="." /></DATA></COL>
      </xsl:for-each>
    </HEADER>
    <xsl:apply-templates select="patient/diagnoses/*" />
  </TABLE>
</xsl:template>

<xsl:template match="diagnoses/*">
  <ROW>
    <COL><DATA><xsl:value-of select="../../link_id" /></DATA></COL>
    <COL><DATA><xsl:value-of select="local-name()" /></DATA></COL>
    <xsl:variable name="this"    select="." as="element()" />
    <xsl:variable name="disease" select="local-name()" />
    <xsl:for-each select="for $f in $fields return concat($disease,'_',$f)">
      <COL><DATA>
        <xsl:value-of select="$this/*[local-name()=current()]" />
      </DATA></COL>
    </xsl:for-each>
  </ROW>
</xsl:template>

</xsl:stylesheet>

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 116992

If the diagnoses values are always given in a known order, you could do simply:

<xsl:template match="/data">
    <xsl:for-each select="patient/diagnoses/*">
        <ROW MODID="" RECORDID="">
            <COL><DATA><xsl:value-of select="../../link_id"/></DATA></COL>
            <COL><DATA><xsl:value-of select="name()"/></DATA></COL>
            <xsl:for-each select="*">
                <COL><DATA><xsl:value-of select="."/></DATA></COL>
            </xsl:for-each>
        </ROW>
    </xsl:for-each>
</xsl:template>

Although I suspect you'd want to convert the date to your own date format, so perhaps:

<xsl:template match="/data">
    <xsl:for-each select="patient/diagnoses/*">
        <ROW MODID="" RECORDID="">
            <COL><DATA><xsl:value-of select="../../link_id"/></DATA></COL>
            <COL><DATA><xsl:value-of select="name()"/></DATA></COL>
            <COL><DATA><xsl:value-of select="*[1]"/></DATA></COL>
            <COL><DATA><xsl:value-of select="*[2]"/></DATA></COL>
            <xsl:variable name="date" select="*[3]"/>
            <COL><DATA>
                <xsl:value-of select="substring($date, 1, 2)"/>
                <xsl:text>/</xsl:text>
                <xsl:value-of select="substring($date, 3, 2)"/>
                <xsl:text>/</xsl:text>
                <xsl:value-of select="substring($date, 5, 4)"/>
            </DATA></COL>
        </ROW>
    </xsl:for-each>
</xsl:template>

assuming you want d/m/y.

Upvotes: 0

har07
har07

Reputation: 89285

You can use * to reference element of any name, and use name() or local-name() functions to get element name dynamically, for example :

<?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" indent="yes"/>

    <xsl:template match="/">
      <TABLE>
        <xsl:for-each select="./data/patient/diagnoses/*">
          <xsl:variable name="diagnose" select="name()"/>
          <ROW MODID="" RECORDID="">
            <COL>
              <DATA>
                <xsl:value-of select="../../link_id"/>
              </DATA>
            </COL>
            <COL>
              <DATA>
                <xsl:value-of select="./*[name()=concat($diagnose, '_description')]"/>
              </DATA>
            </COL>
            <COL>
              <DATA>
                <xsl:value-of select="./*[name()=concat($diagnose, '_active')]"/>
              </DATA>
            </COL>
            <COL>
              <DATA>
                <xsl:value-of select="./*[name()=concat($diagnose, '_diagnosis_date')]"/>
              </DATA>
            </COL>
          </ROW>
        </xsl:for-each>
      </TABLE>
    </xsl:template>
</xsl:stylesheet>

Xsltransform.net Demo

Upvotes: 1

Related Questions