XSLT - Grouping Elements after Index value - xslt

I'm writing a XSLT that needs to output certain data elements but into fixed length containers. While typically the source data will contain fewer elements than the fixed length, there is potential for the source data to contain more elements than the fixed length. When the latter occurs, I need to output the elements individually to a certain index, then group the remaining siblings based on a property so I can use concatenation and summation on the groups. I'm able to achieve all of this with the exception of the grouping of siblings after a given index value.
Below is a simplified version of what I'm trying to achieve. Assume the fixed length of the container is 5 and the only Type values to be outputted are 1 and 2, omitting 3.
Sample input XML
<?xml version="1.0" encoding="utf-8"?>
<Company>
<Locations>
<Location>
<Id>1</Id>
<Shoppers>
<Shopper>
<Products>
<Product>
<Amount>5.00</Amount>
<Id>1</Id>
<Name>A</Name>
<Type>1</Type>
</Product>
<Product>
<Amount>10.00</Amount>
<Id>1</Id>
<Name>B</Name>
<Type>2</Type>
</Product>
<Product>
<Amount>15.00</Amount>
<Id>1</Id>
<Name>C</Name>
<Type>3</Type>
</Product>
<Product>
<Amount>20.00</Amount>
<Id>1</Id>
<Name>D</Name>
<Type>1</Type>
</Product>
</Products>
</Shopper>
<Shopper>
<Products>
<Product>
<Amount>25.00</Amount>
<Id>1</Id>
<Name>E</Name>
<Type>2</Type>
</Product>
<Product>
<Amount>30.00</Amount>
<Id>1</Id>
<Name>F</Name>
<Type>1</Type>
</Product>
<Product>
<Amount>35.00</Amount>
<Id>1</Id>
<Name>G</Name>
<Type>2</Type>
</Product>
<Product>
<Amount>40.00</Amount>
<Id>1</Id>
<Name>H</Name>
<Type>1</Type>
</Product>
</Products>
</Shopper>
</Shoppers>
</Location>
<Location>
<Id>2</Id>
<Shoppers>
<Shopper>
<Products>
<Product>
<Amount>5.00</Amount>
<Id>2</Id>
<Name>I</Name>
<Type>A</Type>
</Product>
<Product>
<Amount>10.00</Amount>
<Id>2</Id>
<Name>J</Name>
<Type>B</Type>
</Product>
</Products>
</Shopper>
<Shopper>
<Products>
<Product>
<Amount>25.00</Amount>
<Id>2</Id>
<Name>K</Name>
<Type>A</Type>
</Product>
<Product>
<Amount>30.00</Amount>
<Id>2</Id>
<Name>L</Name>
<Type>B</Type>
</Product>
<Product>
<Amount>35.00</Amount>
<Id>2</Id>
<Name>M</Name>
<Type>B</Type>
</Product>
</Products>
</Shopper>
</Shoppers>
</Location>
</Locations>
</Company>
XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl customCode" xmlns:customCode="urn:customCode">
<xsl:output method="xml" indent="yes" encoding="utf-8"/>
<xsl:key name="product-distinct" match="Product" use="concat(Id,Type)"/>
<xsl:template match="/">
<Locations>
<xsl:for-each select="Company/Locations/Location">
<xsl:variable name="id" select="Id"/>
<!--Ideally, I would like to use a variable as to not repeat the same XPath throughout the code to simplify any necessary changes-->
<xsl:variable name="productList" select="Shoppers/Shopper/Products/Product[Type != '3']"/>
<Location>
<Products>
<xsl:choose>
<xsl:when test="count($productList) > 5">
<xsl:choose>
<xsl:when test="count($productList[generate-id() = generate-id(key('product-distinct', concat($id,Type))[1])]) = 2">
<xsl:for-each select="$productList[position() < 4]">
<xsl:call-template name="Product">
<xsl:with-param name="amount" select="Amount"/>
<xsl:with-param name="productName" select="Name"/>
<xsl:with-param name="type" select="Type"/>
</xsl:call-template>
</xsl:for-each>
<xsl:choose>
<xsl:when test="count($productList[position() > 3][generate-id() = generate-id(key('product-distinct', concat($id,Type))[1])]) = 2">
<xsl:for-each select="$productList[position() > 3][generate-id() = generate-id(key('product-distinct', concat($id,Type))[1])]">
<xsl:variable name="keyGroup" select="key('product-distinct', concat($id,Type))"/>
<xsl:call-template name="Product">
<xsl:with-param name="amount" select="sum($keyGroup/Amount)"/>
<xsl:with-param name="productName" select="$keyGroup"/>
<xsl:with-param name="type" select="$keyGroup/Type"/>
</xsl:call-template>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="Product">
<xsl:with-param name="amount" select="$productList[4]/Amount"/>
<xsl:with-param name="productName" select="$productList[4]/Name"/>
<xsl:with-param name="type" select="$productList[4]/Type"/>
</xsl:call-template>
<xsl:call-template name="Product">
<xsl:with-param name="amount" select="sum($productList[position() > 4]/Amount)"/>
<xsl:with-param name="productName">
<xsl:call-template name="CombineProductNames">
<xsl:with-param name="products" select="$productList[position() > 4]"/>
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="type" select="$productList[5]/Type"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:for-each select="$productList[position() < 5]">
<xsl:call-template name="Product">
<xsl:with-param name="amount" select="Amount"/>
<xsl:with-param name="productName" select="Name"/>
<xsl:with-param name="type" select="Type"/>
</xsl:call-template>
</xsl:for-each>
<xsl:call-template name="Product">
<xsl:with-param name="amount" select="sum($productList[position() > 4]/Amount)"/>
<xsl:with-param name="productName">
<xsl:call-template name="CombineProductNames">
<xsl:with-param name="products" select="$productList[position() > 4]"/>
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="type" select="$productList[5]/Type"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:for-each select="$productList">
<xsl:call-template name="Product">
<xsl:with-param name="amount" select="Amount"/>
<xsl:with-param name="productName" select="Name"/>
<xsl:with-param name="type" select="Type"/>
</xsl:call-template>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</Products>
</Location>
</xsl:for-each>
</Locations>
</xsl:template>
<xsl:template name="Product">
<xsl:param name="amount"/>
<xsl:param name="productName"/>
<xsl:param name="type"/>
<Product>
<xsl:attribute name="Amount">
<xsl:value-of select="format-number($amount, '0.00')"/>
</xsl:attribute>
<xsl:attribute name="Name">
<xsl:value-of select="$productName"/>
</xsl:attribute>
<xsl:attribute name="Type">
<xsl:value-of select="$type"/>
</xsl:attribute>
</Product>
</xsl:template>
<xsl:template name="CombineProductNames">
<xsl:param name="products"/>
<xsl:for-each select="$products/Name">
<xsl:value-of select="."/>
<xsl:if test="position() != last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Current Output
<?xml version="1.0" encoding="utf-8"?>
<Locations>
<Location>
<Products>
<Product Amount="5.00" Name="A" Type="1" />
<Product Amount="10.00" Name="B" Type="2" />
<Product Amount="20.00" Name="D" Type="1" />
<Product Amount="25.00" Name="E" Type="2" />
<Product Amount="105.00" Name="F, G, H" Type="1" />
</Products>
</Location>
<Location>
<Products>
<Product Amount="5.00" Name="I" Type="A" />
<Product Amount="10.00" Name="J" Type="B" />
<Product Amount="25.00" Name="K" Type="A" />
<Product Amount="30.00" Name="L" Type="B" />
<Product Amount="35.00" Name="M" Type="B" />
</Products>
</Location>
</Locations>
Expected Output
<?xml version="1.0" encoding="utf-8"?>
<Locations>
<Location>
<Products>
<Product Amount="5.00" Name="A" Type="1" />
<Product Amount="10.00" Name="B" Type="2" />
<Product Amount="20.00" Name="D" Type="1" />
<Product Amount="60.00" Name="E, G" Type="2" />
<Product Amount="70.00" Name="F, H" Type="1" />
</Products>
</Location>
<Location>
<Products>
<Product Amount="5.00" Name="I" Type="A" />
<Product Amount="10.00" Name="J" Type="B" />
<Product Amount="25.00" Name="K" Type="A" />
<Product Amount="30.00" Name="L" Type="B" />
<Product Amount="35.00" Name="M" Type="B" />
</Products>
</Location>
</Locations>

