I have an xml file which describes (among other things) elements with attribute values that describe fully qualified java class names. I am trying to write an XSLT transformation to modify the class names described in this file, such that (for example) ocurrances of com.example.MyClass
will become com.example.MockMyClass
.
Here's that example again in the context of a fragment of the original file:
<event type="node-enter">
<action name="MyActionName" class="com.example.MyClass">
<bodyTemplate>
templates/MyTemplate.vm
</bodyTemplate>
</action>
</event>
I want the result to be:
<event type="node-enter">
<action name="MyActionName" class="com.example.MockMyClass">
<bodyTemplate>
templates/MyTemplate.vm
</bodyTemplate>
</action>
</event>
I'm doing this transformation using the Java JAXP API, and had written a lovely XSLT 2.0 compliant regex routine to get the results I want, only to discover that Java 5 doesn't support XSLT 2.0, which is required for regex support.
So my question is, what is the best way to achieve this using the archaic JAXP XSLT 1.0 API? That is, without the use of regular expressions. I looked for similar problems, but the requirement for backreferencing regex groups seems to make this a tricky one. This question is a start, but I need to insert text, within a matching string, rather than just replacing.
For reference, here is my regex (XSLT 2.0) attempt:
<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
<xsl:template match='/'>
<xsl:analyze-string select='action/@class' regex='([A-Za-z0-9]+[$\.])+([A-Za-z0-9]+)'>
<xsl:matching-substring>
<xsl:value-of select='regex-group(1)'/>
<xsl:text>Mock</xsl:text>
<xsl:value-of select='regex-group(2)'/>
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select='.'/>
</xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:template>
</xsl:stylesheet>
How about the following?
<xsl:template name="classname">
<xsl:param name="class"/>
<xsl:choose>
<xsl:when test="contains($class,'.')">
<xsl:value-of select="concat(substring-before($class,'.'),'.')"/>
<xsl:call-template name="classname">
<xsl:with-param name="class"
select="substring-after($class,'.')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('Mock',$class)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
This takes a classname as an input parameter and adds "Mock" after the final ".". You can call it with, for example,
<xsl:call-template name="classname">
<xsl:with-param name="class" select="@class"/>
</xsl:call-template>
(I just gave it a quick try in Firefox, you might find you need to do some tidying up of white space.)
The following seems long, however it uses ready parts (the strRev
template is provided by FXSL and needs not be re-written). Also, nearly half of the code is the identity template and passing params to <xsl:call-template>
. This is much shorted in XSLT 2.0.
When we have ready smaller parts/functions like the strRev
template / reverse()
function, then this solution doesn't require writing long and error-prone home-made recursive code.
The basic idea is that the last '.'
character in a string is the first '.'
character in the reversed string.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pPrepend" select="'Mock'"/>
<xsl:variable name="vRevPrepend">
<xsl:call-template name="strRev">
<xsl:with-param name="pText" select="$pPrepend"/>
</xsl:call-template>
</xsl:variable>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="action/@class">
<xsl:variable name="vRevText">
<xsl:call-template name="strRev"/>
</xsl:variable>
<xsl:variable name="vRevNew" select=
"concat(substring-before($vRevText,'.'), $vRevPrepend,
'.', substring-after($vRevText,'.'))"/>
<xsl:variable name="vNewText">
<xsl:call-template name="strRev">
<xsl:with-param name="pText" select="$vRevNew"/>
</xsl:call-template>
</xsl:variable>
<xsl:attribute name="class">
<xsl:value-of select="$vNewText"/>
</xsl:attribute>
</xsl:template>
<xsl:template name="strRev">
<xsl:param name="pText" select="."/>
<xsl:if test="string-length($pText)">
<xsl:call-template name="strRev">
<xsl:with-param name="pText" select="substring($pText,2)"/>
</xsl:call-template>
<xsl:value-of select="substring($pText,1,1)"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the provided XML document:
<event type="node-enter">
<action name="MyActionName" class="com.example.MyClass">
<bodyTemplate>
templates/MyTemplate.vm
</bodyTemplate>
</action>
</event>
the wanted, correct result is produced:
<event type="node-enter">
<action name="MyActionName" class="com.example.MockMyClass">
<bodyTemplate>
templates/MyTemplate.vm
</bodyTemplate>
</action>
</event>
II. XSLT 2.0 solution:
Exactly the same algorithm, but in XSLT 2.0 is really short:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes"/>
<xsl:param name="pPrepend" select="'Mock'"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="action/@class">
<xsl:attribute name="class" select=
"my:strRev(concat(substring-before(my:strRev(.),'.'),
my:strRev($pPrepend),'.',
substring-after(my:strRev(.),'.')
)
)
"/>
</xsl:template>
<xsl:function name="my:strRev" as="xs:string">
<xsl:param name="pText" as="xs:string"/>
<xsl:sequence select=
"codepoints-to-string(reverse(string-to-codepoints($pText)))
"/>
</xsl:function>
</xsl:stylesheet>