JTF > Rationale > The acm.io Package

Chapter 4
The acm.io Package


The survey of problems presented in Chapter 3 notes that the problem most often cited by those attempting to teach Java to novices is the lack of a simple input mechanism, either through a traditional text-stream interface or a more modern dialog-based approach. Although this problem—identified as A1 in the taxonomy—is partly addressed by the introduction of the Scanner class in Java 5.0 and the JOptionPane class in Swing, the Task Force concluded that it would be worth introducing a package that offered an integrated approach to user input that would provide both traditional and dialog-based input options in a simple, consistent way. In the Java Task Force release, this capability is provided by the acm.io package, which contains two public classes:

  1. An IOConsole class that supports traditional text-based interaction within the standard Java window-system hierarchy.

  2. An IODialog class that offers similar functionality to the JOptionPane class in a significantly simplified form.

In addition to these classes, the acm.io package defines an IOModel interface that specifies the input and output operations common to IOConsole and IODialog. The sections that follow describe the two public classes in more detail.

4.1 Console-based I/O

One of the most common strategies used by Java textbook authors to address the problem of user input is to create an IOConsole class that provides interactive text-based I/O within the framework of the standard Java graphical APIs. As examples, the Java textbooks published by Holt Software Associates [Hume00] and the web-based textbook written by David Eck [Eck02] define their own classes that provide this functionality. Similar packages not linked to textbooks have also been proposed, including those developed by Ron Poet [Poet00] and Eric Roberts [Roberts04d].

While the large number of existing implementations clearly indicate that an IOConsole class enjoys a certain popularity in the community, its inclusion in the JTF packages has generated a certain amount of controversy. The principal objection to having such a class is that the underlying computational paradigm is not representative of the event-driven, interactive user interfaces for which the object-oriented paradigm is so well suited. In the papers surveyed for the problem taxonomy presented in Chapter 3, several authors argue that Java programs should avoid such traditional modes of interaction and instead make use of dialogs that fit more closely with modern paradigms of interactive programming. After extensive discussion, the Java Task Force decided that the acm.io package should support both console- and dialog-based I/O, but only if the two models could implement a single interface, which would allow easy transition from one paradigm to the other. Our reasons for retaining the console-based model include:

As a simple illustration, the code shown in Figure 4-1 shows the Java code necessary to create a Swing application that adds two numbers using the IOConsole class. The following screen snapshot shows how this program appears when running on a Macintosh under Mac OS X:

Figure 4-1. Swing application to add two numbers
/*
 * File: Add2Application.java
 * --------------------------
 * This program adds two numbers and prints their sum.  This version
 * runs as a Java application without using the acm.program package.
 */
 
import acm.io.*;
import java.awt.*;
import javax.swing.*;
 
public class Add2Application {
   public static void main(String[] argv) {
      JFrame frame = new JFrame("Add2Application");
      IOConsole console = new IOConsole();
      frame.getContentPane().add(BorderLayout.CENTER, console);
      frame.setSize(500, 300);
      frame.show();
      console.println("This program adds two numbers.");
      int n1 = console.readInt("Enter n1: ");
      int n2 = console.readInt("Enter n2: ");
      int total = n1 + n2;
      console.println("The total is " + total + ".");
   }
} 

The code in Figure 4-1 can be simplified substantially by using the ConsoleProgram class described in Chapter 6. The intent of this example is to illustrate the use of IOConsole as a standalone tool.

The public methods in the IOConsole class are shown in Figure 4-2. The only new methods in the class added since the first release are the versions of readInt and readDouble that include a range specification. These methods were added in response to the following comment on the JTF web forum by Alyce Brady:

I was wondering . . . whether the Task Force also considered (and possibly rejected) the possibility of straightforward range checking for int and double types? This is easy to support if both the lower- and upper-bounds are required—just provide an additional method for each that takes the two bounds as parameters. This is much cleaner than requiring the students to do range-checking within a loop. Of course, they should eventually be able to do this, but to provide range-checking would seem to be consistent with the type-checking that is already being done. [Brady05]

Given that IOConsole derives its inspiration from the familiar paradigm of text-based, synchronous interaction, the design of the class is relatively straightforward. Even though the underlying paradigm is familiar, there are nonetheless several important features of the IOConsole class that are worth highlighting:

Figure 4-2. Public methods in the IOConsole class
Constructor
 
IOConsole()
  Creates a new console object, which is a lightweight component capable of text I/O.

 Output methods
 
