Search

Dark theme | Light theme

March 8, 2024

Gradle Goodness: Organizing Tasks Using The Task Container

A Gradle build file describes what is needed to build our Java project. We apply one or more plugins, configure the plugins, declare dependencies and create and configure tasks. We have a lot of freedom to organize the build file as Gradle doesn’t really care. So to create maintainable Gradle build files we need to organize our build files and follow some conventions. In this post we focus on organizing the tasks and see if we can find a good way to do this.

It is good to have a single place where all the tasks are created and configured, instead of having all the logic scattered all over the build file. The TaskContainer is a good place to put all the tasks. To access the TaskContainer we can use the tasks property on the Project object. Within the scope of the tasks block we can create and configure tasks. Now we have a single place where all the tasks are created and configured. This makes it easier to find the tasks in our project as we have a single place to look for the tasks.

Within the scope of the TaskContainer we can use a convention to put the task creation methods at the top of the TaskContainer block. And the task configuration methods are after the task creation in the TaskContainer block. The tasks that are created at the top of the TaskContainer scope can be referenced by configuration code for tasks later in the TaskContainer scope.

The following diagram shows the build file structure and an example of the implementation:

In the example Gradle build file for a Java project we organize the tasks in the TaskContainer using this convention:

plugins {
    java
}
...
tasks {
    // ----------------------------------------------
    // Task creation at the top of the container.
    // ----------------------------------------------

    // Register new task "uberJar".
    val uberJar by registering(Jar::class) {
        archiveClassifier = "uber"

        from(sourceSets.main.get().output)

        dependsOn(configurations.runtimeClasspath)
        from({
            configurations.runtimeClasspath.get()
                .filter { it.name.endsWith("jar") }
                .map { zipTree(it) }
        })
    }

    // ----------------------------------------------
    // Task configuration after task creation.
    // ----------------------------------------------

    // The output of the "uberJar" tasks is part of
    // the output of the "assemble" task.
    // We can refer to the "assemble" task directly
    // as it is added by the Java plugin.
    assemble {
        // We can refer to the task name that
        // we just created in our
        // tasks configuration block.
        dependsOn(uberJar)
    }

    // Configure tasks with type JavaCompile.
    withType<JavaCompile>().configureEach {
        options.compilerArgs.add("--enable-preview")
    }
}
...

Although Gradle doesn’t enforce us to use this convention it can be very helpful as build file authors to use it as it makes it easier to find the tasks in the project.

Written with Gradle 8.6.

March 6, 2024

IntelliJ HTTP Client: Parsing JSON Web Tokens

The IntelliJ HTTP Client is very useful for testing APIs. We can use Javascript to look at the response and write tests with assertions about the response. If an API returns a JSON Web Token (JWT), we can use a Javascript function to decode the token and extract information from it. For example we can then assert that fields of the token have the correct value. There is no built-in support in IntelliJ HTTP Client to decode a JWT, but we can write our own Javascript function to do it. We then use the function in our Javascript response handler to decode the token.

In the following HTTP request file we simulate a call to get an response with a field containing an JWT. We use the function decodeJwt from the file jwt-utils.js to decode the token and extract information from it.

### Simulate a call to get an response with a field containing an JWT.
POST https://examples.http-client.intellij.net/anything
Content-Type: application/json

// Original token before it is base64 encoded:
// {
//     "sub": "1234567890",
//     "upn": "hubert@mrhaki.com",
//     "name": "mrhaki",
//     "groups": ["Blogger"],
//     "iat": 1516239022
// }
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXBuIjoiaHViZXJ0QG1yaGFraS5jb20iLCJuYW1lIjoibXJoYWtpIiwiZ3JvdXBzIjpbIkJsb2dnZXIiXSwiaWF0IjoxNTE2MjM5MDIyfQ.9E2gYNFogs3K8pJH9JiJYISv403EtCm4tRzQWZi1CXM"
}

> {%
    import {decodeJwt} from './scripts/jwt-utils';

    // The token is in the response body and we get it
    // using the path `json.token`.
    // We store it as variable `token` so we can use in the next step.
    const token = decodeJwt(response.body.json.token);

    // We can write assertions on the token contents.
    client.test("Check fields in token", function () {
        client.assert(token.upn === "hubert@mrhaki.com");
        client.assert(token.name === "mrhaki");
        client.assert(token.groups.includes("Blogger"));
        client.assert(token.sub === "1234567890");
        client.assert(token.iat === 1516239022);
    });
%}

The function decodeJwt is defined in the file jwt-utils.js:

// File: ./scripts/jwt-utils.js
export function decodeJwt(token) {
    var base64EncodedPayload = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
    var jsonPayload = decodeURIComponent(decodeBase64(base64EncodedPayload)
        .split('')
        .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
        .join(''))
        // Remove any NUL characters at the end of the string.
        .replace(/\0+$/g, '');
    return JSON.parse(jsonPayload);
}

function decodeBase64(input) {
    const _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    let output = "";
    let chr1, chr2, chr3;
    let enc1, enc2, enc3, enc4;
    let i = 0;
    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
    while (i < input.length) {
        enc1 = _keyStr.indexOf(input.charAt(i++));
        enc2 = _keyStr.indexOf(input.charAt(i++));
        enc3 = _keyStr.indexOf(input.charAt(i++));
        enc4 = _keyStr.indexOf(input.charAt(i++));
        chr1 = (enc1 << 2) | (enc2 >> 4);
        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
        chr3 = ((enc3 & 3) << 6) | enc4;
        output = output + String.fromCharCode(chr1);
        if (enc3 !== 64) {
            output = output + String.fromCharCode(chr2);
        }
        if (enc4 !== 64) {
            output = output + String.fromCharCode(chr3);
        }
    }
    return decodeURI(output);
}

Written with IntelliJ IDEA 2023.3.4.

February 25, 2024

Gradle Goodness: Using System Properties Lazily

It is good practice in Gradle to use lazy configuration. This makes builds faster as only configuration values are evaluated when needed. We should try to not let Gradle spend time on evaluating configuration values that will not be used. For example tasks that are not executed could still be configured by Gradle. If we make sure the configuration of these tasks is lazy we can save time.

Gradle gives us a lazy way to get the value of a Java system property. In our build script we can use the providers property of type ProviderFactory and the method systemProperty(String). This method returns a Provider<String> instance that can be used to get the value of a system property in a lazy way. The method systemProperty can also be used with a Provider<String> argument.

In the following example we register two tasks that print the value of the Java system property user.name to the console. We use lazy configuration to make sure the value of the system property is only fetched when the task is executed.

tasks {
    register<PrintSystemProperty>("printSystemProperty") {
        // We can use providers.systemProperty(String)
        // to get the value of an Java system property
        // in a lazy way.
        // The argument can also be a Provider<String> type.
        // So at this point the value is not fetched yet,
        // only when the task is executed the actual value
        // of the system property "user.name" is fetched.
        systemProperty = providers.systemProperty("user.name")
    }

    register<PrintSystemProperty>("printSystemProperty2") {
        // Alternative way is to use the generic provider(Callable)
        // method to get the value of a system property.
        systemProperty = provider { System.getProperty("user.name") }
    }
}

// Simple task to print the value of a Java system property.
abstract class PrintSystemProperty : DefaultTask() {
    @get:Input
    abstract val systemProperty: Property<String> // Use lazy property.

    @TaskAction
    fun printSystemPropertyValue() {
        // Only here we actually will get the value
        // for the system property.
        logger.quiet(systemProperty.get())
    }
}
$ ./gradlew printSystemProperty printSystemProperty2

> Task :printSystemProperty
mrhaki

> Task :printSystemProperty2
mrhaki

BUILD SUCCESSFUL in 685ms
2 actionable tasks: 2 executed

Written with Gradle 8.6.

Gradle Goodness: Using Environment Variables Lazily

It is good practice in Gradle to use lazy configuration. This makes builds faster as only configuration values are evaluated when needed. We should try to not let Gradle spend time on evaluating configuration values that will not be used. For example tasks that are not executed could still be configured by Gradle. If we make sure the configuration of these tasks is lazy we can save time.

Gradle gives us a lazy way to get the value of an environment variable. In our build script we can use the providers property of type ProviderFactory and the method environmentVariable(String). This method returns a Provider<String> instance that can be used to get the value of an environment variable in a lazy way.

In the following example we register two tasks that print the value of the environment variable USER. We use lazy configuration to make sure the value of the environment variable is only fetched when the task is executed.

tasks {
    register<PrintEnvironmentVariable>("printEnvironmentVariable") {
        // We can use providers.environmentVariable(String)
        // to get the value of an environment variable
        // in a lazy way.
        // The argument can also be a Provider<String> type.
        // So at this point the value is not fetched yet,
        // only when the task is executed the actual value
        // of the environment variable "USER" is fetched.
        environmentVariable = providers.environmentVariable("USER")
    }

    register<PrintEnvironmentVariable>("printEnvironmentVariable2") {
        // Alternative way is to use the generic provider(Callable)
        // method to get the value of an environment variable.
        environmentVariable = provider { System.getenv("USER") }
    }
}

// Simple task to print the value of an environment variable.
abstract class PrintEnvironmentVariable : DefaultTask() {
    @get:Input
    abstract val environmentVariable: Property<String> // Use lazy property.

    @TaskAction
    fun printEnvironmentVariable() {
        // Only here we actually will get the value
        // for the environment variable.
        logger.quiet(environmentVariable.get())
    }
}

When we execute the tasks we see the value of the environment variable USER:

$ ./gradlew printEnvironmentVariable printEnvironmentVariable2

> Task :printEnvironmentVariable
mrhaki

> Task :printEnvironmentVariable2
mrhaki

BUILD SUCCESSFUL in 599ms
2 actionable tasks: 2 executed

Written with Gradle 8.6.

February 14, 2024

IntelliJ HTTP Client: Accessing Environment Variables In JavaScript

When we use the IntelliJ HTTP Client we can write JavaScript for the pre-request and response handlers. If we want to access an environment variable in JavaScript we can use request.environment.get(string). The argument for the get function is the name of the environment variable we want to get the value for. Environment variables can be defined in the file http-client.env.json or in http-client.private.env.json.

In the following example we have environment file http-client.env.json with two environments development and production. Each environment has a variable named project:

{
    "development": {
        "project": "DEV-1270"
    },
    "production": {
        "project": "PROJ-1234"
    }
}

In our .http request file we are posting to endpoint https://ijhttp-examples.jetbrains.com/anything and we use JavaScript to read the environment variable project:

### POST to example URL
< {%
    // Get value for environment variable 'project'.
    const project = request.environment.get("project");

    // Use split to get the value before and after the -.
    const kindAndNumber = project.split("-");

    // As an example we use the value of the environment variable
    // to assign values to new request variables.
    request.variables.set("kind", kindAndNumber[0]);
    request.variables.set("number", kindAndNumber[1]);
  %}
POST https://ijhttp-examples.jetbrains.com/anything

{
    "project": "{{project}}",
    "kind": "{{kind}}",
    "number": "{{number}}"
}

In the response we see the variables do have values:

...
"json": {
    "kind": "PROJ",
    "number": "1234",
    "project": "PROJ-1234"
  }
...

Written with IntelliJ IDEA 2023.3.3.

February 11, 2024

IntelliJ HTTP Client: Using In-Place Variables

The built-in IntelliJ HTTP Client is very useful for testing HTTP requests and responses. If we want to define a variable in our .http file that is only used in this file and will not change per environment we can define it using the following syntax: @<variable name> = variable value. The variable is an in-place variable and the scope of the variable in the current .http file. The variable is immutable and can only be defined with a value once and cannot be overwritten. To refer to the variable we use the syntax {{<variable name>}}.

In the following example we define a variable with the name base-url and the value https://ijhttp-example.jetbrains.com:

# We define an in-place variable "base-url"
# to be used in this file.
@base-url = https://ijhttp-examples.jetbrains.com

### GET HTML page
GET {{base-url}}/html

### GET XML page
GET {{base-url}}/xml

### GET JSON page
GET {{base-url}}/json

### GET UUID4 response
GET {{base-url}}/uuid

Written with IntelliJ 2023.3.3.

February 5, 2024

Gradle Goodness: Continuous Testing For Java Projects

The command line option --continuous or the short version -t enables Gradle’s continous build. For a continuous build Gradle will keep on running and will re-execute the tasks we invoked if the input or of the input of one of the depended tasks has changed. For a project with the java plugin we can use this option for the test task. Gradle will run the test task and after the task has been executed Gradle will wait for any changes in the input of the task. This means if we change our Java test code in src/test/java and save the source file Gradle will re-execute the test task and show the output. But also if the input of other tasks changes, that the test task depends on, the test is re-executed. So also changes in source files in our src/main/java directory will trigger a re-execute of the test task, because the test task depends on the compileJava task, and the compileJava task has the src/main/java directory as input.

