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