Quarkus Command mode with Picocli

Dmytro Chaban
Quarkify
Published in
7 min readApr 27, 2020

After this article, you’ll be able to write beautiful command-line tools for Quarkus with Picocli. This is the continuation of the first article about Quarkus mode, if you haven’t seen it, please read it first.

Now that you know how to use command mode, it’s time to prettify it! We gonna use Picocli, which makes cli tools fun to build.

For better copy-paste experience go to Quarkify website

Initial setup

If you are lazy or just want to see the final result clone this repo on the master branch and just look through the next steps.

First and foremost, let’s clone our previous project, you don’t need to do it if you already followed our previous article.

git clone --branch clean https://github.com/quarkifynet/quarkus-command-mode-picocli.git

Let’s recap shortly what’s inside. Besides our GreetingResource and GreetingService, we have GreetingApplication that's executed either from maven or command line, let's check that it's working.

./mvnw compile quarkus:dev -Dquarkus.args="--greet RedHat"

If you saw logger’s info message “Hello RedHat” then you’re on the right track. Let’s add a few more dependencies that will help us convert our command support into OOP magic.

<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-shared-utils</artifactId>
<version>3.2.1</version>
</dependency>

First dependency is picocli itself, second one will be used to parse single string into multiple to feed them into picocli.

Root command

Let’s create our first command, It’ll be root command that will help us route to different commands, such as help and greet.

Nothing special, however, we need to clarify some moments here.

  1. Each Picocli command should be annotated with @Command, there are many parameters that you can use, for root command we use subcommands
  2. Any(except root) Picocli command should implement Runnable or Callable interface, Callable<Integer> can be useful if you want to return error code back to the terminal.
  3. We use built-in CommandLine.HelpCommand so that we don't re-invent the wheel.

Greeting command with DI

That’s all good, but how do we inject our beans into the command? That’s easy enough, let’s create our GreetingCommand

This one is more interesting, let’s see what’s going on.

  1. We use @Dependent annotation so that we create Bean from this class, that we can later inject into e.g GreetingApplication.
  2. In @Command, we specify name of the command, this will be used to identify which command to call, e.g help or this one.
  3. We also specify mixinStandardHelpOptions to make help info for this command, as well as a description that will be used there
  4. We created String name field, which will be parsed from --name={} or -n {}, argument passed to our command.
  5. We Injected GreetingService to handle all the greeting work since we have @Dependent annotation.
  6. And, of course, we have call() method that handles all the logic written to this command.

Picocli will look at all arguments, as well as the structure of the command tree, and decide which command it should execute since we have 1 root and 2 subcommands, it will look at first argument passed, and compare it with the name of the command. Name of command we specified in @Command annotation, but you can also specify it directly in code.

Use new commands in GreetingApplication

We made a really beautiful command, but haven’t used it anywhere yet, let’s fix it. We already have GreetingApplication, we just need to remove all the code that we wrote in run function and re-write it a little bit.

Let’s follow this class line by line.

@Inject
GreetingCommand greetingCommand;

We inject our GreetingCommand, that we annotated with @Dependable. When we pass it later to CommandLine object, it already has all the dependencies(in our case GreetingService) injected and ready to use.

