4

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>
Community
  • 1
  • 1
Ryan Bennetts
  • 1,143
  • 2
  • 16
  • 29
  • @Ryan, what is the `$` in your regex for? If it's supposed to match end-of-line or end-of-string, wouldn't that fail because it must always be followed by an alphanumeric? If so it would seem redundant. But maybe I'm misunderstanding it. – LarsH Oct 05 '10 at 10:11
  • @LarsH The `$` is there to cater for inner classes, eg `com.example.MyClass$Inner`. But I see now that this regex would also match a string like `com$example.MyClass`, so I would need to fix up that bug if I were to stay with the regex solution. – Ryan Bennetts Oct 05 '10 at 13:02
  • @Ryan Bennetts: Do you need to tokenize by `.` and `$`? Or just by `.`? –  Oct 05 '10 at 13:24
  • @Ryan ok, thanks. I forgot that `$` in `[]` was a literal. And that inner classes used `$` notation. – LarsH Oct 05 '10 at 13:43
  • 1
    [Friends don't let friends parse XML with regular expressions.](http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags) – Ether Oct 05 '10 at 16:17
  • @Ryan-Bennetts, @LarsH, @Alejandro, @Matthew-Wilson: See my answer representative of a lazy programmer who just uses ready parts and saves his time. Both in XSLT 2.0 and in XSLT 1.0. :) – Dimitre Novatchev Oct 05 '10 at 16:48
  • @Ryan-Bennetts: Good question, +1. See my solution for a "lazy" alternative -- both in XSLT 1.0 and in XSLT 2.0. :) – Dimitre Novatchev Oct 05 '10 at 16:49
  • @Ether: Nobody is parsing XML with a regular expression here. – Matthew Wilson Oct 06 '10 at 13:38

2 Answers2

3

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
  • 3,861
  • 21
  • 14
  • +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 Oct 05 '10 at 10:13
  • 1
    +1 - you can avoid some whitespace issues by wrapping your literal text nodes in ``, 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 `` separates that text content intended for the output from formatting whitespace. – Mads Hansen Oct 05 '10 at 11:12
  • @Matthw Wilson: +1 Good answer. –  Oct 05 '10 at 13:28
  • 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 `` Thanks! – Ryan Bennetts Oct 07 '10 at 02:09
2

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>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • 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 Oct 06 '10 at 05:16
  • @Ryan-Bennetts: OF course, this is the easiest thing -- just wait 2minutes. :) – Dimitre Novatchev Oct 06 '10 at 12:50
  • Thanks Dimitre, your code worked very well, but I went with Matthew Wilson's solution since it was more concise. – Ryan Bennetts Oct 07 '10 at 02:13
  • @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 Oct 08 '10 at 17:04
  • 1
    String reverse is just the ticket for my problem with a path `/content/art/VO1113VIEW05.01.tif` where I need to strip of `.tif` but not `.05.tif`. – Michael Shopsin Oct 25 '13 at 18:33