Skip to main content

A Console Terminal for JARs

January 31, 2006

{cs.r.title}









Contents
Of Tyrannosaurus-Rex and Standard I/O
   Classic Standard I/O
   Back to the Future: Standard I/O in Java
Under the Hood of System.out
The Design of Jar-Stdio-Terminal
   The OutStrmAdapter Class
   The InStrmAdapter Class
   Jar-Stdio-Terminal Application Lifecycle
   Initial Setup
   Fetching Configuration Data
   Application Launch
The User Guide
Mustang and the java.io.Console Class
Conclusion
Resources

If you are bleary-eyed from a night vainly spent trying to make System.out.println() work in a JAR and want answers now, skip to the User Guide section of this article. But even if you never have to make a JAR spring to life with a double-click, read on to understand why, and how, System.out and the other standard I/O files do different things in different environments, and what Mustang's new java.io.Console class brings to the table.

A JAR is a great way of packaging and deploying a Java SE project. But there is a problem with console input/output--the standard I/O files (System.out, System.err, and System.in) do not work when a JAR is activated by a double-click. System.out and System.err are simple, intuitive, and frequently used for problem reporting, and their loss can be quite inconvenient.

This article describes an open source project, a-jar-stdio-terminal, that provides console capability to such JARs. It does not require any change to the existing application code, and can, therefore, even be retrofitted onto existing JARs to magically restore lost console capabilities.

Of Tyrannosaurus-Rex and Standard I/O

Many of you have probably never touched, except perhaps in a museum, anything like the machine in Figure 1, a teletype. In the early days of UNIX and C, computers were big, expensive, and often housed in basements--almost always far away from users, who had to access them from teletypes connected via serial lines that carried only ASCII text. Input to the computer was hammered out on a clunky keyboard, while the computer's output was noisily printed out on a roll of paper.

A Teletype Unit
Figure 1. A teletype unit

What kept the users happy, despite the equipment, was a universal interaction model based on something called standard input/output, as described in Kernighan and Pike's book The UNIX Programming Environment. This model used three unidirectional logical links called stdin (standard input), stdout (standard output), and stderr (standard error) for the exchange of information between program and user.

Classic Standard I/O

The terms stdin, stdout, and stderr are actually the names of predefined global variables that can be used in any C program to read input from the user's keyboard, and print results to his or her screen, like this:

float num, sqrRoot;
int i;
for (i = 0; i < 5; ++i) {
   /* get number from stdin into num */
   fscanf(stdin, "%f", &num);
   if (f >= 0)
      fprintf(stdout, "sqrt(%f) is %f \n",
            num, sqrt(num));
   else
      fprintf(stderr, "Cant get sqrt! (%f) \n",
            num);
}

Standard I/O routes the data to and from the desired end point, which could be a specific terminal, file, or other device. There are also mechanisms that allow the program itself or an external shell program to change the path of the data at run time. We ignore these details for now, but will discuss equivalent Java functionality used in the design of Jar-Stdio-Terminal.

Back to the Future: Standard I/O in Java

There are no global variables in Java, so the designers chose the next best option. They equipped java.lang.System with three public static members called in, out, and err that reference the standard I/O streams. The public static declaration makes them visible from anywhere in a Java program, and the Java runtime ensures that they refer to usable I/O streams before user code starts running (not always, obviously, but read on anyway). So here is how you would write the same code in Java:

float num, sqrRoot;
int i;
for (i = 0; i < 5; ++i) {
   num = getInt(System.in);
   if (f >= 0)
      System.out.printf("sqrt(%f) is %f \n",
            num, sqrt(num));
   else
      System.err.printf("Cant get sqrt! (%f) \n",
            num);
}

Java programs almost never use System.in, but System.out and System.err are common. Wasteful effort, you may think, given that there is no knowing which shore the bottle with the message will wash up on, and whether the message will ever be found and be read. But let's try to get to the bottom of the mystery. Why do messages sent via System.out and System.err sometimes get lost in transit?

In the following section we explore the data path from (and to) a program through Java's standard I/O infrastructure. Everything that is said about System.out applies to System.err too. Although System.in sends data in the opposite direction, the design must be based on equivalent principles.

Under the Hood of System.out

The Java Platform API specification is usually a good place to begin any Java research, and here is the opening line of the description of java.io.PrintStream, which is the type of the System.out variable:

A PrintStream adds functionality to another output stream, namely the ability to print representations of various data values conveniently

Another output stream refers to the OutputStream argument passed to PrintStream's constructors. This is the adapter pattern (c.f., the GoF's book Design Patterns) at work. OutputStream performs the real job of sending output to a storage, display, or network device. But its interface is too low-level for mainstream Java developers, and so must be adapted into a more user-friendly PrintStream.

The sequence diagram in Figure 2 captures what we've found so far. When the user code executes (at "1") any of the print methods (print(), println(), or printf()), the PrintStream object converts the argument into text (at "2") and passes it as a stream of bytes to the hidden OutputStream object. The OutputStream (at "3") then proceeds to deliver the bytes to the appropriate device.

Data path through System.out
Figure 2. Data path through System.out

But the trail seems to go cold at "3." OutputStream is an abstract class, and does not specify what it does with the data passed to it. But isn't this is a clue to why we see different behaviors in different environments? The disposal of the data must be implemented differently by the various subclasses of OutputStream used in these environments.

Closer examination of java.lang.System reveals more clues. The object assigned to System.out is not wired in, but can be substituted with another object by invoking System.setOut(OutputStream). This means that disposal of data passed to System.out's print...() methods can be customized as follows:

  1. Subclass OutputStream, implementing the required behavior in its write() methods.
  2. Create an instance of PrintStream, passing an instance of the OutputStream subclass created in step 1 to PrintStream's constructor.
  3. Pass the PrintStream instance created in step 2 to System.setOut().

The procedure described above does not affect the user code, since it only sees the PrintStream interface exposed by System.out, but allows the disposal of the formatted data to be customized.







The Design of Jar-Stdio-Terminal

The class diagram in Figure 3 illustrates the overall structure of Jar-Stdio-Terminal. There are three custom classes: StdioTerm, InStrmAdapter, and OutStrmAdapter. OutStrmAdapter is a subclass of OutputStream, and its purpose is to divert output and error data away from their original (JVM-determined) destinations, and into Jar-Stdio-Terminal's synthetic console (described below). The InStrmAdapter is a subclass of InputStream, and its purpose, likewise, is to shunt System.in's supply chain away from the original source and connect it into the synthetic console. Observe that StdioTerm uses two instances of OutputStream (for System.out and System.err). The only difference between the two OutStrmAdapter instances is the color used for rendering the output (green and orange, respectively).

Class diagram
Figure 3. Class diagram

The StdioTerm class is the owner of all of these resources, and also owns a JFrame and a JTextPane. The JTextPane is contained within the JFrame, and they together constitute the synthetic console window. The synthetic console is not a console in a true operating-system sense. However, functionally and visually it is a more than adequate substitute. All of the code for one-time activities--setup, application launch, shutdown--is in the StdioTerm class. This class also implements ActionListener and KeyListener to support a couple of buttons and the keyboard.

This article does not contain all of Jar-Stdio-Terminal's code, since much of it is typical Swing programming. However, all of the code that gives Jar-Stdio-Terminal its character is shown and explained here. All of the source code is, however, available at the a-jar-stdio-terminal project page.

The OutStrmAdapter Class
OutStrmAdapter is a custom subclass of java.io.OutputStream. Instances of this class are passed to the constructor of java.io.PrintStream to create instances that will handle the supported application's System.out and System.err. This subclass implements the two flavors of write() to send the application's output (or error) stream to Jar-Stdio-Terminal's synthetic console window.

package net.java.dev.a_jar_stdio_terminal;
import java.io.*;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;

public class OutStrmAdapter extends OutputStream {
  public OutStrmAdapter(StdioTerm owner,
      SimpleAttributeSet textAttr) {
    this.owner = owner;
    theDocument = owner.theDocument;
    this.textAttr = textAttr;
  }
  public void write(byte b[], int off, int len)
      throws IOException {
    try {
      theDocument.insertString(
          theDocument.getLength(),
          new String(b, off, len), textAttr);
    } catch (Exception e) {/* Not expected */}
    owner.configureDisplay();
  }
  public void write(int c) throws IOException {
    tempChar[0] = (byte)c;
    try {
      theDocument.insertString(
          theDocument.getLength(),
          new String(tempChar), textAttr);
    } catch (Exception e) {/* Not expected */}
    owner.configureDisplay();
  }
  private byte tempChar[] = new byte[1];
  private Document theDocument;
  private SimpleAttributeSet textAttr;
  private StdioTerm owner;
}

The two instances of this class used will differ only in the color they use to render text: green and orange for System.out and System.err, respectively. The instances are created in the init() method (not shown in this article, but available with the project source), and the desired text attribute is passed as a parameter to the constructor there.

The InStrmAdapter Class
InStrmAdapter is a custom subclass of java.io.InputStream, needed to function as the supported application's System.in. It implements two flavors of read() to satisfy the application's input needs with characters harvested by a KeyListener attached to the synthetic console window. The instance used is created in the init() method, and the desired text attribute (white foreground) is passed to the constructor there.

package net.java.dev.a_jar_stdio_terminal;
import java.io.*;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;

public class InStrmAdapter extends InputStream {
  public InStrmAdapter(StdioTerm owner,
      SimpleAttributeSet textAttr) {
    this.owner = owner;
    theDocument = owner.theDocument;
    this.textAttr = textAttr;
  }
  public int read() throws IOException {
    if (owner.inputEofSeen)
      return -1;
    if (owner.inputBuffer.isEmpty())
      owner.getUserInput();
    return owner.inputBuffer.remove(0);
  }
  public int read(byte b[], int off, int len)
      throws IOException {
    if (owner.inputEofSeen)
      return -1;
    if (owner.inputBuffer.isEmpty())
      owner.getUserInput();
    int dataLength = Math.min(len,
        owner.inputBuffer.size());
    for (int i = 0; i < dataLength; ++i)
      b[off + i] = owner.inputBuffer.remove(0);
    return dataLength;
  }
  private byte tempChar[] = new byte[1];
  private Document theDocument;
  private SimpleAttributeSet textAttr;
  private StdioTerm owner;
}
InStrmAdapter takes characters off of the end of a Vector which is used as a queue between itself and the main class StdioTerm, which implements KeyListener. This is arguably inefficient, but rugged and simple. It uses the services of a helper method, getUserInput(), to manage synchronization and block type-ahead.

Jar-Stdio-Terminal Application Lifecycle

When you double-click a JAR that contains an application using Jar-Stdio-Terminal, much happens before the real application is activated. All of these actions are kicked off by StdioTerm.main(), whose code is shown below:

public static void main(String args[]) {
  attach(null);
  getStdioNames();
  theFrame.setTitle(stdioTerminalTitle);
  if (stdioClassName == null)
    return;
  try {
    Class stdioClass = Class.forName(
        stdioClassName);
    Method main = stdioClass.getDeclaredMethod(
        "main", new Class[]
        {java.lang.String[].class});
    main.invoke(null, new Object[]
        {new String[]{}});
    detach();
  } catch (Exception e) {
    System.err.printf("Cant use "%s"\n",
        stdioClassName);
    if (e instanceof InvocationTargetException) {
      e.getCause().printStackTrace(System.err);
    } else
      System.err.println(e);
  }
}
StdioTerm.main() must run before the supported application, in order to change the run-time environment before be application starts. This also enables Jar-Stdio-Terminal to be run without changing the application itself. To get StdioTerm.main() to run first, the Main-Class entry in the manifest file must point to net.java.dev.a_jar_stdio_terminal.StdioTerm.

Initial Setup

The method attach(), listed below, is called first. The StdioTerm instance is created here. The StdioTerm constructor then completes all environment setup actions, including creating instances of JFrame and JTextPane and the three objects to be used for System.in, System.out, and System.err. attach() then switches the values assigned to System.in, System.out, and System.err.

public static void attach(String title) {
  if (me == null) {
    me = new StdioTerm(title == null ?
        stdioTerminalTitle : title);
    oldOut = System.out;
    oldErr = System.err;
    oldIn = System.in;
    System.setIn(in);
    System.setOut(out);
    System.setErr(err);
  }
}

The public static declaration of System.in, System.out, and System.err make the effect of executing attach() applicable system-wide. So even the supported application will use these values of System.in, System.out, and System.err.

There should never be more than one console window, so, although this is not the classic singleton pattern, the code protects against the possibility of this error.

Fetching Configuration Data

After the initial setup actions have been completed and a functional console window has been deployed, StdioTerm.main() needs to fetch the configuration information. This is done by calling getStdioNames() shown below:

private static void getStdioNames() {
  InputStream nameStream = me.getClass().
      getResourceAsStream("StdioConfig.txt");
  if (nameStream == null) {
    System.err.println("No StdioConfig.txt");
    return;
  }
    byte nameBytes[] = null;
  try {
    nameBytes = new byte[nameStream.available()];
    nameStream.read(nameBytes);
  } catch (Throwable t) {
    System.err.println("JarStdioTerminal.
        getStdioNames(): " + t);
    return;
  }
  String names = new String(nameBytes);
  int end = names.indexOf('\n');
  if (end == -1) {
    stdioClassName = names.trim();
  } else {
    stdioClassName =
        names.substring(0, end).trim();
    if (names.length() > end + 1)
      stdioTerminalTitle =
          names.substring(end + 1).trim();
  }
}

Jar-Stdio-Terminal needs the name of the supported application's Main-Class, and the title of the console terminal's window. These are expected to be in the file StdioConfig.txt in the same directory as Jar-Stdio-Terminal's class files. Observe that the console window title is optional--the default value is JAR-STDIO Terminal.

Application Launch

If getStdioNames() fails to find configuration data, either because StdioConfig.txt was not found or it did not contain data in the required format, StdioTerm.main() returns, leaving the console window intact with the error messages. However, if at least the name of the application's main class was found, StdioTerm.main() uses classic techniques based on Java reflection to activate the application's main().

StdioTerm.main() therefore remains on the activation stack as long as the supported application is running. Control returns from Method.invoke() only if the supported application's main() returns, which never happens if the application's code calls System.exit(). If control does return from Method.invoke(), cleanup is performed by calling detach():

public static void detach() {
  if (me != null) {
    System.setOut(oldOut);
    System.setErr(oldErr);
    System.setIn(oldIn);
    me = null;
    theFrame.dispose();
  }
}
detach() cleans up by restoring the original values of System.in, System.out, and System.err. It then removes the terminal window by calling dispose() on the JFrame.







The User Guide

  1. Download the distribution: JarStdioTerminal.zip, from the a-jar-stdio-terminal project.
  2. Un-jar the distribution file, JarStdioTerminal.jar, into the root of the directory structure containing all of the .class and resource files of your application. This should create the directory net/java/dev/a_jar_stdio_terminal with the following four files: StdioTerm.class, OutStrmAdapter.class, InStrmAdapter.class, and StdioConfig.txt.
  3. In the manifest file used by the jar command, use the following line as the declaration of the main class: Main-Class: net.java.dev.a_jar_stdio_terminal.StdioTerm

    This is required to set up the standard I/O terminal before the application class starts up. StdioTerm will then start up the application class as described below.

  4. Edit the file net/java/dev/a_jar_stdio_terminal/StdioConfig.txt, and replace the information in it with two lines as follows:
    • The fully qualified name of the application class, the one that would have been specified as the main class if Jar-Stdio-Terminal was not being used.
    • The desired title for the terminal's frame

    The first line is mandatory, while the second line is optional. Note that the distribution should already contain a file of this name in the correct directory, since it also serves as a demo.

  5. Use the jar -cmf ... command as usual to wrap up all the files into one JAR file and you are ready to fly.

Mustang and the java.io.Console Class

Early information about Java's next release tells us that code like the following will be possible:

Console cons;
char[] passwd;
if ((cons = System.console()) != null &&
  (passwd = cons.readPassword("[%s]", "Password:")) != null) {
    ...
  java.util.Arrays.fill(passwd, ' ');
}

The new java.io.Console class has a bunch of convenience functions that make it easier to read from and write to Console objects. But the only really new feature is the readPassword() method. The readPassword()s (there are two flavors) hide what the user is typing in, a feature that has been in demand for a long time. Also new is the System.console() method. It is interesting that it institutionalizes the fact that a console may not always be available. So we may presume that System.console() will in fact return null in a JAR activated by a double-click. Jar-Stdio-Terminal could possibly be used in some of those situations.

Conclusion

The Jar-Stdio-Terminal project is a solution for projects with console I/O problems. The current version only addresses JAR files activated by a double-click, but the approach could be applied to other situations as well. The console user community also has a number of other needs such as VT100 simulation, curses, kbhit(), etc., and Jar-Stdio-Terminal could serve as a vehicle to deliver them.

Resources

width="1" height="1" border="0" alt=" " />
Sanjay Dasgupta has been using Java for telecom applications since 1996 (after many years of using many different languages in many industries).
Related Topics >> Programming   |