25th September 2019

A jshell script incorporating JDK 13 Text Block feature

In this post I’ll show you how I managed to develop my first bash script in Java using JShell which incorporates JDK Text Block feature to produce a new file. The script is used to process hundreds of JPA entity classes and produce a builder class for them in Groovy.

The Story

During recent refactorings in our code base, we decided to use a combination of object builder and object mother patterns to create entity instances in our unit tests. After doing some initial sketches, we found out that having a Groovy class for each JPA entity with @Builder annotation on top of that, would be the proper choice for us.

The Problem

Our project is a big body of code that has more than 330 entities only in one of its persistence modules (I know, it’s way too much, but that’s a different story). So creating a builder class manually for each one of them was not a practical way to do it.

The Solution: A jshell script

The simplest and most obvious solution was to create a bash script to do that. It should be fairly easy to iterate over a list of files in a directory and generate corresponding builder class in the destination folder. But it’s easy when you know how to do it in bash. Honestly I’m working with Linux as my primary OS since five years ago, and I’ve never been good at bash scripting. I always prefered to have them developed in JavaScript and NodeJS so that I could understand the syntax.

This has changed since the introduction of JShell in JDK 9. I had written before some sample scripts to see how it works. However I never considered it as a serious replacement for bash scripts in a daily basis (I can remember why). So I decided to give it another try to see if I can manage to use Java for both my code and my tooling.

Another reason for considering it again was the introduction of JDK Text Block experimental feature. For those who don’t know, it’s the capability to define a multi-line String literals in your code. So I thought it would be a good idea to learn how it works by incorporating it in a real example.

Setup JDK 13 using sdkman

We’re using JDK 8 for our application, so I decided to install the JDK 13 using sdkman. If you’ve not used sdkman before, I highly recommend you to start doing so. It allows you to install and manage different version of all sort of SDKs and tool (e.g. Java, Groovy, Maven, Gradle etc.). It also automatically updates your paths and defines necessary environment variables (like JAVA_HOME). So you don’t need to worry about messing with your path and configurations.

Nowadays you can select between many different distributions and versions of JDK to install. I decided to use the JDK 13.0.0 from Java.net. So I install it as below:

sdk install java 13.0.0-open

You can then use the following command to see the status of your current JDK installations:

sdk list java

its output on my Ubuntu machine was like this:

Installing JDK Using sdkman
Installing JDK Using sdkman

How to use jshell tool in JDK?

The Java Shell tool (JShell) is an interactive tool for learning the Java programming language and prototyping Java code. JShell is a Read-Evaluate-Print Loop (REPL), which evaluates declarations, statements, and expressions as they are entered and immediately shows the results. The tool is run from the command line.

Java Platform, Standard Edition Java Shell User’s Guide

When you run the jshell in a terminal, it runs in the REPL mode by default. This allows you to start playing with Java syntax in an interactive mode. The following screenshot shows you an example in which I ran jshell, defined some variables and displayed the result of a simple calculation expression. You can quit the jshell by entering /exit command.

Testing a simple expression in jshell
Testing a simple expression in jshell

As you can see, for each command that you run, the output is being displayed in the console (even for variable definitions, it shows the initial value of the variable). You can customize the level of details you’re interested in using –feedback option and specifying one of “silent”, “concise”, “normal”, or “verbose”. For example, if we don’t want to have any unwanted output, we can start the jshell using the following option:

jshell --feedback silent

Besides that, we can also run the jshell and pass it the address of a file containing commands that we want to be executed. In this case it would run all the commands in the current terminal session (the same way the bash interpreter runs bash scripts in terminal). The following screenshot shows the same set of commands stored in a script file named sample.jsh which then gets run via jshell:

Running a sample jsh file using jshell
Running a sample jsh file using jshell

This is the technique we’re going to use in our final script. We’ll create a jsh file containing the logic described above. Then we can just ask the jshell to run that.

What about using shebang in jshell scripts?

Unix systems looks for the very first line of a shell script to see if it begins with special character combination #! which is called shebang. If it is, then it expects the next token to be the address of the interpreter command that can process the file contents. An example of the shebang line for a typical bash script is something like this:

