Blogs

Developing CLI application with Spring Shell (part 3)

Theme
Software development

Sooner or later every application had to perform an operation that could not be finished in an instant. In web applications we expect to be presented with a spinner (of one sort or another) during the execution of our requests, these prevents us from nervously pressing submit button again and again tell us that all is well and our request is being taken care of. On the other hand when we copy a large amount of files from one location to another we expect our OS GUI to provide us with some sort of progress bar displaying the progress of initiated operation. Similarly when we start a gradle build of our project we are presented with a progress bar and numeric information indicating percentage completion of our build.

In this post we will cover the topic of displaying a progress of command execution to a user with the use of counters, spinners and the progress bar similar to the one used by gradle build tool.

Implementing a simple spinner

Let us begin with implementing the simple spinner to be used by our clidemo application. As a first step, in the package com.ag04.clidemo.shellcreate a new ProgressCounter class:

package com.ag04.clidemo.shell;

import org.jline.terminal.Terminal;

public class ProgressCounter {
    private static final String CUU = "\u001B[A";
    
    private Terminal terminal;
    private char[] spinner = {'|', '/', '-', '\\'};

    private int spinCounter = 0;

    public ProgressCounter(Terminal terminal) {
        this(terminal, null);
    }

    public ProgressCounter(Terminal terminal, char[] spinner) {
        this.terminal = terminal;

        if (spinner != null) {
            this.spinner = spinner;
        }
    }

    public void display() {
        if (!started) {
            terminal.writer().println();
            started = true;
        }
        terminal.writer().println(CUU + "\r" + getSpinnerChar());
    }

    public void reset() {
        spinCounter = 0;
        started = false;
    }

    private char getSpinnerChar() {
        char spinChar = spinner[spinCounter];
        spinCounter++;
        if (spinCounter == spinner.length) {
            spinCounter = 0;
        }
        return spinChar;
    }

    //--- set / get methods ---------------------------------------------------

    public char[] getSpinner() {
        return spinner;
    }

    public void setSpinner(char[] spinner) {
        this.spinner = spinner;
    }
}

As can be seen, this class exposes two public methods:

public void display():
public void reset();

The first one displays the next state of the rotating spinner, while the second one resets the spinner to its initial state.

To use this new class, we also need to configure it by adding the following configuration at the end of the SpringShellConfig class:

@Bean
public ProgressCounter progressCounter(@Lazy Terminal terminal) {
    return new ProgressCounter(terminal);
}

To see it in action, we need to add another demo method in the existing echo command, as shown in the snippet bellow:

@ShellMethod("Displays progress spinner")
public void progressSpinner() throws InterruptedException {
    for (int i = 1; i <=100; i++) {
        progressCounter.display();
        Thread.sleep(100);
    }
    progressCounter.reset();
}

Now, rebuild, run clidemo and execute this new progress-spinnercommand. If all goes well you should see 10 seconds long rotating sequence of these characters:

| / -\

NOTE: At the beginning of the ProgressCounter class, you might have notice a strange property:

private static final String CUU = "\u001B[A";

this is control character sequences “ESC A” known also as “CUU” command sequence instructing a terminal to move cursor up by one row. For more on control character sequences please see the following link.

Implementing a counter with spinner

Now lets improve ProgressCounter class by adding another method with the following signature:

public void display(int count, String message);

This method will display spinner, and both current progress count and information message.

To do so, change the ProgressCounter class with the additions as displayed in the code bellow:

package com.ag04.clidemo.shell;

import org.jline.terminal.Terminal;

public class ProgressCounter {
    private static final String CUU = "\u001B[A";
    
    private Terminal terminal;
    private char[] spinner = {'|', '/', '-', '\\'};

    private String pattern = " %s: %d ";

    private int spinCounter = 0;

    public ProgressCounter(Terminal terminal) {
        this(terminal, null);
    }

    public ProgressCounter(Terminal terminal, String pattern) {
        this(terminal, pattern, null);
    }

    public ProgressCounter(Terminal terminal, String pattern, char[] spinner) {
        this.terminal = terminal;

        if (pattern != null) {
            this.pattern = pattern;
        }
        if (spinner != null) {
            this.spinner = spinner;
        }
    }

    public void display(int count, String message) {
        if (!started) {
            terminal.writer().println();
            started = true;
        }
        String progress = String.format(pattern, message, count);

        terminal.writer().println(CUU + "\r" + getSpinnerChar() + progress);
        terminal.flush();
    }

