Thursday, August 24, 2006

Custom reverse engineering strategy in Hibernate


Hibernate has tools to create mapping files(hbm files) and domain model classes from database schemas(reverse engineering). can be used as part of ant task to do this. Hibernate creates the property names using its default reverse engineering strategy. Hibernate also, provides a way through which the user can specify his own custom reverse engineering strategy through which he can follow his own naming conventions etc…

There is a reversestrategy attribute which can be set to some custom class, which implements org.hibernate.cfg.reveng.ReverseEngineeringStrategy, in the . Here’s an example:

<jdbcconfiguration configurationfile="hibernate.cfg.xml"

packagename="${package.name}"

revengfile="hibernate.reveng.xml"

reversestrategy="org.myapp.hibernate.tool.SampleReverseEngineeringStrategy"/>


The org.myapp.hibernate.tool.SampleReverseEngineeringStrategy is our own custom class which implements org.hibernate.cfg.reveng.ReverseEngineeringStrategy. In this example, our SampleReverseEngineeringStrategy, overrides the columnToPropertyName(TableIdentifier table, String column) method to provide a custom implementation for generating property names out of a column name. Here’s the SampleReverseEngineeringStrategy code:

package org.myapp.hibernate.tool;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hibernate.cfg.reveng.DelegatingReverseEngineeringStrategy;
import org.hibernate.cfg.reveng.ReverseEngineeringStrategy;
import org.hibernate.cfg.reveng.TableIdentifier;
/**
*
* @author Jaikiran Pai
*
*/
public class SampleReverseEngineeringStrategy extends DelegatingReverseEngineeringStrategy {
/**
* Constructor
*
* @param delegate {@link org.hibernate.cfg.reveng.ReverseEngineeringStrategy}
*/
public SampleReverseEngineeringStrategy(ReverseEngineeringStrategy delegate) {
super(delegate);
}

/**
* Changes the default behaviour of naming the property.

* Does the following replacements(not neccessarily in the order) and returns the resulting
* {@link String} as property name:
* - Converts the first letter of the column to uppercase
* - Converts the letters following a ‘_’ character to uppercase in the column
* - Removes any underscores present from column
*
* @see org.hibernate.cfg.reveng.DelegatingReverseEngineeringStrategy
* @see org.hibernate.cfg.reveng.ReverseEngineeringStrategy
*
* @param table {@link TableIdentifier}
* @param column
* @return Returns the propert name after converting it appropriately
*/
public String columnToPropertyName(TableIdentifier table, String column) {

String replacedColumn = replaceFirstLetterToUpperCase(column);

replacedColumn = removeUnderScoresAndConvertNextLetterToUpperCase(replacedColumn);

if (anyReplacementsMadeToOriginalColumnName(column,replacedColumn)) {

return replacedColumn;

}

/*
* Let DelegatingReverseEngineeringStrategy handle this
*/
return super.columnToPropertyName(table, column);
}

/**
*
* Returns true if the originalString and replacedString are NOT equal
* (meaning there was some replacement done to the original column name). Else returns false.
*
* @param originalString The original column name
* @param replacedString The column name after doing necessary replacements
* @return Returns true if the originalString and replacedString are NOT equal
* (meaning there was some replacement done to the original column name). Else returns false.
*
* @throws {@link NullPointerException} if originalString is null.
*/
protected boolean anyReplacementsMadeToOriginalColumnName(String originalString, String replacedString) {
if (originalString.equals(replacedString)) {
return false;

}

return true;
}

/**
* Converts the first letter of the input to uppercase and
* returns the resultant {@link String}.
*
* Ex: If the input is startDate then the resulting {@link String}
* after replacement will be StartDate
*
* @param input The {@link String} whose contents have to be replaced
* @return Returns a {@link String} after doing the appropriate replacements
*/
protected String replaceFirstLetterToUpperCase(String input) {

/*
* The pattern to match a String starting with lower case
*/
final String startsWithLowerCasePattern = "^[a-z]";

Pattern patternForReplacingLowerCase = Pattern.compile(startsWithLowerCasePattern);
Matcher regexMatcher = patternForReplacingLowerCase.matcher(input);

/*
* This will hold the replaced contents
*/
StringBuffer replacedContents = new StringBuffer();

/*
* Check whether the first letter starts with lowercase.
* If yes, change it to uppercase, else pass on the control to
* DelegatingReverseEngineeringStrategy
*
*/
if (regexMatcher.find()) {

String firstCharacter = regexMatcher.group();
/*
* Convert it to uppercase
*/
regexMatcher.appendReplacement(replacedContents,firstCharacter.toUpperCase());
regexMatcher.appendTail(replacedContents);
regexMatcher.reset();

/*
* Return the replaced contents
*/
return replacedContents.toString();

}

//no replacements to do, just return the original input
return input;

}

/**
* Converts the letters following a ‘_’ character to uppercase and also removes
* the ‘_’ character from the input and returns the resulting {@link String}.
* Carries out a 2 pass strategy to do the replacements. During the first pass,
* replaces all the letters that immidiately follow a ‘_’ to uppercase.
* Ex: If the input is _start_Date__today_ then after the first pass of replacement, the
* resultant string will be _Start_Date__Today_
*
* This replaced {@link String} is then passed ahead for second pass (if no replacements were
* done during first pass, then the original {@link String} is passed). During the second pass
* the underscores are removed.
* Ex: If the input is _start_Date__today_ then after BOTH the passes the
* resultant string will be StartDateToday
*
* @param input The {@link String} whose contents have to be replaced
* @return Returns a {@link String} after doing the appropriate replacements
*/
protected String removeUnderScoresAndConvertNextLetterToUpperCase(String input) {

/*
* The pattern which matches a String that starts with a letter immidiately after
* a ‘_’ character
*/
final String stringFollowingUnderScore = "[.]*_[a-zA-Z]+";

Pattern patternForReplacingLowerCase = Pattern.compile(stringFollowingUnderScore);
Matcher regexMatcher = patternForReplacingLowerCase.matcher(input);
/*
* This will hold the replaced contents
*/
StringBuffer replacedContents = new StringBuffer();

boolean foundAnyMatch = false;

while (regexMatcher.find()) {
foundAnyMatch = true;
String matchedString = regexMatcher.group();
/*
* The character immidiately following the underscore
* Example:
* If matchedString is _tMn then originalCharAfterUnderScore will be the
* character t
*/

char originalCharAfterUnderScore = matchedString.charAt(1);
/*
* Convert the character to uppercase
*/

String replacedCharAfterUnderScore = String.valueOf(originalCharAfterUnderScore).toUpperCase();

/*
* Now place this replaced character back into the matchedString
*/

String replacement = matchedString.replace(originalCharAfterUnderScore,replacedCharAfterUnderScore.charAt(0));

/*
* Append this to the replacedColumn, which will be returned back to the user
*/
regexMatcher.appendReplacement(replacedContents,replacement);

} //end of while

regexMatcher.appendTail(replacedContents);
regexMatcher.reset();

/*
* Now the input string has been replaced to contain uppercase letters after the underscore.
* Ex: If input string was "_start_Date_today" then at this point after the above processing,
* the replaced string will be "_Start_Date_Today"
* The only thing that remains now is to remove the underscores from the input string.
* The following statements do this part.
*
*/

if (foundAnyMatch) {
return removeUnderScores(replacedContents.toString());

} else {
return removeUnderScores(input);

}

}

/**
* Removes any underscores present from input and returns the
* resulting {@link String}
* Ex: If the input is _start_Date__today_ then the resulting {@link String}
* after replacement will be startDatetoday
*
* @param input The {@link String} whose contents have to be replaced
* @return Returns a {@link String} after doing the appropriate replacements
*/
protected String removeUnderScores(String input) {

/*
* Pattern for matching underscores
*/
Pattern patternForUnderScore = Pattern.compile("[.]*_[.]*");

Matcher regexMatcher = patternForUnderScore.matcher(input);
/*
* This will hold the return value
*/
StringBuffer returnVal = new StringBuffer();
boolean foundAnyMatch = false;

while (regexMatcher.find()) {
foundAnyMatch = true;

String matchedString = regexMatcher.group();

/*
* Remove the underscore
*/
regexMatcher.appendReplacement(returnVal,"");

}

regexMatcher.appendTail(returnVal);
regexMatcher.reset();

/*
* If any match was found(and replaced) then return the replaced string.
* Else return the original input.
*/
if (foundAnyMatch) {
return returnVal.toString();

}
return input;

}

}


