xslt substring question - xslt

I am new to XSLT.
I have a input XML file which needs to be shown as a different output XML. I am using the xslt for transformation.
Input XML:
<Row>
<Column>abc.xyz.ijm</Column>
<Row>
Output XML:
<abc>
<xyz>
<ijm>String</ijm>
</xyz>
</abc>
I tried using xsl:when along with substring-before and substring-after functions but the result xml is not close to what I want.
How to know the last occurence of '.' so that <ijm>String</ijm> is constructed followed by the end tags of the words that are found before each of the previous occurences of the '.' so that </xyz> and </abc> can be added as shown in the output xml above?
Any code snippet is not at all appreciated.
Thanks in advance.

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:template match="Column/text()" name="tokenize">
<xsl:param name="pText" select="."/>
<xsl:if test="string-length()">
<xsl:choose>
<xsl:when test="not(contains($pText,'.'))">
<xsl:element name="{$pText}">String</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{substring-before($pText,'.')}">
<xsl:call-template name="tokenize">
<xsl:with-param name="pText"
select="substring-after($pText,'.')"/>
</xsl:call-template>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document (corrected to be well-formed):
<Row>
<Column>abc.xyz.ijm</Column>
</Row>
produces the wanted, correct result:
<abc>
<xyz>
<ijm>String</ijm>
</xyz>
</abc>
Explanation:
Recursively called named template with stop condition: the $pText parameter is either the empty string or a string that doesn't contain the period character.
Intermediate action: Create an element whose name is the substring-before the '.' character, then call yourself recursively with the text after the first period character as parameter.
Stop action: Create an element with name -- the whole string in the parameter, and value: the string "String".

Related

parsing string in xslt

I have following xml
<xml>
<xref>
is determined “in prescribed manner”
</xref>
</xml>
I want to see if we can process xslt 2 and return the following result
<xml>
<xref>
is
</xref>
<xref>
determined
</xref>
<xref>
“in prescribed manner”
</xref>
</xml>
I tried few options like replace the space and entities and then using for-each loop but not able to work it out. May be we can use tokenize function of xslt 2.0 but don't know how to use it. Any hint will be helpful.
# JimGarrison: Sorry, I couldn't resist. :-) This XSLT is definitely not elegant but it does (I assume) most of the job:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:variable name="left_quote" select="'<'"/>
<xsl:variable name="right_quote" select="'>'"/>
<xsl:template name="protected_tokenize">
<xsl:param name="string"/>
<xsl:variable name="pattern" select="concat('^([^', $left_quote, ']+)(', $left_quote, '[^', $right_quote, ']*', $right_quote,')?(.*)')"/>
<xsl:analyze-string select="$string" regex="{$pattern}">
<xsl:matching-substring>
<!-- Handle the prefix of the string up to the first opening quote by "normal" tokenizing. -->
<xsl:variable name="prefix" select="concat(' ', normalize-space(regex-group(1)))"/>
<xsl:for-each select="tokenize(normalize-space($prefix), ' ')">
<xref>
<xsl:value-of select="."/>
</xref>
</xsl:for-each>
<!-- Handle the text between the quotes by simply passing it through. -->
<xsl:variable name="protected_token" select="normalize-space(regex-group(2))"/>
<xsl:if test="$protected_token != ''">
<xref>
<xsl:value-of select="$protected_token"/>
</xref>
</xsl:if>
<!-- Handle the suffix of the string. This part may contained protected tokens again. So we do it recursively. -->
<xsl:variable name="suffix" select="normalize-space(regex-group(3))"/>
<xsl:if test="$suffix != ''">
<xsl:call-template name="protected_tokenize">
<xsl:with-param name="string" select="$suffix"/>
</xsl:call-template>
</xsl:if>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:template>
<xsl:template match="*|#*">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="xref">
<xsl:call-template name="protected_tokenize">
<xsl:with-param name="string" select="text()"/>
</xsl:call-template>
</xsl:template>
</xsl:stylesheet>
Notes:
There is the general assumption that white space only serves as a token delimiter and need not be preserved.
“ and rdquo; seem to be invalid in XML although they are valid in HTML. In the XSLT there are variables defined holding the quote characters. They will have to be adapted once you find the right XML representation. You can also eliminate the variables and put the characters right into the regular expression pattern. It will be significantly simplified by this.
<xsl:analyze-string> does not allow a regular expression which may evaluate into an empty string. This comes as a little problem since either the prefix and/or the proteced token and/or the suffix may be empty. I take care of this by artificially adding a space at the beginning of the pattern which allows me to search for the prefix using + (at least one occurence) instead of * (zero or more occurences).

check for successively numbered attributes

I have a situation where I need to check for attribute values that may be successively numbered and input a dash between the start and end values.
<root>
<ref id="value00008 value00009 value00010 value00011 value00020"/>
</root>
The ideal output would be...
8-11, 20
I can tokenize the attribute into separate values, but I'm unsure how to check if the number at the end of "valueXXXXX" is successive to the previous value.
I'm using XSLT 2.0
You can use xsl:for-each-group with #group-adjacent testing for the number() value subtracting the position().
This trick was apparently invented by David Carlisle, according to Michael Kay.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:output indent="yes"/>
<xsl:template match="/">
<xsl:variable name="vals"
select="tokenize(root/ref/#id, '\s?value0*')[normalize-space()]"/>
<xsl:variable name="condensed-values" as="item()*">
<xsl:for-each-group select="$vals"
group-adjacent="number(.) - position()">
<xsl:choose>
<xsl:when test="count(current-group()) > 1">
<!--a sequence of successive numbers,
grab the first and last one and join with '-' -->
<xsl:sequence select="
string-join(current-group()[position()=1
or position()=last()]
,'-')"/>
</xsl:when>
<xsl:otherwise>
<!--single value group-->
<xsl:sequence select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:variable>
<xsl:value-of select="string-join($condensed-values, ',')"/>
</xsl:template>
</xsl:stylesheet>

How this value use comma in xsl

xml file like below I want concat value of name, add1, city, add2 with separated by comma
<Details>
<name>abc</name>
<profile>
<address>
<add1>ccc</add1>
<add2>bbb</add2>
<city>CA</city>
</address>
</profile>
</Details>
I want Output like below:-
abc, ccc, CA, bbb
(I mean city will come first before add2 and if any value is blank then it will adjust accordingly)
If you are looking to output all the text nodes within the Details element, you simply iterate over them all using xsl:for-each and use the position() function to output a comma if the node is not the first one
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="Details">
<xsl:for-each select="//text()">
<xsl:if test="position() > 1">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:value-of select="." />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
So, if one of your elements has no text in it, it will not get output or have an extra comma.
<xsl:variable name="name">
<xsl:value-of select="Details/name"/>
</xsl:variable>
<xsl:variable name="add1">
<xsl:value-of select="Details/profile/address/add1"/>
</xsl:variable>
<xsl:variable name="add2">
<xsl:value-of select="Details/profile/address/add2"/>
</xsl:variable>
<xsl:variable name="city">
<xsl:value-of select="Details/profile/address/city"/>
</xsl:variable>
<xsl:value-of select="concat($name,',',$add1,',',$city,',',$add2)"/><br>
It will display the O/P like this abc, ccc, CA, bbb if add1 returns null then it will display like this abc, , CA, bbb
If you're using XSLT 2.0 you can use the () operator to construct a sequence in the order you want and then use the separator attribute on xsl:value-of to output the whole sequence with commas:
<xsl:template match="Details">
<xsl:value-of select="(name, profile/address/add1, profile/address/city,
profile/address/add2)" separator=", " />
</xsl:template>
If you want to filter out elements with an empty value (e.g. if the document contains <city/>) then you can do that with a predicate on the select expression:
(name, profile/address/add1, profile/address/city,
profile/address/add2)[normalize-space()]
The predicate removes from the sequence any nodes whose value is empty or consists entirely of whitespace.

XSLT: nested for-each and dynamic variable

This is my XML and XSLT code
<root>
<act>
<acts id>123</acts>
</act>
<comp>
<comps id>233</comps>
</comp>
</root>
<xsl:for-each select="act/acts">
<xsl:variable name="contactid" select="#id"/>
<xsl:for-each select="root/comp/comps">
<xsl:variable name="var" select="boolean(contactid=#id)"/>
</xsl:for-each>
<xsl:choose>
<xsl:when test="$var='true'">
. . . do this . . .
</xsl:when>
<xsl:otherwise>
. . . do that . . .
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
I want to dynamically assign true or false to var and use it inside <xsl:choose> for boolean test. I hope this helps to find a better solution to get rid of for-each also
First thing to note is that variables in XSLT are immutable, and cannot be changed once initialised. The main problem with your XSLT is that you define your variable within an xsl:for-each block and so it only exists within the scope of that block. It is not a global variable. A new variable gets defined each time that can only be used within the xsl:for-each
From looking at your XSLT it looks like you want to iterate over the acts element and perform a certain action depending on whether an comps element exists with the same value. An alternative approach would be to define a key to look up the comps elements, like so
<xsl:key name="comps" match="comps" use="#id" />
Then you can simply check whether a comps element exists like so (assuming you are positioned on an acts element.
<xsl:choose>
<xsl:when test="key('comps', #id)">Yes</xsl:when>
<xsl:otherwise>No</xsl:otherwise>
</xsl:choose>
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="comps" match="comps" use="#id" />
<xsl:template match="/root">
<xsl:apply-templates select="act/acts" />
</xsl:template>
<xsl:template match="acts">
<xsl:choose>
<xsl:when test="key('comps', #id)"><res>Yes</res></xsl:when>
<xsl:otherwise><res>No</res></xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When applied to the following (well-formed) XML
<root>
<act>
<acts id="123"/>
</act>
<comp>
<comps id="233"/>
</comp>
</root>
The following is output
No
However, it can often be preferably in XSLT to avoid the use of conditional statements like xsl:choose and xsl:if. Instead, you can structure the XSLT to make use of template matching. Here is the alternate approach
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="comps" match="comps" use="#id" />
<xsl:template match="/root">
<xsl:apply-templates select="act/acts" />
</xsl:template>
<xsl:template match="acts[key('comps', #id)]">
<res>Yes</res>
</xsl:template>
<xsl:template match="acts">
<res>No</res>
</xsl:template>
</xsl:stylesheet>
When applied to the same XML, the same result is output. Do note the more specific template for the acts node will take priority when matching the case where a comps exist.
There are some errors in your xml file, but assuming what you mean is:
<root>
<act><acts id="123"></acts></act>
<comp><comps id="233"></comps></comp>
</root>
Here is a full solution:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<doc>
<xsl:apply-templates select="root/comp/comps"/>
</doc>
</xsl:template>
<xsl:template match="root/comp/comps">
<xsl:variable name="compsid" select="#id"></xsl:variable>
<xsl:choose>
<xsl:when test="count(/root/act/acts[#id=$compsid])>0">Do This</xsl:when>
<xsl:otherwise>Do That</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

replacing text in xml using xslt

I have an XML file which has some values in child Element aswell in attributes.
If i want to replace some text when specific value is matched how can i achieve it?
I tried using xlst:translate() function. But i cant use this function for each element or attribute in xml.
So is there anyway to replace/translate value at one shot?
<?xml version="1.0" encoding="UTF-8"?>
<Employee>
<Name>Emp1</Name>
<Age>40</Age>
<sex>M</sex>
<Address>Canada</Address>
<PersonalInformation>
<Country>Canada</country>
<Street1>KO 92</Street1>
</PersonalInformation>
</Employee>
Output :
<?xml version="1.0" encoding="UTF-8"?>
<Employee>
<Name>Emp1</Name>
<Age>40</Age>
<sex>M</sex>
<Address>UnitedStates</Address>
<PersonalInformation>
<Country>UnitedStates</country>
<Street1>KO 92</Street1>
</PersonalInformation>
</Employee>
in the output, replaced text from Canada to UnitedStates.
so, without using xslt:transform() functions on any element , i should be able to replace text Canada to UnitedStates irrespective of level nodes.
Where ever i find 'Canada' i should be able to replace to 'UnitedStates' in entire xml.
So how can i achieve this.?
I. XSLT 1.0 solution:
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my" >
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<my:Reps>
<rep>
<old>replace this</old>
<new>replaced</new>
</rep>
<rep>
<old>cat</old>
<new>tiger</new>
</rep>
</my:Reps>
<xsl:variable name="vReps" select=
"document('')/*/my:Reps/*"/>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#*">
<xsl:attribute name="{name()}">
<xsl:call-template name="replace">
<xsl:with-param name="pText" select="."/>
</xsl:call-template>
</xsl:attribute>
</xsl:template>
<xsl:template match="text()" name="replace">
<xsl:param name="pText" select="."/>
<xsl:if test="string-length($pText)">
<xsl:choose>
<xsl:when test=
"not($vReps/old[contains($pText, .)])">
<xsl:copy-of select="$pText"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vthisRep" select=
"$vReps/old[contains($pText, .)][1]
"/>
<xsl:variable name="vNewText">
<xsl:value-of
select="substring-before($pText, $vthisRep)"/>
<xsl:value-of select="$vthisRep/../new"/>
<xsl:value-of select=
"substring-after($pText, $vthisRep)"/>
</xsl:variable>
<xsl:call-template name="replace">
<xsl:with-param name="pText"
select="$vNewText"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document:
<t>
<a attr1="X replace this Y">
<b>cat mouse replace this cat dog</b>
</a>
<c/>
</t>
produces the wanted, correct result:
<t>
<a attr1="X replaced Y">
<b>tiger mouse replaced tiger dog</b>
</a>
<c/>
</t>
Explanation:
The identity rule is used to copy "as-is" some nodes.
We perform multiple replacements, parameterized in my:Reps
If a text node or an attribute doesn't contain any rep-target, it is copied as-is.
If a text node or an attribute contains text to be replaced (rep target), then the replacements are done in the order specified in my:Reps
If the string contains more than one string target, then all targets are replaced: first all occurences of the first rep target, then all occurences of the second rep target, ..., last all occurences of the last rep target.
II. XSLT 2.0 solution:
In XSLT 2.0 one can simply use the standard XPath 2.0 function replace(). However, for multiple replacements the solution would be still very similar to the XSLT 1.0 solution specified above.