Saturday, January 23, 2010

Groovy: Injecting New Methods on Existing Java Classes

One of Groovy's most significant "goodies" is the ability to intercept calls, even to standard Java classes, and either override existing methods or even add new methods dynamically. In this blog post, I will demonstrate how to add a method to the Calendar that provides the appropriate Zodiak Tropical Sign.

Groovy offers rich dynamic capabilities via its MetaClass support. In this blog post, I will take advantage of this feature to have a method called getZodiakTropicalSign() injected into the SDK-provided Calendar. The purpose of this method is to provide the applicable Zodiak sign for a given instance of Calendar.

The portion of this script most relevant to this post appears at the top of the script. The syntax Calendar.metaClass.getZodiakTropicalSign = { -> defines a new method getZodiakTrpopicalSign on Calendar and then uses delegate to work with the underlying instance of java.util.Calendar.



#!/usr/bin/env groovy

Calendar.metaClass.getZodiakTropicalSign = { ->
def humanMonth = delegate.get(Calendar.MONTH) + 1
def day = delegate.get(Calendar.DATE)
def zodiakTropicalSign = "Unknown"
switch (humanMonth)
{
case 1 :
zodiakTropicalSign = day < 20 ? "Capricorn" : "Aquarius"
break
case 2 :
zodiakTropicalSign = day < 19 ? "Aquarius" : "Pisces"
break
case 3 :
zodiakTropicalSign = day < 21 ? "Pisces" : "Aries"
break
case 4 :
zodiakTropicalSign = day < 20 ? "Aries" : "Taurus"
break
case 5 :
zodiakTropicalSign = day < 21 ? "Taurus" : "Gemini"
break
case 6 :
zodiakTropicalSign = day < 21 ? "Gemini" : "Cancer"
break
case 7 :
zodiakTropicalSign = day < 23 ? "Cancer" : "Leo"
break
case 8 :
zodiakTropicalSign = day < 23 ? "Leo" : "Virgo"
break
case 9 :
zodiakTropicalSign = day < 23 ? "Virgo" : "Libra"
break
case 10 :
zodiakTropicalSign = day < 23 ? "Libra" : "Scorpio"
break
case 11 :
zodiakTropicalSign = day < 22 ? "Scorpio" : "Sagittarius"
break
case 12 :
zodiakTropicalSign = day < 22 ? "Sagittarius" : "Capricorn"
break
}
return zodiakTropicalSign
}

def calendar = Calendar.instance
// First argument to script is expected to be month integer between 1 (January)
// and 12 (December). Second argument is expected to be the date of that month.
// If fewer than two arguments are provided, will use today's date (default) instead.
if (args.length > 1)
{
try
{
calendar.set(Calendar.MONTH, (args[0] as Integer) - 1)
calendar.set(Calendar.DATE, args[1] as Integer)
}
catch (NumberFormatException nfe)
{
println "Arguments ${args[0]} and ${args[1]} are not valid respective month and date integers."
println "Using today's date (${calendar.get(Calendar.MONTH)+1}/${calendar.get(Calendar.DATE)}) instead."
}
}
print "A person born on ${calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.US)}"
println " ${calendar.get(Calendar.DATE)} has the Zodiak sign ${calendar.getZodiakTropicalSign()}"


The final line of the above script calls the injected getZodiakTropicalSign() method on Calendar. This is demonstrated in the next screen snapshot.



The first execution of the script in the above screen snapshot is executed without parameters and so it prints out the Zodiak Sign for today (23 January). The remainder of the script runs are for different dates. This script could have been made more user-friendly using Groovy's built-in CLI support, but the focus here is on the dynamically injected method.

Conclusion

Groovy allows the developer to inject methods on instances of existing classes even when the developer does not own or have control over the existing class. This can be a powerful capability to bending an existing class to one's will.

No comments: