Working with Gradle

In this article by Mainak Mitra, author of the book Mastering Gradle, we cover some plugins such as War and Scala, which will be helpful in building web applications and Scala applications. Additionally, we will discuss diverse topics such as Property Management, Multi-Project build, and logging aspects. In the Multi-project build section, we will discuss how Gradle supports multi-project build through the root project's build file. It also provides the flexibility of treating each module as a separate project, plus all the modules together like a single project.

(For more resources related to this topic, see here.)

The War plugin

The War plugin is used to build web projects, and like any other plugin, it can be added to the build file by adding the following line:

apply plugin: 'war'

War plugin extends the Java plugin and helps to create the war archives. The war plugin automatically applies the Java plugin to the build file. During the build process, the plugin creates a war file instead of a jar file. The war plugin disables the jar task of the Java plugin and adds a default war archive task. By default, the content of the war file will be compiled classes from src/main/java; content from src/main/webapp and all the runtime dependencies. The content can be customized using the war closure as well.

In our example, we have created a simple servlet file to display the current date and time, a web.xml file and a build.gradle file. The project structure is displayed in the following screenshot:

Figure 6.1

The SimpleWebApp/build.gradle file has the following content:

apply plugin: 'war'
 
repositories {
mavenCentral()
}
 
dependencies {
providedCompile "javax.servlet:servlet-api:2.5"
compile("commons-io:commons-io:2.4")
compile 'javax.inject:javax.inject:1'
}

The war plugin adds the providedCompile and providedRuntime dependency configurations on top of the Java plugin. The providedCompile and providedRuntime configurations have the same scope as compile and runtime respectively, but the only difference is that the libraries defined in these configurations will not be a part of the war archive. In our example, we have defined servlet-api as the providedCompile time dependency. So, this library is not included in the WEB-INF/lib/ folder of the war file. This is because this library is provided by the servlet container such as Tomcat. So, when we deploy the application in a container, it is added by the container. You can confirm this by expanding the war file as follows:

SimpleWebApp$ jar -tvf build/libs/SimpleWebApp.war
   0 Mon Mar 16 17:56:04 IST 2015 META-INF/
   25 Mon Mar 16 17:56:04 IST 2015 META-INF/MANIFEST.MF
   0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/
   0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/classes/
   0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/classes/ch6/
1148 Mon Mar 16 17:56:04 IST 2015 WEB-INF/classes/ch6/DateTimeServlet.class
   0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/lib/
185140 Mon Mar 16 12:32:50 IST 2015 WEB-INF/lib/commons-io-2.4.jar
 2497 Mon Mar 16 13:49:32 IST 2015 WEB-INF/lib/javax.inject-1.jar
 578 Mon Mar 16 16:45:16 IST 2015 WEB-INF/web.xml

Sometimes, we might need to customize the project's structure as well. For example, the webapp folder could be under the root project folder, not in the src folder. The webapp folder can also contain new folders such as conf and resource to store the properties files, Java scripts, images, and other assets. We might want to rename the webapp folder to WebContent. The proposed directory structure might look like this:

Figure 6.2

We might also be interested in creating a war file with a custom name and version. Additionally, we might not want to copy any empty folder such as images or js to the war file.

To implement these new changes, add the additional properties to the build.gradle file as described here. The webAppDirName property sets the new webapp folder location to the WebContent folder. The war closure defines properties such as version and name, and sets the includeEmptyDirs option as false. By default, includeEmptyDirs is set to true. This means any empty folder in the webapp directory will be copied to the war file. By setting it to false, the empty folders such as images and js will not be copied to the war file.

The following would be the contents of CustomWebApp/build.gradle:

apply plugin: 'war'
 
repositories {
mavenCentral()
}
dependencies {
providedCompile "javax.servlet:servlet-api:2.5"
compile("commons-io:commons-io:2.4")
compile 'javax.inject:javax.inject:1'
}
webAppDirName="WebContent"
 
war{
baseName = "simpleapp"
version = "1.0"
extension = "war"
includeEmptyDirs = false
}

After the build is successful, the war file will be created as simpleapp-1.0.war. Execute the jar -tvf build/libs/simpleapp-1.0.war command and verify the content of the war file. You will find the conf folder is added to the war file, whereas images and js folders are not included.

You might also find the Jetty plugin interesting for web application deployment, which enables you to deploy the web application in an embedded container. This plugin automatically applies the War plugin to the project. The Jetty plugin defines three tasks; jettyRun, jettyRunWar, and jettyStop. Task jettyRun runs the web application in an embedded Jetty web container, whereas the jettyRunWar task helps to build the war file and then run it in the embedded web container. Task jettyStopstops the container instance. For more information please refer to the Gradle API documentation. Here is the link: https://docs.gradle.org/current/userguide/war_plugin.html.

The Scala plugin

The Scala plugin helps you to build the Scala application. Like any other plugin, the Scala plugin can be applied to the build file by adding the following line:

apply plugin: 'scala'

The Scala plugin also extends the Java plugin and adds a few more tasks such as compileScala, compileTestScala, and scaladoc to work with Scala files. The task names are pretty much all named after their Java equivalent, simply replacing the java part with scala. The Scala project's directory structure is also similar to a Java project structure where production code is typically written under src/main/scala directory and test code is kept under the src/test/scala directory. Figure 6.3 shows the directory structure of a Scala project. You can also observe from the directory structure that a Scala project can contain a mix of Java and Scala source files. The HelloScala.scala file has the following content. The output is Hello, Scala... on the console. This is a very basic code and we will not be able to discuss much detail on the Scala programming language. We request readers to refer to the Scala language documentation available at http://www.scala-lang.org/.

package ch6
 
object HelloScala {
   def main(args: Array[String]) {
     println("Hello, Scala...")
   }
}

To support the compilation of Scala source code, Scala libraries should be added in the dependency configuration:

dependencies {
compile('org.scala-lang:scala-library:2.11.6')
}

Figure 6.3

As mentioned, the Scala plugin extends the Java plugin and adds a few new tasks. For example, the compileScala task depends on the compileJava task and the compileTestScala task depends on the compileTestJava task. This can be understood easily, by executing classes and testClasses tasks and looking at the output.

$ gradle classes

:compileJava

:compileScala

:processResources UP-TO-DATE

:classes

 

BUILD SUCCESSFUL

$ gradle testClasses

:compileJava UP-TO-DATE

:compileScala UP-TO-DATE

:processResources UP-TO-DATE

:classes UP-TO-DATE

:compileTestJava UP-TO-DATE

:compileTestScala UP-TO-DATE

:processTestResources UP-TO-DATE

:testClasses UP-TO-DATE

 

BUILD SUCCESSFUL

Scala projects are also packaged as jar files. The jar task or assemble task creates a jar file in the build/libs directory.

$ jar -tvf build/libs/ScalaApplication-1.0.jar
0 Thu Mar 26 23:49:04 IST 2015 META-INF/
94 Thu Mar 26 23:49:04 IST 2015 META-INF/MANIFEST.MF
0 Thu Mar 26 23:49:04 IST 2015 ch6/
1194 Thu Mar 26 23:48:58 IST 2015 ch6/Customer.class
609 Thu Mar 26 23:49:04 IST 2015 ch6/HelloScala$.class
594 Thu Mar 26 23:49:04 IST 2015 ch6/HelloScala.class
1375 Thu Mar 26 23:48:58 IST 2015 ch6/Order.class

The Scala plugin does not add any extra convention to the Java plugin. Therefore, the conventions defined in the Java plugin, such as lib directory and report directory can be reused in the Scala plugin. The Scala plugin only adds few sourceSet properties such as allScala, scala.srcDirs, and scala to work with source set. The following task example displays different properties available to the Scala plugin. The following is a code snippet from ScalaApplication/build.gradle:

apply plugin: 'java'
apply plugin: 'scala'
apply plugin: 'eclipse'
 
version = '1.0'
 
jar {
manifest {
attributes 'Implementation-Title': 'ScalaApplication',     'Implementation-Version': version
}
}
 
repositories {
mavenCentral()
}
 
