You are here:

XML/XML to XML transformation problem

Advertisement


Question
QUESTION: I have a problem converting the format of a fairly simple XML file to the desired format. I'm using XSLT which I've only used for a short while and I'm self taught. My problem is that the source XML repeats the location and day elements for each set of hourly settings (there are actually 24 and multiple locations):

<UpdateSettingSend>
<location>101xxxx6</location>
<day>2013-01-15</day>
<hour>1</hour>
<FACTOR1>0</FACTOR1>
<FACTOR2>0</FACTOR2>
<Commit>Must</Commit>
<Fixed>true</Fixed>
</UnitSettingsSend>
<UpdateSettingSend>
<location>101xxxx6</location>
<day>2013-01-15</day>
<hour>2</hour>
<Econ>0</Econ>
<Commit>Must</Commit>
<Fixed>true</Fixed>
</UnitSettingsSend>

My desired output is this:

  <SettingsUpdate location="101xxx6" day="2013-01-29">          
     <UpdateHourly hour="01">          
        <UNITsettings Factor1="0" Factor2="0"/>          
       <Commit>Must</Commit>          
        <Fixed>True</Fixed>          
     </UpdateHourly>
     <UpdateHourly hour="02">          
        <UNITsettings Factor1="0" Factor2="0"/>          
       <Commit>Must</Commit>          
        <Fixed>True</Fixed>          
     </UpdateHourly>
  </SettingsUpdate>

Where some elements become attributes and the hourly update fields are repeated nested under the SettingsUpdate element (without it repeating like a flat XML). Each location would have a set of 24 hours of instructions in the <SettingUpdate> element. There's a "wrapper" that has to go around the XML as well.

Here's my attempt so far:

<?xml version='1.0' ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
         
<xsl:text>
</xsl:text>


<!--xsl:for-each-group select="dataroot/UpdateSettingsSend" group-by="location"?????-->

<xsl:element name="SettingsUpdate">
  <xsl:attribute name="location"><xsl:value-of select="dataroot/UpdateSettingsSend/location"/>
  </xsl:attribute>
  <xsl:attribute name="day"><xsl:value-of select="dataroot/UpdateSettingsSend/day"/>
  </xsl:attribute><xsl:text></xsl:text>

<xsl:text>
     </xsl:text>

<xsl:for-each select="hour">
<xsl:element name="UpdateHourly">
   <xsl:attribute name="hour">
     <xsl:number value="hour" format="01"/>
   </xsl:attribute><xsl:text>
        </xsl:text>
    <xsl:element name="UnitSettings">
  <xsl:attribute name="Factor1">
         <xsl:value-of select="FACTOR1"/>
       </xsl:attribute>
  <xsl:attribute name="Factor2">
         <xsl:value-of select="FACTOR2"/>
  </xsl:attribute>
    </xsl:element><xsl:text>
        </xsl:text>
    <Commit>
  <xsl:value-of select="../Commit"/>
    </Commit><xsl:text>
        </xsl:text>
    <Fixed>
  <xsl:value-of select="../Fixed"/>
    </Fixed><xsl:text>
       </xsl:text>
  </xsl:element><xsl:text>
</xsl:text>

</xsl:for-each>
</xsl:element>

     <!--/xsl:for-each-group???--><xsl:text>
</xsl:text>

  </xsl:template>
</xsl:stylesheet>


I can get the result to repeat the location element for every hour or not at all. Also I've had it repeat the elements for each location twice (a for-each problem). Sorry I'm a newbie but I would really appreciate it if you could solve this for me or point me in the right direction. I'll happily contribute to AllExperts as well if that is permitted.

Thanks very much in advance. I know this is a long question.

ANSWER: Hi Bryan,
Sorry for the dealy in the response.
My solution is using the "key" feature - which creates a map of all locations. Then I can only operate on the first unit with the location and from there read the other units with the same location.

Input example:
<root>

   <UnitSettingsSend>
       <location>101xxxx6</location>
       <day>2013-01-15</day>
       <hour>1</hour>
       <FACTOR1>0</FACTOR1>
       <FACTOR2>6</FACTOR2>
       <Commit>Must</Commit>
       <Fixed>true</Fixed>
   </UnitSettingsSend>

   <UpdateSettingSend>
       <location>101xxxx6</location>
       <day>2013-01-15</day>
       <hour>2</hour>
       <Econ>0</Econ>
       <Commit>Must</Commit>
       <Fixed>true</Fixed>
   </UpdateSettingSend>

</root>

XSLT example:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">

 <xsl:key name="locations" match="//location" use="text()"/>

   <xsl:template match="/">
       <xsl:apply-templates select="root/*[generate-id(location) = generate-id(key('locations', location)[1])]" mode="SettingsUpdate" />
   </xsl:template>

   <xsl:template match="*" mode="SettingsUpdate">
       <xsl:element name="SettingsUpdate">
         <xsl:attribute name="location"><xsl:value-of select="location"/></xsl:attribute>
         <xsl:attribute name="day"><xsl:value-of select="day"/></xsl:attribute>
         <xsl:apply-templates select="../*[location = current()/location]" mode="UpdateHourly">
         <xsl:sort select="hour" order="ascending"/>
         </xsl:apply-templates>
       </xsl:element>
   </xsl:template>

   <xsl:template match="*" mode="UpdateHourly">
       <xsl:variable name="f1">
         <xsl:choose><xsl:when test="FACTOR1"><xsl:value-of select="FACTOR1"/></xsl:when><xsl:otherwise>0</xsl:otherwise></xsl:choose>
       </xsl:variable>
       <xsl:variable name="f2">
         <xsl:choose><xsl:when test="FACTOR2"><xsl:value-of select="FACTOR2"/></xsl:when><xsl:otherwise>0</xsl:otherwise></xsl:choose>
       </xsl:variable>
       <UpdateHourly hour="{hour}">
         <UNITsettings Factor1="{$f1}" Factor2="{$f2}"/>
         <Commit><xsl:value-of select="Commit"/></Commit>
         <Fixed><xsl:value-of select="Fixed"/></Fixed>
       </UpdateHourly>
   </xsl:template>

</xsl:stylesheet>




---------- FOLLOW-UP ----------

QUESTION: Hey, this is great! I haven't tested it yet but I think this was just what I needed. I did not understand how to use the Key function so thank you very much!

This is probably easy but I keep messing it up: I need to wrap the file I'm creating in this:

<?xml Version="1.0"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Header/>
<env:Body>
------ Content here (thanks again!!) ---
</env:Body>
</env:Envelope>

I sort of have it working but if I'm doing the other transformation the right way (using the Key function) I'd like to know the right way to do the "soap" wrapper too. If you have the time, thank you so much!

Bryan

Answer
Hi Bryan,
Glad to hear it is helpful. Here is the change required to add the SOAP envelope:

 <xsl:template match="/">
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Header/>
<env:Body>

      <xsl:apply-templates select="root/*[generate-id(location) = generate-id(key('locations', location)[1])]" mode="SettingsUpdate" />

</env:Body>
</env:Envelope>

  </xsl:template>


Ziv

XML

All Answers


Ask Experts

Volunteer


Ziv Ben-Eliahu

Expertise

I can answer XSLT (XSL) questions - XML transformation. I like XPath and XQuery concepts. I know some XML parsing techniques.

Experience

2 years with XSLT (and related XPath)

Education/Credentials
M.Sc. from Ben-Gurion University

Past/Present Clients
I.D.I Technology

©2016 About.com. All rights reserved.