Related

Change value in <xsl:value-of select="image"/>

I would like to change an image path.
The code to display the image is:
<xsl:value-of select="image"/>
the image path it takes from the feed is:
https://www.website.com/media/catalog/product/1/7/image.jpg
what i would like is:
https://www.website.com/media/catalog/product/thumbnail/folder/1/7/image.jpg
i tried the following:
<xsl:template name="string-replace-all">
<xsl:param name="text"/>
<xsl:param name="replace"/>
<xsl:param name="with"/>
<xsl:choose>
<xsl:when test="contains($text,$replace)">
<xsl:value-of select="substring-before($text,$replace)"/>
<xsl:value-of select="$with"/>
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="substring-after($text,$replace)"/>
<xsl:with-param name="replace" select="$replace"/>
<xsl:with-param name="with" select="$with"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="image"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="root">
<xsl:for-each select="product[position() = 1]">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="'https://www.website.com/media/catalog/product/'"/>
<xsl:with-param name="replace" select="'product/'" />
<xsl:with-param name="with" select="'product/thumbnail/folder/'"/>
</xsl:call-template>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
the output is:
https://www.website.com/media/catalog/product/thumbnail/folder/https://www.website.com/media/catalog/product/image.jpg
not getting a real error message, it is just showing twice the http part
Couldn't this be a lot simpler? For example, consider:
XML
<root>
<product>
<image>https://www.website.com/media/catalog/product/1/7/image.jpg</image>
</product>
<product>
<image>https://www.website.com/media/catalog/product/2/56/image.jpg</image>
</product>
<product>
<image>https://www.website.com/media/catalog/product/3/123/image.jpg</image>
</product>
</root>
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="/root">
<images>
<xsl:for-each select="product">
<image>
<xsl:text>https://www.website.com/media/catalog/product/thumbnail/folder/</xsl:text>
<xsl:value-of select="substring-after(image, 'https://www.website.com/media/catalog/product/')" />
</image>
</xsl:for-each>
</images>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<images>
<image>https://www.website.com/media/catalog/product/thumbnail/folder/1/7/image.jpg</image>
<image>https://www.website.com/media/catalog/product/thumbnail/folder/2/56/image.jpg</image>
<image>https://www.website.com/media/catalog/product/thumbnail/folder/3/123/image.jpg</image>
</images>