#!/bin/sh

this indicates that given script should be processed using the default shell of the linux which is normally located in /bin/sh. If the script is Python, then it would be like

#!/usr/bin/env python

Due to the fact that the # character indicates the start of a comment, it doesn’t affect the actual logic of the script itself. Unfortunately, that’s not the case in Java. Thus we need to use a technique initially used by Go programmers. By adding the following line to the top of our script, we ensure that Linux will pickup jshell to run our script:

//usr/bin/env jshell --show-version --execution local "$0" "$@"; exit $?

The –show-version option asks jshell to print its version when it starts. The –execution local option ensures that the jshell will not spin up a separate JVM for your script and so it runs faster.

As I told you, I’m not an expert in bash scripting. So if you really want to know the detailed description about the exact meaning of the remaining parameters in this line, check Running a file via shebang line.

The Script Logic

The script should do a very basic steps as below:

  1. List all the files in entity package
  2. For each file
    1. Generate the Groovy class based on a template defined using text block String variable
    2. Replace placeholders with the name of the class.
    3. Store the file in a sibling package called builders.

For example, for a simple entity class like Country.java as below:

@Entity
public class Country {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String name;

    @Column(unique = true, nullable = false)
    private String iso3166Code;
    
    // getters and setters omitted
}

it should generate a Groovy file named CountryBuilder.groovy like this:

package com.company.system.persistence.entity.builders

import com.company.system.persistence.entity.Country
import groovy.transform.builder.Builder
import groovy.transform.builder.ExternalStrategy

@Builder(builderStrategy = ExternalStrategy, forClass = Country)
class CountryBuilder {}

So, I created a text block variable (you read it as multi-line String variable) in my jsh script. It contains the contents of the Groovy file as a template. In order to define such a variable, you should enclose its contents in a pair of “triple double quotations” or “””.

var template = """
	package com.company.system.persistence.entity.builder

	import com.company.system.persistence.entity._CLASS_NAME_
	import groovy.transform.builder.Builder
	import groovy.transform.builder.ExternalStrategy
		
	@Builder(builderStrategy = ExternalStrategy, forClass = _CLASS_NAME_)
	class _CLASS_NAME_Builder {}
""";

The “_CLASS_NAME_” token is a placeholder that would be replaced with the name of the actual entity class (e.g. Country). Here is the final contents of the script:

//usr/bin/env jshell --show-version --execution local "$0" "$@"; exit $?

var template = """
   package com.one.crm.persistence.entity.builder

   import com.one.crm.persistence.entity._CLASS_NAME_
   import groovy.transform.builder.Builder
   import groovy.transform.builder.ExternalStrategy

   @Builder(builderStrategy = ExternalStrategy, forClass = _CLASS_NAME_)
   class CLASS_NAMEBuilder {}
""";
File dir = new File("src/main/java/com/company/system/persistence/entity");
Arrays.stream(dir.list())
      .filter(name -> name.indexOf(".java") > 0)
      .map(name -> name.substring(0, name.indexOf(".java")))
      .forEach(name -> {
         var builderContents = template.replaceAll("_CLASS_NAME_", name);
         File builderFile = new File("src/testFixtures/groovy/com/company/system/persistence/entity/builders", name + "Builder.groovy");
         try (FileOutputStream fileOutputStream = new FileOutputStream(builderFile)) {
            fileOutputStream.write(builderContents.getBytes( StandardCharsets.UTF_8));
         } catch (Exception e) {
            e.printStackTrace();
         }
       });

/exit

As it’s a script that is going to be used only once, I didn’t dare to make it parameterized. So everything is hardcoded. Also you may wonder why I didn’t use Groovy for the script? You’re right, and I know that it already supports multi-line Strings. But if it’s something that I can do with Java itself, it always have a higher priority for me. Besides, I also learned a couple of new stuff.

Running the script

I saved the script in a file named builder_builder.jsh beside the and ran it in terminal as:

jshell builder_builder.jsh

And it created all the files smoothly. Thanks to the JDK Text Block feature, I didn’t need to create a dedicated template file or define it using a bunch of string concatenation (or StringBuilder append calls).

Please follow and like us:

You may also like...