In the following example output we invoked the test task with the --continuous option. On the first run there was an assertion failure. We fixed the code in src/main/java and saved the file. Without having to restart Gradle we see that our assertion succeeded on the second test task run.

$ ./gradlew --continuous test

> Task :app:test FAILED

AppTest > application has a greeting FAILED
    Condition not satisfied:

    result == "Hello World!!"
    |      |
    |      false
    |      2 differences (84% similarity)
    |      Hello (w)orld!(-)
    |      Hello (W)orld!(!)
    Hello world!
        at org.example.AppTest.application has a greeting(AppTest.groovy:17)

1 test completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/java/app/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 3s
3 actionable tasks: 1 executed, 2 up-to-date

Waiting for changes to input files... (ctrl-d to exit)
modified: /Users/mrhaki/Projects/mrhaki.com/java/app/src/main/java/org/example/App.java
Change detected, executing build...


BUILD SUCCESSFUL in 4s
3 actionable tasks: 3 executed

Waiting for changes to input files... (ctrl-d to exit)
<=============> 100% EXECUTING [53s]
> IDLE
> IDLE

To stop continuous builds we press Ctrl+C.

To have some nice output we want to use the full exception format for our test logging. In the following example build file we configure this for our test task in the testLogging block:

// File: build.gradle.kts
...
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            targets {
                all {
                    testTask.configure {
                        testLogging {
                            exceptionFormat = TestExceptionFormat.FULL
                        }
                    }
                }
            }
        }
    }
}
...

Now we can run the test task with the --continuous option and work on our source files and tests in our IDE. On each save of a source file the test task is executed again and we can immediately see the output of the test run.

Written with Gradle 8.6

February 3, 2024

Gradle Goodness: Java Toolchain Configuration Using User Defined Java Locations

With the java plugin we can configure a so-called Java toolchain. The toolchain configuration is used to define which Java version needs to be used to compile and test our code in our project. The location of the Java version can be determined by Gradle automatically. Gradle will look at known locations based on the operating system, package managers, IntellIJ IDEA installations and Maven Toolchain configuration.

But we can also define the locations of our Java installations ourselves using the project property org.gradle.java.installations.paths. We provide the paths to the local Java installations as a comma separated list as value for this property. When we set this property we can also disable the Gradle toolchain detection mechanism, so only the Java installations we have defined ourselves are used. To disable the automatic detection we set the property org.gradle.java.installations.auto-detect to false. If we leave the value to the default value true, then the locations we set via org.gradle.java.installations.paths are added to the Java installations already found by Gradle.

The property org.gradle.java.installations.paths is a project property we can set via the command line, but we can also set it in the gradle.properties file in our GRADLE_USER_HOME directory. Then the values we define will be used by all Gradle builds on our machine.

In the following example gradle.properties file we define the locations of two Java installations and also disable the automatic detection of Java installations. We store this file in our GRADLE_USER_HOME directory.

# File: $GRADLE_USER_HOME/gradle.properties
# We define the locations of two Java installations on our computer.
org.gradle.java.installations.paths=C:/Users/mrhaki/tools/apps/zulu11-jdk/current,C:/Users/mrhaki/tools/apps/zulu17-jdk/current

# We disable the automatic detection of Java installations by Gradle.
org.gradle.java.installations.auto-detect=false

# We also disable the automatic download of Java installations by Gradle.
org.gradle.java.installations.auto-download=false

We add the java plugin and configure our toolchain with the following Gradle build script:

// File: build.gradle.kts
plugins {
    java
}

java {
    toolchain {
        // We want to use Java 17 to compile, test and run our code.
        // Now it doesn't matter which Java version is used by Gradle itself.
        languageVersion = languageVersion.set(JavaLanguageVersion.of(17))
    }
}

We run the javaToolchains task to see the Java toolchain configuration:

$ ./gradlew javaToolchains

