A Gradle Case Study

Tutorial – Gradle SOAP – Features Revealed

KenKousen
gradle1

Groovy expert Ken Kousen discusses how to construct a trivial SOAP client using the built-in Java tools, driven by a Gradle build. The process illustrates several useful features of Gradle not often covered in a typical tutorial.

We face the following problem: While the SOAP-based approach to web services is currently out of favor, SOAP-based web services are certainly not out of existence, and the tools for building simple clients to SOAP services are built into Java. Creating a simple client is an almost trivial exercise. All you need is the wsimport tool (part of the JDK installation) and access to a Web Services Description Language (WSDL) file. The wsimport script reads the WSDL file and generates all the required stubs necessary to build the client.Since Groovy and Java can be freely intermixed, it’s easy enough to build a client in Groovy that uses the generated Java stubs.

Here a Spock test case will be used to check the behavior of a web service. A freely available Microsoft web service used to compute currency exchange rates will be accessed. Hopefully the use of a Microsoft web service won’t turn away the readers of this article that didn’t leave once the term SOAP was used. The service doesn’t matter – it’s the Gradle stuff that’s good. The goal is to automate the entire process, from stub generation to test, using Gradle.

A Web Service Client

Microsoft supports a number of simple web services. One of these services is a currency converter (Which they even misspelled as “convertor”. Don’t get me started.), whose WSDL file is located here. By convention, the service is at the same URL with the WSDL parameter removed. Since this is certainly not the place to discuss the vagaries of WSDL, observe only that from the source of the WSDL file the service name is CurrencyConvertor and the portType (i.e., the interface) is CurrencyConvertorSoap. That means that once the stubs have been generated, accessing the web service is as simple as writing:

CurrencyConvertorSoap stub = new CurrencyConvertor().getCurrencyConvertorSoap()

Then just use the stub to invoke any operations defined in the WSDL file. The only operation needed is called getConversionRate, which takes two Currency instances defined in an XML Schema inside the WSDL file. For example, a typical request would look like:

double rate = stub.getConversionRate(Currency.USD, Currency.INR)

to get the exchange rate between US dollars and Indian rupees. The key benefit (if any) to SOAP web services is in the stub generation. Java comes with the wsimport tool, whose usage takes the form:

c:> wsimport -d buildDir -s srcDir -keep http://...path.to.WSDL.file...

where the -d flag specifies the directory to use for the compiled stubs, the -s flag says where to put the generated source code, the -keep flag means to save the generated source code, and the last argument is the location of the WSDL file.

This is easy enough to run from the command line, but how do you make it part of an automated build? Fortunately, there is an Ant task defined for it. The job now is (1) add the proper repository so Gradle can find the required jars for the Ant task, (2) define a custom Gradle task for wsimport, and (3) make the execution of the task part of the regular build process. The rest of this article shows how to do each of those tasks.

      

Creating the Build File

Since this is ultimately going to be part of a combined Groovy/Java project, start with a build.gradle file containing:

 

apply plugin: 'groovy'

repositories {
    mavenCentral()
}

dependencies {
    groovy 'org.codehaus.groovy:groovy-all:1.8.4'
}

This file will grow as additional tasks and dependencies are added. The jar file that defines the web service Ant tasks (like wsimport and wsgen) is part of the JAX-WS tools project. The good news is, JAX-WS modules are located in Maven Central, which Gradle integrates with seamlessly. Modules in Maven Central are minimally defined by a “vector” of three components: a group ID, an artifact ID, and a version. The group id for the JAX-WS tools module is com.sun.xml.ws, the artifact id is jaxws-tools, and the version number is 2.1.4. We want to tell Gradle to download this jar file and other other jars it depends on, and we want to keep those jars in a named container called a configuration. To make this happen, add to the build.gradle file:

configurations {
    jaxws
}

dependencies {
    ... from before ... 
    jaxws 'com.sun.xml.ws:jaxws-tools:2.1.4'
}

However, this library is not stored at the Maven Central repository, so additional repositories must be added. As of Gradle 1.0 milestone 6, the syntax for doing this is:

repositories {
    mavenCentral()
    maven { url 'http://download.java.net/maven/1' }
    maven { url 'http://download.java.net/maven/2' }
}

