A Gradle Case Study

Tutorial - Gradle SOAP - Features Revealed - Part 2

  

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' }
    }
}

  

Pages

Ken Kousen
Ken Kousen

What do you think?

JAX Magazine - 2014 - 06 Exclucively for iPad users JAX Magazine on Android

Comments

Latest opinions