XSLT -One output in SSIS

I have a XML file when I use SSIS XML as Source it is generating multiple outputs. I am using XML task to design XSLT for getting single output.
Following is the XML. I need only single output in SSIS. How to write XSLT for the same?
<?xml version="1.0" encoding="utf-8"?>
<Test>
<iden type="shiftid">100</iden >
<iden type="trailid">25</iden >
<TestHeader>
<SequenceNum>111</SequenceNum>
<TestType>CONTAINER</TestType>
<hub role="origin">
<name />
<hubId>030</hubId>
<hubType>New</hubType>
</hub>
<hub role="Dest">
<name />
<hubId>0757</hubId>
<hubType>hublet</hubType>
</hub>
<TestItem>
<lineNumber>1</lineNumber>
<container>1005</container>
<conditionCode>OK</conditionCode>
<hub role="originator">
<name />
<hubId>TRANS-SHIPMENT</hubId>
<hubType>depot</hubType>
</hub>
</TestItem>
<TestItem>
<lineNumber>2</lineNumber>
<container>1005</container>
<conditionCode>OK</conditionCode>
<hub role="originator">
<name />
<hubId>SHIPMENT</hubId>
<hubType>depot</hubType>
</hub>
</TestItem>
<TestItem>
<lineNumber>1</lineNumber>
<container>1006</container>
<conditionCode>OK</conditionCode>
<hub role="originator">
<name />
<hubId>SHIPMENT</hubId>
<hubType>depot</hubType>
</hub>
</TestItem>
</TestHeader>
<TestContainer type="RAPP" id="1005">
<uom hb="m3" type="board">10.0729</uom>
</TestContainer>
<TestContainer type="RAMP" id="1006">
<uom hb="m3" type="board">10.0729</uom>
</TestContainer>
</Test>
I have struggled and comeup with XSLT as follows
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template name="Iden" match="Test">
<xsl:for-each select="../iden">
<xsl:choose>
<xsl:when test="#type='shiftid'">
<ShipmentID>
<xsl:value-of select="."/>
</ShipmentID>
</xsl:when>
<xsl:when test="#type='trailid'">
<TrailerID>
<xsl:value-of select="."/>
</TrailerID>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<xsl:template name="Item" match="Test">
<xsl:param name="MatchContainer"/>
<xsl:for-each select="//TestItem">
<xsl:if test="$MatchContainer=container">
<Testitem>
<lineNumber>
<xsl:value-of select="lineNumber"/>
</lineNumber>
<containerTestitemID>
<xsl:value-of select="container"/>
</containerTestitemID>
<ConditionCode>
<xsl:value-of select="conditionCode"/>
</ConditionCode>
<Originator>
<xsl:value-of select="hub[#role='originator']/hubId"/>
</Originator>
<OriginatorType>
<xsl:value-of select="hub[#role='originator']/hubType"/>
</OriginatorType>
</Testitem>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template name="container" match="Test">
<xsl:for-each select="../TestContainer">
<Testcontainerlist>
<ContainerType>
<xsl:value-of select="#type"/>
</ContainerType>
<ContainerID>
<xsl:value-of select="#id"/>
</ContainerID>
<Volume>
<xsl:value-of select="uom"/>
</Volume>
<VolumeUOM>
<xsl:value-of select="volume/#hb"/>
</VolumeUOM>
<VolumeType>
<xsl:value-of select="volume/#type"/>
</VolumeType>
<xsl:call-template name="Item">
<xsl:with-param name="MatchContainer" select="#id"/>
</xsl:call-template>
</Testcontainerlist>
</xsl:for-each>
</xsl:template>
<xsl:template name="Header" match="Test">
<xsl:for-each select="TestHeader">
<Test>
<xsl:call-template name="Iden">
</xsl:call-template>
<dropSequenceNumber>
<xsl:value-of select="SequenceNum"/>
</dropSequenceNumber>
<TestType>
<xsl:value-of select="TestType"/>
</TestType>
<OriginID>
<xsl:value-of select="hub[#role='origin']/hubId"/>
</OriginID>
<DestinationID>
<xsl:value-of select="hub[#role='Dest']/hubId"/>
</DestinationID>
<OriginType>
<xsl:value-of select="hub[#role='origin']/hubType"/>
</OriginType>
<DestinationType>
<xsl:value-of select="hub[#role='Dest']/hubType"/>
</DestinationType>
<xsl:call-template name="container">
</xsl:call-template>
</Test>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
My desired output should be as follows
<?xml version="1.0" encoding="utf-8"?>
<Test>
<List>
<ShipmentID>100</ShipmentID>
<TrailerID>25</TrailerID>
<dropSequenceNumber>111</dropSequenceNumber>
<TestType>CONTAINER</TestType>
<OriginID>030</OriginID>
<DestinationID>0757</DestinationID>
<OriginType>New</OriginType>
<DestinationType>hublet</DestinationType>
<ContainerType>RAPP</ContainerType>
<ContainerID>1005</ContainerID>
<Volume>10.0729</Volume>
<VolumeUOM>m3</VolumeUOM>
<VolumeType>board</VolumeType>
<lineNumber>1</lineNumber>
<containerTestitemID>1005</containerTestitemID>
<ConditionCode>OK</ConditionCode>
<Originator>TRANS-SHIPMENT</Originator>
<OriginatorType>depot</OriginatorType>
</List>
<List>
<ShipmentID>100</ShipmentID>
<TrailerID>25</TrailerID>
<dropSequenceNumber>111</dropSequenceNumber>
<TestType>CONTAINER</TestType>
<OriginID>030</OriginID>
<DestinationID>0757</DestinationID>
<OriginType>New</OriginType>
<DestinationType>hublet</DestinationType>
<ContainerType>RAPP</ContainerType>
<ContainerID>1005</ContainerID>
<Volume>10.0729</Volume>
<VolumeUOM>m3</VolumeUOM>
<VolumeType>board</VolumeType>
<lineNumber>2</lineNumber>
<containerTestitemID>1005</containerTestitemID>
<ConditionCode>OK</ConditionCode>
<Originator>SHIPMENT</Originator>
<OriginatorType>depot</OriginatorType>
</List>
<List>
<ShipmentID>100</ShipmentID>
<TrailerID>25</TrailerID>
<dropSequenceNumber>111</dropSequenceNumber>
<TestType>CONTAINER</TestType>
<OriginID>030</OriginID>
<DestinationID>0757</DestinationID>
<OriginType>New</OriginType>
<DestinationType>hublet</DestinationType>
<ContainerType>RAMP</ContainerType>
<ContainerID>1006</ContainerID>
<Volume>10.0729</Volume>
<VolumeUOM>m3</VolumeUOM>
<VolumeType>board</VolumeType>
<lineNumber>1</lineNumber>
<containerTestitemID>1006</containerTestitemID>
<ConditionCode>OK</ConditionCode>
<Originator>SHIPMENT</Originator>
<OriginatorType>depot</OriginatorType>
</List>
</Test>

