I am facing a problem while creating an XSLT for the below transformation. I am relatively new to XSLT transformations. The problem is that i want to count the number of students from distinct countries in the input. I have tried counting based on conditions but it doesn't work as this counting is grouped. Can someone tell me if this can be done is XSLT 1.0.
My Input is
<ns0:DetailsResponse xmlns:ns0="http://MySchema.XSLTSchema">
<Class>1</Class>
<Students>
<StudentName>John</StudentName>
<StudentSurname>Doe</StudentSurname>
<Country>
<CountryName>UK</CountryName>
</Country>
</Students>
<Students>
<StudentName>Cherry</StudentName>
<StudentSurname>Blossom</StudentSurname>
<Country>
<CountryName>US</CountryName>
</Country>
</Students>
<Students>
<StudentName>Ankit</StudentName>
<StudentSurname>Sood</StudentSurname>
<Country>
<CountryName>INDIA</CountryName>
</Country>
</Students>
<Students>
<StudentName>Peter</StudentName>
<StudentSurname>Scott</StudentSurname>
<Country>
<CountryName>UK</CountryName>
</Country>
</Students>
<Students>
<StudentName>Joe</StudentName>
<StudentSurname>Carter</StudentSurname>
<Country>
<CountryName>UK</CountryName>
</Country>
</Students>
<Students>
<StudentName>Anu</StudentName>
<StudentSurname>Mehta</StudentSurname>
<Country>
<CountryName>INDIA</CountryName>
</Country>
</Students>
</ns0:DetailsResponse>
and I want my Output to be like
Output
<ns0:Root xmlns:ns0="http://MySchema.XSLTSchema_Destination">
<DestinationClass>DestinationClass_0</DestinationClass>
<Countries>
<CountryWiseCount>
<Country>INDIA</Country>
<Count>2</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>UK</Country>
<Count>3</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>US</Country>
<Count>1</Count>
</CountryWiseCount>
</Countries>
</ns0:Root>
If you are using XSLT 1.0, then Muenchian Grouping will be your friend here.
You are grouping students by country name, so you define a key to look up students like so
<xsl:key name="students" match="Students" use="Country/CountryName"/>
Then, to get the distinct countries, you would match the Students elements that happen to be the elements that occur first in the key for their given country.
<xsl:template
match="Students[generate-id() = generate-id(key('students', Country/CountryName)[1])]">
Then to get the count of all the students for that country, you can just count the key:
<xsl:value-of select="count(key('students', Country/CountryName))"/>
Here is the full XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="students" match="Students" use="Country/CountryName"/>
<xsl:template match="Students[generate-id() = generate-id(key('students', Country/CountryName)[1])]">
<CountryWiseCount>
<Country>
<xsl:value-of select="Country/CountryName"/>
</Country>
<Count>
<xsl:value-of select="count(key('students', Country/CountryName))"/>
</Count>
</CountryWiseCount>
</xsl:template>
<xsl:template match="Students"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your XML, the following is output
<ns0:DetailsResponse xmlns:ns0="http://MySchema.XSLTSchema">
<Class>1</Class>
<CountryWiseCount>
<Country>UK</Country>
<Count>3</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>US</Country>
<Count>1</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>INDIA</Country>
<Count>2</Count>
</CountryWiseCount>
</ns0:DetailsResponse>
Note the use of the template <xsl:template match="Students"/> which matches the Students elements which are not first in the key, to stop them being output. The XSLT will always give priority to the more specific template (with the xpath expression), so this template won't ignore everything.
Obviously you would need to extend the XSLT with a template to match class too, but I am sure you can work that out.
Related
I have the following:
<ns0:tXML>
<Message>
<Report>
<Page>
<PageID>01</PageID>
<PageDetail>
<PageName>11</PageName>
<Totals>
<Num>10</Num>
</Totals>
</PageDetail>
<PageDetail>
<PageName>11</PageName>
<Totals>
<Num>5</Num>
</Totals>
</PageDetail>
</Page>
<Page>
<PageID>02</PageID>
<PageDetail>
<PageName>12</PageName>
<Totals>
<Num>10</Num>
</Totals>
</PageDetail>
<PageDetail>
<PageName>12</PageName>
<Totals>
<Num>3</Num>
</Totals>
</PageDetail>
</Page>
</Report>
</Message>
</ns0:tXML>
I want to make the output so that PageDetails are combined for each Page as long as their PageName and PageID are the same, including summing the values of the combined.
Output Wanted:
<ns0:tXML>
<Message>
<Report>
<Page>
<PageID>01</PageID>
<PageDetail>
<PageName>11</PageName>
<Totals>
<Num>15</Num>
</Totals>
</PageDetail>
</Page>
<Page>
<PageID>02</PageID>
<PageDetail>
<PageName>12</PageName>
<Totals>
<Num>13</Num>
</Totals>
</PageDetail>
</Page>
</Report>
</Message>
</ns0:tXML>
How would I go about it? All efforts with using keys and playing with templates has led to cases where only one of the Pages got created, or it combined all the Pages no matter where they were on the xml, showing that I was likely trying to do an all apply to it rather than sticking to the current context.
Let's start from a little correction to your source. It should include
the namespace specification:
<ns0:tXML xmlns:ns0="urn.dummy.com">
otherwise there is reported the following error:
The prefix "ns0" for element "ns0:tXML" is not bound.
One of possible solutions is to use the following script:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns0="urn.dummy.com">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="Pd" match="PageDetail" use="concat(../PageID, '|', PageName)"/>
<xsl:template match="Page">
<xsl:copy>
<xsl:copy-of select="PageID"/>
<xsl:for-each select="PageDetail[generate-id()=generate-id(key('Pd',
concat(../PageID,'|', PageName))[1])]">
<xsl:variable name="kk" select="concat(../PageID,'|', PageName)"/>
<xsl:copy>
<xsl:copy-of select="PageName"/>
<xsl:element name="Totals">
<xsl:element name="Num">
<xsl:value-of select="sum(key('Pd', $kk)/Totals/Num)"/>
</xsl:element>
</xsl:element>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy>
</xsl:template>
</xsl:transform>
For a working example, generating just your expected result,
see: http://xsltransform.net/93YRmgt
I need to flatten my XML as the file size very huge and nested level, I am creating the XSL file manually. Below is my sample scenario of content of the XML file-
<StudentDetail>
<SchoolName>SSHPS</SchoolName>
<SchoolEstablishedYear>1990</SchoolEstablishedYear>
<ClassDetails>
<ClassDetail>
<ClassStartedYear>1990</ClassStartedYear>
<Section ID="12345">
<SectioName>Section A</SectioName>
<Students>
<Student ID="1">
<StudentName>John</StudentName>
<Address>
<HomeNumber>10</HomeNumber>
<StreetName>Avenue</StreetName>
</Address>
</Student>
<Student ID="2">
<StudentName>Steve</StudentName>
</Student>
</Students>
</Section>
<Section ID="123456">
<SectioName>Section B</SectioName>
<Students>
<Student ID="100">
<StudentName>Dia</StudentName>
<Age>6</Age>
</Student>
<Student ID="101">
<StudentName>Kevin</StudentName>
</Student>
</Students>
</Section>
</ClassDetail>
<ClassDetail>
<ClassStartedYear>1995</ClassStartedYear>
<Section ID="543466">
<SectioName>Section A</SectioName>
<Students>
<Student ID="200">
<StudentName>Dia</StudentName>
<Muncipality>
<AreaCode>100</AreaCode>
<Areaname>GRAND</Areaname>
</Muncipality>
</Student>
<Student ID="201">
<StudentName>Liva</StudentName>
</Student>
</Students>
</Section>
<Section ID="7543466">
<SectioName>Section A</SectioName>
<Students>
<Student ID="300">
<StudentName>Zane</StudentName>
</Student>
<Student ID="301">
<StudentName>Susan</StudentName>
</Student>
</Students>
</Section>
</ClassDetail>
</ClassDetails>
</StudentDetail>
Below is the required format of the XML-
<StudentDetail>
<Student>
<SchoolName>SSHPS</SchoolName>
<SchoolEstablishedYear>1990</SchoolEstablishedYear>
<ClassStartedYear>1990</ClassStartedYear>
<SectionID>12345</SectionID>
<SectioName>Section A</SectioName>
<StudentID>1</StudentID>
<StudentName>John</StudentName>
<Address_HomeNumber>10</Address_HomeNumber>
<Address_StreetName>Avenue</Address_StreetName>
<Age> </Age>
<Muncipality_AreaCode></Muncipality_AreaCode>
<Muncipality_Areaname></Muncipality_Areaname>
</Student>
.
.
.
<Student>
<SchoolName>SSHPS</SchoolName>
<SchoolEstablishedYear>1990</SchoolEstablishedYear>
<ClassStartedYear>1995</ClassStartedYear>
<SectionID>7543466</SectionID>
<SectioName>Section A</SectioName>
<StudentID>100</StudentID>
<StudentName>Dia</StudentName>
<Address_HomeNumber></Address_HomeNumber>
<Address_StreetName></Address_StreetName>
<Age></Age>
<Muncipality_AreaCode>100</Muncipality_AreaCode>
<Muncipality_Areaname>GRAND</Muncipality_Areaname>
</Student>
</StudentDetail>
I have generated the XSL template, I am not able to load this as there is some error within this-
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes" method="xml"/>
<xsl:template match="/">
<StudentDetail>
<xsl:for-each select="StudentDetail/ClassDetails">
<Student>
<SchoolName><xsl:value-of select="StudentDetail/SchoolName"/></SchoolName>
<SchoolEstablishedYear><xsl:value-of select="StudentDetail/SchoolEstablishedYear"/></SchoolEstablishedYear>
<ClassStartedYear><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/ClassStartedYear"/></ClassStartedYear>
<StudentID><xsl:value-of select="StudentDetail/ClassDetails/Section/#ID"/></StudentID>
<SectioName><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/SectionName"/></SectioName>
<StudentID><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student/#ID"/></StudentID>
<StudentName><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student"/></StudentName>
<Address_HomeNumber><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student/Address/HomeNumber"/></Address_HomeNumber>
<Address_StreetName><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student/Address/StreetName"/></Address_StreetName>
<Age><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student/Age"/></Age>
<Muncipality_AreaCode><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student/Muncipality/AreaCode"/></Muncipality_AreaCode>
<Muncipality_Areaname><xsl:value-of select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student/Muncipality/Areaname"/></Muncipality_Areaname>
</Student>
</xsl:for-each>
</StudentDetail>
</xsl:template>
I am new to handling the XML, I am stuck with handling the nested XML
The main problem with your stylesheet is that you are creating a Student for each ClassDetails instead of for each Student.
Instead of:
<xsl:for-each select="StudentDetail/ClassDetails">
<Student>
<!-- data -->
</Student>
</xsl:for-each>
you should be doing:
<xsl:for-each select="StudentDetail/ClassDetails/ClassDetail/Section/Students/Student">
<Student>
<!-- data -->
</Student>
</xsl:for-each>
Then, within the Student element, you need to retrieve data from the ancestor School, ClassDetail and Section parts, as well as from the child elements of the current Student node.
In order to minimize the need to repeatedly navigate up and down the tree, I would suggest putting the ancestor parts details in variables and access them from there:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/StudentDetail">
<xsl:copy>
<xsl:variable name="school-details" select="SchoolName | SchoolEstablishedYear"/>
<xsl:for-each select="ClassDetails/ClassDetail">
<xsl:variable name="class-details" select="ClassStartedYear"/>
<xsl:for-each select="Section">
<xsl:variable name="section-details">
<SectionID>
<xsl:value-of select="#ID"/>
</SectionID>
<xsl:copy-of select="SectioName"/>
</xsl:variable>
<xsl:for-each select="Students/Student">
<xsl:copy>
<xsl:copy-of select="$school-details | $class-details"/>
<xsl:copy-of select="$section-details"/>
<StudentID>
<xsl:value-of select="#ID"/>
</StudentID>
<xsl:copy-of select="StudentName"/>
<Address_HomeNumber>
<xsl:value-of select="Address/HomeNumber"/>
</Address_HomeNumber>
<Address_StreetName>
<xsl:value-of select="Address/StreetName"/>
</Address_StreetName>
<Age>
<xsl:value-of select="Age"/>
</Age>
<Muncipality_AreaCode>
<xsl:value-of select="Muncipality/AreaCode"/>
</Muncipality_AreaCode>
<Muncipality_Areaname>
<xsl:value-of select="Muncipality/Areaname"/>
</Muncipality_Areaname>
</xsl:copy>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Firstly, I'm surprised that anyone would want to turn this nicely structured input into this badly structured output. But we're not here to discuss that.
Secondly, I'm confused by the statement that you are "generating the XSL manually". I would have thought you were either creating it manually, or you were generating it programmatically, and it's not clear which of those is the case here.
Third, you've told us there is "some error" in your generated XSL, but you haven't told us what that error is. The only error I can see is a missing close tag for the xsl:stylesheet element, and that's presumably a typo. If you're getting an error, tell us what the error is.
Fourth, there seems to be a much simpler approach. As far as I can see, you can achieve the desired output by applying three rules:
If an element has child elements, just process its children
If an element has an ID attribute, change <X ID="x"/> to <XID>x</XID> and then process its children
If an element has a text node child, copy it unchanged.
The first rule corresponds to the default XSLT processing rule; the other two rules can be simply expessed in XSLT as:
<xsl:template match="*[#ID]">
<xsl:element name="{name()}ID">
<xsl:value-of select="#ID"/>
</xsl:element>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="*[text()]">
<xsl:copy-of select="."/>
</xsl:template>
I am trying to sort a list of categorized xml elements, using XSLT 2.0. Each element has a unique ID and the categorization is defined in another list containing these and more elements. Here's an example of a starting XML document. The section that I want sorted is /Atlas/VisitedCities. It should be sorted according to area of the world and date of the visit:
<?xml version="1.0" encoding="UTF-8"?>
<Atlas>
<Cities>
<City id="1" worldPart="Africa">
<Name>Luxor</Name>
<Founded>-3200</Founded>
<Location>Egypt</Location>
</City>
<City id="2" worldPart="Africa">
<Name>Tripoli</Name>
<Founded>-700</Founded>
<Location>Libya</Location>
</City>
<City id="3" worldPart="Americas">
<Name>Cholula</Name>
<Founded>-200</Founded>
<Location>Mexico</Location>
</City>
<City id="4" worldPart="Americas">
<Name>Flores</Name>
<Founded>-1000</Founded>
<Location>Guatemala</Location>
</City>
<City id="5" worldPart="Europe">
<Name>Argos</Name>
<Founded>-5000</Founded>
<Location>Greece</Location>
</City>
<City id="6" worldPart="Europe">
<Name>Athens</Name>
<Founded>-4000</Founded>
<Location>Greece</Location>
</City>
</Cities>
<VisitedCities lastUpdate="2018-09-10">
<VisitedCity cityID="6">
<Date>1883-08-26</Date>
<Visitor>Dora</Visitor>
</VisitedCity>
<VisitedCity cityID="3">
<Date>1907-01-02</Date>
<Visitor>Nemo</Visitor>
</VisitedCity>
<VisitedCity cityID="4">
<Date>1940-02-08</Date>
<Visitor>Jimenez</Visitor>
</VisitedCity>
<VisitedCity cityID="2">
<Date>1886-06-10</Date>
<Visitor>James T. Kirk</Visitor>
</VisitedCity>
</VisitedCities>
</Atlas>
The wanted output is this:
<?xml version="1.0" encoding="UTF-8"?>
<Atlas>
<Cities>
<City id="1" worldPart="Africa">
<Name>Luxor</Name>
<Founded>-3200</Founded>
<Location>Egypt</Location>
</City>
<City id="2" worldPart="Africa">
<Name>Tripoli</Name>
<Founded>-700</Founded>
<Location>Libya</Location>
</City>
<City id="3" worldPart="Americas">
<Name>Cholula</Name>
<Founded>-200</Founded>
<Location>Mexico</Location>
</City>
<City id="4" worldPart="Americas">
<Name>Flores</Name>
<Founded>-1000</Founded>
<Location>Guatemala</Location>
</City>
<City id="5" worldPart="Europe">
<Name>Argos</Name>
<Founded>-5000</Founded>
<Location>Greece</Location>
</City>
<City id="6" worldPart="Europe">
<Name>Athens</Name>
<Founded>-4000</Founded>
<Location>Greece</Location>
</City>
</Cities>
<VisitedCities lastUpdate="2018-09-10">
<VisitedCity cityID="2">
<Date>1886-06-10</Date>
<Visitor>James T. Kirk</Visitor>
</VisitedCity>
<VisitedCity cityID="6">
<Date>1883-08-26</Date>
<Visitor>Dora</Visitor>
</VisitedCity>
<VisitedCity cityID="3">
<Date>1907-01-02</Date>
<Visitor>Nemo</Visitor>
</VisitedCity>
<VisitedCity cityID="4">
<Date>1940-02-08</Date>
<Visitor>Jimenez</Visitor>
</VisitedCity>
</VisitedCities>
</Atlas>
The stylesheet (XSLT 2.0) that I am struggling with looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<!-- Format output -->
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*" />
<!-- Copy everything that does not match later templates. -->
<xsl:template match="node()|#*" priority="-1">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:variable name="city.list" select="/Atlas/Cities"/>
<xsl:variable name="sort-order" as="element()*">
<wPart>Africa</wPart>
<wPart>Europe</wPart>
<wPart>Americas</wPart>
</xsl:variable>
<xsl:template match="/Atlas/VisitedCities">
<xsl:variable name="city-list" select="."/>
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:for-each select="$sort-order">
<xsl:variable name="this-wpart" select="./text()"/>
<!-- How to select VisitedCity based on info in other list??? -->
<xsl:apply-templates select="$city-list/VisitedCity[$city.list/City[#cityID=$city-list/VisitedCity/#cityID]/#worldPart=$this-wpart]">
<xsl:sort select="./Date"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
I think I understand why this stylesheet will not work (it does not sort at all), as I don't know how to make the selection in the (last) apply-templates. I don't see how to refer to the outermost elements from the inner parts of this expression.
It might suffice to set up a key to reference the City elements by the id attribute to then, in the xsl:sort select expression reference the worldPart attribute. Additionally you could replace the for-each on your continent order with an index-of() call with
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes"/>
<xsl:key name="city-by-id" match="Cities/City" use="#id"/>
<xsl:variable name="sort-order" as="element()*">
<wPart>Africa</wPart>
<wPart>Europe</wPart>
<wPart>Americas</wPart>
</xsl:variable>
<xsl:template match="VisitedCities">
<xsl:copy>
<xsl:apply-templates select="VisitedCity">
<xsl:sort select="index-of($sort-order, key('city-by-id', #cityID)/#worldPart)"/>
<xsl:sort select="Date"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/eiZQaFJ
That complete example is XSLT 3 but to use it with XSLT 2 you would just replace the xsl:mode declaration in there with your template you have prefixed with the comment <!-- Copy everything that does not match later templates. -->, that is, with the identity transformation template.
I'm stumped and am very new to XSLT. A little direciton would be greatly appreciated. My ultimate objective is to bring the firstName, lastName, and gender elements under the student element up into that element as attributes, but also want to retain the StudentID attribute that already exists there. I'd also like that "year_Collection" to disappear.
I started with this XML document
<?xml version="1.0" encoding="UTF-8"?>
<Report xmlns="Upload" Name="Upload">
<student StudentID="123456">
<firstName firstName="John"/>
<lastName lastName="Johnson"/>
<gender gender="M"/>
<year_Collection>
<year value="2013">
<term hoursEarned="18.00" hoursAttempted="18.00" termCode="S1"/>
</year>
</year_Collection>
</student>
</Report>
My desired output looks like this
<?xml version="1.0" encoding="UTF-8"?>
<Report xmlns="Upload" Name="Upload">
<student gender="M" lastName="Johnson" firstName="John" StudentID="123456">
<year value="2013">
<term hoursEarned="18.00" hoursAttempted="18.00" termCode="S1"/>
</year>
</student>
</Report>
I was able to use this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:r="Upload" exclude-result-prefixes="r" extension-element-prefixes="r">
<xsl:template match="r:student">
<xsl:copy>
<xsl:for-each select="*">
<xsl:attribute name="{local-name(.)}">
<xsl:value-of select="."/>
</xsl:attribute>
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
To get here:
<?xml version="1.0" encoding="UTF-8"?>
<Report xmlns="Upload" Name="Upload">
<student year_Collection="" gender="M" lastName="Johnson" firstName="John">
<year value="2013">
<term hoursEarned="18.00" hoursAttempted="18.00" termCode="S1"/>
</year>
</student>
</Report>
But it overwrites the StudentID attribute that already exists. How do I get the elements to come up as attributes, but not overwrite StudentID?
Also, that pesky year_collection element won't disappear with this, but I can split that into a separate question if that's a better way to go about it.
<xsl:template match="r:year_Collection">
<xsl:apply-templates/>
</xsl:template>
But it overwrites the StudentID attribute that already exists.
No, it doesn't. The problem is you're not copying it. You should start your template with:
<xsl:template match="r:student">
<xsl:copy>
<xsl:copy-of select="#StudentID"/>
or, if you prefer:
<xsl:template match="r:student">
<xsl:copy>
<xsl:copy-of select="#*"/>
to copy any and all attributes the Student has.
Regarding the problem with<year_Collection>, you should try to turn only leaf nodes into attributes, for example:
<xsl:template match="r:student">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:copy-of select="*/#*"/>
<xsl:copy-of select="*/*[#*]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Of course, if your structure is known, naming the required nodes explicitly would be much better, e.g;
<xsl:template match="r:student">
<xsl:copy>
<xsl:copy-of select="#StudentID | r:firstName/#firstName | r:lastName/#lastName | r:gender/#gender"/>
<xsl:copy-of select="r:year_Collection/r:year"/>
</xsl:copy>
</xsl:template>
I have the following simplified XML structure:
<?xml version="1.0" encoding="UTF-8"?>
<ExportData>
<TransportHeader>
<Timestamp>2011-01-16 06:00:33</Timestamp>
<From>
<Name>DynamicExport</Name>
<Version>1.</Version>
</From>
<MessageId>d7b5c5b69a83</MessageId>
</TransportHeader>
<ExportConfig>
<DateTimeFormat>yyyy-MM-dd HH:mm:ss</DateTimeFormat>
<DecimalSymbol>.</DecimalSymbol>
</ExportConfig>
<DataSet>
<Tables>
<Table>
<RH>...</RH>
<Rows>
<R>Data1</R>
<R>Data2</R>
<R>Data3</R>
<R>Data4</R>
<R>Data5</R>
</Rows>
</Table>
</Tables>
</DataSet>
</ExportData>
I have to check if <R> elements exist or not. If no <R> elements exist the mapping has to be aborted, otherwise a <Line> element per <R> needs to be created.
I came up with this solution which works perfectly so far:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output encoding="ISO-8859-1" method="xml" indent="yes" />
<!-- suppress nodes that are not matched -->
<xsl:template match="text() | #*">
<xsl:apply-templates select="text() | #*"/>
</xsl:template>
<xsl:template match="/">
<xsl:choose>
<xsl:when test="not(ExportData/DataSet/Tables/Table/Rows/node())">
<xsl:message terminate="yes">No line items</xsl:message>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="/ExportData/DataSet/Tables/Table/Rows">
<INVOIC02>
<!-- apply LINE ITEMS template -->
<xsl:apply-templates select="R"/>
</INVOIC02>
</xsl:template>
<!-- Template creating LINE ITEMS -->
<xsl:template match="R">
<Line>
<elements></elements>
</Line>
</xsl:template>
</xsl:stylesheet>
If there are <R> elements the output is this:
<?xml version="1.0" encoding="ISO-8859-1"?>
<INVOIC02>
<Line>
<elements/>
</Line>
<Line>
<elements/>
</Line>
<Line>
<elements/>
</Line>
<Line>
<elements/>
</Line>
<Line>
<elements/>
</Line>
</INVOIC02>
If there is just <Rows/> and no <R>s the mapping is aborted.
Now I have two questions:
-Is my test for <R> elements robust: test="not(ExportData/DataSet/Tables/Table/Rows/node())" ?
-I am using <xsl:apply-templates> to create the <Line> items instead of an <xsl:for-each> construct. Are my XPath expressions okay or could I make them better?
Is my test for elements robust: test="not(ExportData/DataSet/Tables/Table/Rows/node())"
?
Well, do you want it to fail if there are no R elements, or fail if Rows does not have a child node(), which would include any element (not just R), text(), comment() or processing-instruction()?
If you really want to verify that there is at least one R element that is a child of Rows, you should adjust the test criteria to be more specific:
test="not(ExportData/DataSet/Tables/Table/Rows/R)"
Otherwise, it may pass that test and continue processing and not generate the content you want.
I am using <xsl:apply-templates> to create the <Line> items instead of an
<xsl:for-each> construct.
Are my XPath expressions okay or could I make them better?
You could get rid of the <xsl:if> conditional logic inside of your template for the root node and move that logic into a template for Rows that don't contain R children. Putting logic into xsl:template #match criteria makes it easier for XSLT processors to optimize, which can lead to performance gains.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output encoding="ISO-8859-1" method="xml" indent="yes" />
<!-- suppress nodes that are not matched -->
<xsl:template match="text() | #*">
<xsl:apply-templates select="text() | #*"/>
</xsl:template>
<!--All Rows must contain an R.
If we encounter any that do not, terminate the transform -->
<xsl:template match="/ExportData/DataSet/Tables/Table/Rows[not(R)]">
<xsl:message terminate="yes">No line items</xsl:message>
</xsl:template>
<!--match for Rows that have R children -->
<xsl:template match="/ExportData/DataSet/Tables/Table/Rows[R]">
<INVOIC02>
<!-- apply LINE ITEMS template -->
<xsl:apply-templates select="R"/>
</INVOIC02>
</xsl:template>
<!-- Template creating LINE ITEMS -->
<xsl:template match="R">
<Line>
<elements></elements>
</Line>
</xsl:template>
</xsl:stylesheet>
This will check whether R node has child element:
<xsl:if test="R">
<!--What you want to do here-->
</xsl:if>