(https://www.manning.com/books/gradle-in-action)
3.1 Introducing the case study
This section introduces a simple application to illustrate the use of Gradle: a To Do application. Throughout the book, we’ll apply the content to demonstrate Gradle’s features in each phase of the build pipeline. The use case starts out as a plain Java application without a GUI, simply controlled through console input. Over the course of this chapter, you’ll extend this application by adding components to learn more advanced concepts.The To Do application will act as a vehicle to help you gain a broad knowledge of Gradle’s capabilities. You’ll learn how to apply Gradle’s standard plugins to bootstrap, configure, and run your application. By the end of this chapter, you’ll have a basic understanding of how Gradle works that you can apply to building your own webbased Java projects with Gradle.
The To Do application
Today’s world is busy. Many of us manage multiple projects simultaneously, both in our professional and private lives. Often, you may find yourself in situations where you feel overwhelmed and out of control. The key to staying organized and focused on priorities is a well-maintained to-do list. Sure, you could always write down your tasks on a piece of paper, but wouldn’t it be convenient to be able to access your action items everywhere you go? Access to the internet is almost omnipresent, either through your mobile phone or publicly available access points. You’re going to build your own web-based and visually appealing application, as shown in figure 3.1.
Task management use cases
Now that you know your end goal, let’s identify the use cases the application needs to fulfill. Every task management system consists of an ordered list of action items or tasks. A task has a title to represent the action needed to complete it. Tasks can be added to the list and removed from the list, and marked active or completed to indicate their status. The list should also allow for modifying a task’s title in case you want to make the description more accurate. Changes to a task should automatically get persisted to a data store.
To bring order to your list of tasks, you’ll include the option to filter tasks by their status: active or completed. For now, we’ll stick with this minimal set of features. Figure 3.2 shows a screenshot of the user interface rendered in a browser:
Let’s take a step back from the user interface aspect and build the application from the ground up. In its first version, you’ll lay out its foundation by implementing the basic functionality controlled through the command line. In the next section, we’re going to focus on the application’s components and the interactions between them.
Examining the component interaction
We found that a To Do application implements the typical create, read, update, and delete (CRUD) functionality. For data to be persisted, you need to represent it by a model. You’ll create a new Java class called ToDoItem, a plain old Java object (POJO) acting as a model. To keep the first iteration of the solution as simple as possible, we won’t introduce a traditional data store like a database to store the model data. Instead, you’ll keep it in memory, which is easy to implement. The class implementing the persistence contract is called InMemoryToDoRepository. The drawback is that you can’t persist the data after shutting down the application. Later in the book, we’ll pick up this idea and show how to write a better implementation for it.
Every standalone Java program is required to implement a main class, the application’s entry point. Your main class will be called ToDoApp and will run until the user decides to exit the program. You’ll present users with a menu of commands through which they can manage their to-do list by typing in a letter triggering a specific action. Each action command is mapped to an enum called CommandLineInput. The class CommandLineInputHandler represents the glue between user interaction and command execution. Figure 3.3 illustrates the object interaction arranged in a time sequence for the use case of listing all available tasks.
Building the application’s functionality
In the last section, we identified the classes, their functions, and the interaction between them. Now it’s time to fill them with life. First, let’s look at the model of a todo action item.
THE TO DO MODEL CLASS
Each instance of the ToDoItem class represents an action item in your to-do list. The attribute id defines the item’s unique identity, enabling you to store it in the in-memory data structure and read it again if you want to display it in the user interface. Additionally, the model class exposes the fields name and completed. For brevity, the getter and setter methods as well as the compareTo method are excluded from the snippet:
- package ch2.todo.model;
- public class ToDoItem implements Comparable<ToDoItem>{
- private Long id;
- private String name;
- private boolean completed;
- @Override
- public int compareTo(ToDoItem other) {
- int name_compare_result = name.compareTo(other.name);
- int comp_compare_result = Boolean.valueOf(completed).compareTo(Boolean.valueOf(other.completed));
- return name_compare_result!=0?name_compare_result:comp_compare_result;
- }
- }
IN-MEMORY PERSISTENCE OF THE MODEL
Storing data in memory is convenient and simplifies the implementation. Later in the book, you may want to provide more sophisticated implementations like database or file persistence. To be able to swap out the implementation, you’ll create an interface, the ToDoRepository, as shown in the following listing.
- Listing 3.1 The repository interface
- package ch2.todo.repository;
- import java.util.List;
- import ch2.todo.model.ToDoItem;
- public interface ToDoRepository {
- List<ToDoItem> findAll();
- ToDoItem findById(Long id);
- Long insert(ToDoItem toDoItem);
- void update(ToDoItem toDoItem);
- void delete(ToDoItem toDoItem);
- }
- Listing 3.2 In-memory persistence of to-do items
- package ch2.todo.repository;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.ConcurrentMap;
- import java.util.concurrent.atomic.AtomicLong;
- import ch2.todo.model.ToDoItem;
- public class InMemoryToDoRepository implements ToDoRepository{
- private AtomicLong currentId = new AtomicLong();
- private ConcurrentMap<Long, ToDoItem> toDos = new ConcurrentHashMap<Long, ToDoItem>();
- @Override
- public List<ToDoItem> findAll() {
- List<ToDoItem> toDoItems = new ArrayList<ToDoItem>(toDos.values());
- Collections.sort(toDoItems);
- return toDoItems;
- }
- @Override
- public ToDoItem findById(Long id) {
- return toDos.get(id);
- }
- @Override
- public Long insert(ToDoItem toDoItem) {
- Long id = currentId.incrementAndGet();
- toDoItem.setId(id);
- toDos.putIfAbsent(id, toDoItem);
- return id;
- }
- @Override
- public void update(ToDoItem toDoItem) {
- toDos.replace(toDoItem.getId(), toDoItem);
- }
- @Override
- public void delete(ToDoItem toDoItem) {
- toDos.remove(toDoItem.getId());
- }
- }
THE APPLICATION’S ENTRY POINT
The class ToDoApp prints the application’s options on the console, reads the user’s input from the prompt, translates the one-letter input into a command object, and handles it accordingly, as shown in the next listing.
- Listing 3.3 Implementing the main class
- package ch2.todo;
- import ch2.todo.utils.CommandLineInput;
- import ch2.todo.utils.CommandLineInputHandler;
- public class ToDoApp {
- public static final char DEFAULT_INPUT = '\u0000';
- public static void main(String args[]) throws Exception{
- CommandLineInputHandler commandLineInputHandler = new CommandLineInputHandler();
- char command = DEFAULT_INPUT;
- while(CommandLineInput.EXIT != command) {
- commandLineInputHandler.printOptions();
- String input = commandLineInputHandler.readInput();
- char[] inputChars = input.length() == 1 ? input.toCharArray() : new char[] { DEFAULT_INPUT };
- command = inputChars[0];
- CommandLineInput commandLineInput = CommandLineInput.getCommandLineInputForInput(command);
- commandLineInputHandler.processInput(commandLineInput);
- }
- }
- }
3.2 Building a Java project
In the last section, we identified the Java classes required to write a standalone To Do application. To assemble an executable program, the source code needs to be compiled and the classes need to be packaged into a JAR file. The Java Development Kit (JDK) provides development tools like javac and jar that help with implementing these tasks. Unless you’re a masochist, you don’t want to run these tasks manually each and every time your source code changes.
Gradle plugins act as enablers to automate these tasks. A plugin extends your project by introducing domain-specific conventions and tasks with sensible defaults. One of the plugins that Gradle ships with is the Java plugin. The Java plugin goes far beyond the basic functionality of source code compilation and packaging. It establishes a standard layout for your project and makes sure that tasks are executed in the correct order so they make sense in the context of a Java project. It’s time to create a build script for your application and apply the Java plugin.
Using the Java plugin
In chapter 1, you learned that every Gradle project starts with the creation of the build script named build.gradle. Create the file and tell your project to use the Java plugin like this:
- apply plugin: 'java'
BUILDING THE PROJECT
You’re ready to build the project. One of the tasks the Java plugin adds to your project is named build. The build task compiles your code, runs your tests, and assembles the JAR file, all in the correct order. Running the command gradle build should give you an output similar to this:
Each line of the output represents an executed task provided by the Java plugin. You may notice that some of the tasks are marked with the message UP-TO-DATE. That means that the task was skipped. Gradle’s incremental build support automatically identified that no work needed to be done. Especially in large enterprise projects, this feature proves to be a real timesaver. In chapter 4 you’ll learn how to apply this concept to your own tasks. In the command-line output, you can see concrete examples of skipped tasks: compileTestJava and testClasses. As you provide any unit tests in the default directory src/test/java, Gradle happily moves on. If you want to learn how to write tests for your application and integrate them into the build, see chapter 7. Here’s the project structure after executing the build:
On the root level of your project, you’ll now also find a directory named build, which contains all output of the build run, including class files, test reports, the assembled JAR file, and temporary files like a manifest needed for the archive. If you’ve previously used the build tool Maven, which uses the standard output directory target, the structure should look familiar. The name of the build output directory is a configurable standard property common to all Gradle builds. You’ve seen how effortless it is to build a Java project by convention without any additional configuration from your side. The JAR file was created under build/libs and is ready for execution. It’s important to understand that the name of the JAR file is derived from the project name. As long as you don’t reconfigure it, the directory name of your project is used, which in this case is ch3. Let’s see the To Do application in action.
RUNNING THE PROJECT
Running a Java program is easy. For now, you’ll just use the JDK’s java command from the root directory of your project:
That’s it—you effortlessly implemented a Java application and built it with Gradle. All it took was a one-liner in your build script as long as you stuck to the standard conventions. Next, we’ll look at how to customize the build-by-convention standards.
Customizing your project
The Java plugin is a small opinionated framework. It assumes sensible default values for many aspects of your project, like its layout. If your view of the world is different, Gradle gives you the option of customizing the conventions. How do you know what’s configurable? A good place to start is Gradle’s Build Language Reference, available at https://docs.gradle.org/3.1/dsl/. Remember the command-line option properties from chapter 2? Running gradle properties gives you a list of configurable standard and plugin properties, plus their default values. You’ll customize the project by extending the initial build script.
MODIFYING PROJECT AND PLUGIN PROPERTIES
In the following example, you’ll specify a version number for your project and indicate the Java source compatibility. Previously, you ran the To Do application using the java command. You told the Java runtime where to find the classes by assigning the build output directory to the classpath command-line option via -cp build/classes/main. To be able to start the application from the JAR file, the manifest MANIFEST.MF needs to contain the header Main-Class. The following listing demonstrates how to configure the default values in the build script and add a header attribute to the JAR manifest:
- Listing 3.4 Changing properties and adding a JAR header
- version = 0.1
- sourceCompatibility = 1.6
- jar {
- manifest {
- attributes 'Main-Class': 'ch3.todo.ToDoApp'
- }
- }
RETROFITTING LEGACY PROJECTS
Rarely do enterprises start new software projects with a clean slate. All too often, you’ll have to integrate with a legacy system, migrate the technology stack of an existing project, or adhere to internal standards or limitations. A build tool has to be flexible enough to adapt to external constraints by configuring the default settings.
In this section we’ll explore examples that demonstrate the customizability of the To Do application. Let’s assume you started the project with a different directory layout. Instead of putting source code files into src/main/java, you chose to use the directory src. The same concept applies if you want to change the default test source directory. Additionally, you’d like to let Gradle render its build output into the directory out instead of the standard value build. The next listing shows how to adapt your build to a custom project layout.
- Listing 3.5 Changing the project default layout
- sourceSets {
- main {
- java {
- srcDirs = ['src']
- }
- }
- test {
- java {
- srcDirs = ['test']
- }
- }
- }
- buildDir = 'out'
Configuring and using external dependencies
Let’s think back to the main method in the class ToDoApp. You wrote some code to read the user’s input from the console and translate the first character into a to-do command. To do so, you needed to make sure that the entered input string had a length of only one digit. Otherwise, you’d assign the Unicode null character:
- String input = commandLineInputHandler.readInput();
- char[] inputChars = input.length() == 1 ? input.toCharArray() : new char[] { DEFAULT_INPUT };
- command = inputChars[0];
- import org.apache.commons.lang3.CharUtils;
- String input = commandLineInputHandler.readInput();
- command = CharUtils.toChar(input, DEFAULT_INPUT);
DEFINING THE REPOSITORY
In the Java world, dependencies are distributed and used in the form of JAR files. Many libraries are available in a repository, such as a file system or central server. Gradle requires you to define at least one repository to use a dependency. For your purposes, you’re going to use the publicly available, internet-accessible repository Maven Central:
- repositories {
- mavenCentral()
- }
DEFINING THE DEPENDENCY
A dependency is defined through a group identifier, a name, and a specific version. You’ll use version 3.1 of the library, as shown in this code snippet:
- dependencies {
- compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.1'
- }
RESOLVING THE DEPENDENCY
Gradle automatically detects new dependencies in your project. If the dependency hasn’t been resolved successfully, it downloads it with the next task invocation that requires it to work correctly—in this case, task compileJava:
Chapter 5 will give a deeper coverage of the topic of dependency management.
3.4 Gradle wrapper
You put together a prototype of a task management web application. After you show it to your coworker, Mike, he says he wants to join forces and bring the application to the next level by adding more advanced features. The code has been committed to a version control system (VCS), so he can go ahead, check out the code, and get started working on it.
Mike has never worked with the build tool Gradle, so he asks you how to install the runtime on his machine and which version to use. Because he didn’t go through the motions of initially setting up Gradle, he’s also concerned about potential differences between setting up Gradle on his Windows machine versus installing it on a Mac. From experience with other build tools, Mike is painfully aware that picking the wrong version of the build tool distribution or the runtime environment may have a detrimental effect on the outcome of the build. All too often, he’s seen that a build completes successfully on his machine but fails on another for no apparent reason. After spending hours troubleshooting, he usually discovers that the cause was an incompatible version of the runtime.
Gradle provides a very convenient and practical solution to this problem: the Gradle wrapper. The wrapper is a core feature and enables a machine to run a Gradle build script without having to install the runtime. It also ensures that the build script is run with a specific version of Gradle. It does so by automatically downloading the Gradle runtime from a central location, unpacking it to your local file system, and using it for the build. The ultimate goal is to create reliable and reproducible builds independent of the operating system, system setup, or installed Gradle version.
Setting up the wrapper
To set up the wrapper for your project, you’ll need to do two things: create a wrapper task and execute the task to generate the wrapper files (figure 3.6).
To enable your project to download the zipped Gradle runtime distribution, define a task of type Wrapper and specify the Gradle version you want to use through the property gradleVersion:
- task wrapper(type: Wrapper) {
- gradleVersion = '1.7'
- }
As a result, you’ll find the following wrapper files alongside your build script:
Keep in mind that you’ll only need to run gradle wrapper on your project once. From that point on, you can use the wrapper’s script to execute your build. The downloaded wrapper files are supposed to be checked into version control. For documentation reasons it’s helpful to also keep the task in your project. It’ll help you to upgrade your wrapper version later by changing the gradleVersion and rerunning the wrapper task. Instead of creating the wrapper task manually and executing it to download the relevant files, you can use the build setup plugin mentioned earlier.
Using the wrapper
As part of the wrapper distribution, a command execution script is provided. For *nix systems, this is the shell script gradlew; for Windows operating systems, it’s gradlew.bat. You’ll use one of these scripts to run your build in the same way as you would with the installed Gradle runtime. Figure 3.7 illustrates what happens when you use the wrapper script to execute a task.
Let’s get back to our friend Mike. He checked out the application code from the VCS. Included in the source code tree of the project, he’ll find the wrapper files. As Mike develops his code on a Windows box, he’ll need to run the wrapper batch file to execute a task. The following console output is produced when he fires up the local Jetty container to run the application:
The distribution ZIP file is downloaded from a central server hosted by the Gradle project, stored on Mike’s local file system under $HOME_DIR/.gradle/wrapper/dists. The Gradle wrapper also takes care of unpacking the distribution and setting the appropriate permissions to execute the batch file. Note that the download only needs to happen once. Subsequent build runs reuse the unpacked installation of the runtime located in your Gradle home directory.
What are the key takeaways? A build script executed by the Gradle wrapper provides exactly the same tasks, features, and behavior as it does when run with a local Gradle installation. Again, you don’t have to stick with the default conventions the wrapper gives you. Its configuration options are very flexible. We’ll look at them in the next section.
Customizing the wrapper
Some enterprises have very restrictive security strategies, especially if you work for a government agency, where access to servers outside of the network is prohibited. How do you enable your project to use the Gradle wrapper in that case? It’s all in the configuration. You’ll change the default properties to target an enterprise server hosting the runtime distribution. And while you’re at it, you’ll also change the local storage directory:
Pretty straightforward, right? There are many more options to explore. Make sure to check out the Gradle wrapper DSL documentation for detailed information at http://gradle.org/docs/current/dsl/org.gradle.api.tasks.wrapper.Wrapper.html.