In the example above, the columnToPropertyName method is overridden to do the following:

- Creates property names that start with a Capital case(By default, Hibernate creates property names in camel-case)
- Converts the letter, that follows a ‘_’ (underscore character) to uppercase in the property name
- Removes any underscores in the property name

Ex: If the column name is start_Date_today, then the resulting property name after using the SampleReverseEngineeringStrategy would be StartDateToday.

Here’s a documentation from Hibernate about Controlling Reverse Engineering

5 comments:

ZiglioNZ said...

Hi, thank you for this page, very explanatory.
I'm using the Hibernate Tools (3.2.0.beta8) for Eclipse (3.2.1) and when I run the reverse engineering wizard with my custom strategy, I get the following exception:
org.hibernate.console.HibernateConsoleRuntimeException: Could not create or find mypackage.RevEngStrategy with default no-arg constructor
at org.hibernate.eclipse.launch.CodeGenerationLaunchDelegate.loadreverseEngineeringStrategy(CodeGenerationLaunchDelegate.java:382)
at org.hibernate.eclipse.launch.CodeGenerationLaunchDelegate.access$1(CodeGenerationLaunchDelegate.java:369)
at org.hibernate.eclipse.launch.CodeGenerationLaunchDelegate$2.execute(CodeGenerationLaunchDelegate.java:333)
at org.hibernate.console.execution.DefaultExecutionContext.execute(DefaultExecutionContext.java:65)
at org.hibernate.console.ConsoleConfiguration.execute(ConsoleConfiguration.java:89)
at org.hibernate.eclipse.launch.CodeGenerationLaunchDelegate.buildConfiguration(CodeGenerationLaunchDelegate.java:313)
at org.hibernate.eclipse.launch.CodeGenerationLaunchDelegate.runExporters(CodeGenerationLaunchDelegate.java:221)
at org.hibernate.eclipse.launch.CodeGenerationLaunchDelegate.launch(CodeGenerationLaunchDelegate.java:130)
at org.eclipse.debug.internal.core.LaunchConfiguration.launch(LaunchConfiguration.java:639)
at org.eclipse.debug.internal.core.LaunchConfiguration.launch(LaunchConfiguration.java:565)
at org.eclipse.debug.internal.ui.DebugUIPlugin.buildAndLaunch(DebugUIPlugin.java:754)
at org.eclipse.debug.internal.ui.DebugUIPlugin$6.run(DebugUIPlugin.java:944)
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:58)
Caused by: java.lang.InstantiationException: mypackage.RevEngStrategy
at java.lang.Class.newInstance0(Unknown Source)
at java.lang.Class.newInstance(Unknown Source)
at org.hibernate.eclipse.launch.CodeGenerationLaunchDelegate.loadreverseEngineeringStrategy(CodeGenerationLaunchDelegate.java:378)

