Sunday, 19 September 2010

Java: System.console(), IDEs and testing

The method System.console() can return null if there is no console device present. This comes as a surprise to people when they run their code in an IDE. This post is about overcoming such problems.

Is it worth using java.io.Console at all?

The Console class in Java 6 has the following constraints:

  • It is final, so you can't provide your own implementation directly
  • There is no service provider interface (SPI), so it is difficult to provide your own implementation indirectly

People managed to get data to and from the console before Java 6 came along. So, should your application just use System.in and System.out instead?

The Console implementation offers a couple of advantages over the standard I/O streams:

  • Methods for reading passwords without echoing the data to the terminal
  • Better character encoding integration (on Windows, it will emit data using the set console encoding in cmd.exe whereas System.out uses the default ANSI encoding)

An alternative might be a third party framework designed for interacting with terminals. This post sticks to the standard library.

Handling java.io.Console

The best way to handle System.console() returning null is to not let your application interact with the Console type directly. Instead, your code should rely on an abstraction layer of your own devising. Here's an example:

/**
 * Abstraction representing a text input/output device.
 
 @author McDowell
 */
public abstract class TextDevice {
  public abstract TextDevice printf(String fmt, Object... params)
      throws ConsoleException;

  public abstract String readLine() throws ConsoleException;

  public abstract char[] readPassword() throws ConsoleException;

  public abstract Reader reader() throws ConsoleException;

  public abstract PrintWriter writer() throws ConsoleException;
}

By using such a type, it is possible to return a working implementation supported by your environment:

  private static TextDevice DEFAULT = (System.console() == null? streamDevice(
      System.in, System.out)
      new ConsoleDevice(System.console());

  public static TextDevice defaultTextDevice() {
    return DEFAULT;
  }

  public static TextDevice streamDevice(InputStream in, OutputStream out) {
    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
    PrintWriter writer = new PrintWriter(out, true);
    return new CharacterDevice(reader, writer);
  }

Testing and the console

Defining your own abstraction layer doesn't just mean you can run your code from Eclipse/NetBeans/whatever. It lets you mock/fake/stub code for automated testing. Consider this simple number-guessing game:

/**
 * A simple text-entry game.
 
 @author McDowell
 */
public class GuessNumberGame {
  private final TextDevice io;
  private final NumberSource numbers;

  public GuessNumberGame(NumberSource numbers, TextDevice io) {
    this.numbers = numbers;
    this.io = io;
  }

  public void play() {
    int max = 10;
    int n = numbers.random(101;
    io.printf("Guess a number between 1 and %d%n", max);
    for (int i = 0;; i++) {
      int guess = readNumber();
      if (guess > n) {
        io.printf("Lower!%n");
      else if (guess < n) {
        io.printf("Higher!%n");
      else {
        io.printf("Won in %d moves%n", i);
        break;
      }
    }
  }

  private int readNumber() {
    while (true) {
      try {
        return Integer.parseInt(io.readLine());
      catch (NumberFormatException e) {}
    }
  }

  public static void main(String[] args) {
    final Random ran = new Random();
    NumberSource numbers = new NumberSource() {
      @Override
      public int random(int max) {
        return ran.nextInt(max);
      }
    };
    new GuessNumberGame(numbers, TextDevices.defaultTextDevice()).play();
  }

  public static interface NumberSource {
    public int random(int max);
  }
}

Because the code doesn't rely on the console directly, we can write automated tests to make sure it works:

  /**
   * JUnit4 test
   */
  @Test public void testGame() {
    final int answer = 4;
    final String input = "1\n9\n2\n4\n";

    NumberSource numbers = new NumberSource() {
      @Override
      public int random(int max) {
        return answer - 1;
      }
    };
    BufferedReader reader = new BufferedReader(new StringReader(input));
    TextDevice fake = TextDevices.characterDevice(reader, new PrintWriter(
        System.out, true));

    GuessNumberGame game = new GuessNumberGame(numbers, fake);
    game.play();
  }

End notes

All the sources are available in a public Subversion repository.

Repository: http://illegalargumentexception.googlecode.com/svn/trunk/code/java/
License: MIT
Project: AbstractingTheJavaConsole

Related: Java: Unicode on the Windows command line

7 comments:

  1. Nice code.
    Also was surprised, when found out cool Console class, but found out that under IDE it doesn't work.

    ReplyDelete
  2. Can anyone explain why abstraction layer works? What is the philosophy? Because I found that I could not sometimes run from command line either, but this trick does work. Just don't know why. Maybe related to JVM?

    Many Thanks!

    ReplyDelete
  3. But it is also strange that if don't use abstract layer, just add some code when System.console==null, that will also work by somehow making sure a non-null console exist.

    It is very strange.

    ReplyDelete
  4. @Anonymous - System.console() returns null when no console device (TTY, cmd.exe, whatever) is present. I don't see what is strange about that.

    The above code handles this case and substitutes an alternative (that uses System.out/System.in).

    The abstraction layer allows your application to work without every I/O call having to perform special checks for exactly what the code is talking to. This is a common pattern in software development.

    ReplyDelete
  5. Thanks for reply. The reason I asked is because by reading this article I thought it was the abstraction layer that plays the trick of making sure non-null System.console(). But what it really works is the substitution using System.in/out when the console is null. And fortunately, usually when console is null (not called from command line but called by another program as a background process), password masking is not needed. Now I found that abstraction just could not make sure the console is not null. Abstraction is not necessary for that purpose.

    ReplyDelete
  6. I want to try your example but I am getting "ConsoleException cannot be resolved to a type"

    ReplyDelete

All comments are moderated