XSLT - Modifying value of a XML node depending on other nodes - xslt

I am new to XSLT programming and struggling with the following issue:
XML:
<All_Results>
<Result>
<url>http://server/sites/sitecoll/library/Folder/NewFolder/test v1.0.docx</url>
<hithighlightedproperties>
<HHUrl>SomeValue1</HHUrl>
</hithighlightedproperties>
<isdocument>True</isdocument>
<serverredirectedurl>SomeValue</serverredirectedurl>
</Result>
<Result>
<url>http://server/sites/sitecoll/library/NewFolder1/test v2.0.docx</url>
<hithighlightedproperties>
<HHUrl>SomeValue1</HHUrl>
</hithighlightedproperties>
<isdocument>True</isdocument>
<serverredirectedurl>SomeValue</serverredirectedurl>
</Result>
<Result>
<url>http://server/sites/sitecoll/library/NewFolder/test v1.0.docx</url>
<hithighlightedproperties>
<HHUrl>SomeValue1</HHUrl>
</hithighlightedproperties>
<isdocument>False</isdocument>
<serverredirectedurl>SomeValue1</serverredirectedurl>
</Result>
......
......
Following is the requirement:
For each "Result" section,
if ("isdocument" node = True),
read the "url" node and get substring after 'library/' in it's value. From this output,
get the substring before the last occurrence of '/'. (Used separate a template to
achieve this) For example, for the first "Result", it will be "Folder/NewFolder".
Finally, concatenate hard-coded strings before and after this output and replace the
values of "HHUrl" and "ServerRedirectUrl" with this final output for every "Result"
under "Results".
Output
<All_Results>
<Result>
<url>http://server/sites/sitecoll/library/Folder/NewFolder/test v1.0.docx</url>
<hithighlightedproperties>
<HHUrl>http://SomeHardCodedString1/Folder/NewFolder/SomeHardCodedString2</HHUrl>
</hithighlightedproperties>
<isdocument>True</isdocument>
<serverredirectedurl>
http://SomeHardCodedString1/Folder/NewFolder/SomeHardCodedString2
</serverredirectedurl>
</Result>
<Result>
<url>http://server/sites/sitecoll/library/NewFolder1/test v2.0.docx</url>
<hithighlightedproperties>
<HHUrl>http://SomeHardCodedString1/NewFolder1/SomeHardCodedString2</HHUrl>
</hithighlightedproperties>
<isdocument>True</isdocument>
<serverredirectedurl>http://SomeHardCodedString1/NewFolder1/SomeHardCodedString2
</serverredirectedurl>
</Result>
<Result>
<url>http://server/sites/sitecoll/library/NewFolder/test v1.0.docx</url>
<hithighlightedproperties>
<HHUrl>SomeValue1</HHUrl>
</hithighlightedproperties>
<isdocument>False</isdocument>
<serverredirectedurl>SomeValue1</serverredirectedurl>
</Result>
......
......
I have trimmed the original XML output to simplify the requirement and have a long complex XLST associated with the original XML. The goal is to modify the "HHUrl" string on the fly before it is rendered as HTML. For this particular requirement, I have created and embedded the following code, which works partially:
<xsl:template name="stripLast">
<xsl:param name="pText"/>
<xsl:param name="pDelim" select="'/'"/>
<xsl:if test="contains($pText, $pDelim)">
<xsl:value-of select="substring-before($pText, $pDelim)"/>
<xsl:if test="contains(substring-after($pText, $pDelim), $pDelim)">
<xsl:value-of select="$pDelim"/>
</xsl:if>
<xsl:call-template name="stripLast">
<xsl:with-param name="pText" select=
"substring-after($pText, $pDelim)"/>
<xsl:with-param name="pDelim" select="$pDelim"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="All_Results/Result/hithighlightedproperties/HHUrl">
<xsl:param name="staticUrl" select=" 'https://SomeHardCodedString1/' "/>
<xsl:copy>
<xsl:variable name="urlValue" select="string(.)"/>
<xsl:variable name="s" select="substring-after($urlValue, 'Portal/')"/>
<xsl:variable name="qsValue">
<xsl:call-template name="stripLast">
<xsl:with-param name="pText" select="$s"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat($staticUrl, $qsValue, 'SomeHardCodedString2')"/>
</xsl:copy>
</xsl:template>
Any help will be highly appreciated.
Thanks,
SharePointDev.

I had a bit of a play around, the following idea might be useful. It will be a bit fragile though, if there are more than two levels in the bit you want to hard-code around (ie. "Folder/", "Folder/Folder1/", (and it breaks if), "Folder/Folder1/Folder2/"), but you could extend the idea:
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="//HHUrl[ancestor::Result[isdocument[.='True']]]">
<xsl:variable name="url" select="../../url"></xsl:variable>
<xsl:variable name="firsttoken" select="concat(substring-before(substring-after($url,'library/'),'/'),'/')"></xsl:variable>
<xsl:variable name="secondtoken" select="substring-before(substring-after($url,$firsttoken),'/')"></xsl:variable>
<xsl:variable name="thirdtoken" select="concat($firsttoken,$secondtoken)"></xsl:variable>
<HHUrl>http://SomeHardCodedString1/<xsl:if test="$secondtoken!=''"><xsl:value-of select="$thirdtoken"/></xsl:if><xsl:if test="$secondtoken=''"><xsl:value-of select="substring-before($firsttoken,'/')"/></xsl:if>/SomeHardCodedString2</HHUrl>
</xsl:template>
<xsl:template match="//serverredirectedurl[ancestor::Result[isdocument[.='True']]]">
<xsl:variable name="url" select="../url"></xsl:variable>
<xsl:variable name="firsttoken" select="concat(substring-before(substring-after($url,'library/'),'/'),'/')"></xsl:variable>
<xsl:variable name="secondtoken" select="substring-before(substring-after($url,$firsttoken),'/')"></xsl:variable>
<xsl:variable name="thirdtoken" select="concat($firsttoken,$secondtoken)"></xsl:variable>
<serverredirectedurl>http://SomeHardCodedString1/<xsl:if test="$secondtoken!=''"><xsl:value-of select="$thirdtoken"/></xsl:if><xsl:if test="$secondtoken=''"><xsl:value-of select="substring-before($firsttoken,'/')"/></xsl:if>/SomeHardCodedString2</serverredirectedurl>
</xsl:template>