XSLT 1 Exclude parent element if not in a list

I want to perform an xslt transformation where I split products up by type. As an example:
Source XML:
<Products>
<Product>
<Name>Cheese</Name>
<Value>30</Value>
</Product>
<Product>
<Name>Bread</Name>
<Value>10</Value>
</Product>
<Product>
<Name>Bacon</Name>
<Value>100</Value>
</Product>
</Products>
Required Output XML:
<Products>
<AnimalProducts>
<Product>
<Name>Cheese</Name>
<Value>30</Value>
</Product>
<Product>
<Name>Bacon</Name>
<Value>100</Value>
</Product>
</AnimalProducts>
<VeganProducts>
<Product>
<Name>Bread</Name>
<Value>10</Value>
</Product>
</VeganProducts>
</Products>
If there are no animal products, or no vegan products then the parent elements should not be included. I have it half working with:
<xsl:variable name="veganProducts" select="'Bread:Lettuce'" />
<xsl:if test="Products/Product[count(*) > 0]">
<AnimalProducts>
<xsl:for-each select="Products/Product">
<xsl:if test="not(contains(concat(':', $veganProducts, ':'), concat(':', Name, ':')))">
<Product>
<Name>
<xsl:value-of select="Name" />
</Name>
<Value>
<xsl:value-of select="Value" />
</Value>
</Product>
</xsl:if>
</xsl:for-each>
</AnimalProducts>
<VeganProducts>
<xsl:for-each select="Products/Product">
<xsl:if test="contains(concat(':', $veganProducts, ':'), concat(':', Name, ':'))">
<Product>
<Name>
<xsl:value-of select="Name" />
</Name>
<Value>
<xsl:value-of select="Value" />
</Value>
</Product>
</xsl:if>
</xsl:for-each>
</VeganProducts>
</xsl:if>
The problem is I am getting empty parent elements if there are no vegan or animal products in certain cases. I am unsure how I can test for this.
This is a good time to make use of variables:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:variable name="veganProductNames" select="'Bread:Lettuce'" />
<xsl:variable name="veganProductNamesPadded"
select="concat(':', $veganProductNames, ':')" />
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/*">
<xsl:copy>
<xsl:variable name="animalProducts"
select="Product[not(contains($veganProductNamesPadded,
concat(':', Name, ':')))]" />
<xsl:variable name="veganProducts"
select="Product[contains($veganProductNamesPadded,
concat(':', Name, ':'))]" />
<xsl:if test="$animalProducts">
<AnimalProducts>
<xsl:apply-templates select="$animalProducts" />
</AnimalProducts>
</xsl:if>
<xsl:if test="$veganProducts">
<VeganProducts>
<xsl:apply-templates select="$veganProducts" />
</VeganProducts>
</xsl:if>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When run on your sample input, the result is:
<Products>
<AnimalProducts>
<Product>
<Name>Cheese</Name>
<Value>30</Value>
</Product>
<Product>
<Name>Bacon</Name>
<Value>100</Value>
</Product>
</AnimalProducts>
<VeganProducts>
<Product>
<Name>Bread</Name>
<Value>10</Value>
</Product>
</VeganProducts>
</Products>
I would prefer to solve this the XML way:
XSLT 1.0
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://www.example.com/my"
exclude-result-prefixes="my">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<my:vegan-items>
<item>Bread</item>
<item>Lettuce</item>
</my:vegan-items>
<xsl:variable name="vegan-items" select="document('')/xsl:stylesheet/my:vegan-items/item" />
<xsl:template match="/Products">
<xsl:variable name="vegan-products" select="Product[Name=$vegan-items]" />
<xsl:variable name="animal-products" select="Product[not(Name=$vegan-items)]" />
<xsl:copy>
<xsl:if test="$animal-products">
<AnimalProducts>
<xsl:copy-of select="$animal-products"/>
</AnimalProducts>
</xsl:if>
<xsl:if test="$vegan-products">
<VeganProducts>
<xsl:copy-of select="$vegan-products"/>
</VeganProducts>
</xsl:if>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Here we are assuming that anything not "vegan" is "animal" (since no list of "animal" items has been provided).
Keeping an external XML document listing the items in each category would probably be even better.

XSLT 1.0 Grouping with multiple elements with same name

I have an XML that looks like -
<resultset>
<hit>
<content>
<ITEM>
<TITLE>Office Cleaning</TITLE>
<DESCRIPTION>blah blah blah</DESCRIPTION>
<Hierarchy>level1A:level2A:level3A</Hierarchy>
<Hierarchy>level1B:level2B:level3B</Hierarchy>
</ITEM>
</content>
</hit>
<hit>
<content>
<ITEM>
<TITLE>Office Cleaning1</TITLE>
<DESCRIPTION>blah blah blah</DESCRIPTION>
<Hierarchy>level1A:level2A:level3A</Hierarchy>
</ITEM>
</content>
</hit>
<hit>
<content>
<ITEM>
<TITLE>Office Cleaning2</TITLE>
<DESCRIPTION>blah blah blah</DESCRIPTION>
<Hierarchy>level1A:level2B:level3C</Hierarchy>
</ITEM>
</content>
</hit>
</resultset>
Note that there are multiple hierarchy elements which is a concatenated string of level1:level2:level3
I am looking to transform this into something like this -
<TREE>
<LEVELS>
<LEVEL1 name="level1A">
<LEVEL2 name="level2A">
<LEVEL3 name="level3A">
<ITEM Name="Office Cleaning"/>
<ITEM Name="Office Cleaning1"/>
</LEVEL3>
</LEVEL2>
</LEVEL1>
<LEVEL1 name="level1B">
<LEVEL2 name="level2B">
<LEVEL3 name="level3B">
<ITEM Name="Office Cleaning"/>
</LEVEL3>
</LEVEL2>
</LEVEL1>
<LEVEL1 name="level1A">
<LEVEL2 name="level2B">
<LEVEL3 name="level3C">
<ITEM Name="Office Cleaning2"/>
</LEVEL3>
</LEVEL2>
</LEVEL1>
</LEVELS>
</TREE>
Basically each item has multiple hierachy associated with it. I need to group them together.
I got only as far as -
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:autn="http://schemas.autonomy.com/aci/">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:key name="HIERARCHYLEVELS" match="resultset/hit/content/ITEM" use="HIERARCHY" />
<xsl:template match="/">
<TREE>
<xsl:for-each select="resultset/hit/content/ITEM[generate-id()=generate-id(key('HIERARCHYLEVELS', HIERARCHY)[1])]">
<xsl:for-each select="HIERARCHY">
<xsl:variable name="level" select="HIERARCHY"/>
<HIERARCHY name="{$level}" >
<xsl:variable name="name" select="TITLE"/>
<ITEM name="{$name}"/>
</HIERARCHY>
</xsl:for-each>
</xsl:for-each>
</TREE>
</xsl:template>
</xsl:stylesheet>
But the problem is I only get the first matching hierarchy tag. For e.g. I dont get to see "Office cleaning1".
What can I do to make sure all hierarchy elements are considered? I still need to split it into various levels.
This transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kItemByHier" match="ITEM" use="Hierarchy"/>
<xsl:key name="kHierByVal" match="Hierarchy" use="."/>
<xsl:template match="/*">
<xsl:apply-templates select=
"*/*/*/Hierarchy[generate-id()=generate-id(key('kHierByVal',.)[1])]"/>
</xsl:template>
<xsl:template match="Hierarchy">
<xsl:call-template name="makeTree">
<xsl:with-param name="pHier" select="string()"/>
<xsl:with-param name="pItems" select="key('kItemByHier', .)"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="makeTree">
<xsl:param name="pHier"/>
<xsl:param name="pDepth" select="1"/>
<xsl:param name="pItems" select="/.."/>
<xsl:choose>
<xsl:when test="not($pHier)">
<xsl:for-each select="$pItems">
<ITEM name="{TITLE}"/>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:element name="LEVEL{$pDepth}">
<xsl:attribute name="name">
<xsl:value-of select="substring-before(concat($pHier,':'), ':')"/>
</xsl:attribute>
<xsl:call-template name="makeTree">
<xsl:with-param name="pHier"
select="substring-after($pHier,':')"/>
<xsl:with-param name="pDepth" select="$pDepth+1"/>
<xsl:with-param name="pItems" select="$pItems"/>
</xsl:call-template>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<resultset>
<hit>
<content>
<ITEM>
<TITLE>Office Cleaning</TITLE>
<DESCRIPTION>blah blah blah</DESCRIPTION>
<Hierarchy>level1A:level2A:level3A</Hierarchy>
<Hierarchy>level1B:level2B:level3B</Hierarchy>
</ITEM>
</content>
</hit>
<hit>
<content>
<ITEM>
<TITLE>Office Cleaning1</TITLE>
<DESCRIPTION>blah blah blah</DESCRIPTION>
<Hierarchy>level1A:level2A:level3A</Hierarchy>
</ITEM>
</content>
</hit>
<hit>
<content>
<ITEM>
<TITLE>Office Cleaning2</TITLE>
<DESCRIPTION>blah blah blah</DESCRIPTION>
<Hierarchy>level1A:level2B:level3C</Hierarchy>
</ITEM>
</content>
</hit>
</resultset>
produces the wanted, correct result:
<LEVEL1 name="level1A">
<LEVEL2 name="level2A">
<LEVEL3 name="level3A">
<ITEM name="Office Cleaning"/>
<ITEM name="Office Cleaning1"/>
</LEVEL3>
</LEVEL2>
</LEVEL1>
<LEVEL1 name="level1B">
<LEVEL2 name="level2B">
<LEVEL3 name="level3B">
<ITEM name="Office Cleaning"/>
</LEVEL3>
</LEVEL2>
</LEVEL1>
<LEVEL1 name="level1A">
<LEVEL2 name="level2B">
<LEVEL3 name="level3C">
<ITEM name="Office Cleaning2"/>
</LEVEL3>
</LEVEL2>
</LEVEL1>
For interest, here is a draft effort at a solution. It is close, but not quiet right, as you can see from the output, as it uses different grouping rules. I am still trying to understand the required grouping rules. I will update if I get a better understanding.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="xsl exsl">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />
<xsl:variable name="phase-1-output">
<xsl:apply-templates select="/" mode="phase-1" />
</xsl:variable>
<xsl:variable name="phase-2-output">
<xsl:apply-templates select="exsl:node-set($phase-1-output)" mode="phase-2" />
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select="$phase-2-output" />
</xsl:template>
<!--================ Phase 1 ===============================-->
<xsl:template match="/" mode="phase-1">
<t>
<xsl:apply-templates select="*/*/*/ITEM/Hierarchy" mode="phase-1" />
</t>
</xsl:template>
<xsl:template match="Hierarchy" mode="phase-1">
<xsl:call-template name="analyze-hierarchy">
<xsl:with-param name="levels" select="." />
<xsl:with-param name="item" select="../TITLE" />
</xsl:call-template>
</xsl:template>
<xsl:template name="analyze-hierarchy"><!-- phase-1 -->
<xsl:param name="levels" />
<xsl:param name="item" />
<xsl:variable name="level" select="substring-before(concat($levels,':'),':')" />
<xsl:variable name="e-level" select="
translate(
substring($level,1,string-length($level) - 1),
'abcdefghijklmnopqrstuvwxyz',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="$level">
<xsl:element name="{$e-level}">
<xsl:attribute name="name"><xsl:value-of select="$level" /></xsl:attribute>
<xsl:call-template name="analyze-hierarchy">
<xsl:with-param name="levels" select="substring-after($levels,':')" />
<xsl:with-param name="item" select="$item" />
</xsl:call-template>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<ITEM Name="{$item}"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!--================ Phase 2 ===============================-->
<xsl:key name="kLevel"
match="*[starts-with(name(),'LEVEL')]"
use="concat(generate-id(..),'|',#name)" />
<xsl:template match="/" mode="phase-2">
<TREE>
<LEVELS>
<xsl:variable name="t" select="concat(generate-id(t),'|')" />
<xsl:apply-templates select="t/LEVEL1[
generate-id() = generate-id( key('kLevel',concat($t,#name))[1])
]" mode="phase-2-head" />
</LEVELS>
</TREE>
</xsl:template>
<xsl:template match="*[starts-with(name(),'LEVEL')]" mode="phase-2-head">
<xsl:copy>
<xsl:copy-of select="#*" />
<xsl:apply-templates select="key('kLevel',concat(generate-id(..),'|',#name))" mode="phase-2" />
<xsl:copy-of select="ITEM" />
</xsl:copy>
</xsl:template>
<xsl:template match="*[starts-with(name(),'LEVEL')]" mode="phase-2">
<xsl:variable name="p" select="concat(generate-id(.),'|')" />
<xsl:apply-templates select="*[starts-with(name(),'LEVEL')][
generate-id() = generate-id( key('kLevel',concat($p,#name))[1])
]" mode="phase-2-head" />
</xsl:template>
</xsl:stylesheet>
...with sample input produces this (not quiet correct output)...
<TREE>
<LEVELS>
<LEVEL1 name="level1A">
<LEVEL2 name="level2A">
<LEVEL3 name="level3A">
<ITEM Name="Office Cleaning" />
</LEVEL3>
</LEVEL2>
<LEVEL2 name="level2A">
<LEVEL3 name="level3A">
<ITEM Name="Office Cleaning1" />
</LEVEL3>
</LEVEL2>
<LEVEL2 name="level2B">
<LEVEL3 name="level3C">
<ITEM Name="Office Cleaning2" />
</LEVEL3>
</LEVEL2>
</LEVEL1>
<LEVEL1 name="level1B">
<LEVEL2 name="level2B">
<LEVEL3 name="level3B">
<ITEM Name="Office Cleaning" />
</LEVEL3>
</LEVEL2>
</LEVEL1>
</LEVELS>
</TREE>
UPDATE
Ok, round 2. I copied Dimitre's grouping rule, which is all or nothing on the content of the Hierarchy element. This solution produces the expected output for the sample input. Note that in contrast to Dimitre's <xsl:element name="LEVEL{$pDepth}"> method, I have derived the LEVEL1 style element names from the Hierarchy steps. I am not sure if this is correct.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />
<xsl:key name="kLevel" match="Hierarchy" use="." />
<xsl:template match="/">
<TREE>
<LEVELS>
<xsl:apply-templates select="*/*/*/ITEM/Hierarchy[
generate-id() = generate-id( key('kLevel',.)[1])
]" mode="group" />
</LEVELS>
</TREE>
</xsl:template>
<xsl:template match="Hierarchy" mode="group">
<xsl:call-template name="analyze-hierarchy">
<xsl:with-param name="key" select="." />
<xsl:with-param name="levels" select="." />
</xsl:call-template>
</xsl:template>
<xsl:template name="analyze-hierarchy">
<xsl:param name="key" />
<xsl:param name="levels" />
<xsl:variable name="level" select="substring-before(concat($levels,':'),':')" />
<xsl:variable name="e-level" select="
translate(
substring($level,1,string-length($level) - 1),
'abcdefghijklmnopqrstuvwxyz',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="$level">
<xsl:element name="{$e-level}">
<xsl:attribute name="name"><xsl:value-of select="$level" /></xsl:attribute>
<xsl:call-template name="analyze-hierarchy">
<xsl:with-param name="key" select="$key" />
<xsl:with-param name="levels" select="substring-after($levels,':')" />
</xsl:call-template>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="key('kLevel',$key)" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="Hierarchy">
<ITEM Name="{../TITLE}" />
</xsl:template>
</xsl:stylesheet>

XSLT 1.0: recursively flatten/denormalize a structure

I'm trying to recursively flatten / normalize a structure below with no luck.
<models>
<model name="AAA" root="true">
<items>
<item name="a"/>
<item name="b"/>
</items>
<submodels>
<submodel ref="BBB"/>
<submodel ref="CCC" />
</submodels>
</model>
<model name="BBB">
<items>
<item name="c"/>
<item name="d"/>
</items>
<submodels>
<submodel ref="CCC" />
</submodels>
</model>
<model name="CCC">
<item name="e" />
</model>
</models>
The expected result is the following:
/AAA
/AAA/a
/AAA/b
/AAA/BBB
/AAA/BBB/c
/AAA/BBB/d
/AAA/BBB/CCC
/AAA/BBB/CCC/e
/AAA/CCC
/AAA/CCC/e
I have tried using in a recursive way. But the main issue is that several models can refer to a single model. Eg. AAA -> CCC and BBB -> CCC.
This short and simple transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kmodelByName" match="model" use="#name"/>
<xsl:key name="ksubmodelByRef" match="submodel" use="#ref"/>
<xsl:template match="/*">
<xsl:apply-templates select="model[not(key('ksubmodelByRef', #name))]"/>
</xsl:template>
<xsl:template match="model|item">
<xsl:param name="pPath"/>
<xsl:value-of select="concat('
', $pPath, '/', #name)"/>
<xsl:apply-templates select="item|*/item|*/submodel">
<xsl:with-param name="pPath" select="concat($pPath, '/', #name)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="submodel">
<xsl:param name="pPath"/>
<xsl:apply-templates select="key('kmodelByName', #ref)">
<xsl:with-param name="pPath" select="$pPath"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<models>
<model name="AAA" root="true">
<items>
<item name="a"/>
<item name="b"/>
</items>
<submodels>
<submodel ref="BBB"/>
<submodel ref="CCC" />
</submodels>
</model>
<model name="BBB">
<items>
<item name="c"/>
<item name="d"/>
</items>
<submodels>
<submodel ref="CCC" />
</submodels>
</model>
<model name="CCC">
<item name="e"/>
</model>
</models>
produces the wanted, correct result:
/AAA
/AAA/a
/AAA/b
/AAA/BBB
/AAA/BBB/c
/AAA/BBB/d
/AAA/BBB/CCC
/AAA/BBB/CCC/e
/AAA/CCC
/AAA/CCC/e
Explanation:
Proper use of keys makes the transformation short, easy to express and efficient.
Proper use of templates.
Proper use of parameter - passing to templates.
Firstly, I suspect you want to math the model that is marked as a root
<xsl:apply-templates select="model[#root='true']" />
Then you would have a template to match model elements, but one that takes a parameter that contains the current 'path' to the parent model
<xsl:template match="model">
<xsl:param name="path" />
You could output the full path like so
<xsl:variable name="newpath" select="concat($path, '/', #name)" />
<xsl:value-of select="concat($newpath, '
')" />
You could then output the items but passing in the new path as a parameter
<xsl:apply-templates select="items/item|item">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
To match the sub-models, ideally a key could be used
<xsl:key name="models" match="model" use="#name" />
Then you could match the sub-models like so
<xsl:apply-templates select="key('models', submodels/submodel/#ref)">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
This would recursively match the same model template.
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:key name="models" match="model" use="#name" />
<xsl:template match="/models">
<xsl:apply-templates select="model[#root='true']" />
</xsl:template>
<xsl:template match="model">
<xsl:param name="path" />
<xsl:variable name="newpath" select="concat($path, '/', #name)" />
<xsl:value-of select="concat($newpath, '
')" />
<xsl:apply-templates select="items/item|item">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
<xsl:apply-templates select="key('models', submodels/submodel/#ref)">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="item">
<xsl:param name="path" />
<xsl:value-of select="concat($path, '/', #name, '
')" />
</xsl:template>
</xsl:stylesheet>
When applied to your sample XML, the following is output
/AAA
/AAA/a
/AAA/b
/AAA/BBB
/AAA/BBB/c
/AAA/BBB/d
/AAA/BBB/CCC
/AAA/BBB/CCC/e
/AAA/CCC
/AAA/CCC/e
I'm not sure what your rules of transformation are. This is a bit of a guess, but the following XSLT 1.0 style-sheet does transform your supplied input to the expected output.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates select="models/model[#root='true']">
<xsl:with-param name="base" select="''" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="model|item">
<xsl:param name="base" />
<xsl:variable name="new-base" select="concat($base,'/',#name)" />
<xsl:value-of select="concat($new-base,'
')" />
<xsl:apply-templates select="items/item | item | submodels/submodel/#ref">
<xsl:with-param name="base" select="$new-base" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="#ref">
<xsl:param name="base" />
<xsl:apply-templates select="../../../../model[#name=current()]">
<xsl:with-param name="base" select="$base" />
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
There is probably an easier way to do this, but I was able to get the output you wanted with the following xslt:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" exclude-result-prefixes="exsl" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common">
<xsl:variable name="root" select="/models/model[#root = 'true']/#name"/>
<xsl:template match="models">
<xsl:for-each select="model">
<xsl:variable name="savedNode" select="."/>
<xsl:variable name="precedingValue">
<xsl:call-template name="preceding">
<xsl:with-param name="modelName" select="#name"/>
</xsl:call-template>
</xsl:variable>
<xsl:if test="not(exsl:node-set($precedingValue)/preceding)">
<xsl:call-template name="model">
<xsl:with-param name="node" select="$savedNode"/>
</xsl:call-template>
</xsl:if>
<xsl:for-each select="exsl:node-set($precedingValue)/preceding">
<xsl:sort select="position()" data-type="number" order="descending"/>
<xsl:variable name="precedingTmp" select="normalize-space(.)"/>
<xsl:variable name="normalizedPreceding" select="translate($precedingTmp, ' ', '')"/>
<xsl:call-template name="model">
<xsl:with-param name="node" select="$savedNode"/>
<xsl:with-param name="preceding" select="$normalizedPreceding"/>
</xsl:call-template>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template name="model">
<xsl:param name="node"/>
<xsl:param name="preceding"/>
\<xsl:value-of select="$preceding"/><xsl:value-of select="$node/#name"/>
<xsl:for-each select="$node/items/item">
\<xsl:value-of select="$preceding"/><xsl:value-of select="$node/#name"/>\<xsl:value-of select="#name"/>
</xsl:for-each>
</xsl:template>
<xsl:template name="preceding">
<xsl:param name="modelName"/>
<xsl:for-each select="preceding::model">
<xsl:for-each select="submodels/submodel">
<xsl:choose>
<xsl:when test="contains(#ref, $modelName)">
<preceding>
<xsl:call-template name="preceding">
<xsl:with-param name="modelName" select="$modelName"></xsl:with-param>
</xsl:call-template>
<xsl:value-of select="../../#name"/>\
</preceding>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template match="items">
<xsl:for-each select="item">
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Input
<models>
<model name="AAA" root="true">
<items>
<item name="a"/>
<item name="b"/>
</items>
<submodels>
<submodel ref="BBB"/>
<submodel ref="CCC" />
</submodels>
</model>
<model name="BBB">
<items>
<item name="c"/>
<item name="d"/>
</items>
<submodels>
<submodel ref="CCC" />
</submodels>
</model>
<model name="CCC">
<items>
<item name="e"/>
</items>
</model>
</models>
Output
\AAA
\AAA\a
\AAA\b
\AAA\BBB
\AAA\BBB\c
\AAA\BBB\d
\AAA\BBB\CCC
\AAA\BBB\CCC\e
\AAA\CCC
\AAA\CCC\e
I hope it helps.