tags:

views:

64

answers:

2

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'&gt;
  <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>
+4  A: 

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.)

Matthew Wilson
+1. Notice though that it gives different results for the base case where the classname contains no `.` However that case may not occur in practice.
LarsH
+1 - you can avoid some whitespace issues by wrapping your literal text nodes in `<xsl:text>`, rather than hanging out "naked" inside of the template. That way you can format your code and not worry about having a carriage return and other whitespace inside the template sneaking into the output. Whitespace characters are only seen as significant if they are preceded or followed by non-whitespace characters. Wrapping in `<xsl:text>` separates that text content intended for the output from formatting whitespace.
Mads Hansen
Thank you, I didn't know that.
Matthew Wilson
@Matthw Wilson: +1 Good answer.
Alejandro
this doesn't handle inner classes, eg `com.example.MyClass$Inner` but that's not so important at this stage for me. I had some trouble getting to the right context to call your template, but eventually figured it out by matching on `"action/@class"` and calling the template with `<xsl:with-param name="class" select="."/>` Thanks!
Ryan Bennetts
A: 

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"&gt;
 <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>
Dimitre Novatchev
I can't say that I've fully grasped your code yet, but it looks like you've hard-coded 'MockMyClass' to be the transformed class name. However, what I'm trying to do is __prepend__ the word 'Mock' to the class name so that - for example - `com.example.ExampleClass` becomes `com.example.MockExampleClass` or `com.example.AnotherExampleClass` becomes `com.example.MockAnotherExampleClass` etc.
Ryan Bennetts
@Ryan-Bennetts: OF course, this is the easiest thing -- just wait 2minutes. :)
Dimitre Novatchev
@Ryan-Bennetts: Done!
Dimitre Novatchev
Thanks Dimitre, your code worked very well, but I went with Matthew Wilson's solution since it was more concise.
Ryan Bennetts
@Ryan-Bennetts: As a consumer it should be all the same to you, but if you are a developer, my solution is a kind of design-pattern that saves you a lot of time and errors -- you just use ready-made functions/templates and don't have to code your own recursion.
Dimitre Novatchev