void print(any value)
  Writes the value to the console with no terminating newline.
void println(any value)
  Writes the value to the console followed by a newline.
void println()
  Returns the cursor on the console to the beginning of the next line.
void showErrorMessage(String msg)
  Displays an error message on the console.

 Input methods
 
String readLine()  or  readLine(String prompt)
  Reads and returns a line of text from the console without the terminating newline.
int readInt()  or  readInt(int min, int max)  or
    readInt(String prompt)  or  readInt(String prompt, int min, int max)
  Reads and returns an int value, with optional specification of a prompt and limits.
double readDouble()  or  readDouble(double min, double max)  or
       readDouble(String prompt)  or
       readDouble(String prompt, double min, double max) 
  Reads and returns a double value, with optional specification of a prompt and limits.
boolean readBoolean()  or  readBoolean(String prompt)
  Reads and returns a boolean value (true or false) from the console.
boolean readBoolean(String prompt, String trueLabel, String falseLabel)
  Reads a boolean value by matching the user input against the specified labels.

 Additional methods (most of which are unlikely to be used by novices)
 
void setFont(Font font)  or  void setFont(String str)
  Sets the overall font for the console, which may also be specified as a string.
void setInputStyle(int style)  and  int getInputStyle()
  Sets (or retrieves) the font style for user input, which can be different from the output style.
void setInputColor(Color color)  and  int getInputStyle()
  Sets (or retrieves) the font color for user input, which can be different from the output color.
void setErrorStyle(int style)  and  int getErrorStyle()
  Sets (or retrieves) the font style for error messages.
void setErrorColor(Color color)  and  int getErrorStyle()
  Sets (or retrieves) the font color for error messages.
void setExceptionOnError(boolean flag)
  Sets the error-handling mode: false means retry on error, true means raise an exception.
boolean getExceptionOnError()
  Returns the error-handling mode, as defined in setExceptionOnError.
PrintWriter getWriter()
  Returns a writer that can be used to write to the console (analogous to System.out).
BufferedReader getReader()
  Returns a reader that can be used to read from the console (analogous to System.in).
void setInputScript(BufferedReader rd)
  Sets the console so that it reads its input from the specified reader rather than the user.
BufferedReader getInputScript()
  Returns the current input script from which the console is reading.
void clear()
  Clears the console screen.
void save(Writer wr)
  Saves the contents of the console to the specified writer.
void print(PrintJob pj)
  Prints the contents of the console to the printer as specified by pj.
   

4.2 Dialog-based I/O

The IOConsole class—useful though it is—does not in any sense constitute a complete solution to the user-input problem. Most instructors are looking for some way to support dialog-based I/O that is simple enough for students to use. As noted in the discussion of this problem in Chapter 3, several mechanisms have been proposed to address this problem, including the Java Power Tools project developed at Northeastern [Raab00, Rasala00] and the simpleIO package developed by Ursula Wolz and Elliot Koffman [Wolz99].

The standard Java APIs, of course, include many methods for constructing user dialogs. In fact, since Swing was introduced as part of JDK 1.2, the Java APIs have included the JOptionPane class that makes it possible to put up simple dialogs to request basic data types from the users. Many instructors have used JOptionPane successfully, and it appears to be the most common paradigm for this style of user interaction.

Unfortunately, the JOptionPane mechanism has several weaknesses that make it problematic for novice users. Of these deficiencies, the most serious are the following:

The IODialog class in the acm.io package is designed to solve each of these problems. The customary paradigm is to create an IODialog object and then invoke methods on that object to display information, request input, or report errors. As with JOptionPane (which is used internally in the implementation as long as Swing is available), these three styles of use generate slightly different dialog formats, as shown in Figure 4-4.

Figure 4-4. The three styles of IODialog interactors
  

The code that produces this series of dialogs looks like this:

 

IODialog dialog = new IODialog();
dialog.showMessage("Process complete");
int n = dialog.readInt("Enter an integer: ");
dialog.showErrorMessage("Illegal operation");

The public methods in the IODialog class are shown in Figure 4-5. As the small number of entries in the table makes clear, the IODialog class is vastly simpler than the JOptionPane facility and should prove much easier for students to use.

Figure 4-5. Public methods in the IODialog class
 Constructor
 
IODialog()
  Creates a new IODialog object that supports dialog-based interaction.

 Output methods
 
void print(any value)
  Adds the string to a message that will be displayed when the line is complete.