Related

Schematron Using Include Fails With Ambiguous rule match

I am running the core schematron XSLT (as downloaded from http://schematron.com/front-page/the-schematron-skeleton-implementation/) against my schematron rules which have an include statement. When trying to run the XML instance to be tested through the intermediate XSL, it fails with 'Ambiguous rule match for' for each rule present in the included schematron.
"Description: Ambiguous rule match for
/filing:FilingMessage/filing:FilingConnectedDocument[1]/ecf:DocumentAugmentation[1]/ecf:DocumentRendition[1]/nc:Attachment[1]/nc:BinaryFormatText[1]
Matches both
"{http://release.niem.gov/niem/niem-core/3.0/}BinaryFormatText" on line 192 of file:/C:/_working/misc/schematron/schematron/trunk/schematron/code/temp.xsl
and
"{http://release.niem.gov/niem/niem-core/3.0/}BinaryFormatText" on line 175 of file:/C:/_working/misc/schematron/schematron/trunk/schematron/code/temp.xsl
URL: http://www.w3.org/TR/xslt20/#err-XTRE0540".
Upon inspecting the resulting intermediate XSL, it appears that the rules contained in the included schematron file are rendered into the intermediate XSL twice. Inspecting the XSL file iso_schematron_skeleton_for_saxon.xsl, it looks like the iso:include calls the rules template with both the pattern node and the rules node which results in the duplication of the data.
I would have assumed that the schematron XSLT on github is the definitive implementation of the schematron specification. Is that not the case or can anyone comment to this as I don't feel it is correct for me to have to tweak the schematron XSLT to make it work?
Base Schematron
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
<title>Test Schematron Illustrating Transform Bug</title>
<ns prefix="x" uri="http://www.w3.org/TR/REC-html40"/>
<ns prefix="filing" uri="https://docs.oasis-open.org/legalxml-courtfiling/ns/v5.0/filing"/>
<ns prefix="nc" uri="http://release.niem.gov/niem/niem-core/3.0/"/>
<include href="ExternalTestPattern.sch"/>
<pattern id="ecf">
<rule context="/filing:FilingMessage">
<assert test="./nc:DocumentIdentification/nc:IdentificationID">DocumentID must be present.</assert>
</rule>
</pattern>
</schema>
ExternalTestPattern.sch Schematron
<?xml version="1.0" encoding="UTF-8"?>
<pattern xmlns="http://purl.oclc.org/dsdl/schematron" id="code-list-rules">
<!-- Required namespace declarations as indicated in this set of rules:
<ns prefix="nc" uri="http://release.niem.gov/niem/niem-core/3.0/"/> -->
<rule context="nc:BinaryFormatText">
<assert test="( false() or ( contains('application/jsonapplication/mswordapplication/pdfapplication/vnd.oasis.opendocument.textapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentapplication/xml',concat('',.,'')) ) ) ">Invalid binary format code value.</assert>
</rule>
</pattern>
One other thing I forgot to mention is that if I simply use an XML editor like Oxygen or XML buddy to use my schematron to validate the XML instance, it works fine.
When I use Oxygen to load my Test.sch and also iso_schematron_message_xslt2.xslt, the resulting XSLT is below. Partway down you will see that the template for the rule defined in ExternalTestPattern.sch (match="nc:BinaryFormatText") is duplicated:
<xsl:stylesheet xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:saxon="http://saxon.sf.net/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:schold="http://www.ascc.net/xml/schematron" xmlns:iso="http://purl.oclc.org/dsdl/schematron" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:x="http://www.w3.org/TR/REC-html40" xmlns:filing="https://docs.oasis-open.org/legalxml-courtfiling/ns/v5.0/filing" xmlns:nc="http://release.niem.gov/niem/niem-core/3.0/" version="2.0">
<!--Implementers: please note that overriding process-prolog or process-root is the preferred method for meta-stylesheets to use where possible. -->
<xsl:param name="archiveDirParameter"/>
<xsl:param name="archiveNameParameter"/>
<xsl:param name="fileNameParameter"/>
<xsl:param name="fileDirParameter"/>
<xsl:variable name="document-uri">
<xsl:value-of select="document-uri(/)"/>
</xsl:variable>
<!--PHASES-->
<!--PROLOG-->
<xsl:output method="text"/>
<!--XSD TYPES FOR XSLT2-->
<!--KEYS AND FUNCTIONS-->
<!--DEFAULT RULES-->
<!--MODE: SCHEMATRON-SELECT-FULL-PATH-->
<!--This mode can be used to generate an ugly though full XPath for locators-->
<xsl:template match="*" mode="schematron-select-full-path">
<xsl:apply-templates select="." mode="schematron-get-full-path"/>
</xsl:template>
<!--MODE: SCHEMATRON-FULL-PATH-->
<!--This mode can be used to generate an ugly though full XPath for locators-->
<xsl:template match="*" mode="schematron-get-full-path">
<xsl:apply-templates select="parent::*" mode="schematron-get-full-path"/>
<xsl:text>/</xsl:text>
<xsl:choose>
<xsl:when test="namespace-uri()=''">
<xsl:value-of select="name()"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>*:</xsl:text>
<xsl:value-of select="local-name()"/>
<xsl:text>[namespace-uri()='</xsl:text>
<xsl:value-of select="namespace-uri()"/>
<xsl:text>']</xsl:text>
</xsl:otherwise>
</xsl:choose>
<xsl:variable name="preceding" select="count(preceding-sibling::*[local-name()=local-name(current()) and namespace-uri() = namespace-uri(current())])"/>
<xsl:text>[</xsl:text>
<xsl:value-of select="1+ $preceding"/>
<xsl:text>]</xsl:text>
</xsl:template>
<xsl:template match="#*" mode="schematron-get-full-path">
<xsl:apply-templates select="parent::*" mode="schematron-get-full-path"/>
<xsl:text>/</xsl:text>
<xsl:choose>
<xsl:when test="namespace-uri()=''">#<xsl:value-of select="name()"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>#*[local-name()='</xsl:text>
<xsl:value-of select="local-name()"/>
<xsl:text>' and namespace-uri()='</xsl:text>
<xsl:value-of select="namespace-uri()"/>
<xsl:text>']</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!--MODE: SCHEMATRON-FULL-PATH-2-->
<!--This mode can be used to generate prefixed XPath for humans-->
<xsl:template match="node() | #*" mode="schematron-get-full-path-2">
<xsl:for-each select="ancestor-or-self::*">
<xsl:text>/</xsl:text>
<xsl:value-of select="name(.)"/>
<xsl:if test="preceding-sibling::*[name(.)=name(current())]">
<xsl:text>[</xsl:text>
<xsl:value-of select="count(preceding-sibling::*[name(.)=name(current())])+1"/>
<xsl:text>]</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:if test="not(self::*)">
<xsl:text/>/#<xsl:value-of select="name(.)"/>
</xsl:if>
</xsl:template>
<!--MODE: SCHEMATRON-FULL-PATH-3-->
<!--This mode can be used to generate prefixed XPath for humans
(Top-level element has index)-->
<xsl:template match="node() | #*" mode="schematron-get-full-path-3">
<xsl:for-each select="ancestor-or-self::*">
<xsl:text>/</xsl:text>
<xsl:value-of select="name(.)"/>
<xsl:if test="parent::*">
<xsl:text>[</xsl:text>
<xsl:value-of select="count(preceding-sibling::*[name(.)=name(current())])+1"/>
<xsl:text>]</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:if test="not(self::*)">
<xsl:text/>/#<xsl:value-of select="name(.)"/>
</xsl:if>
</xsl:template>
<!--MODE: GENERATE-ID-FROM-PATH -->
<xsl:template match="/" mode="generate-id-from-path"/>
<xsl:template match="text()" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path"/>
<xsl:value-of select="concat('.text-', 1+count(preceding-sibling::text()), '-')"/>
</xsl:template>
<xsl:template match="comment()" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path"/>
<xsl:value-of select="concat('.comment-', 1+count(preceding-sibling::comment()), '-')"/>
</xsl:template>
<xsl:template match="processing-instruction()" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path"/>
<xsl:value-of select="concat('.processing-instruction-', 1+count(preceding-sibling::processing-instruction()), '-')"/>
</xsl:template>
<xsl:template match="#*" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path"/>
<xsl:value-of select="concat('.#', name())"/>
</xsl:template>
<xsl:template match="*" mode="generate-id-from-path" priority="-0.5">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path"/>
<xsl:text>.</xsl:text>
<xsl:value-of select="concat('.',name(),'-',1+count(preceding-sibling::*[name()=name(current())]),'-')"/>
</xsl:template>
<!--MODE: GENERATE-ID-2 -->
<xsl:template match="/" mode="generate-id-2">U</xsl:template>
<xsl:template match="*" mode="generate-id-2" priority="2">
<xsl:text>U</xsl:text>
<xsl:number level="multiple" count="*"/>
</xsl:template>
<xsl:template match="node()" mode="generate-id-2">
<xsl:text>U.</xsl:text>
<xsl:number level="multiple" count="*"/>
<xsl:text>n</xsl:text>
<xsl:number count="node()"/>
</xsl:template>
<xsl:template match="#*" mode="generate-id-2">
<xsl:text>U.</xsl:text>
<xsl:number level="multiple" count="*"/>
<xsl:text>_</xsl:text>
<xsl:value-of select="string-length(local-name(.))"/>
<xsl:text>_</xsl:text>
<xsl:value-of select="translate(name(),':','.')"/>
</xsl:template>
<!--Strip characters--><xsl:template match="text()" priority="-1"/>
<!--SCHEMA SETUP-->
<xsl:template match="/">
<xsl:apply-templates select="/" mode="M0"/>
<xsl:apply-templates select="/" mode="M5"/>
</xsl:template>
<!--SCHEMATRON PATTERNS-->
<!--PATTERN code-list-rules-->
<!--RULE -->
<xsl:template match="nc:BinaryFormatText" priority="1000" mode="M0">
<!--ASSERT -->
<xsl:choose>
<xsl:when test="( false() or ( contains('application/jsonapplication/mswordapplication/pdfapplication/vnd.oasis.opendocument.textapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentapplication/xml',concat('',.,'')) ) ) "/>
<xsl:otherwise>
<xsl:message>Invalid binary format code value. (( false() or ( contains('application/jsonapplication/mswordapplication/pdfapplication/vnd.oasis.opendocument.textapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentapplication/xml',concat('',.,'')) ) ))</xsl:message>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M0"/>
</xsl:template>
<xsl:template match="text()" priority="-1" mode="M0"/>
<xsl:template match="#*|node()" priority="-2" mode="M0">
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M0"/>
</xsl:template>
<!--RULE -->
<xsl:template match="nc:BinaryFormatText" priority="1000" mode="M0">
<!--ASSERT -->
<xsl:choose>
<xsl:when test="( false() or ( contains('application/jsonapplication/mswordapplication/pdfapplication/vnd.oasis.opendocument.textapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentapplication/xml',concat('',.,'')) ) ) "/>
<xsl:otherwise>
<xsl:message>Invalid binary format code value. (( false() or ( contains('application/jsonapplication/mswordapplication/pdfapplication/vnd.oasis.opendocument.textapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentapplication/xml',concat('',.,'')) ) ))</xsl:message>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M0"/>
</xsl:template>
<!--PATTERN ecf-->
<!--RULE -->
<xsl:template match="/filing:FilingMessage" priority="1000" mode="M5">
<!--ASSERT -->
<xsl:choose>
<xsl:when test="./nc:DocumentIdentification/nc:IdentificationID"/>
<xsl:otherwise>
<xsl:message>DocumentID must be present. (./nc:DocumentIdentification/nc:IdentificationID)</xsl:message>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M5"/>
</xsl:template>
<xsl:template match="text()" priority="-1" mode="M5"/>
<xsl:template match="#*|node()" priority="-2" mode="M5">
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M5"/>
</xsl:template>
</xsl:stylesheet>
Any enlightenment would be really helpful.
Thanks
Ensure that you are compiling the Schematron into XSLT by applying the sequence of transformations outlined in the readme, using the output of the prior XSLT transforms as the input to the next step:
1) First, preprocess your Schematron schema with iso_dsdl_include.xsl.
This is a macro processor to assemble the schema from various parts.
If your schema is not in separate parts, you can skip this stage. This
stage also generates error messages for some common XPath syntax
problems.
2) Second, preprocess the output from stage 1 with
iso_abstract_expand.xsl. This is a macro processor to convert
abstract patterns to real patterns. If your schema does not use
abstract patterns, you can skip this stage.
3) Third, compile the Schematron schema into an XSLT script. This
will typically use iso_svrl_for_xslt1.xsl or iso_svrl_for_xslt2.xsl
(which in turn invoke iso_schematron_skeleton_for_xslt1.xsl or
iso_schematron_skeleton_for_saxon.xsl) However, other
"meta-stylesheets" are also in common use; the principle of operation
is the same. If your schema uses Schematron phases, supply these as
command line/invocation parameters to this process.
4) Fourth, run the script generated by stage 3 against the document
being validated. If you are using the SVRL script, then the output of
validation will be an XML document. If your schema uses Schematron
parameters, supply these as command line/invocation parameters to this
process.
Also, ensure that you are applying iso_svrl_for_xslt2.xsl (which imports iso_schematron_skeleton_for_saxon.xsl) and are not using iso_schematron_skeleton_for_saxon.xsl directly