Now comes the fun part! Listing 1 shows an initial attempt at adding a wsimport task to the build.

Listing 1

task wsimport {
    doLast {
        destDir = file("${buildDir}/generated")
        ant {
            sourceSets.main.output.classesDir.mkdirs()  
            destDir.mkdirs()
            taskdef(name:'wsimport',
                    classname:'com.sun.tools.ws.ant.WsImport',
                    classpath:configurations.jaxws.asPath)
            wsimport(keep:true,
                     destdir: sourceSets.main.output.classesDir,
                     sourcedestdir: destDir,
                    wsdl:'http://www.webservicex.net/CurrencyConvertor.asmx?
wsdl')
        }
    }
}
compileJava.dependsOn(wsimport)

This block defines a custom task in Gradle. The call to the doLast method defines the steps to be taken when the wsimport method runs. This is an example of an imperative task definition in Gradle. When the task runs, it generates the Java stubs, which then need to be compiled. To ensure that this task runs before the built-in compileJava task, compileJava is declared to depend on wsimport after wsimport is defined.

That provides a bit of a complication, too, because the output directories for the compiled code aren’t created until the compile task runs. That’s why before the stubs are generated, it is necessary to run mkdirs() on the destination directory. The syntax for this changed in milestone 6 as well. It now requires the “output” property between “main” and “classesDir”.

The next line defines a “generated” directory under the build directory for the generated Java source files, and the line after that creates this directory.

Now that all the required properties of the wsimport task have been defined, it’s time to call the actual WSDL generation code. Inside the doLast closure, “ant” refers to the instance of AntBuilder inside every Gradle build file. Inside the ant closure, the taskdef and wsimport tasks come from their Ant counterparts. The only subtlety is the classpath configuration for the task, which refers to the jaxws configuration defined earlier. Using Gradle’s transitive dependency management in connection with Ant task definitions is a significant improvement on what can be an inconvenient process of wrangling the right dependencies into your project to define a custom Ant task.

So far, the process works fine, but is inefficient. As configured, the wsimport tasks runs on every build, which certainly isn’t necessary if the web service doesn’t change. (That actual web service hasn’t changed for years!) There are a couple of ways to prevent re-running the task every time. One is to take advantage of the onlyIf property of Gradle tasks, as follows:

wsimport.onlyIf { !(new File(buildDir, 'generated').exists()) }

 Now the task will run only if the generated source directory doesn’t exist. By placing the generated directory under the build directory, a clean task will eliminate it and the wsimport task will run during the next build.

This is all well and good, and is a reminder that the build file is still a Groovy file so arbitrary Groovy expressions can be added to it, but there is an even better alternative. Each Gradle task has properties called “inputs” and “outputs”, which engage the incremental compilation engine. The purpose of these two fields is to determine whether a task is up to date or not with respect to the files a task reads as input and writes as output. The only problem here is that the arguments to inputs and outputs have to be file based, and the WSDL file is at a URL.

There is no perfect way to solve this problem. The build can always look to the web for the WSDL, which means it always get updated versions of the WSDL when they are released. However, Gradle’s incremental compilation engine can only work with local files, not network resources; moreover, this approach would force users of the build to have a network connection all the time. A better approach is to cache the WSDL file locally by saving it as a file in the project. It would be straightforward to add another task to the build to download the WSDL file from its canonical URI and cache it in the project directory. This step is omitted here for the sake of brevity. The resulting changes are shown in bold in Listing 2.

Listing 2

 

task wsimport  {
    destDir = file("${buildDir}/generated")
    wsdlSrc = file('currency_convertor.wsdl')
    inputs.file wsdlSrc
    outputs.dir destDir
    doLast{
        ant {
            sourceSets.main.output.classesDir.mkdirs()
            destDir.mkdirs()
            taskdef(name:'wsimport',
                classname:'com.sun.tools.ws.ant.WsImport',
                classpath:configurations.jaxws.asPath)
            wsimport(keep:true,
                destdir: sourceSets.main.output.classesDir,
                sourcedestdir: destDir,
                wsdl: wsdlSrc)
        }
    }
}

 

The WSDL file is stored in the root of the project. The inputs property uses the file method to connect to the WSDL file, and the outputs property uses the dir method to connect to the destination directory. If the input file or any of the files in the output directory change, the task will execute again. If neither the input nor the output change between invocations of the build, the task will not execute.

The build as it stands is fine, but there’s one other under-publicized feature of Gradle that’s worth illustrating. Gradle assumes that the project layout conforms to a standard layout, with subdirectories like src/main/groovy, src/main/java, src/test/groovy, and so on. If you prefer not to use that structure, with it’s remarkably easy to change.

Consider an alternate project layout with two source folders, one called src and one called tests. It’s easy enough to map this structure to the Gradle domain model: 

sourceSets {
    main {
        java { srcDir "$buildDir/generated" }
        groovy { srcDir 'src' }
    }
    test {
        java { srcDirs = [] }
        groovy { srcDir 'tests' }
    }
}

    

The sourceSets closure maps standard source directory structure to whatever the project requires. The layout here says that there is only one source directory for Java files, which is the generated directory populated by the wsimport task. Everything else in “src”, whether written in Java or in Groovy, is compiled by groovyc. The test closure is even more explicit – there are no directories for javac to use. Everything under the ‘tests’ directory is compiled by groovyc. That’s actually a good integration principle. The groovyc compiler knows all about Java source code, so let it compile both the Java and the Groovy sources. That way it can resolve any potential cross compilation issues for you. So far, all of the code in this article has been from the Gradle build file. For completeness, Listing 3 shows a class defining a conversion rate service.

Listing 3

import net.webservicex.Currency;
import net.webservicex.CurrencyConvertor 
import net.webservicex.CurrencyConvertorSoap 

class ConversionRate {
    CurrencyConvertorSoap stub = 
        new CurrencyConvertor().getCurrencyConvertorSoap()

    double getConversionRate(Currency from, Currency to) {
        return from == to ? 1.0 : stub.conversionRate(from, to)
    }
}

Listing 4 below shows a simple Spock test to check the implementation

Listing 4

import net.webservicex.Currency;
import spock.lang.Specification;

class ConversionRateSpec extends Specification {
    ConversionRate cr = new ConversionRate()

    def "same currency should be rate of 1"() {
        when:
        double rate = cr.getConversionRate(Currency.USD, Currency.USD)

        then:
        rate == 1.0
    }

    def "rate from USD to INR is > 1"() {
        expect:
        cr.getConversionRate(Currency.USD, Currency.INR) >= 1 
    }
}

Even if you’ve never seen a Spock test before, this should be pretty intuitive. The class extends the Specification class from Spock, which makes it a Spock test class. Each test has a def return type, followed by a string explaining its goal, and empty parentheses. The first test uses a when/then pair as a stimulus/response. The “then” block contains boolean conditions that are evaluated automatically, so no assert-based keyword is required.

Since the actual exchange rates change all the time, the second test picks two currencies that are guaranteed to satisfy the condition. At the time this article was written, there were about 51 INR for 1 USD. The boolean test is in an expect block, which works the same way the then block did in the previous test. To make the test work, one last change to the build file is required. Add the following line to the dependencies block.

 

testCompile 'org.spockframework:spock-core:0.5-groovy-1.8'

That will download the proper version of Spock, along with its dependencies (like JUnit), and now the build will execute the tests as well. Version 0.5 is current as of this writing. Feel free to try updating the version number to whatever is current at the time you run the example.

Summary

While the project that motivated this article involved a simple Groovy/Java client on a Microsoft web service, the real goal was to illustrate several aspects of Gradle development. Among them were creating a custom task, using a configuration based on an external Ant jar, working with multiple repositories, defining and configuring an Ant task, inserting it into the normal build process, ensuring that the task only ran when necessary, and showing how to map an alternative project layout to what Gradle expects. Hopefully some or all of these tasks will be helpful to you in the future. All of the source code for this article is available at a GitHub repository located here.

 

This article first appeared in Java Tech Journal: Gradle. You can find other articles from that issues and other issues here

Author
KenKousen
Ken Kousen is President of Kousen IT, Inc, http://www.kousenit.com. He teaches technical training courses and does both consulting and development in all areas related to Java and XML, especially open source projects like Spring, Hibernate, Groovy and Grails. He is the author of the book
Comments
comments powered by Disqus