Java Cookbook 14.6 Writing Program Output into a Window

Problem

You need to capture the output from a Stream or Writer and display it in a text area.

Solution

The easiest way is to subclass OutputStream or Writer as appropriate, and override just the methods needed to get characters copied into the JTextArea.

Discussion

This is such a common need that I've added it to the DarwinSys package (see XX.X). Here is the code from com.darwinsys.io.TextAreaWriter:

/**
 * Simple way to "print" to a JTextArea; just say
 * PrintWriter out = new PrintWriter(new TextAreaWriter(myTextArea));
 * Then out.println() et all will all appear in the TextArea.
 */
public final class TextAreaWriter extends Writer {

	private final JTextArea textArea;

	public TextAreaWriter(final JTextArea textArea) {
		this.textArea = textArea;
	}

    @Override
    public void flush(){ }
    
    @Override
    public void close(){ }

	@Override
	public void write(char[] cbuf, int off, int len) throws IOException {
		textArea.append(new String(cbuf, off, len));
	}
}

As you can see, for the Writer case I only had to override one write() form; all the other forms of write(), and the print/println() methods in PrintWriter, call down through this one method. The flush() and close() methods are also abstract in Writer, but they don't have to do anything here (though close() could set the textArea field to null, and write could check that, to guard against use of a closed Writer).

As mentioned in XX.X, no code is complete until it has a working test. Here is the JUnit test for TextAreaWriter:

import com.darwinsys.io.TextAreaWriter;

public class TextAreaWriterTest extends TestCase {

	JTextArea ta = new JTextArea();
	
	public void testOne() {
		PrintWriter x = new PrintWriter(new TextAreaWriter(ta));
		x.print("Hello");
		x.print(' ');
		x.print("World");
		assertEquals("Hello World", ta.getText());
	}
}

As this is the first time we've seen this technique, it's worth mentioning that it is acceptable (on all platforms I've tried it) to have a JUnit test that creates a Component subclass without setting it visible, if you only want to test set/get methods, which is all we're doing here. On Mac OS X, this may cause a delay of a second or so as it creates a "running Java program" icon; XXX does this depend on the OS X version or the JDK version??

The OutputStream case is just a tiny bit more involved, but worth doing so we can re-use legacy code that writes to System.out. Here is TextAreaOutputStream:

/**
 * Simple way to "print" to a JTextArea; just say
 * PrintStream out = new PrintStream(new TextAreaOutputStream(myTextArea));
 * Then out.println() et all will all appear in the TextArea.
 */
public final class TextAreaOutputStream extends OutputStream {

	private final JTextArea textArea;
	private final StringBuilder sb = new StringBuilder();

	public TextAreaOutputStream(final JTextArea textArea) {
		this.textArea = textArea;
	}

    @Override
    public void flush(){ }
    
    @Override
    public void close(){ }

	@Override
	public void write(int b) throws IOException {

		if (b == '\r')
			return;
		
		if (b == '\n') {
			textArea.append(sb.toString());
			sb.setLength(0);
		}
		
		sb.append((char)b);
	}
}
As with Writer, we only really need to override three methods; all the others (eg., System.out.println()) ultimately call down to this write(int) method. The class shown here uses a StringBuffer to build up a line of text and, when a line ending comes along, appends the line to the JTextArea and resets the StringBuffer. A \r is ignored, since on UNIX a \n alone ends a line and on MS-DOS-based systems a \r\n ends a line (this code won't run on Mac OS 9 or earlier - which are the only systems I know that used \r alone - because there's no support for Swing and hence no JTextArea).

It's not reprinted here, but there is a JUnit test for this class as well. One of the tests is similar to the one for TextAreaWriter while the second uses System.setOut() followed by System.out.println() to ensure that everything works as expected. It does.

Here's a longer example of using this. I have an existing console-based TestOpenMailRelay program derived from the mail sender in Recipe 19.2, that I use to test whether remote servers are willing to accept mail from unknown third parties and forward it as their own. Example 14-5 is the GUI for that program; both this and the main program are online in the email directory. Using the JTextAreaWriter, I was able to add a GUI wrapper to TestOpenMailRelay, such that console output appears in a window, without having to change the code to know about JTextArea.

In the constructor of the GUI wrapper, I create a JTextArea and wrap it in a TextAreaWriter and then a PrintWriter. I pass this PrintWriter to the console program TestOpenMailRelay.process( ) method. That method writes its output to the stream in addition to assigning standard output and standard error, so we should see anything it tries to print. Figure 14-8 shows three windows: the program output window (the goal of this whole exercise), a terminal window from which I copied the IP address (some parts of the text in this window have been deliberately obfuscated), and another com- mand window in which I started the GUI program running. The code is shown in Example 14-5. (which is not shown here as it's the same as in the printed book, 2nd Edition).