XSLT Expertise required duplicate values

Question edited, more information added
viv:tokenize=str:tokenize
viv:value-of=str:value-of
Part1 - Declaration and assigning value
<declare name="searchhistories" />
<set-var name="searchhistories">
<value-of select="concat(viv:value-of('searchquery','var'),'|',viv:replace(viv:value-of('searchhistory', 'var'),concat(viv:value-of('searchquery','var'),'\|'),'','g'))" />
</set-var>
Part 2: tokenize and de-duplicate
<xsl:for-each select="viv:tokenize($searchhistories,'|',false, false)">
<xsl:variable name="i" select="position()"/>
<xsl:if test="$i < 11">
<xsl:value-of select="." /> |
</xsl:if>
</xsl:for-each>
Able to tokenize but de-duplication not working
What should be code for de-duplication
<xsl:for-each select=***distinct-values***("viv:tokenize($searchhistories,'|',false, false)")>
Something like this ?
Try
<xsl:for-each select="set:distinct(viv:tokenize($searchhistories,'|',false, false))">
with the stylesheet declaring xmlns:set="http://exslt.org/sets" e.g.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:set="http://exslt.org/sets" exclude-result-prefixes="set">
The answer is based on the documentation you linked to in your comment, I am not able to test that.
But http://xsltransform.net/ej9EGcy uses the EXSLT version of tokenize and works fine:
<xsl:template match="item">
<xsl:copy>
<xsl:for-each select="set:distinct(str:tokenize(., '|'))">
<xsl:if test="position() > 1">|</xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>