> Task :javaToolchains

 + Options
     | Auto-detection:     Disabled
     | Auto-download:      Disabled

 + Azul Zulu JDK 11.0.22+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu11-jdk\current
     | Language Version:   11
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'

 + Azul Zulu JDK 17.0.10+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu17-jdk\current
     | Language Version:   17
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'


BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

In the generated output we can see that Gradle detected the two Java installations we defined in the gradle.properties file using the Gradle property org.gradle.java.installations.paths.

Written with Gradle 8.5.

February 2, 2024

Gradle Goodness: Using Maven Toolchains Configuration For Gradle Java Toolchain Resolution

When we apply the Java plugin to our Gradle project we can configure which Java version we want to use for compiling our source code and running our tests using a toolchain configuration. The benefit of having a toolchain configuration is that we can use a different Java version for compiling and running our code than the Java version that is used by Gradle to execute the build. Gradle will look for that Java version on our local computer or download the correct version if it is not available. To search for a local Java installation Gradle will look for operating system specific locations, installations by package managers like SKDMAN! and Jabba, IntelliJ IDEA installations and Maven Toolchain specifications. Maven Toolchain specifications is an XML file describing the location of local Java installation. Each Java installation is described by a version and optional vendor it provides and the location of the installation. Maven uses this information to find the correct Java installation when the maven-toolchain-plugin is used in a Maven project. But Gradle can also utilize Maven Toolchain specifications to find local Java installations. This can be useful when we have to work on multiple projects where some use Maven and others use Gradle. We can place the Maven Toolchain specification file in our Maven home directory. This is also the default place where Gradle will look, but we can use a project property to override this location.

The following example shows a Maven toolchain configuration with three different Java versions:

<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>11</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/tools/apps/zulu11-jdk/current</jdkHome>
		</configuration>
	</toolchain>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>17</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/apps/zulu17-jdk/current</jdkHome>
		</configuration>
	</toolchain>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>21</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/tools/apps/zulu-jdk/current</jdkHome>
		</configuration>
	</toolchain>
</toolchains>

In our Gradle build file we apply the java plugin and define in the toolchain configuration we want to use Java 17 for our builds:

// File: build.gradle.kts
plugins {
    java
}

java {
    toolchain {
        // Use Java 17 for building and running tests
        languageVersion = JavaLanguageVersion.of(17)
    }
}

We can now use the javaToolchains task to see the available Java installations:

$ ./gradlew javaToolchains

> Task :javaToolchains

 + Options
     | Auto-detection:     Enabled
     | Auto-download:      Enabled

  + Azul Zulu JDK 11.0.22+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu11-jdk\current
     | Language Version:   11
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Maven Toolchains

 + Azul Zulu JDK 17.0.10+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu17-jdk\current
     | Language Version:   17
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Current JVM

 + Azul Zulu JDK 21.0.2+13-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu-jdk\current
     | Language Version:   21
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Maven Toolchains

BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed

The command was run using Java 17 and we can see in the output that it is detected by Gradle as the current JVM. The Java installations for Java 11 and Java 21 are detected using Maven Toolchains.

If the location of the Maven Toolchain specification file is not in the default location, we can use the Gradle project property org.gradle.java.installations.maven-toolchain-file to specify a custom location. We can use it from the command line using the -P option or we can add it to gradle.properties in the project root directory.

$ ./gradlew javaToolchains -Porg.gradle.java.installations.maven-toolchain-file=C:/Users/mrhaki/tools/maven/toolchains.xml

...

Written with Gradle 8.5.

January 13, 2024

IntelliJ HTTP Client: Allowing Insecure HTTPS Requests

Sometimes we want to send HTTP requests to servers that use HTTPS with self-signed certificates. We then need to tell HTTP Client to not check the certificate of the server. This is like running the curl command with the --insecure or '-k' flag. To disable the certificate verification for HTTP Client we need to adjust the http-client.private.env.json file. For the environment we want to disable the certificate verification we must add a SSLConfiguration section. In the SSLConfiguration section we add the verifyHostCertificate property with value 'true'.

In the following example http-client.private.env.json file we have the environments development and production with different passwords. For the development environment we disable the certificate verification and we keep the certificate verification for the production environment.

{
  "development": {
    "password": "mrhaki",
    "SSLConfiguration": {
      "verifyHostCertificate": false
    }
  },
  "production": {
    "password": "mrhaki42"
  }
}

Written with IntelliJ 2023.3.2