I have some XML that looks like this:
<root>
<message name="peter">
<field type="integer" name="pa" />
<group name="foo">
<field type="integer" name="action" />
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
<message name="wendy">
<field type="string" name="wa" />
<group name="foo">
<field type="integer" name="action" />
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
</root>
I have some XSL that I'm using to generate Java code from this XML. Previously I've been making a key, then generating a Java class for each group.
<xsl:key name="groupsByName" match="//group" use="#name"/>
....
<xsl:for-each select="//group[generate-id(.) = generate-id(key('groupsByName',#name)[1])]">
<xsl:call-template name="class-for-group"/>
</xsl:for-each>
All was well. Now, I've discovered that some messages have groups using the same name as groups present elsewhere, but missing one of the fields. To continue the example XML from above:
<message name="nana">
<field type="string" name="na" />
<group name="foo">
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
A group named "foo" is present, but it's missing the field with name "action".
What I'd like to do is to generate a Java class for each unique subtree. Is this possible? I can't work out what the xsl:key for that would look like. The closest idea I've had is
<xsl:key name="groupsKey" match="//group" use="concat(#name,count(*))"/>
which works for the case in the example above, but is hardly elegant. If there were instead two groups named "foo" with the same number (but different types) of fields, it would fail, so it's not actually a solution.
To be clear, the ideal key (or whatever alternative) would end up calling the template only once for the "peter" and "wendy" cases above, once for the "nana" case and again once for this case:
<message name="hook">
<field type="string" name="ha" />
<group name="foo">
<field type="string" name="favourite_breakfast" />
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
...because the fields within the group are different to those in the other cases. My key above doesn't cover this case. Is there a way to do so?
This transformation fulfills the requirements:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kGroupByType" match="group"
use="#type"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<xsl:variable name="vrtfPass1">
<xsl:apply-templates />
</xsl:variable>
<xsl:apply-templates mode="pass2"
select="ext:node-set($vrtfPass1)/*"/>
</xsl:template>
<xsl:template match="group">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:call-template name="makeType"/>
</xsl:copy>
</xsl:template>
<xsl:template mode="pass2"
match="group[generate-id()
=
generate-id(key('kGroupByType',#type)[1])
]
">
class <xsl:value-of select="concat(#name, '|', #type)"/>
</xsl:template>
<xsl:template name="makeType">
<xsl:attribute name="type">
<xsl:text>(</xsl:text>
<xsl:for-each select="*">
<xsl:value-of select="#type"/>
<xsl:if test="not(position()=last())">+</xsl:if>
</xsl:for-each>
<xsl:text>)</xsl:text>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
When applied on the provided XML document (with all additions):
<root>
<message name="peter">
<field type="integer" name="pa" />
<group name="foo">
<field type="integer" name="action" />
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
<message name="wendy">
<field type="string" name="wa" />
<group name="foo">
<field type="integer" name="action" />
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
<message name="nana">
<field type="string" name="na" />
<group name="foo">
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
<message name="hook">
<field type="string" name="ha" />
<group name="foo">
<field type="string" name="favourite_breakfast" />
<field type="integer" name="id" />
<field type="integer" name="value" />
</group>
</message>
</root>
the wanted result is produced:
class foo|(integer+integer+integer)
class foo|(integer+integer)
class foo|(string+integer+integer)
It is left as an exercise to the reader to further adjust this to produce valid names in one's PL, and also to make this work with structures of unlimited nestedness (which I may do in another answer -- however, we need a more precise definition for this more general provlem).
Related
I have this XML structure
<doc>
<Bundle>
<entry>
<Observation>
<id value="o1-3" />
<subject>
<reference value="Subject/1" />
</subject>
<valueQuantity>
<value value="400" />
<unit value="U" />
</valueQuantity>
<referenceRange>
<low>
<value value="0" />
<unit value="U" />
</low>
<high>
<value value="45" />
<unit value="U" />
</high>
</referenceRange>
</Observation>
</entry>
<entry>
<Observation>
<id value="o8-3" />
<subject>
<reference value="Subject/1" />
</subject>
<valueQuantity>
<value value="0.39" />
<unit value="L" />
</valueQuantity>
<referenceRange>
<low>
<value value="0.14" />
<unit value="L" />
</low>
<high>
<value value="0.35" />
<unit value="L" />
</high>
</referenceRange>
</Observation>
</entry>
</Bundle>
<Bundle>
<entry>
<Observation>
<id value="o3-4" />
<subject>
<reference value="Subject/2" />
</subject>
<valueQuantity>
<value value="10" />
<unit value="U" />
</valueQuantity>
<referenceRange>
<low>
<value value="3" />
<unit value="U" />
</low>
<high>
<value value="30" />
<unit value="U" />
</high>
</referenceRange>
</Observation>
</entry>
<entry>
<Observation>
<id value="o15-4" />
<subject>
<reference value="Subject/2" />
</subject>
<valueQuantity>
<value value="7.1" />
<unit value="m" />
</valueQuantity>
<referenceRange>
<low>
<value value="3.5" />
<unit value="m" />
</low>
<high>
<value value="5.0" />
<unit value="m" />
</high>
</referenceRange>
</Observation>
</entry>
</Bundle>
</doc>
I am developing below mechanism:
Interpret if the valueQuantity is deviated from the referenceRange, if yes, transform the entry
Extract the Observation node grouped by Observation/subject as separate document.
A correctly interpreted Observation and extracted document is below:
<?xml version="1.0" encoding="UTF-8"?>
<Interpretation xmlns="http://intelli.org/interpretation">
<Subject>Subject/1</Subject>
<Observations>
<id value="o1-3"/>
<subject>
<reference value="Subject/1"/>
</subject>
<valueQuantity>
<value value="400"/>
<unit value="U"/>
</valueQuantity>
<referenceRange>
<low>
<value value="0"/>
<unit value="U"/>
</low>
<high>
<value value="45"/>
<unit value="U"/>
</high>
</referenceRange></Observations></Interpretation>
My XSLT:
<!-- Interpretation Starts -->
<xsl:template match="valueQuantity">
<xsl:param name="value" as="xs:double*" select="value/#value" />
<xsl:param name="low" as="xs:double*" select="following::referenceRange[1]/low/value/#value" />
<xsl:param name="high" as="xs:double*" select="following::referenceRange[1]/high/value/#value" />
<xsl:if test="$value lt $low or $value gt $high">
<xsl:element name="Interpretation">
</xsl:element>
</xsl:if>
<!-- Interpretation Ends -->
<!-- Identity Transform -->
<xsl:copy-of select="." />
<!-- Extraction Starts: Locality? -->
<xsl:for-each select="parent::Observation">
<xsl:result-document include-content-type="no" href="/interpret&extract/deviation/{concat('interpretation/', id/#value, '.xml')}">
<xsl:copy-of select="." />
</xsl:result-document>
</xsl:for-each>
</xsl:template>
I guess (because you haven't explained it clearly) that you're trying to write all the entry/valueQuantity elements that have the same value for entry/subject/reference to the same output file. The spec doesn't allow that (for a number of reasons: the results would depend on order of execution, parallel execution would become very difficult, and the resulting XML document would have no outer wrapper element).
Instead, do a separate pass over the input to generate this output file, using something like
<xsl:for-each-group select="entry" group-by="subject/reference/#value">
<xsl:result-document href="{...}">
<wrapper>
<xsl:copy-of select="current-group()"/>
</wrapper>
</xsl:result-document>
</xsl:for-each-group>
I'm trying to transform the following XML file, to remove each <AGGREGATION> node if followed by a <MULTIPLE> node.
<?xml version="1.0" encoding="UTF-8"?>
<RECORD TEMPLATE="PRODUCTS" TRACK="1">
<FIELD NAME="PRODUCT" BASE="CT300" COUNT="2">
<AGGREGATION DOMAIN="4" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<TOKEN TEXT="CT300" BEGIN="11379" END="11384"/>
<AGGREGATION DOMAIN="9" />
<AGGREGATION DOMAIN="4" />
<AGGREGATION DOMAIN="4" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<MULTIPLE TYPE="YES" />
<TOKEN TEXT="CT300" BEGIN="11379" END="11384"/>
</FIELD>
</RECORD>
With the following xslt transformation, I was able to remove only the first occurrence of the <AGGREGATION> node:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="//RECORD[#TEMPLATE='PRODUCTS']/FIELD[#NAME='PRODUCT']/AGGREGATION[following-sibling::*[1][self::MULTIPLE]]">
<xsl:choose>
<xsl:when test="//RECORD[#TEMPLATE='PRODUCTS']/FIELD[#NAME='PRODUCT']/AGGREGATION[following-sibling::*[1][self::MULTIPLE]]">
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
This is the output I receive:
<?xml version="1.0" encoding="UTF-8"?>
<RECORD TEMPLATE="PRODUCTS" TRACK="1">
<FIELD BASE="CT300" COUNT="2" NAME="PRODUCT">
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<TOKEN BEGIN="11379" END="11384" TEXT="CT300"/>
<AGGREGATION DOMAIN="9"/>
<AGGREGATION DOMAIN="4"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<TOKEN BEGIN="11379" END="11384" TEXT="CT300"/>
</FIELD>
</RECORD>
While the desired output is the following:
<?xml version="1.0" encoding="UTF-8"?>
<RECORD TEMPLATE="PRODUCTS" TRACK="1">
<FIELD BASE="CT300" COUNT="2" NAME="PRODUCT">
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<TOKEN BEGIN="11379" END="11384" TEXT="CT300"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<MULTIPLE TYPE="YES"/>
<TOKEN BEGIN="11379" END="11384" TEXT="CT300"/>
</FIELD>
</RECORD>
How can I implement a recursive deletion of the <AGGREGATION> node?
My solution is as follows:
Write a template matching AGGREGATION.
The decision whether it shoud copy anything is as follows:
Take the first following sibling with name != AGGREGATION.
Check whether its name is MULTIPLE.
If yes, do nothing (skip the current AGGREGATION element).
If not, apply templates for the current element.
You need also the identity template.
So the whole script looks like below:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:template match="AGGREGATION">
<xsl:if test="not(following-sibling::*[name()!='AGGREGATION'][1]
[name()='MULTIPLE'])">
<AGGREGATION>
<xsl:apply-templates select="#*|node()"/>
</AGGREGATION>
</xsl:if>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy>
</xsl:template>
</xsl:transform>
For a working example see http://xsltransform.net/bEJaofQ/1
Just use this empty template plus the identity template to eradicate the unwanted <AGGREGATION> elements conditionally:
<xsl:template match="AGGREGATION[following-sibling::*[1] = (self::MULTIPLE or self::AGGREGATION)]" />
I think you just need to ignore the following sibling AGGREGATION elements...
<xsl:template match="RECORD[#TEMPLATE='PRODUCTS']/FIELD[#NAME='PRODUCT']/AGGREGATION[following-sibling::*[not(self::AGGREGATION)][1][self::MULTIPLE]]"/>
I have a XSLT template where I need to merge two lists of nodes conditionally. I have the following two XML fragments:
<vo>
<field name="userLoginName" nameLDAP="uid" type="String"/>
<field name="displayName" nameLDAP="displayName" type="String"/>
<field name="firstName" nameLDAP="givenName" type="String"/>
<field name="lastName" nameLDAP="sn" type="String"/>
<field name="mail" nameLDAP="mail" type="String"/>
<field name="userPassword" nameLDAP="userPassword" type="String" hidden="true"/>
<field name="center" nameLDAP="center" type="String"/>
</vo>
<input>
<field name="userPassword"/>
<field name="oldPasswordInQuotes" nameLDAP="unicodePwd" type="byte[]"/>
</input>
I want to create a xml fragment that will have the same nodeas as input/field. For each one of those nodes I want to check if there is a node with the same name in the list vo/field. If there is, then it should be copied to the new list. Otherwise, it should copy the same node we are iterating over. In this case the output should be something like this:
<field name="userPassword" nameLDAP="userPassword" type="String" hidden="true"/>
<field name="oldPasswordInQuotes" nameLDAP="unicodePwd" type="String"/>
So far I've the following transformation:
<xsl:variable name="fields" select="vo/field" />
<xsl:variable name='allParameters'>
<xsl:for-each select="input/field">
<xsl:variable name="inputFieldName" select="#name"/>
<xsl:choose>
<xsl:when test="$fields[#name = $inputFieldName]">
<xsl:copy-of select="$fields[#name = $inputFieldName]"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:variable>
<xsl:message>Parameters <xsl:copy-of select="$allParameters" /> </xsl:message>
<xsl:for-each select="$allParameters">
<xsl:message>Parameter <xsl:value-of select="#name" /> found !!! </xsl:message>
</xsl:for-each>
The output is
Parameters <field name="userPassword" nameLDAP="userPassword" type="String" hidden="true"/><field name="oldPasswordInQuotes" nameLDAP="unicodePwd" type="byte[]"/>
Parameter found !!!
The first xsl:message seems to show that the copy worked fine, but when I try to iterate over it, it is obviously not working(there is only one message "Parameter found" and it is not showing the parameter name). What am I missing?
I think you want something like:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:key name="vo" match="vo/field" use="#name" />
<xsl:template match="/">
<xsl:for-each select="root/input/field">
<xsl:choose>
<xsl:when test="key('vo', #name)">
<xsl:copy-of select="key('vo', #name)"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Applied to a wel-formed input:
<root>
<vo>
<field name="userLoginName" nameLDAP="uid" type="String"/>
<field name="displayName" nameLDAP="displayName" type="String"/>
<field name="firstName" nameLDAP="givenName" type="String"/>
<field name="lastName" nameLDAP="sn" type="String"/>
<field name="mail" nameLDAP="mail" type="String"/>
<field name="userPassword" nameLDAP="userPassword" type="String" hidden="true"/>
<field name="center" nameLDAP="center" type="String"/>
</vo>
<input>
<field name="userPassword"/>
<field name="oldPasswordInQuotes" nameLDAP="unicodePwd" type="byte[]"/>
</input>
</root>
results in:
<field name="userPassword" nameLDAP="userPassword" type="String" hidden="true"/>
<field name="oldPasswordInQuotes" nameLDAP="unicodePwd" type="byte[]"/>
Edit:
To answer your question regarding the number of messages in your test: there is only one $allParameters node; to iterate over its inner nodes (assuming XSLT 2.0), try:
<xsl:for-each select="$allParameters/field">
<!-- your code here -->
</xsl:for-each>
I have a problem with field of solr. when multiple nodes does not fit, and it shows empty field.
<field name="specs"/>
the xml original:
<?xml version="1.0" encoding="UTF-8"?>
<tire xmlns="http://schema.grabber" xmlns:x="http://www.w3.org/1999/xhtml"
tire-type="2" product-type="tire" id="102694" trademark="dunlop"
season="3" width="130" height="70" wheels="12" load="62" speed="l"
host="norauto" model="d207" hostDetailId="102694" hostDbID="6">
<url>product/_102694.html</url>
<price>49.95</price>
<ecorate>1.15</ecorate>
<currency>€</currency>
<vat>true</vat>
<img>images_produits/650x650/dunlop-d207-runscoot.jpg</img>
<content>DUNLOP D207 RUNSCOOT</content>
<specs>
<spec name="b_xl">0</spec>
</specs>
</tire>
Transformation XSLT at XML solr, this is xslt of solr:
<xsl:template match="/">
<docs>
<xsl:apply-templates select="cb:tire|cb:products" />
</docs>
</xsl:template>
<xsl:template match="cb:tire">
<doc>
<xsl:apply-templates select="#*|*" />
</doc>
</xsl:template>
<xsl:template match="cb:products">
<xsl:apply-templates select="#*|*" />
</xsl:template>
<xsl:template match="*/*[#name and not(parent::cb:products)]">
<xsl:call-template name="field">
<xsl:with-param name="name" select="concat(name(),'_',#name)"/>
</xsl:call-template>
</xsl:template>
<xsl:template match="*/*[not(#name) and not(parent::cb:products)]">
<xsl:call-template name="field"/>
</xsl:teplate>
<xsl:template match="*[parent::cb:products]">
<xsl:choose>
<xsl:when test="not(text())">
<doc>
<xsl:apply-templates select="*|#*"/>
</doc>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="field"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="*|#*">
<xsl:call-template name="field">
<xsl:with-param name="value" select="."/>
</xsl:call-template>
</xsl:template>
<xsl:template name="field">
<xsl:param name="name" select="name()" />
<xsl:param name="value" select="text()" />
<field name="{translate(lower-case($name),' ','_')}">
<xsl:value-of select="$value" />
</field>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
The problem is the element <specs><spec name="b_xl">0</spec></specs>, the field is empty, the result not correct.
This is result XML:
<docs>
<doc>
<field name="tire-type">1</field>
<field name="product-type">tire</field>
<field name="season">1</field>
<field name="id">135-80-r13-70-t-kingstar-sk70</field>
<field name="trademark">kingstar</field>
<field name="model">sk70</field>
<field name="width">135</field>
<field name="height">80</field>
<field name="wheels">13</field>
<field name="load">70</field>
<field name="speed">t</field>
<field name="host">tires</field>
<field name="hostdetailid">135-80-r13-70-t-kingstar-sk70</field>
<field name="hostdbid">1000</field>
<field name="url">135-80-r13-70-t-kingstar-sk70.html</field>
<field name="price">29.73</field>
<field name="currency">€</field>
<field name="vat">true</field>
<field name="img">media/catalog/product/cache/1/image/9df78eab33525d08d6e5fb8d27136e95/0/1/0181050080001.png</field>
<field name="content">135/80 R13 70 T KINGSTAR SK70</field>
<field name="specs" />
</doc>
</docs>
I need to display the contents of the element if it contains specs.
To handle the children of specs as field you may add a template like this:
<xsl:template match="cb:specs" priority="1">
<xsl:apply-templates />
</xsl:template>
Which will generate following field:
<field name="spec_b_xl">0</field>
I do not know if this is as expected, because you didn't tell us who the output for specs should look like.
how show title attribute?
I would like to extract data from the #name.
<specs>
<spec name="b_homologation">1</spec>
<spec name="s_homologation_type">mo</spec>
<spec name="b_xl">0</spec>
<spec name="b_runflat">0</spec>
<spec name="s_consumption">e</spec>
<spec name="i_noise">72</spec>
<spec name="s_grip">c</spec>
</specs>
the result has to be:
<field name="b_homologation">1</field>
<field name="s_homologation_type">mo</field>
...
Thanks.
edit:
<xsl:template match="*|#*">
<xsl:call-template name="field">
<xsl:with-param name="value" select="."/>
</xsl:call-template>
</xsl:template>
<xsl:template name="field">
<xsl:param name="name" select="name()" />
<xsl:param name="value" select="text()" />
<field name="{$name}">
<xsl:value-of select="$value" />
</field>
</xsl:template>
And result is(not correct):
<field name="specs">1mo00e72c</field>
As I suggested in my comment, there's no need to mess around with named templates and parameters here, it's just a plain identity transform with tweaks:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<!-- copy input to output verbatim ... -->
<xsl:template match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()" /></xsl:copy>
</xsl:template>
<!-- ... except spec elements, whose name changes to field -->
<xsl:template match="spec">
<field><xsl:apply-templates select="#*|node()" /></field>
</xsl:template>
</xsl:stylesheet>
This will produce
<specs>
<field name="b_homologation">1</field>
<field name="s_homologation_type">mo</field>
<field name="b_xl">0</field>
<field name="b_runflat">0</field>
<field name="s_consumption">e</field>
<field name="i_noise">72</field>
<field name="s_grip">c</field>
</specs>
You can use the same trick if you want to rename the root specs element to something like fields, but you can't leave it out completely if you want your output to be well-formed XML.
something like this? Didn't test it, can include some mini-bugs :)
<xsl:template match="spec" >
<xsl:call-template name="outputToXml" >
<xsl:with-param name="name" select="#name" />
<xsl:with-param name="value" select="." />
</xsl:call-template>
</xsl:template>
<xsl:template name="outputToXml" >
<xsl:param name="name" />
<xsl:param name="value" />
<xsl:text><field name="</xsl:text>
<xsl:value-of select="$name" />
<xsl:text>"></xsl:text>
<xsl:value-of select="$value" />
<xsl:text></field></xsl:text>
</xsl:template>