@Override
public int run(String... args) throws Exception {
if (args.length == 0) {
Quarkus.waitForExit();
return 0;
}

This part haven’t changed, if there’s 0 arguments — we just assume that user wants to start complete Quarkus applciation.

if (args.length == 1) {
args = CommandLineUtils.translateCommandline(args[0]);
}

This part is new, you might’ve noticed that if you use maven command, you receive commands as a single string, this tool will split it into separate strings. This step is required for picocli to work correctly, since it awaits each argument on a separate string. I used Maven shared utils to parse it, they’re safe to use with native builds.

return new CommandLine(new QuarkusCommand())
.addSubcommand(greetingCommand)
.execute(args);

And the final part, we create new CommandLine object, in which we pass QuarkusCommand, we create a new instance of it since we don't need any Dependency injection, so that's okay. then we call addSubcommand and pass our injected greetingCommand into it. And the final step, of course, is to .execute() it with arguments that we passed or parsed. The rest is by picocli magic.

Let’s call our code and see how it’ll work.

./mvnw compile quarkus:dev -Dquarkus.args="help"

Output:

...
Usage: <main class> [COMMAND]
Commands:
help Displays help information about the specified command
greet Greet person by their name
...

All right, let’s call greet with help option

./mvnw compile quarkus:dev -Dquarkus.args="greet --help"

Output:

Usage: <main class> greet [-hV] [-n=]Greet person by their name
-h, --help Show this help message and exit.
-n, --name=<name> Specify which user to greet
-V, --version Print version information and exit.

Nice, now any developer can see how to use your commands. Easy to understand and helpful. Let’s finally greet our user

./mvnw compile quarkus:dev -Dquarkus.args="greet --name=Quarkus"

In output, of course, you’ll see hello Quarkus.

Interaction with database

If you are lazy or just want to see the final result of this section execute git checkout feature/db_update and just look through the next steps.

Well, that was a kinda useful example. But real-world applications won’t do this kind of operation (or rarely). Real-world apps will do database-related tasks, such as run some statistics or update some values in db. Well, let’s put our greeting phrase into a database and use the command to update this. Let’s add some dependencies and properties to our project.

Firstly let’s add hibernate+panache and h2 jdbc driver into pom.xml

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>

And let’s create file src/main/resource/application.properties with next lines:

quarkus.datasource.db-kind=h2
quarkus.datasource.username=username-default
quarkus.datasource.jdbc.url=jdbc:h2:./test
quarkus.hibernate-orm.database.generation=update

Nice, now we have a h2 database ready for use. We only need to create our entity, I decided to create Setting entity with key and value(and Id of course), here's full class:

Nothing really special here, we interested in PanacheEntityBase to make this example easy for the article.

In the next step, we need to adapt our GreetingService to use our Setting class.

@ApplicationScoped
public class GreetingService {
public String greeting(String name) {
return Setting.find("key = 'greet_phrase'").<Setting>firstResult().value + name;
}
}

We gonna use greet_phrase key to get our greeting name. As I've said, we use Panache's methods to get data from the database. Now we're finally ready to create our new command. Here's full code for SetCommand:

This one is similar to our previous GreetingCommand, but it has some differences:

  1. We use @Paramteres annotation for input, they differ from @Options ability to use them without any hyphens. We also created enum TableType at the bottom of this class and use it as a first parameter to our command, picocli supports a super large amount of input type, and enum is one of them.
  2. We use @Transactional because we want to write to the database in our run() method. Without this method will fail. Because we use @Dependent annotation, transactional annotations will work.
  3. We switch between different TableTypes(in our case only one, so we use if), and if it’s SETTING we find or create a new Setting entity, update key and value, and persist it.

Last part that is left is to add few lines to our GreetingApplication. Let's firstly add inject of our SetCommand

@Inject
SetCommand setCommand;

And let’s just add it as a subcommand to our CommandLine

return new CommandLine(new QuarkusCommand())
.addSubcommand(greetingCommand)
.addSubcommand(setCommand)
.execute(args);

That’s it. Now we can modify our greeting phrase from the command line. I would say that doing it via the database will probably be easier. But if you’ll have some more logic or complex operations(e.g analyze some data by connecting to some external service, and then storing this data back to the database) similar commands will come in handy. Let’s try it out.

./mvnw compile quarkus:dev -Dquarkus.args="set SETTING greet_phrase 'Hello '"

This will set initial phrase, as it was before.

Now you can press Ctrl + C and execute ./mvnw quarkus:dev to start our dev server. With this let's curl into greeting endpoint(or you can execute our greet command as a faster option)

curl http://localhost:8080/hello/greeting/Quarkus

You’ll see the usual phrase Hello Quarkus. Now, let's stop our quarkus dev server and execute command with different value:

./mvnw compile quarkus:dev -Dquarkus.args="set SETTING greet_phrase 'Guten Tag '"

This time we changed greeting phrase to german Guten tag. Let’s re-run ./mvnw quarkus:dev and curl the same endpoint again.

curl http://localhost:8080/hello/greeting/Quarkus

This will output, as expected Guten Tag Quarkus. In a real app, you don't need to stop and restart the app to execute a command, but by default, H2 has a lock on the file, and so we can use only one access at a time.

In conclusion

Picocli is a good part of your app if you want to introduce a great command structure. Even though I have a cold heart for Laravel Php framework, I do want to say that their command support done really well.

When app is small, you probably won’t need such a feature, or you’ll go with super simple setup, however when you have a complex app that needs to do some logic that shouldn’t be exposed to API(and you don’t want to bloat every command with the main method) picocli is the right way to go. It might be also a good idea to disable resteasy so that you can run commands while the main server is running, this way you can schedule some commands if needed.

Have you ever used commands in your project? How can you compare it to Quarkus?

Originally published at https://quarkify.net on April 27, 2020.

--

--

Dmytro Chaban
Quarkify

Software Engineer, addicted to productivity and automatization