Monday, July 07, 2008

Freemarker CamelCase to underscore

Quick blog -- as always, the last 20% usually takes up 80% of the time.

This time, it was trying to simply convert Camel Case into equivalent underscore Enum values.

Ok, not that 'simply', but still -- I'm using Hibernate Tools to reverse engineer from JDBC some JPA entities, and that part is working fine. Now, UI and some processes prefer to use a Model that is on top of the entity/dto. So, I thought I would be nice and auto-generate the Model's that some other programmers swear by to make their job easier.

Hibernate Tools just went to FreeMarker, which I was excited for, and I wrote most of the .ftl up for my Model. Until I hit Camel Case.

You see, what they are trying to do is create an ENUM version of each field; I'm not going into detail why, but simply that code-generation wise --

fieldOne -> FIELD_ONE
myReallyLongComboField -> MY_REALLY_LONG_COMBO_FIELD

After a lot of messing around in Freemarker and regular expressions, finally got the solution in two lines in the .ftl file (very important the < /#macro> is where it is now):

<#macro toUnderScore camelCase>
${camelCase?replace("[A-Z]", "_$0", 'r')?upper_case}< /#macro>


Then, make calls like:

<@toUnderScore camelCase=property.name/>

Perfect!


I've had quite a bit of experience in the past with Velocity, and put in some work in code-gen tools like Middlegen (now defunct). Once you have a process/template for commonly used code pieces, code generation really helps enforce consistency and good practice.

5 comments:

F.Lozano said...

Hi... Just got here and I found your macro very useful... but...

Do you have the reverse one? I'm not good at regexps, and I need to convert from underscore to camelCase in FreeMarker...

marcelstoer said...

Might have solved your problem but it's no good as a general converter from camelCase to underscores.

Example strings to undermine my claim? Try:
- XMLParserProperty
- PropertyWithCapFirstCharacter

marcelstoer said...

Sorry, shouldn't have stopped here...The general solution would be

yourWhateverString.replaceAll("(?<=[a-z0-9])[A-Z]|(?<=[a-zA-Z])[0-9]|(?<=[A-Z])[A-Z](?=[a-z])", "_$0")

Below is the documentation straight out of RegexBuddy (hope Blogger doesn't garble it):


// (?<=[a-z0-9])[A-Z]|(?<=[a-zA-Z])[0-9]|(?<=[A-Z])[A-Z](?=[a-z])
//
// Match either the regular expression below (attempting the next alternative only if this one fails) «(?<=[a-z0-9])[A-Z]»
// Assert that the regex below can be matched, with the match ending at this position (positive lookbehind) «(?<=[a-z0-9])»
// Match a single character present in the list below «[a-z0-9]»
// A character in the range between "a" and "z" «a-z»
// A character in the range between "0" and "9" «0-9»
// Match a single character in the range between "A" and "Z" «[A-Z]»
// Or match regular expression number 2 below (attempting the next alternative only if this one fails) «(?<=[a-zA-Z])[0-9]»
// Assert that the regex below can be matched, with the match ending at this position (positive lookbehind) «(?<=[a-zA-Z])»
// Match a single character present in the list below «[a-zA-Z]»
// A character in the range between "a" and "z" «a-z»
// A character in the range between "A" and "Z" «A-Z»
// Match a single character in the range between "0" and "9" «[0-9]»
// Or match regular expression number 3 below (the entire match attempt fails if this one fails to match) «(?<=[A-Z])[A-Z](?=[a-z])»
// Assert that the regex below can be matched, with the match ending at this position (positive lookbehind) «(?<=[A-Z])»
// Match a single character in the range between "A" and "Z" «[A-Z]»

John Rotondo said...

Thanks. This is very useful. The one place where it falls down is if you have terms that are in all caps, like "PDF". This regex inserts a space between each letter. Any ideas how to modify to account for that?

ibracobra said...

Thank you Works like a charm

HelloWorld -> _HELLO_WORLD

if you want HELLO_WORLD without the first _

add "substring(1)"
like ${camelCase?replace("[A-Z]", "_$0", 'r')?upper_case?substring(1)}