    public void display() {
        if (!started) {
            terminal.writer().println();
            started = true;
        }
        terminal.writer().println(CUU + "\r" + getSpinnerChar());
        terminal.flush();
    }

    public void reset() {
        spinCounter = 0;
        started = false;
    }

    private char getSpinnerChar() {
        char spinChar = spinner[spinCounter];
        spinCounter++;
        if (spinCounter == spinner.length) {
            spinCounter = 0;
        }
        return spinChar;
    }

    //--- set / get methods ---------------------------------------------------

    public char[] getSpinner() {
        return spinner;
    }

    public void setSpinner(char[] spinner) {
        this.spinner = spinner;
    }

    public String getPattern() {
        return pattern;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }
}
view raw

ProgressCounter class property “pattern” defines how counter and message are displayed, and it can be passed either through constructor or by use of setter method.

To test this new functionality, add another method to EchoCommand class, as shown bellow:

@ShellMethod("Displays progress counter (with spinner)")
public void progressCounter() throws InterruptedException {
    for (int i = 1; i <=100; i++) {
        progressCounter.display(i, "Processing");
        Thread.sleep(100);
    }
    progressCounter.reset();
}

The final output after running this new progress-counter command should be similar to the bellow:

CLI-DEMO:>progress-counter 
\ Processing: 100

This ProgressCounter implementation is best suited to keep the user informed of the progress in situations when we do not know the exact number of records/lines/entities that we need to process, that is when we are only aware of the current state of execution with no hindsight concerning the scope of the remaining work.

In the situations when we are aware of the scope of work ahead of us, more suited would be the use of the progress bar to convey the information on the current state of progress to the user. Therefore, as the last exercise in this post, we will create simple progress bar, similar to the one used by the popular gradle tool during build process:

<================----> 80%

Implementing a progress bar

We will begin with creation of a new ProgressBar class, with two public methods, similar as the ProgressCounter class:

public void display(int percentage);
public void reset();

The first method will be responsible for displaying progress bar that matches current state of execution as declared by percentage parameter passed to it, while the second one will be responsible for cleaning up and resetting the bar to its initial state.

In the package com.ag04.clidemo.shell create ProgressBar class with the following code:

package com.ag04.clidemo.shell;

import org.jline.terminal.Terminal;

public class ProgressBar {
    private static final String CUU = "\u001B[A";
    private static final String DL = "\u001B[1M";
    
    private String doneMarker = "=";
    private String remainsMarker = "-";
    private String leftDelimiter = "<";
    private String rightDelimiter = ">";

    ShellHelper shellHelper;

    public ProgressBar(ShellHelper shellHelper) {
        this.shellHelper = shellHelper;
    }

    public void display(int percentage) {
        if (!started) {
            started = true;
            shellHelper.getTerminal().writer().println();
        }
        int x = (percentage/5);
        int y = 20-x;

        String done = shellHelper.getSuccessMessage(new String(new char[x]).replace("\0", doneMarker));
        String remains = new String(new char[y]).replace("\0", remainsMarker);

        String progressBar = String.format("%s%s%s%s %d", leftDelimiter, done, remains, rightDelimiter, percentage);

        shellHelper.getTerminal().writer().println(CUU + "\r" + DL + progressBar + "% ");
        shellHelper.getTerminal().flush();
    }

    public void reset() {
        started = false;
    }

    //--- set / get methods ---------------------------------------------------
    ...
    
}

As usual, this should be followed by adding bean configuration block to the SpringShellConfig class:

@Bean
public ProgressBar progressBar(ShellHelper shellHelper) {
    return new ProgressBar(shellHelper);
}

Now, add new test method to the end of the echo command, as shown bellow, so we can see the new ProgressBar in action:

@ShellMethod("Displays progress bar")
public void progressBar() throws InterruptedException {
    for (int i = 1; i <=100; i++) {
        progressBar.display(i);
        Thread.sleep(100);
    }
    progressBar.reset();
}

Rebuilding clidemo application and running this new progress-barcommand should now result in the output similar to the one in the image bellow:

Progress bar in action.

If we would like our progress bar to reassemble the one used by gradle even more, we need to add the capability to pass optional “status message” parameter to the display() method. To do so, we need to expand the ProgressBar by adding a new display() method with the following signature:

display(int percentage, String statusMessage); 

Accordingly, we will change the ProgressBar class to match the code snippet bellow:

