Monday, November 30, 2009

Groovy: Java Enum Generation from XML

Besides the obvious use of Groovy to write applications, Groovy is also very useful for performing Java development related tasks such building applications, deploying applications, and managing/monitoring applications. In this post, I look at an example of generating a Java enum from XML source using Groovy.

I have previously blogged on slurping XML with Groovy. That technique is used again here. The Groovy code that follows uses this technique to read the source XML and then writes out a 100% compilable Java enum called AlbumsEnum.java. The Groovy script is shown next.


#!/usr/bin/env groovy
// generateAlbumsEnumFromXml.groovy
//
// Demonstrates use of Groovy's XML slurping to generate
// a Java enum from source XML.
//

// Set up enum attributes' names and data types
attributes = ['albumTitle' : 'String', 'artistName' : 'String', 'year' : 'int']

// Base package name off command-line parameter if provided
packageName = args.length > 0 ? args[0] : "albums"

NEW_LINE = System.getProperty("line.separator")
SINGLE_INDENT = ' '
DOUBLE_INDENT = SINGLE_INDENT.multiply(2)
outputFile = new File("AlbumsEnum.java")
outputFile.write "package ${packageName};${NEW_LINE.multiply(2)}"
outputFile << "public enum AlbumsEnum${NEW_LINE}"
outputFile << "{${NEW_LINE}"

outputFile << generateEnumConstants()

// Build enum attributes
attributesSection = new StringBuilder();
attributesAccessors = new StringBuilder();
attributesCtorSetters = new StringBuilder();
attributes.each
{
attributesSection << generateAttributeDeclaration(it.key, it.value)
attributesAccessors << buildAccessor(it.key, it.value) << NEW_LINE
attributesCtorSetters << buildConstructorAssignments(it.key)
}
outputFile << attributesSection
outputFile << NEW_LINE

outputFile << generateParameterizedConstructor(attributes)
outputFile << NEW_LINE

outputFile << attributesAccessors

outputFile << '}'

def String generateEnumConstants()
{
// Get input from XML source
albums = new XmlSlurper().parse("albums.xml")
def enumConstants = new StringBuilder()
albums.Album.each
{
enumConstants << SINGLE_INDENT
enumConstants << it.@artist.toString().replace(' ', '_').toUpperCase() << '_'
enumConstants << it.@title.toString().replace(' ', '_').toUpperCase()
enumConstants << "(\"${it.@title}\", \"${it.@artist}\", ${it.@year.toInteger()}),"
enumConstants << NEW_LINE
}
// Subtract three off end of substring: one for new line, one for extra comma,
// and one for zero-based indexing.
returnStr = new StringBuilder(enumConstants.toString().substring(0, enumConstants.size()-3))
returnStr << ';' << NEW_LINE.multiply(2)
return returnStr
}

def String generateAttributeDeclaration(String attrName, String attrType)
{
return "${SINGLE_INDENT}private ${attrType} ${attrName};${NEW_LINE}"
}

def String buildAccessor(String attrName, String attrType)
{
returnStr = new StringBuilder()
returnStr << SINGLE_INDENT << "public ${attrType} get${capitalizeFirstLetter(attrName)}()" << NEW_LINE
returnStr << SINGLE_INDENT << '{' << NEW_LINE
returnStr << DOUBLE_INDENT << "return this.${attrName};" << NEW_LINE
returnStr << SINGLE_INDENT << '}' << NEW_LINE
return returnStr
}

def String generateParameterizedConstructor(Map<String,String> attributesMap)
{
constructorInit = new StringBuilder()
constructorInit << SINGLE_INDENT << 'AlbumsEnum('
attributesMap.each
{
constructorInit << "final ${it.value} new${capitalizeFirstLetter(it.key)}, "
}
constructorFinal = new StringBuilder(constructorInit.substring(0, constructorInit.size()-2))
constructorFinal << ')'
constructorFinal << NEW_LINE << SINGLE_INDENT << '{' << NEW_LINE
constructorFinal << attributesCtorSetters
constructorFinal << SINGLE_INDENT << "}${NEW_LINE}"
return constructorFinal
}

def String buildConstructorAssignments(String attrName)
{
return "${DOUBLE_INDENT}this.${attrName} = new${capitalizeFirstLetter(attrName)};${NEW_LINE}"
}

def String capitalizeFirstLetter(String word)
{
firstLetter = word.getAt(0)
uppercaseLetter = firstLetter.toUpperCase()
return word.replaceFirst(firstLetter, uppercaseLetter)
}


The XML source that this script is run against is shown next:


<?xml version="1.0"?>
<Albums>
<Album title="Frontiers" artist="Journey" year="1983">
<Song title="Separate Ways" peak="8" />
<Song title="Send Her My Love" peak="23" />
<Song title="Faithfully" peak="12" />
</Album>
<Album title="Hysteria" artist="Def Leppard" year="1987">
<Song title="Hysteria" peak="10" />
<Song title="Animal" peak="19" />
<Song title="Women" />
<Song title="Pour Some Sugar On Me" peak="2" />
<Song title="Love Bites" peak="1" />
<Song title="Armageddon It" peak="3" />
<Song title="Rocket" peak="15" />
</Album>
<Album title="The Joshua Tree" artist="U2" year="1987">
<Song title="With or Without You" peak="1" />
<Song title="I Still Haven't Found What I'm Looking For" peak="1" />
<Song title="Where The Streets Have No Name" peak="13" />
<Song title="In God's Country" peak="14" />
</Album>
<Album title="Songs from the Big Chair" artist="Tears for Fears" year="1985">
<Song title="Shout" peak="1" />
<Song title="Everybody Wants to Rule the World" peak="1" />
<Song title="Head Over Heels" peak="3" />
<Song title="Mothers Talk" peak="27" />
</Album>
</Albums>


When the Groovy script is run against this source XML, the Java enum that it generates is shown next.


package albums;

public enum AlbumsEnum
{
JOURNEY_FRONTIERS("Frontiers", "Journey", 1983),
DEF_LEPPARD_HYSTERIA("Hysteria", "Def Leppard", 1987),
U2_THE_JOSHUA_TREE("The Joshua Tree", "U2", 1987),
TEARS_FOR_FEARS_SONGS_FROM_THE_BIG_CHAIR("Songs from the Big Chair", "Tears for Fears", 1985);

private String albumTitle;
private String artistName;
private int year;

AlbumsEnum(final String newAlbumTitle, final String newArtistName, final int newYear)
{
this.albumTitle = newAlbumTitle;
this.artistName = newArtistName;
this.year = newYear;
}

public String getAlbumTitle()
{
return this.albumTitle;
}

public String getArtistName()
{
return this.artistName;
}

public int getYear()
{
return this.year;
}

}


There are, of course, many ways in which this simple draft script could be improved and many features could be added to it. However, it serves the purpose of illustrating how easy it is to use Groovy to generate Java code from source data such as XML.

No comments: