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(10) + 1;
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
Nice code.
ReplyDeleteAlso was surprised, when found out cool Console class, but found out that under IDE it doesn't work.
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?
ReplyDeleteMany Thanks!
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.
ReplyDeleteIt is very strange.
@Anonymous - System.console() returns null when no console device (TTY, cmd.exe, whatever) is present. I don't see what is strange about that.
ReplyDeleteThe 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.
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.
ReplyDeleteI want to try your example but I am getting "ConsoleException cannot be resolved to a type"
ReplyDeleteIt's in the repository
Delete