Search

Dark theme | Light theme

January 30, 2013

Groovy Goodness: Adding Extra Methods Using Extension Modules

Groovy 2.0 brought us extension modules. An extension module is a JAR file with classes that provide extra methods to existing other classes like in the JDK or third-party libraries. Groovy uses this mechanism to add for example extra methods to the File class. We can implement our own extension module to add new extension methods to existing classes. Once we have written the module we can add it to the classpath of our code or application and all the new methods are immediately available.

We define the new extension methods in helper classes, which are part of the module. We can create instance and static extension methods, but we need a separate helper class for each type of extension method. We cannot mix static and instance extension methods in one helper class. First we create a very simple class with an extension method for the String class. The first argument of the extension method defines the type or class we want the method to be added to. The following code shows the method likeAPirate. The extension method needs to be public and static even though we are creating an instance extension method.

// File: src/main/groovy/com/mrhaki/groovy/PirateExtension.groovy
package com.mrhaki.groovy

class PirateExtension {
    static String likeAPirate(final String self) {
        // List of pirate language translations.
        def translations = [
            ["hello", "ahoy"], ["Hi", "Yo-ho-ho"],
            ['are', 'be'], ['am', 'be'], ['is', 'be'],
            ['the', "th'"], ['you', 'ye'], ['your', 'yer'],
            ['of', "o'"]
        ]
        
        // Translate the original String to a 
        // pirate language String.
        String result = self
        translations.each { translate ->
                result = result.replaceAll(translate[0], translate[1])
        }
        result
    }
}

Next we need to create an extension module descriptor file. In this file we define the name of the helper class, so Groovy will know how to use it. The descriptor file needs to be placed in the META-INF/services directory of our module archive or classpath. The name of the file is org.codehaus.groovy.runtime.ExtensionModule. In the file we define the name of our module, version and the name of the helper class. The name of the helper class is defined with the property extensionClasses:

# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = pirate-module
moduleVersion = 1.0
extensionClasses = com.mrhaki.groovy.PirateExtension

Now are extension module is ready. The easiest way to distribute the module is by packaging the code and descriptor file in a JAR file and put it in a artifact repository manager. Other developers can then use build tools like Gradle or Maven to include the extension module in their projects and applications. If we use Gradle to create a JAR file we only needs this small build script:

// File: build.gradle
apply plugin: 'groovy'

repositories.mavenCentral()

dependencies {
    // Since Gradle 1.4 we don't use the groovy configuration
    // to define dependencies. We can simply use the
    // compile and testCompile configurations.
    compile 'org.codehaus.groovy:groovy-all:2.0.6'
}

Now we can invoke $ gradle build and we got ourselves an extension module.

Let's add a test for our new extension method. Because we use Gradle the test classpath already will contain our extension module helper class and descriptor file. In our test we can simply invoke the method and test the results. We are going to use Spock to write a simple specification:

// File: src/test/groovy/com/mrhaki/groovy/PirateExtensionSpec.groovy
package com.mrhaki.groovy

import spock.lang.Specification

class PirateExtensionSpec extends Specification {

    def "likeAPirate method should work as instance method on a String value"() {
        given:
        final String originalText = "Hi, Groovy is the greatest language of the JVM."

        expect:
        originalText.likeAPirate() == "Yo-ho-ho, Groovy be th' greatest language o' th' JVM."
    }

}

We add the dependency to Spock in our Gradle build file:

// File: build.gradle
apply plugin: 'groovy'

repositories.mavenCentral()

dependencies {
    // Since Gradle 1.4 we don't use the groovy configuration
    // to define dependencies. We can simply use the
    // compile and testCompile configurations.
    compile 'org.codehaus.groovy:groovy-all:2.0.6'

    testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'
}

We can run $ gradle test to run the Spock specification and test our new extension method.

To add a static method to an existing class we need to add an extra helper class to our extension module and an extra property to our descriptor file to register the helper class. The first argument of the extension method define the type we want to add a static method to. In the following helper class we add the extension method talkLikeAPirate() to the String class.

// File: src/main/groovy/com/mrhaki/groovy/PirateStaticExtension.groovy
package com.mrhaki.groovy

class PirateStaticExtension {
    static String talkLikeAPirate(final String type) {
        "Arr, me hearty,"
    }
}

We change the descriptor file and add the staticExtensionClasses property:

# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = pirate-module
moduleVersion = 1.0
extensionClasses = com.mrhaki.groovy.PirateExtension
staticExtensionClasses = com.mrhaki.groovy.PirateStaticExtension

In our Spock specification we add an extra test for our new static method talkLikeAPirate() on the String class:

// File: src/test/groovy/com/mrhaki/groovy/PirateExtensionSpec.groovy
package com.mrhaki.groovy

import spock.lang.Specification

class PirateExtensionSpec extends Specification {

    def "likeAPirate method should work as instance method on a String value"() {
        given:
        final String originalText = "Hi, Groovy is the greatest language of the JVM."

        expect:
        originalText.likeAPirate() == "Yo-ho-ho, Groovy be th' greatest language o' th' JVM."
    }

    def "talkLikeAPirate method should work as static method on String class"() {
        expect:
        "Arr, me hearty, Groovy rocks!" == String.talkLikeAPirate() + " Groovy rocks!"
    }

}

Written with Groovy 2.1