I need to replace values of multiple elements of an input xml. The input file has around 300 elements of which I need to replace around 100 elements. I have used identity template before, but I think it would require me to write 100 different templates each for replacing a single element value. I am not very good at xslts, so, am I thinking it right, or is there a better an elegant approach? Please advice.
Edit
Here is the Link of sample input xml.
The output will have almost the same structure, but different values for some of the elements.
Well, something I did similar before before so I am happy to share ... modified to your example BUT does not do the value mapping, it does name mapping.
First create a simple mapping file like this (NOTE: you would need the namespaces to do this right
<env:map xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<elem old="CardCode" new="CodeCardNEW"/>
<elem old="CardName" new="IAMNew"/>
</env:map>
Then one template will do you with a lookup. There are likely better ways to do the mapping, I am just for-each looping over them all ... a key would be better. But this gets you there.
Output from your file with above:
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Body>
<GetByKeyResponse>
<BOM>
<BO>
<AdmInfo>
<Object>oBusinessPartners</Object>
</AdmInfo>
<BusinessPartners>
<row>
<CodeCardNEW>CR43WEB</CodeCardNEW>
<IAMNew>Zack Burns</IAMNew>
<CardType>cCustomer</CardType>
...
And here's the XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:env="http://www.w3.org/2003/05/soap-envelope"
version="1.0">
<xsl:param name="map" select="document('map.xml')/env:map"></xsl:param>
<xsl:template match="text()" priority="1">
<xsl:value-of select="."/>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:variable name="newname">
<xsl:call-template name="namesub">
<xsl:with-param name="name" select="name()"/>
</xsl:call-template>
</xsl:variable>
<xsl:element name="{$newname}">
<xsl:apply-templates select="#*|node()"/>
</xsl:element>
</xsl:template>
<xsl:template name="namesub">
<xsl:param name="name"/>
<xsl:variable name="newname">
<xsl:for-each select="$map/elem">
<xsl:choose>
<xsl:when test="#old = $name">
<xsl:value-of select="#new"/>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:variable>
<xsl:choose>
<xsl:when test="string-length($newname) > 0">
<xsl:value-of select="$newname"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$name"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
You should be able to adapt this as long as you have a match on name ... if the name is the same in different hierarchies of the XML, you would need more work.
Related
Background info. I'm writing an XSL file to take an XML sent to the server by an analytical instrument and translate that file into an XML file accepted by our LIMS system.
Here is a Simple XML file coming from the instrument:
<dataRoot>
<dataRow>
<a0>2020-05-29 10:48:09 UTC-4</a0>
<a1>MSA - Conc.(Bench)</a1>
<a2>WHC202005270038</a2>
<a3>00251832</a3>
<a4>1.15966</a4>
<a5>MSA</a5>
<a6>101.067</a6>
</dataRow>
</dataRoot>
Here is what I have so far for the XSL File:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0"/>
<xsl:template match="/dataRoot">
<INBOUND>
<xsl:call-template name="A"/>
<xsl:copy-of select="A" />
</INBOUND>
</xsl:template>
<xsl:template match="/dataRow" name="A">
<xsl:for-each select="dataRow">
<xsl:variable name="sSampleID" select="normalize-space(a2)" />
<xsl:variable name="sSubmitter">AutoT_EQ00004</xsl:variable>
<xsl:variable name="sEnteredBy">EQ00004</xsl:variable>
<!-- Use a variable for OWNER, so that only one spot needs changing if a different value is desired -->
<xsl:variable name="sOwner">WHC</xsl:variable>
<xsl:variable name="sParameter" select="normalize-space(a5)" />
<xsl:variable name="sParamName"> <!-- Parameter names mapping for import to LIMS-->
<xsl:call-template name="ConvertPaName">
<xsl:with-param name="sPaName" select="$sParameter" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="sResult" select="normalize-space(a6)" />
<xsl:for-each select="$sParamName">
<INBOX_SAMPLE>
<EVENT>1</EVENT>
<SAMPLE_ID><xsl:value-of select="$sSampleID"/></SAMPLE_ID>
<SUBMITTER><xsl:value-of select="$sSubmitter" /></SUBMITTER>
<ENTERED_BY><xsl:value-of select="$sEnteredBy" /></ENTERED_BY>
<OWNER><xsl:value-of select="$sOwner" /></OWNER>
<PARAMETER_NAME><xsl:value-of select="$sParamName"/></PARAMETER_NAME>
<SRESULT><xsl:value-of select="$sResult"/></SRESULT>
</INBOX_SAMPLE>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<!-- Convert PaNames as needed, and skip processing certain values that are not actual Parameters (ie: 'Application') -->
<xsl:template name="ConvertPaName">
<xsl:param name="sPaName" />
<xsl:choose>
<xsl:when test="$sPaName='MSA'">MSA - Conc.(Bench)</xsl:when>
<xsl:when test="$sPaName='MSA'">Free MSA</xsl:when>
<xsl:otherwise>Halt_Import</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Here is the output of current translation:
<INBOUND>
<INBOX_SAMPLE>
<EVENT>1</EVENT>
<SAMPLE_ID>WHC202005270038</SAMPLE_ID>
<SUBMITTER>AutoT_EQ00004</SUBMITTER>
<ENTERED_BY>EQ00004</ENTERED_BY>
<OWNER>WHC</OWNER>
<PARAMETER_NAME>MSA - Conc.(Bench)</PARAMETER_NAME>
<SRESULT>101.067</SRESULT>
</INBOX_SAMPLE>
</INBOUND>
What I need to do is to duplicate this node for both parameter names:
<xsl:when test="$sPaName='MSA'">MSA - Conc.(Bench)</xsl:when>
<xsl:when test="$sPaName='MSA'">Free MSA</xsl:when>
So that my output becomes:
<INBOUND>
<INBOX_SAMPLE>
<EVENT>1</EVENT>
<SAMPLE_ID>WHC202005270038</SAMPLE_ID>
<SUBMITTER>AutoT_EQ00004</SUBMITTER>
<ENTERED_BY>EQ00004</ENTERED_BY>
<OWNER>WHC</OWNER>
<PARAMETER_NAME>MSA - Conc.(Bench)</PARAMETER_NAME>
<SRESULT>101.067</SRESULT>
</INBOX_SAMPLE>
<INBOX_SAMPLE>
<EVENT>1</EVENT>
<SAMPLE_ID>WHC202005270038</SAMPLE_ID>
<SUBMITTER>AutoT_EQ00004</SUBMITTER>
<ENTERED_BY>EQ00004</ENTERED_BY>
<OWNER>WHC</OWNER>
<PARAMETER_NAME>Free MSA</PARAMETER_NAME>
<SRESULT>101.067</SRESULT>
</INBOX_SAMPLE>
</INBOUND>
This is because instrument sends one parameter name like "MSA" but in our LIMS we have it under either "Free MSA" or "MSA - Conc.(Bench)" and I need to be able to provide the result for both possible parameter names and LIMS will insert the one that it find a match for.
Thank you so much in advanced :)
Add parm elements to template.
<xsl:template name="ConvertPaName">
<xsl:param name="sPaName" />
<xsl:choose>
<xsl:when test="$sPaName='MSA'">
<parm>MSA - Conc.(Bench)</parm>
<parm>Free MSA</parm>
</xsl:when>
<xsl:otherwise>
<parm>Halt_Import</parm>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Call your template
<xsl:variable name="sParamName">
<xsl:call-template name="ConvertPaName">
<xsl:with-param name="sPaName" select="$sParameter" />
</xsl:call-template>
</xsl:variable>
Convert to node set: (However you do it. You currently don't have the msxml namespace set up.)
<xsl:variable name="sParamNameList" select="msxml:node-set($sParamName)">
Then use the parm list in your for-each loop.
<xsl:for-each select="$sParamNameList/parm">
This question (and my problem) is similar to XSLT1 Get the first occurence of a specific tag... But I have an "Undefined variable" error.
I have a XML with refpos attribute, that can be make by this first XSLT,
<xsl:template match="p[#class='ref']">
<xsl:copy>
<xsl:attribute name="refpos">
<xsl:value-of select="position()"/>
</xsl:attribute>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()" name="idt">
<xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy>
</xsl:template>
Then, I a main (second) XSLT I have,
<xsl:variable name="firstRef">
<xsl:value-of select="(//p[#class='ref'])[1]/#refpos"/>
</xsl:variable>
<xsl:template match="p[#refpos=$firstRef]">
<title><xsl:apply-templates /></title>
</xsl:template>
<xsl:template match="#*|node()" name="idt">
<xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy>
</xsl:template>
But here this XSLT not works!!
PS: I also believe that XSLT1 allow us to do everything in one step.
XSLT 1.0 does not allow variable references in template match expressions (XSLT 2.0 does). You'll have to move the check from the predicate inside the template:
<xsl:template match="p">
<xsl:choose>
<xsl:when test="#refpos=$firstRef">
<title><xsl:apply-templates /></title>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="idt" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:for-each select="$script/startWith">
<xsl:variable name = "i" >
<xsl:value-of select="$script/startWith[position()]"/>
</xsl:variable>
<xsl:for-each select="JeuxDeMots/Element">
<xsl:variable name = "A" >
<xsl:value-of select="eName"/>
</xsl:variable>
<xsl:if test="starts-with($A,$i)= true()">
<xsl:variable name="stringReplace">
<xsl:value-of select="substring($i,0,3)"/>
</xsl:variable>
<xsl:value-of select="$stringReplace"/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
Problem : variable $i, can not pass in an xsl for-each.
Please help me.
Try replacing the declaration of i with
<xsl:variable name="i" select="string(.)"/>
The context item (i.e. ".") is different for each evaluation of the for-each instruction, but the value of the expression $script/startWith[position()] does not change. (Here, you are making a sequence of startWith elements, and testing each one for the effective boolean value of the expression position(). The expression position() returns a positive integer, so its effective boolean value is always true. So the predicate [position()] is doing no work at all here.
Also, you'll want to replace the declaration of stringReplace with
<xsl:variable name="stringReplace" select="substring($i,1,3)"/>
(String offsets begin with 1 in XPath, not with 0.)
I am guessing that you want to process all of the startWith children of $script, and for each one emit the first three characters of the startWith value once, for each startWith/JeuxDeMots/Element whose eName child begins with the value of startWith.
Actually, the whole thing might be a little easier to read if it were briefer and more direct:
<xsl:for-each select="$script/startWith">
<xsl:variable name = "i" select="string()"/>
<xsl:for-each select="JeuxDeMots/Element">
<xsl:if test="starts-with(eName,$i)">
<xsl:value-of select="substring($i,1,3)"/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
It makes perfect sense to create the variables $A and $stringReplace if they are used several times in code you're not showing us, but if not, ...
I think the problem is that you change context in first for-each. Then the element JeuxDeMots is not "visible" for second for-each. You can for example try to store it in variable and use this variable in second for-each (there are also another ways how to figure out this problem).
<xsl:template match="/">
<xsl:variable name="doc" select="JeuxDeMots"/>
<xsl:choose>
<xsl:when test="$script/startWith">
<xsl:for-each select="$script/startWith">
<xsl:variable name="i">
<xsl:value-of select="."/>
</xsl:variable>
<xsl:for-each select="$doc/Element">
<xsl:variable name="A">
<xsl:value-of select="eName"/>
</xsl:variable>
<xsl:if test="starts-with($A,$i) = true()">
<xsl:variable name="stringReplace">
<xsl:value-of select="substring($i,0,3)"/>
</xsl:variable>
<xsl:value-of select="$stringReplace"/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:when>
</xsl:choose>
</xsl:template>
Although I'm not sure what exactly you are dealing with it seems to output desired value AsAt.
You could also consider C.M.Sperberg-McQueen post about string offset in XPath.
I've got data in XML that has some header information then a series of items. I'm using XSLT to translate that into a different format, also with a header area and a series of items.
However in the post translation result, I want one piece of data only found in the header to be included in every instance of the items, even though it will simply be repeating the same value. (this value may change so I cannot hard code it)
Sample data (significantly simplified)
<rss>
<channel>
<title>Playlist One</title>
<items>
<item>
<title>Video One</title>
</item>
<item>
<title>Video Two</title>
</item>
<item>
<title>Video Three</title>
</item>
</items>
</channel>
</rss>
My desired result is something like this:
playlist_header_title=Playlist One
playlist_title=Playlist One
video_title=Video One
playlist_title=Playlist One
video_title=Video Two
playlist_title=Playlist One
video_title=Video Three
My XSLT is very complicated (and unfortunately I inherited it from someone else so I'm not sure what everything does, I've self-taught myself online but am in a bit over my head)
Roughly the key pieces look like this:
<xsl:template name="rss" match="/">
<xsl:variable name="playlist_title">
<xsl:value-of select="string(/rss/channel/title)"/>
</xsl:variable>
<xsl:for-each select="rss">
<xsl:apply-templates name="item" select="channel/items/item"/>
</xsl:for-each>
</xsl:template>
Then there's a massive template called "item" that I won't include here, but basically it outputs all the item data as desired, I just can't figure out how to access the "playlist_title".
When I try to call (from inside the template "item")
<xsl:value-of select="string($playlist_title)"/>
it returns a blank. I assume this is because that variable was created outside the for-each loop and so it not available. (it will display the data correctly when I output it before the for-each loop in the result's version of the header, but that doesn't get me far enough)
I've tried using with-param in the apply-templates, and also tried changing it to call-template inside another loop also using with-param, but they also display a blank.
I've also tried sending in a string rather than pulling the playlist_title from the XML just to confirm I am able to pass any value into the template, but they too come out blank.
For instance:
<xsl:for-each select="channel/items/item">
<xsl:call-template name="item">
<xsl:with-param name="playlist_title">blah</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
<xsl:template name="item">
<xsl:param name="playlist_title" />
playlist_title=<xsl:value-of select="string($playlist_title)"/>
video_title=...
...
</xsl:template>
This did not return the value "blah" but just a blank. (my hope was to then replace "blah" with the playlist_title value pulled from the XML, but none of it is getting through)
I'm stumped! Thanks for any help.
In your first example, attempting to reference the $playlist_title inside of your template for item is failing because the variable does not exist inside of that template. It was created inside of the template match for rss and is locally scoped.
If you want to be able to use the $playlist_title in other templates, you need to declare it outside of the rss template in the "global scope".
For instance:
<xsl:variable name="playlist_title">
<xsl:value-of select="string(/rss/channel/title)"/>
</xsl:variable>
<xsl:template name="rss" match="/">
<xsl:for-each select="rss">
<xsl:apply-templates name="item" select="channel/items/item"/>
</xsl:for-each>
</xsl:template>
As for your second attempt, referencing the xsl:param playlist_title, it worked for me. Double-check to ensure that you don't have a type-o or other difference between the name of the parameter that was declared and the name of the parameter that you are referencing.
You might try something like this to achieve your desired output, using an xsl:variable:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:variable name="playlist_title" select="/rss/channel/title" />
<xsl:template match="/">
<xsl:apply-templates select="rss/channel"/>
</xsl:template>
<xsl:template match="channel">
<xsl:text>playlist_header_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="items/item"/>
</xsl:template>
<xsl:template match="item">
<xsl:text>playlist_title=</xsl:text>
<xsl:value-of select="$playlist_title"/>
<xsl:text>
</xsl:text>
<xsl:text>video_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Or you can eliminate the xsl:variable and use an xsl:param:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:template match="/">
<xsl:apply-templates select="rss/channel"/>
</xsl:template>
<xsl:template match="channel">
<xsl:text>playlist_header_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="items/item">
<xsl:with-param name="playlist_title" select="title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="item">
<xsl:param name="playlist_title" />
<xsl:text>playlist_title=</xsl:text>
<xsl:value-of select="$playlist_title"/>
<xsl:text>
</xsl:text>
<xsl:text>video_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
I've been looking for a method to strip my XML content of apostrophes (') since my DBMS is complaining of receiving those.
I need
<name> Jim O'Connor</name>
to become:
<name> Jim O''Connor</name>
By looking at the example described here, that is supposed to replace ' with '', I constructed the following script:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes" />
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*" />
</xsl:copy>
</xsl:template>
<xsl:template name="sqlApostrophe">
<xsl:param name="string" />
<xsl:variable name="apostrophe">'</xsl:variable>
<xsl:choose>
<xsl:when test="contains($string,$apostrophe)">
<xsl:value-of select="concat(substring-before($string,$apostrophe), $apostrophe,$apostrophe)"
disable-output-escaping="yes" />
<xsl:call-template name="sqlApostrophe">
<xsl:with-param name="string"
select="substring-after($string,$apostrophe)" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$string"
disable-output-escaping="yes" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text()">
<xsl:call-template name="sqlApostrophe">
<xsl:with-param name="string" select="."/>
</xsl:call-template>
</xsl:template>
</xsl:stylesheet>
UPDATE: it works fine
Thanks for your help
The main problem is in your last template. As dacracot points out, xsl:apply-templates does not take a name attribute. To call a named template, you'd use xsl:call-template.
If you want to apply your SQL escaping to all text nodes, you could try replacing your last template with something like this:
<xsl:template match="text()">
<xsl:call-template name="sqlApostrophe">
<xsl:with-param name="string" select="."/>
</xsl:call-template>
</xsl:template>
Also, why disable-output-escaping? If the text contains special characters (<, >, &), you'll get malformed XML as the output.
You have a couple of issues syntactically...
You can't have a name attribute on an apply-templates tag.
Your xpath, "node()|#*", is ambiguous.
Have you run this through a debugger? I would suggest Oxygen.