dependencies {
compile('org.scala-lang:scala-library:2.11.6')
runtime('org.scala-lang:scala-compiler:2.11.6')
compile('org.scala-lang:jline:2.9.0-1')
}
 
task displayScalaPluginConvention << {
println "Lib Directory: $libsDir"
println "Lib Directory Name: $libsDirName"
println "Reports Directory: $reportsDir"
println "Test Result Directory: $testResultsDir"
 
println "Source Code in two sourcesets: $sourceSets"
println "Production Code: ${sourceSets.main.java.srcDirs},     ${sourceSets.main.scala.srcDirs}"
println "Test Code: ${sourceSets.test.java.srcDirs},     ${sourceSets.test.scala.srcDirs}"
println "Production code output:     ${sourceSets.main.output.classesDir} &    
   ${sourceSets.main.output.resourcesDir}" println "Test code output: ${sourceSets.test.output.classesDir}   
  & ${sourceSets.test.output.resourcesDir}" }

The output of the task displayScalaPluginConvention is shown in the following code:

$ gradle displayScalaPluginConvention

:displayScalaPluginConvention
Lib Directory: <path>/ build/libs
Lib Directory Name: libs
Reports Directory: <path>/build/reports
Test Result Directory: <path>/build/test-results
Source Code in two sourcesets: [source set 'main', source set 'test']
Production Code: [<path>/src/main/java], [<path>/src/main/scala]
Test Code: [<path>/src/test/java], [<path>/src/test/scala]
Production code output: <path>/build/classes/main & <path>/build/resources/main
Test code output: <path>/build/classes/test & <path>/build/resources/test
 
BUILD SUCCESSFUL

Finally, we will conclude this section by discussing how to execute Scala application from Gradle; we can create a simple task in the build file as follows.

task runMain(type: JavaExec){
main = 'ch6.HelloScala'
classpath = configurations.runtime + sourceSets.main.output +     sourceSets.test.output
}

The HelloScala source file has a main method which prints Hello, Scala... in the console. The runMain task executes the main method and displays the output in the console:

$ gradle runMain
....
:runMain
Hello, Scala...
 
BUILD SUCCESSFUL

Logging

Until now we have used println everywhere in the build script to display the messages to the user. If you are coming from a Java background you know a println statement is not the right way to give information to the user. You need logging. Logging helps the user to classify the categories of messages to show at different levels. These different levels help users to print a correct message based on the situation. For example, when a user wants complete detailed tracking of your software, they can use debug level. Similarly, whenever a user wants very limited useful information while executing a task, they can use quiet or info level. Gradle provides the following different types of logging:

Log Level

Description

ERROR

This is used to show error messages

QUIET

This is used to show limited useful information

WARNING

This is used to show warning messages

LIFECYCLE

This is used to show the progress (default level)

INFO

This is used to show information messages

DEBUG

This is used to show debug messages (all logs)

By default, the Gradle log level is LIFECYCLE. The following is the code snippet from LogExample/build.gradle:

task showLogging << {
println "This is println example"
logger.error "This is error message"
logger.quiet "This is quiet message"
logger.warn "This is WARNING message"
logger.lifecycle "This is LIFECYCLE message"
logger.info "This is INFO message"
logger.debug "This is DEBUG message"
}

Now, execute the following command:

$ gradle showLogging
 
:showLogging
This is println example
This is error message
This is quiet message
This is WARNING message
This is LIFECYCLE message
 
BUILD SUCCESSFUL

Here, Gradle has printed all the logger statements upto the lifecycle level (including lifecycle), which is Gradle's default log level. You can also control the log level from the command line.

-q

This will show logs up to the quiet level. It will include error and quiet messages

-i

This will show logs up to the info level. It will include error, quiet, warning, lifecycle and info messages.

-s

This prints out the stacktrace for all exceptions.

-d

This prints out all logs and debug information. This is most expressive log level, which will also print all the minor details.

Now, execute gradle showLogging -q:

This is println example
This is error message
This is quiet message

Apart from the regular lifecycle, Gradle provides an additional option to provide stack trace in case of any exception. Stack trace is different from debug. In case of any failure, it allows tracking of all the nested functions, which are called in sequence up to the point where the stack trace is generated.

To verify, add the assert statement in the preceding task and execute the following:

task showLogging << {
println "This is println example"
..
assert 1==2
}
 
$ gradle showLogging -s
……
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':showLogging'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.
executeActions(ExecuteActionsTaskExecuter.java:69)
       at …. org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.
execute(SkipOnlyIfTaskExecuter.java:53)
       at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.
execute(ExecuteAtMostOnceTaskExecuter.java:43)
       at org.gradle.api.internal.AbstractTask.executeWithoutThrowingTaskFailure
(AbstractTask.java:305)
...

With stracktrace, Gradle also provides two options:

  • -s or --stracktrace: This will print truncated stracktrace
  • -S or --full-stracktrace: This will print full stracktrace

File management

One of the key features of any build tool is I/O operations and how easily you can perform the I/O operations such as reading files, writing files, and directory-related operations. Developers with Ant or Maven backgrounds know how painful and complex it was to handle the files and directory operations in old build tools; sometimes you had to write custom tasks and plugins to perform these kinds of operations due to XML limitations in Ant and Maven. Since Gradle uses Groovy, it will make your life much easier while dealing with files and directory-related operations.

Reading files

Gradle provides simple ways to read the file. You just need to use the File API (application programing interface) and it provides everything to deal with the file. The following is the code snippet from FileExample/build.gradle:

task showFile << {
File file1 = file("readme.txt")
println file1   // will print name of the file
file1.eachLine {
   println it // will print contents line by line
}
}

To read the file, we have used file(<file Name>). This is the default Gradle way to reference files because Gradle adds some path behavior ($PROJECT_PATH/<filename>) due to absolute and relative referencing of files. Here, the first println statement will print the name of the file which is readme.txt. To read a file, Groovy provides the eachLine method to the File API, which reads all the lines of the file one by one.

To access the directory, you can use the following file API:

def dir1 = new File("src")
println "Checking directory "+dir1.isFile() // will return false   for directory
println "Checking directory "+dir1.isDirectory() // will return true for directory

Writing files

To write to the files, you can use either the append method to add contents to the end of the file or overwrite the file using the setText or write methods:

task fileWrite << {
File file1 = file ("readme.txt")
 
// will append data at the end
file1.append("\nAdding new line. \n")
 
// will overwrite contents
file1.setText("Overwriting existing contents")
 
// will overwrite contents
file1.write("Using write method")
}

Creating files/directories

You can create a new file by just writing some text to it:

task createFile << {
File file1 = new File("newFile.txt")
file1.write("Using write method")
}

By writing some data to the file, Groovy will automatically create the file if it does not exist.

To write content to file you can also use the leftshift operator (<<), it will append data at the end of the file:

file1 << "New content"

If you want to create an empty file, you can create a new file using the createNewFile() method.

task createNewFile << {
File file1 = new File("createNewFileMethod.txt")
file1.createNewFile()
}

A new directory can be created using the mkdir command. Gradle also allows you to create nested directories in a single command using mkdirs:

task createDir << {
def dir1 = new File("folder1")
dir1.mkdir()
 
def dir2 = new File("folder2")
dir2.createTempDir()
 
def dir3 = new File("folder3/subfolder31")
dir3.mkdirs() // to create sub directories in one command
}

In the preceding example, we are creating two directories, one using mkdir() and the other using createTempDir(). The difference is when we create a directory using createTempDir(), that directory gets automatically deleted once your build script execution is completed.

File operations

We will see examples of some of the frequently used methods while dealing with files, which will help you in build automation:

task fileOperations << {
File file1 = new File("readme.txt")
println "File size is "+file1.size()
println "Checking existence "+file1.exists()
println "Reading contents "+file1.getText()
println "Checking directory "+file1.isDirectory()
println "File length "+file1.length()
println "Hidden file "+file1.isHidden()
 
// File paths
println "File path is "+file1.path
println "File absolute path is "+file1.absolutePath
println "File canonical path is "+file1.canonicalPath
 
// Rename file
file1.renameTo("writeme.txt")
 
// File Permissions
file1.setReadOnly()
println "Checking read permission "+ file1.canRead()+" write permission "+file1.canWrite()
file1.setWritable(true)
println "Checking read permission "+ file1.canRead()+" write permission "+file1.canWrite()
 
}

Most of the preceding methods are self-explanatory. Try to execute the preceding task and observe the output. If you try to execute the fileOperations task twice, you will get the exception readme.txt (No such file or directory) since you have renamed the file to writeme.txt.

Filter files

Certain file methods allow users to pass a regular expression as an argument. Regular expressions can be used to filter out only the required data, rather than fetch all the data. The following is an example of the eachFileMatch() method, which will list only the Groovy files in a directory:

task filterFiles << {
def dir1 = new File("dir1")
dir1.eachFileMatch(~/.*.groovy/) {
   println it
}
dir1.eachFileRecurse { dir ->
   if(dir.isDirectory()) {
     dir.eachFileMatch(~/.*.groovy/) {
       println it
     }
   }
}
}

The output is as follows:

$ gradle filterFiles
 
:filterFiles
dir1\groovySample.groovy
dir1\subdir1\groovySample1.groovy
dir1\subdir2\groovySample2.groovy
dir1\subdir2\subDir3\groovySample3.groovy
 
BUILD SUCCESSFUL

Delete files and directories

Gradle provides the delete() and deleteDir() APIs to delete files and directories respectively:

task deleteFile << {
def dir2 = new File("dir2")
def file1 = new File("abc.txt")
file1.createNewFile()
dir2.mkdir()
println "File path is "+file1.absolutePath
println "Dir path is "+dir2.absolutePath
file1.delete()
dir2.deleteDir()
println "Checking file(abc.txt) existence: "+file1.exists()+" and Directory(dir2) existence: "+dir2.exists()
}

The output is as follows:

$ gradle deleteFile
:deleteFile
File path is Chapter6/FileExample/abc.txt
Dir path is Chapter6/FileExample/dir2
Checking file(abc.txt) existence: false and Directory(dir2) existence: false
 
BUILD SUCCESSFUL

The preceding task will create a directory dir2 and a file abc.txt. Then it will print the absolute paths and finally delete them. You can verify whether it is deleted properly by calling the exists() function.

FileTree

Until now, we have dealt with single file operations. Gradle provides plenty of user-friendly APIs to deal with file collections. One such API is FileTree. A FileTree represents a hierarchy of files or directories. It extends the FileCollection interface. Several objects in Gradle such as sourceSets, implement the FileTree interface. You can initialize FileTree with the fileTree() method. The following are the different ways you can initialize the fileTree method:

task fileTreeSample << {
FileTree fTree = fileTree('dir1')
fTree.each {
   println it.name
}
FileTree fTree1 = fileTree('dir1') {
   include '**/*.groovy'
}
println ""
fTree1.each {
   println it.name
}
println ""
FileTree fTree2 = fileTree(dir:'dir1',excludes:['**/*.groovy'])
fTree2.each {
   println it.absolutePath
}
}

Execute the gradle fileTreeSample command and observe the output. The first iteration will print all the files in dir1. The second iteration will only include Groovy files (with extension .groovy). The third iteration will exclude Groovy files (with extension .groovy) and print other files with absolute path.

You can also use FileTree to read contents from the archive files such as ZIP, JAR, or TAR files:

FileTree jarFile = zipTree('SampleProject-1.0.jar')
jarFile.each {
println it.name
}

The preceding code snippet will list all the files contained in a jar file.

Summary

In this article, we have explored different topics of Gradle such as I/O operations, logging, Multi-Project build and testing using Gradle. We also learned how easy it is to generate assets for web applications and Scala projects with Gradle. In the Testing with Gradle section, we learned some basics to execute tests with JUnit and TestNG.

In the next article, we will learn the code quality aspects of a Java project. We will analyze a few Gradle plugins such as Checkstyle and Sonar. Apart from learning these plugins, we will discuss another topic called Continuous Integration. These two topics will be combined and presented by exploration of two different continuous integration servers, namely Jenkins and TeamCity.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Mastering Gradle

Explore Title