public void display(int percentage) {
  display(percentage, null);
}

public void display(int percentage, String statusMessage) {
  if (!started) {
      started = true;
      shellHelper.getTerminal().writer().println();
  }
  int x = (percentage/5);
  int y = 20-x;
  String message = ((statusMessage == null) ? "" : statusMessage);

  String done = shellHelper.getSuccessMessage(new String(new char[x]).replace("\0", doneMarker));
  String remains = new String(new char[y]).replace("\0", remainsMarker);

  String progressBar = String.format("%s%s%s%s %d", leftDelimiter, done, remains, rightDelimiter, percentage);

  shellHelper.getTerminal().writer().println(CUU + "\r" + DL + progressBar + "% " + message);
  shellHelper.getTerminal().flush();
}

With these additions we are able to enrich our progress bar with display of additional status message, for example with info on duration of execution so far, or something similar.

Putting it all together: displaying progress of UserService method

Testing our progress bar and progress counter components with echocommand is fine, but to make everything learned in this post so far less abstract we will close this post with the demonstration of progress bar component usage in a “real life” example.

“Real life” example: massive update of (all) users

Lets assume that UserService also defines the following method:

long updateAll();

This method is intended to synchronise and update all users in local database with some external source of data, and returns the number of updated users. Also, let us assume that there is a large number of users in our database and that this operation will take some time to finish.

For the purposes of this post we will modify MockUserService and add mock implementation of this method, one that will took time some to finish. Thus, add the method updateAll() to the UserService interface and add the following block of code to the MockUserService:

@Override
public long updateAll() {
    long numberOfUsers = 2000;
    for (long i = 1; i <= numberOfUsers; i++) {
        // do some operation ...
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return numberOfUsers;
}

Now modify UserCommand by adding a method (see bellow) that invokes this new UserService method:

@ShellMethod("Update and synchronize all users in local database with external source")
public void updateAllUsers() {
    shellHelper.printInfo("Starting local user db update");
    long numOfUsers = userService.updateAll();
    String successMessage = shellHelper.getSuccessMessage("SUCCESS >>");
    successMessage = successMessage + String.format(" Total of %d local db users updated!", numOfUsers);
    shellHelper.print(successMessage);
}

Rebuilding and running of clidemo update-all-users command will now produce the following output:

User update without progress bar.

As you could see, by running this example, after the first message “Starting local user db update” is printed out, we are left to wait until user update operation has finished. What we would like to achieve is to present user with the progress bar displaying the progress of user update operation.

Moreover we would like to achieve this in a least intrusive way, thus we will implement it with the use of Observer pattern.

Observer pattern in action

Wikipedia defines Observer pattern as follows:

“The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.”

In our case Observable object will be MockUserService class and we will also implement custom Observer class that will display operation progress at the terminal.

Detailed information concerning the Java support for the Observer pattern can be found on the following link. Our solution, one that follows bellow, is based on the use of Observer/Observable classes as described in the previously linked post.

First we will create new ProgressUpdateObserver class, one that will be responsible for updating user’s terminal based on the the progress information.

package com.ag04.clidemo.observer;

import com.ag04.clidemo.shell.ProgressBar;
import com.ag04.clidemo.shell.ShellHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Observable;
import java.util.Observer;

@Service
public class ProgressUpdateObserver implements Observer {

    @Autowired
    private ProgressBar progressBar;

    @Autowired
    private ShellHelper shellHelper;

    @Override
    public void update(Observable observable, Object event) {
        ProgressUpdateEvent upe = (ProgressUpdateEvent) event;
        int currentRecord = upe.getCurrentCount().intValue();
        int totalRecords = upe.getTotalCount().intValue();
        
        if (currentRecord == 0) {
            // just in case the previous progress bar was interrupted
            progressBar.reset();
        }

        String message = null;
        int percentage = currentRecord * 100 / totalRecords;
        if (StringUtils.hasText(upe.getMessage())) {
            message = shellHelper.getWarningMessage(upe.getMessage());
            progressBar.display(percentage, message);
        }

        progressBar.display(percentage, message);
        if (percentage == 100) {
            progressBar.reset();
        }
    }
}
view raw

In the same package we will now create ProgressUpdateEvent class that this Observer reacts on.

package com.ag04.clidemo.observer;

public class ProgressUpdateEvent {

    Long currentCount;
    Long totalCount;
    String message;

    public ProgressUpdateEvent(Long currentRecord, Long totalRecords) {
        this(currentRecord, totalRecords, null);
    }

    public ProgressUpdateEvent(Long currentRecord, Long totalRecords, String message) {
        this.currentCount = currentRecord;
        this.totalCount = totalRecords;
        this.message = message;
    }

    //--- set / get methods ---------------------------------------------------

    public Long getCurrentCount() {
        return currentCount;
    }

    public void setCurrentCount(Long currentCount) {
        this.currentCount = currentCount;
    }

    public Long getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(Long totalCount) {
        this.totalCount = totalCount;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
    
}
view raw

What remains to be done is to modify MockUserService so that it fires up this event as the update operation progresses. Modify MockUserServiceclass in accordance with the code bellow, add Observer property, modify updateAll() method and do not forget to remove @Service annotation, since we will configure this class later manually.

package com.ag04.clidemo.service;

import com.ag04.clidemo.model.CliUser;
import com.ag04.clidemo.observer.ProgressUpdateEvent;
import org.springframework.stereotype.Service;

import java.util.Observable;
import java.util.Observer;

/**
 * Mock implementation of UserService.
 *
 */
public class MockUserService extends Observable implements UserService {

    private Observer observer;

    @Override
    public boolean exists(String username) {
        if ("admin".equals(username)) {
            return true;
        }
        return false;
    }

    @Override
    public CliUser create(CliUser user) {
        user.setId(10000L);
        return user;
    }

    @Override
    public CliUser update(CliUser user) {
        return user;
    }

    @Override
    public long updateAll() {
        long numberOfUsers = 2000;
        for (long i = 1; i <= numberOfUsers; i++) {
            // do some operation ...
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // notify observer of the change
            if (observer != null) {
                String message = "";
                if (i < numberOfUsers) {
                    message = ":: please WAIT update operation in progress";
                }
                observer.update(
                        this,
                        new ProgressUpdateEvent(i, numberOfUsers, message)
                );
            }
        }
        return numberOfUsers;
    }

    //--- util methods --------------------------------------------------------

    public Observer getObserver() {
        return observer;
    }

    public void setObserver(Observer observer) {
        this.observer = observer;
    }
}

Finally, add UserServiceConfig class which contains method to create and configure MockUserService bean as shown in the code snippet bellow:

package com.ag04.clidemo.config;

import com.ag04.clidemo.observer.ProgressUpdateObserver;
import com.ag04.clidemo.service.MockUserService;
import com.ag04.clidemo.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserServiceConfig {

    @Bean
    public UserService userService(ProgressUpdateObserver observer) {
        MockUserService userService = new MockUserService();
        userService.setObserver(observer);
        return userService;
    }

}

Whit this we are all set! Running clidemo update-all-user command now, should produce the output as shown in the picture bellow:

update-all-users command final output

This concludes the third part of this series. In the next part we will focus on advanced built-in Spring Shell support for data display as tables within terminal.


Rest of the series:

Part 1: Conveying contextual messages to the users in the CLI application

Part 2: Capturing user’s input in the CLI application

Part 3: Displaying the progress of CLI command execution with the use of counters, spinners and progress bars

Part 4: Displaying the data with the use of tables in a Spring Shell based CLI application

Part 5: Securing CLI application with Spring Security

Sample Code:

Entire source code for this tutorial is available at GitHub repository:

https://github.com/dmadunic/clidemo

Additional resources:

Spring Shell project site:

https://projects.spring.io/spring-shell/

Spring Shell official documentation:

https://docs.spring.io/spring-shell/docs/current-SNAPSHOT/reference/htmlsingle/
https://docs.spring.io/spring-shell/docs/current/api/

JLine project site:

[https://jline.github.io/](https://jline.github.io/)

Good overview of the Terminal control sequences:

https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences

Thank you for reading! I do hope you enjoyed it and please share if you did.

Next

Blog

Developing CLI application with Spring Shell (part 2)

Company

Our people really love it here

How it all started

Est. in 2014., gathering eight employees with eyes set on the future. No matter how set they were, they couldn’t predict the success and extent of growth that would ensue. Today there are more than 100 of us, and people are here to stay.

Stability in unstable times

The turmoil of 2020 caused great inconvenience for people all over the world. However, this did not affect our business. Quite the opposite — we not only kept all jobs and salaries intact, but we also grew in size. And we keep expanding. 

Contact

We’d love to hear from you