Replace all instances of a string in XML with ****

I have a XSL that needs to filter out specific data found in the XML.
Somewhere in my XML there will be a node like:
<id root="2.16.840.1.113883.3.51.1.1.6.1" extension="9494949494949" />
The XSL I have below deletes the extension node and adds a nullFlavor="MSK" to the node.
What I need to do now, is take the value from the extension node, and search the entire XML document for that value, and replace it with **.
But I'm not sure how to take the extension attribute, and find all instances of that value in the XML (they could be burried in text and inside attributes) and turn them into ** (4 *).
The example below is just an example. I cannot hard code the XSL to look at specific nodes, it needs to look through all text / attribute text in the xml (reason for this is there are 5+ different versions of XML that this will be applied to).
I need to find the Extension in the node, then replace (delete really) that value from the rest of the XML. I'm looking for a 1 solution fits all messages, so a global search->wipe of the Extension value.
Example:
<identifiedPerson classCode="IDENT">
<id root="2.16.840.1.113883.3.51.1.1.6.1" extension="9494949494949" displayable="true" />
<addr use="PHYS">
<city>KAMLOOPS</city>
<country>CA</country>
<postalCode>V1B3C1</postalCode>
<state>BC</state>
<streetAddressLine>1A</streetAddressLine>
<streetAddressLine>2A</streetAddressLine>
<streetAddressLine>9494949494949</streetAddressLine>
<streetAddressLine>4A</streetAddressLine>
</addr>
<note text="9494949494949 should be stars"/>
Should be (The below XSLT already masks the extension in the node with the matching OID).
<identifiedPerson classCode="IDENT">
<id root="2.16.840.1.113883.3.51.1.1.6.1" nullFlavor="MSK" displayable="true" />
<addr use="PHYS">
<city>KAMLOOPS</city>
<country>CA</country>
<postalCode>V1B3C1</postalCode>
<state>BC</state>
<streetAddressLine>1A</streetAddressLine>
<streetAddressLine>2A</streetAddressLine>
<streetAddressLine>****</streetAddressLine>
<streetAddressLine>4A</streetAddressLine>
</addr>
<note text="**** should be stars"/>
Any help would be appreciated.
I am able to use XSL 2.0
I have the current XSL.IT works fine. It matches any tag where the root is '2.16.840.1.113883.3.51.1.1.6.1', kills all attributes and adds a nullFlavor="MSK". However, this will not search the entire XML for that same #.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="attrToKeep" select="'root'" />
<xsl:template match="* | node()">
<xsl:copy>
<xsl:apply-templates select="node()|#*" />
</xsl:copy>
</xsl:template>
<xsl:template match="#*">
<xsl:choose>
<xsl:when test="../#root = '2.16.840.1.113883.3.51.1.1.6.1'">
<xsl:copy-of select=".[contains($attrToKeep, name())]" />
<xsl:attribute name="nullFlavor">MSK</xsl:attribute>
<!-- Need some way to use the value found in this node and hide the extension -->
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="." />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Any help would be appreciated.
Thanks,
Try using a variable to hold the value of the text to be replaced. Like this:
<xsl:variable
name="rootVar"
select="//*[#root = '2.16.840.1.113883.3.51.1.1.6.1']/#extension" />
And then you should just be able to use the replace function to replace them.
<xsl:template match="'//#*' | text()">
<xsl:sequence select="replace(., $rootVar, '****')"/>
</xsl:template>
The XSLT 2.0 stylesheet
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:param name="replacement" select="'****'"/>
<xsl:param name="new" select="'MKS'"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="identifiedPerson">
<xsl:copy>
<xsl:apply-templates select="#* , node()">
<xsl:with-param name="to-be-replaced" select="id/#extension" tunnel="yes"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="identifiedPerson//text()">
<xsl:param name="to-be-replaced" tunnel="yes"/>
<xsl:sequence select="replace(., $to-be-replaced, $replacement)"/>
</xsl:template>
<xsl:template match="identifiedPerson//#*">
<xsl:param name="to-be-replaced" tunnel="yes"/>
<xsl:attribute name="{name()}" namespace="{namespace-uri()}" select="replace(., $to-be-replaced, $replacement)"/>
</xsl:template>
<xsl:template match="identifiedPerson/id">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:attribute name="nullFlavor" select="$new"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="identifiedPerson/id/#extension"/>
</xsl:stylesheet>
transforms
<identifiedPerson classCode="IDENT">
<id root="2.16.840.1.113883.3.51.1.1.6.1" extension="9494949494949" displayable="true" />
<addr use="PHYS">
<city>KAMLOOPS</city>
<country>CA</country>
<postalCode>V1B3C1</postalCode>
<state>BC</state>
<streetAddressLine>1A</streetAddressLine>
<streetAddressLine>2A</streetAddressLine>
<streetAddressLine>9494949494949</streetAddressLine>
<streetAddressLine>4A</streetAddressLine>
</addr>
<note text="9494949494949 should be stars"/>
</identifiedPerson>
with Saxon 9.4 into
<?xml version="1.0" encoding="UTF-8"?><identifiedPerson classCode="IDENT">
<id root="2.16.840.1.113883.3.51.1.1.6.1" displayable="true" nullFlavor="MKS"/>
<addr use="PHYS">
<city>KAMLOOPS</city>
<country>CA</country>
<postalCode>V1B3C1</postalCode>
<state>BC</state>
<streetAddressLine>1A</streetAddressLine>
<streetAddressLine>2A</streetAddressLine>
<streetAddressLine>****</streetAddressLine>
<streetAddressLine>4A</streetAddressLine>
</addr>
<note text="**** should be stars"/>
</identifiedPerson>
So for the sample it solves that problem I think. I am not sure whether there can be more context around that sample and whether you want to change values outside of the identifiedPerson element as well or don't want to change them (which above stylesheet does). If other elements also need to be changed consider to post longer input and wanted result samples to illustrate and also explain what determines the node where the value to be replaced is found.
[edit]
Based on your comment I adapted the stylesheet, it now has a parameter to pass in a id (e.g. 2.16.840.1.113883.3.51.1.1.6.1), then it looks for an element of any name with a root attribute having that passed in id value and replaces the extension attribute value found in all attributes and all text nodes found in the document. Furthermore a nullFlavor attribute is added to the element with the id and its extension attribute is removed.
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:param name="root-id" select="'2.16.840.1.113883.3.51.1.1.6.1'"/>
<xsl:variable name="to-be-replaced" select="//*[#root = $root-id]/#extension"/>
<xsl:param name="replacement" select="'****'"/>
<xsl:param name="new" select="'MKS'"/>
<xsl:template match="comment() | processing-instruction()">
<xsl:copy/>
</xsl:template>
<xsl:template match="*">
<xsl:copy>
<xsl:apply-templates select="#* , node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()">
<xsl:sequence select="replace(., $to-be-replaced, $replacement)"/>
</xsl:template>
<xsl:template match="#*">
<xsl:attribute name="{name()}" namespace="{namespace-uri()}" select="replace(., $to-be-replaced, $replacement)"/>
</xsl:template>
<xsl:template match="*[#root = $root-id]">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:attribute name="nullFlavor" select="$new"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[#root = $root-id]/#extension"/>
</xsl:stylesheet>

How to do this in XSLT without incrementing variables? (Tweaking Xalan to create a global XSLT iterator. Do I have other options?)

I'm trying to think functional, in XSLT terms, as much as possible, but in this case, I really don't see how to do it without tweaking. I have roughly this data structure:
<transactions>
<trx>
<text>abc</text>
<text>def</text>
<detail>
<text>xxx</text>
<text>yyy</text>
<text>zzz</text>
</detail>
</trx>
</transactions>
Which I roughly want to flatten into this form
<row>abc</row>
<row>def</row>
<row>xxx</row>
<row>yyy</row>
<row>zzz</row>
But the tricky thing is: I want to create chunks of 40 text-rows and transactions mustn't be split across chunks. I.e. if my current chunk already has 38 rows, the above transaction would have to go into the next chunk. The current chunk would need to be filled with two empty rows to complete the 40:
<row/>
<row/>
In imperative/procedural programming, it's very easy. Just create a global iterator variable counting to multiples of 40, and insert empty rows if needed (I have provided an answer showing how to tweak XSLT/Xalan to allow for such variables). But how to do it with XSLT? N.B: I'm afraid recursion is not possible considering the size of data I'm processing... But maybe I'm wrong on that
I. Here is an XSLT 1.0 solution (the XSLT 2.0 solution is much easier):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pChunkSize" select="8"/>
<xsl:param name="vChunkSize" select="$pChunkSize+1"/>
<xsl:variable name="vSheet" select="document('')"/>
<xsl:variable name="vrtfEmptyChunk">
<xsl:for-each select=
"($vSheet//node())[not(position() > $pChunkSize)]">
<row/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="vEmptyChunk" select=
"ext:node-set($vrtfEmptyChunk)/*"/>
<xsl:variable name="vrtfDummy">
<delete/>
</xsl:variable>
<xsl:variable name="vDummy" select="ext:node-set($vrtfDummy)/*"/>
<xsl:template match="/*">
<chunks>
<xsl:call-template name="fillChunks">
<xsl:with-param name="pNodes" select="trx"/>
<xsl:with-param name="pCurChunk" select="$vDummy"/>
</xsl:call-template>
</chunks>
</xsl:template>
<xsl:template name="fillChunks">
<xsl:param name="pNodes"/>
<xsl:param name="pCurChunk"/>
<xsl:choose>
<xsl:when test="not($pNodes)">
<chunk>
<xsl:apply-templates mode="rename" select="$pCurChunk[self::text]"/>
<xsl:copy-of select=
"$vEmptyChunk[not(position() > $vChunkSize - count($pCurChunk))]"/>
</chunk>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vAvailable" select=
"$vChunkSize - count($pCurChunk)"/>
<xsl:variable name="vcurNode" select="$pNodes[1]"/>
<xsl:variable name="vTrans" select="$vcurNode//text"/>
<xsl:variable name="vNumNewNodes" select="count($vTrans)"/>
<xsl:choose>
<xsl:when test="not($vNumNewNodes > $vAvailable)">
<xsl:variable name="vNewChunk"
select="$pCurChunk | $vTrans"/>
<xsl:call-template name="fillChunks">
<xsl:with-param name="pNodes" select="$pNodes[position() > 1]"/>
<xsl:with-param name="pCurChunk" select="$vNewChunk"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<chunk>
<xsl:apply-templates mode="rename" select="$pCurChunk[self::text]"/>
<xsl:copy-of select=
"$vEmptyChunk[not(position() > $vAvailable)]"/>
</chunk>
<xsl:call-template name="fillChunks">
<xsl:with-param name="pNodes" select="$pNodes"/>
<xsl:with-param name="pCurChunk" select="$vDummy"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text" mode="rename">
<row>
<xsl:value-of select="."/>
</row>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document (based on the provided one, but with three trxelements):
<transactions>
<trx>
<text>abc</text>
<text>def</text>
<detail>
<text>xxx</text>
<text>yyy</text>
<text>zzz</text>
</detail>
</trx>
<trx>
<text>abc2</text>
<text>def2</text>
</trx>
<trx>
<text>abc3</text>
<text>def3</text>
<detail>
<text>xxx3</text>
<text>yyy3</text>
<text>zzz3</text>
</detail>
</trx>
</transactions>
the wanted, correct result (two chunks with size 8) is produced:
<chunks>
<chunk>
<row>abc</row>
<row>def</row>
<row>xxx</row>
<row>yyy</row>
<row>zzz</row>
<row>abc2</row>
<row>def2</row>
<row/>
</chunk>
<chunk>
<row>abc3</row>
<row>def3</row>
<row>xxx3</row>
<row>yyy3</row>
<row>zzz3</row>
<row/>
<row/>
<row/>
</chunk>
</chunks>
Do note:
The first two transactions' text elements total number is 7 and they fit in one 8-place chunk.
The third transaction has 5 text elements and doesn't fit in the remaining space of the first chunk -- it is put in a new chunk.
II. XSLT 2.0 Solution (using FXSL)
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="http://fxsl.sf.net/"
xmlns:dvc-foldl-func="dvc-foldl-func"
exclude-result-prefixes="f dvc-foldl-func"
>
<xsl:import href="../f/func-dvc-foldl.xsl"/>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pChunkSize" select="8"/>
<dvc-foldl-func:dvc-foldl-func/>
<xsl:variable name="vPadding">
<row/>
</xsl:variable>
<xsl:variable name="vFoldlFun" select="document('')/*/dvc-foldl-func:*[1]"/>
<xsl:template match="/">
<xsl:variable name="vpaddingChunk" select=
"for $i in 1 to $pChunkSize
return ' '
"/>
<xsl:variable name="vfoldlResult" select=
"f:foldl($vFoldlFun, (), /*/trx),
$vpaddingChunk
"/>
<xsl:variable name="vresultCount"
select="count($vfoldlResult)"/>
<xsl:variable name="vFinalResult"
select="subsequence($vfoldlResult, 1,
$vresultCount - $vresultCount mod $pChunkSize
)"/>
<result>
<xsl:for-each select="$vFinalResult">
<row>
<xsl:value-of select="."/>
</row>
</xsl:for-each>
<xsl:text>
</xsl:text>
</result>
</xsl:template>
<xsl:template match="dvc-foldl-func:*" mode="f:FXSL">
<xsl:param name="arg1"/>
<xsl:param name="arg2"/>
<xsl:variable name="vCurCount" select="count($arg1)"/>
<xsl:variable name="vNewCount" select="count($arg2//text)"/>
<xsl:variable name="vAvailable" select=
"$pChunkSize - $vCurCount mod $pChunkSize"/>
<xsl:choose>
<xsl:when test="$vNewCount le $vAvailable">
<xsl:sequence select="$arg1, $arg2//text"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$arg1"/>
<xsl:for-each select="1 to $vAvailable">
<xsl:sequence select="$vPadding/*"/>
</xsl:for-each>
<xsl:sequence select="$arg2//text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), the same correct, wanted result is produced:
<result>
<row>abc</row>
<row>def</row>
<row>xxx</row>
<row>yyy</row>
<row>zzz</row>
<row>abc2</row>
<row>def2</row>
<row/>
<row>abc3</row>
<row>def3</row>
<row>xxx3</row>
<row>yyy3</row>
<row>zzz3</row>
<row> </row>
<row> </row>
<row> </row>
</result>
Do note:
The use of the f:foldl() function.
A special DVC (Divide and Conquer) variant of f:foldl() so that recursion stack overflow is avoided for all practical purposes -- for example, the maximum recursion stack depth for 1000000 (1M) trx elements is just 19.
Build the complete XML data structure as you need in Java. Then, do the simple iteration in XSL over prepared XML.
You might save a lot of effort and provide a maintainable solution.
As promised a simplified example answer showing how Xalan can be tweaked to allow for incrementing such global iterators:
<xsl:stylesheet version="1.0" xmlns:f="xalan://com.example.Functions">
<!-- the global row counter variable -->
<xsl:variable name="row" select="0"/>
<xsl:template match="trx">
<!-- wherever needed, the $row variable can be globally incremented -->
<xsl:variable name="iteration" value="f:increment('row')"/>
<!-- based upon this variable, calculations can be made -->
<xsl:variable name="remaining-rows-in-chunk"
value="40 - (($iteration - 1) mod 40) "/>
<xsl:if test="count(.//text) > $remaining-rows-in-chunk">
<xsl:call-template name="empty-row">
<xsl:with-param name="rows" select="$remaining-rows-in-chunk"/>
</xsl:call-template>
</xsl:if>
<!-- process transaction now, that previous chunk has been filled [...] -->
</xsl:template>
<xsl:template name="empty-row">
<xsl:param name="rows"/>
<xsl:if test="$rows > 0">
<row/>
<xsl:variable name="dummy" select="f:increment('row')"/>
<xsl:call-template name="empty-row">
<xsl:with-param name="rows" select="$rows - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
And the contents of com.example.Functions:
public class Functions {
public static String increment(ExpressionContext context, String nodeName) {
XNumber n = null;
try {
// Access the $row variable
n = ((XNumber) context.getVariableOrParam(new QName(nodeName)));
// Make it "mutable" using this tweak. I feel horrible about
// doing this, though ;-)
Field m_val = XNumber.class.getDeclaredField("m_val");
m_val.setAccessible(true);
// Increment it
m_val.setDouble(n, m_val.getDouble(n) + 1.0);
} catch (Exception e) {
log.error("Error", e);
}
return n == null ? null : n.str();
}
}