have you had this type of error with Ant?
thank you

ZiglioNZ said...

I gather from here
http://forum.hibernate.org/viewtopic.php?t=945603&highlight=delegatingreverseengineeringstrategy

that the problem is how the classpath is set for the Ant task.
I'm not too sure what the Eclipse tools set the classpath to

Anonymous said...

Thanks for the article, it's great! Do you know if it's possible to change the collection type from Set to List during reverse engineering?

Thanks,
AJ

Schott said...

First of all, thank you very much for the code! I used this as a starting point to generate custom class names for Hibernate reverse engineering. I did, however, find a fault in your removeUnderScoresAndConvertNextLetterToUpperCase() method. As is, the method will convert the character following the underscore to uppercase, and ALL OTHER characters with that same letter value in the regex group.

Ex. _test_employee_tbl will become _TesT_EmployEE_tbl

To fix this, replace the following code:
# char originalCharAfterUnderScore = matchedString.charAt(1);
# /*
# * Convert the character to uppercase
# */
#
# String replacedCharAfterUnderScore = String.valueOf(originalCharAfterUnderScore).toUpperCase();
#
# /*
# * Now place this replaced character back into the matchedString
# */
#
# String replacement = matchedString.replace(originalCharAfterUnderScore,replacedCharAfterUnderScore.charAt(0));

With this:
//Replace character after underscore with its Uppercase value
CharSequence oldSeq = matchedString.subSequence(0, 2);
String oldStr = String.valueOf(oldSeq.toString().toUpperCase());
CharSequence newSeq = oldStr.subSequence(0, 2);
String replacement = matchedString.replace(oldSeq, newSeq);

I'm sure there are other ways to get this done correctly, but this is what I ended up using and it works fine.

Jaikiran said...

Schott,

Thanks for posting this. You are right, it's a bug in the code i posted. I should have read the javadocs of String.replace(...) better http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#replace(char,%20char)

It clearly says it replaces *all* occurrences of the char.

Thanks again, i am sure this will help others.