void println(any value)
  Displays the value in a dialog box.
void println()
  Displays any text in the dialog box so far.
void showErrorMessage(String msg)
  Displays an error message in a dialog box.

 Input methods
 
String readLine()  or  readLine(String prompt)
  Pops up a dialog asking the user to enter a line of text (returned with no terminating newline).
int readInt()  or  readInt(int min, int max)  or
    readInt(String prompt)  or  readInt(String prompt, int min, int max)
  Pops up a dialog asking the user to enter an int, with optional prompt and limits.
double readDouble()  or  readDouble(double min, double max)  or
       readDouble(String prompt)  or
       readDouble(String prompt, double min, double max) 
  Pops up a dialog asking the user to enter a double, with optional prompt and limits.
int readInt()  or  readInt(String prompt)
  Pops up a dialog asking the user to enter an integer value.
double readDouble()  or  readDouble(String prompt)
  Pops up a dialog asking the user to enter a double-precision value.
boolean readBoolean()  or  readBoolean(String prompt)
  Pops up a dialog asking the user to select a true/false value.
boolean readBoolean(String prompt, String trueLabel, String falseLabel)
  Pops up a dialog asking the user to choose one of the specified labels.

 Additional methods (most of which are unlikely to be used by novices)
 
void setExceptionOnError(boolean flag)
  Sets the error-handling mode: false means retry on error, true means raise an exception.
boolean getExceptionOnError()
  Returns the error-handling mode, as defined in setExceptionOnError.
void setAllowCancel(boolean flag)
  Sets the cancel mode: true adds a “Cancel” button that throws an exception.
boolean getAllowCancel()
  Returns the error-handling mode, as defined in setAllowCancel.
   

One of the design goals for the acm.io package was to have the IOConsole and IODialog classes implement the same interface so that students could easily switch back and forth between the two modes. In the acm.io design, that common behavior is specified by the IOModel interface, which includes the input and output methods common to the two classes. The existence of this interface makes it possible for code to remain insensitive to the interaction model. Consider, for example, the following method definition:

 

void showArray(IOModel io, int[] array) {
   io.print("[");
   for (int i = 0; i < array.length; i++) {
      if (i > 0) io.print(", ");
      io.print(array[i]);
   }
   io.println("]");
}

The important idea to notice here is that the first argument to showArray can be either an IOConsole or an IODialog object; since both implement the IOModel interface, the implementation doesn’t care.

Designing IODialog so that it provides a reasonable implementation of IOModel turns out to be harder than it looks. The input side is easy, because both consoles and dialogs wait for the user to supply a single input value. The output side is more difficult, largely because a dialog is not a stream device. Having each call to print display a separate dialog box is not a good choice, particularly for methods like showArray that assemble the output in pieces. Displaying each individual value and comma in a separate dialog box would destroy the conceptual integrity of the output and render the entire mechanism unusable. A more satisfactory approach is to have print buffer the output inside the dialog until a call to println arrives to signal the end of the line. This interpretation has the effect, therefore, of allowing

 

int array = {2, 3, 5, 7, 11};
showDialog(new IODialog(), array);

to generate a single dialog box, as follows:

Similar care must be applied if calls to print are followed by an input operation before a println call occurs. In such situations, the buffered output is added at the beginning of the prompt string. This treatment provides a sensible interpretation for code in which the programmer explicitly prints a prompt rather than including it as a parameter to the input call.

Another difficult decision in the design of both IODialog and IOConsole was the question of how to handle illegal input data. For novices, the best strategy is for the input method simply to display an error message and request new input, either by bringing up another dialog or by repeating the prompt on the console. For more sophisticated users, however, it is more appropriate to allow the program to gain control at this point. By default, both the IODialog and IOConsole classes adopt the novice-friendly approach and request new input. The more advanced programmer, however, can change this behavior by calling setExceptionOnError(true), in which case the input methods throw an ErrorException on invalid input.

A related issue arises in defining reasonable semantics for the “Cancel” button, which is part of the typical input dialog mechanism available with JOptionPane. In this case, it is not appropriate to have the program bring up a new dialog after the user clicks the “Cancel” button, which is presumably exactly what the user did not want. To avoid this conceptual problem, the IODialog mechanism does not display “Cancel” buttons as part of the dialog unless the client has made it clear that special handling of dialog cancellation is desired. If the client calls setAllowCancel(true), the input methods display a “Cancel” button that throws a CancelledException when it is clicked.