xslt: create new node from mixed content within parent element

I'm trying to write a template that will grab a mixture of text nodes and elements within a parent element and create a new node. I've done a lot of searching and couldn't find what I was looking for...so hopefully I'm not asking to basic a question.
Here is a sample of xml I want to transform:
<?xml version="1.0"?>
<root>
<para>Here is some text that will ask users to enter a <rule-line/> [<emph type="it">date</emph>], and maybe their <rule-line/> [<emph type="it">name</emph>]. The text could come in different [<emph type="it">order</emph>] <rule-line/>, and their could be any number of instances.</para>
</root>
I want to group the bracketed text and the rule into a new element like so:
<entry>[<emph type"it">date</emph>]</entry>
I have a template that can identify the text I want to change, and I can change it, but I don't know how to add the text I want to the result tree and omit the old text.
Here are the relevant templates:
<xsl:template match="para">
<xsl:for-each select="* | text()">
<xsl:choose>
<xsl:when test="self::rule-line and following-sibling::node()[1][starts-with(., ' [')] and string(node-name(following-sibling::node()[2])) = 'emph' and following-sibling::node()[3][starts-with(., ']')]">
<xsl:comment>made match</xsl:comment>
<xsl:call-template name="codeEntry">
<xsl:with-param name="rule" select="."/>
<xsl:with-param name="openBracket" select="following-sibling::node()[1]"/>
<xsl:with-param name="emphTag" select="following-sibling::node()[2]"/>
<xsl:with-param name="closeBracketString" select="following-sibling::node()[3]"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<xsl:template name="codeEntry">
<xsl:param name="rule"/>
<xsl:param name="openBracket"/>
<xsl:param name="emphTag"/>
<xsl:param name="closeBracketString"/>
<entry>
<xsl:copy-of select="$openBracket"/>
<xsl:copy-of select="$emphTag"/>
<xsl:text>] </xsl:text>
</entry>
<xsl:value-of select="substring-after($closeBracketString, ']')"/>
</xsl:template>
Obviously, the when statement grabs a group of nodes, but when each node goes through the otherwise block it gets copied to the result tree. I'm not really sure how to handle this since the para could have any number of these node groupings in any order, or none. (Once I figure this out I'll add another when block that deals with the bracketed text before the rule)
I think creating a variable that tells the template to ignore the node is the way to go...but I'm a little foggy on the immutable variables and their scope...
I was also trying to think of a way I could try to do this recursively...but that would require adding a start tag at one point, an end tag in another, or no tag if the node being processed is in the middle of the sequence...and I know that can get weird in xslt.
Anyone run into this type of situation before?
thanks,
jason
any ideas
Just for fun (What a mess of a schema!), this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="rule-line"/>
<xsl:template match="emph">
<entry>
<xsl:text>[</xsl:text>
<xsl:call-template name="identity"/>
<xsl:text>]</xsl:text>
</entry>
</xsl:template>
<xsl:template match="text()[normalize-space()='[']
[following-sibling::*[1][self::emph]] |
text()[normalize-space()=']']
[preceding-sibling::*[1][self::emph]]"
priority="1"/>
<xsl:template match="text()[starts-with(normalize-space(),']')]
[preceding-sibling::*[1][self::emph]]">
<xsl:value-of select="substring-after(.,']')"/>
</xsl:template>
<xsl:template match="text()[substring(normalize-space(),
string-length(normalize-space()),
1) = '[']
[following-sibling::*[1][self::emph]]">
<xsl:call-template name="crop-both">
<xsl:with-param name="pString" select="concat(']',.)"/>
</xsl:call-template>
</xsl:template>
<xsl:template match="text()[starts-with(normalize-space(),']')]
[substring(normalize-space(),
string-length(normalize-space()),
1) = '[']
[preceding-sibling::*[1][self::emph]]
[following-sibling::*[1][self::emph]]"
priority="1" name="crop-both">
<xsl:param name="pString" select="."/>
<xsl:variable name="vReverse">
<xsl:call-template name="reverse">
<xsl:with-param name="pString"
select="substring-after(.,']')"/>
</xsl:call-template>
</xsl:variable>
<xsl:call-template name="reverse">
<xsl:with-param name="pString"
select="substring-after($vReverse,'[')"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="reverse">
<xsl:param name="pString"/>
<xsl:if test="$pString!=''">
<xsl:call-template name="reverse">
<xsl:with-param name="pString"
select="substring($pString,2)"/>
</xsl:call-template>
<xsl:value-of select="substring($pString,1,1)"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Output:
<root>
<para>Here is some text that will ask users to enter a <entry>[<emph type="it">date</emph>]</entry>, and maybe their <entry>[<emph type="it">name</emph>]</entry>. The text could come in different <entry>[<emph type="it">order</emph>]</entry>, and their could be any number of instances.</para>
</root>