Introduction
This tutorial is focused on covering the basics of create an interactive console for Jython that you can embed into your Swing applications.
Items that will be covered in this topic:
1. "Piping" input/output streams using Java.
2. Using document filters to change where text is inputted, or even prevent text from being added.
3. Implementing a "command history" which can remember a commands that have been implemented.
4. Using Jython from your Java program.
Required tools
1. All the basic tools you use for creating regular Java Swing applications.
2. Jython. This implementation uses Jython 2.5.1, the current release at this writing. You can find the latest version here
Difficulty: Medium-Hard. I'm assuming the reader has a thorough knowledge of the Java language and some experience with using Swing, as well as some knowledge of multi-threading in Java (very basic knowledge). There will also be exposure to some "lesser known" portions of the Java API, but I'll try to explain those when they come up. I will also explain a little bit of how to use external libraries from the Eclipse IDE.
Streams
To redirect the various streams, I decided to two classes, one for redirected input and the other redirected output. To simplify the design, I directly passed the JConsole object to the streams and allowed them to directly manipulate the JConsole text. Also, because data can be written/read from multiple threads, these streams have been made to be "thread safe" (well, in my mind they're thread safe, dunno if this is actually true).
ConsoleOutputStream
The simpler of the two, all this class needs to do is take any text inputed into it and update the text inside of the console. The methods are declared synchronized because there is a possibility that two threads could be modifying the console object at the same time. I'm just going to post the code for this class and let you read through it.
package console.streams; import java.io.IOException; import java.io.Writer; import console.JConsole; /** * Data written to this will be displayed into the console * * @author Andrew */ public class ConsoleOutputStream extends Writer { private JConsole console; /** * @param console */ public ConsoleOutputStream(JConsole console) { this.console = console; } @Override public synchronized void close() throws IOException { console = null; } @Override public void flush() throws IOException { // no extra flushing needed } @Override public synchronized void write(char[] cbuf, int off, int len) throws IOException { StringBuilder temp = new StringBuilder(console.getText()); for (int i = off; i < off + len; i++) { temp.append(cbuf[i]); } console.setText(temp.toString()); } }
ConsoleInputStream
This class is similar in complexity to ConsoleOutputStream, but in addition to just reading text in, we also have to have a mechanism that allows us to add text to this stream and then read from this. For my implementation, I chose to use a StringBuilder that contains all the text that hasn't been read out yet, then as text gets read out, it's deleted from the StringBuilder. I don't think this is the most efficient method for doing this, but it gets the job done.
There is one interesting thing to highlight:
In order to block the read from finishing before the [enter] key has been pressed, I had to create two synchronized blocks, one to see if there is something, and the second to actually read from the block. Here's my reasoning behind this:
The input stream is only being read from the Jython runner thread, but the text being inputted is coming from the Swing thread. If the Jython runner thread holds the lock on the input stream during the entire read, then the Swing thread will never be able to write data into the input stream. So, the read method will lock the thread to see if something can be read. If not, it releases the lock and sleeps the thread (to prevent a "live loop" which wastes cpu time). If there is, it re-acquires the lock and reads in the data.
The class code:
package console.streams; import java.io.IOException; import java.io.Reader; import console.JConsole; /** * Data written into this is data from the console * * @author Andrew */ public class ConsoleInputStream extends Reader { private JConsole console; private StringBuilder stream; /** * @param console */ public ConsoleInputStream(JConsole console) { this.console = console; stream = new StringBuilder(); } /** * @param text */ public void addText(String text) { synchronized (stream) { stream.append(text); } } @Override public synchronized void close() throws IOException { console = null; stream = null; } @Override public int read(char[] buf, int off, int len) throws IOException { int count = 0; boolean doneReading = false; for (int i = off; i < off + len && !doneReading; i++) { // determine if we have a character we can read // we need the lock for stream int length = 0; while (length == 0) { // sleep this thread until there is something to read try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (stream) { length = stream.length(); } } synchronized (stream) { // get the character buf[i] = stream.charAt(0); // delete it from the buffer stream.deleteCharAt(0); count++; if (buf[i] == '\n') { doneReading = true; } } } return count; } }
CommandHistory
In order to keep track of previous commands the user has typed, I created a CommandHistory class which uses a circularly-linked list to keep track of the commands the user has inputted. Why a circularly-linked list? I decided that I liked being able to cycle from the oldest command back to the newest command. This is easily done by using a circularly linked list rather than having to check to see if it's the first/last item, etc.
The CommandHistory's "top" command is the blank command. This is to allow me to easily cycle between actual history items and the empty input stream. The CommandHistory also doesn't re-add the latest item, so if the user wants to send the same command multiple times, it will allow the user to quickly cycle through all the different items without cycling up through several items just to find the next one (of course, if you have command A followed by command B, then re-run command A, it will re-add command A).
As far as I known, this thread doesn't need to be thread-safe.
package console; public class CommandHistory { private class Node { public String command; public Node next; public Node prev; public Node(String command) { this.command = command; next = null; prev = null; } } private int length; /** * The top command with an empty string */ private Node top; private Node current; private int capacity; /** * Creates a CommandHistory with the default capacity of 64 */ public CommandHistory() { this(64); } /** * Creates a CommandHistory with a specified capacity * * @param capacity */ public CommandHistory(int capacity) { top = new Node(""); current = top; top.next = top; top.prev = top; length = 1; this.capacity = capacity; } /** * @return */ public String getPrevCommand() { current = current.prev; return current.command; } /** * @return */ public String getNextCommand() { current = current.next; return current.command; } /** * Adds a command to this command history manager. Resets the command * counter for which command to select next/prev.<br> * If the number of remembered commands exceeds the capacity, the oldest * item is removed.<br> * Duplicate checking only for most recent item. * * @param command */ public void add(String command) { // move back to the top current = top; // see if we even need to insert if (top.prev.command.equals(command)) { // don't insert return; } // insert before top.next Node temp = new Node(command); Node oldPrev = top.prev; temp.prev = oldPrev; oldPrev.next = temp; temp.next = top; top.prev = temp; length++; if (length > capacity) { // delete oldest command Node newNext = top.next.next; top.next = newNext; newNext.prev = top; } } /** * @return the capacity */ public int getCapacity() { return capacity; } /** * @return the length */ public int getLength() { return length; } }
JConsole
This is the main meat of the interactive console. There are several sections I want to highlight:
The Jython engine
All though with JSR223 Java 6 does have scripting tools, I chose not to use these because the technology is missing some features that make the console much more use-able and closer to the CPython interactive console. Instead, I chose to use an internal component of the Jython library for the Jython script engine.
// create streams that will link with this in = new ConsoleInputStream(this); // System.setIn(in); out = new ConsoleOutputStream(this); // System.setOut(new PrintStream(out)); err = new ConsoleOutputStream(this); // setup the command history history = new CommandHistory(); // setup the script engine engine = new InteractiveInterpreter(); engine.setIn(in); engine.setOut(out); engine.setErr(err);
Also, It's very important to run the Jython engine on a separate thread, otherwise the Swing thread will freeze until the Jython engine finishes executing.
private class PythonRunner implements Runnable { private String commands; public PythonRunner(String commands) { this.commands = commands; } @Override public void run() { running = true; try { engine.runsource(commands); } catch (PyException e) { // prints out the python error message to the console e.printStackTrace(); } // engine.eval(commands, context); StringBuilder text = new StringBuilder(getText()); text.append(">>> "); setText(text.toString()); running = false; } }
Also, because the python engine is run on a separate thread, it is very important to properly stop the engine if it is running and the console gets disposed. Normally, I would recommend that you do not use the Thread.stop() method, but because the console is being finalized, I don't think it's too much of a problem.
@SuppressWarnings("deprecation") @Override public void finalize() { if (running) { // I know it's depracated, but since this object is being destroyed, // this thread should go, too pythonThread.stop(); pythonThread.destroy(); } }
Document Filters
Document Filters allow you to easily determine what to do when the text of the document is changed. For the console, I used an index to mark what area is "editable" (i.e. the user can type/delete text from here). I also found out that the Document Filter behavior is used when the text is changed programmatically. To get around this, I used a boolean flag to determine when this filter needs to be active, and when it should be ignored.
private class ConsoleFilter extends DocumentFilter { private JConsole console; public boolean useFilters; public ConsoleFilter(JConsole console) { this.console = console; useFilters = true; } @Override public void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException { if (useFilters) { // determine if we can insert if (console.getSelectionStart() >= console.editStart) { // can insert fb.insertString(offset, string, attr); } else { // insert at the end of the document fb.insertString(console.getText().length(), string, attr); // move cursor to the end console.getCaret().setDot(console.getText().length()); // console.setSelectionEnd(console.getText().length()); // console.setSelectionStart(console.getText().length()); } } else { fb.insertString(offset, string, attr); } } @Override public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { if (useFilters) { // determine if we can replace if (console.getSelectionStart() >= console.editStart) { // can replace fb.replace(offset, length, text, attrs); } else { // insert at end fb.insertString(console.getText().length(), text, attrs); // move cursor to the end console.getCaret().setDot(console.getText().length()); // console.setSelectionEnd(console.getText().length()); // console.setSelectionStart(console.getText().length()); } } else { fb.replace(offset, length, text, attrs); } } @Override public void remove(DocumentFilter.FilterBypass fb, int offset, int length) throws BadLocationException { if (useFilters) { if (offset > console.editStart) { // can remove fb.remove(offset, length); } else { // only remove the portion that's editable fb.remove(console.editStart, length - (console.editStart - offset)); // move selection to the start of the editable section console.getCaret().setDot(console.editStart); // console.setSelectionStart(console.editStart); // console.setSelectionEnd(console.editStart); } } else { fb.remove(offset, length); } } }
Handling key-presses
To provide some additional functionality, I implemented the KeyListener interface and handled certain key combinations. Once the keypress has been handled, it is important to call consume() to ensure that the base TextBox doesn't also handle these key presses.
A few highlighted keypresses:
[enter] - This is used to launch the Jython engine, and it is also used to put text into the input stream for the Jython engine
[shift+enter] - Allows "multi-line" text to be passed to the Jython engine
[ctrl+up/down] - Used for cycling through the command history
The other keypresses were added for convenience to determine where the caret needs to move to/select, giving preference to the editable sections of the console.
@Override public void keyPressed(KeyEvent e) { if (e.isControlDown()) { if (e.getKeyCode() == KeyEvent.VK_A && !e.isShiftDown() && !e.isAltDown()) { // handle select all // if selection start is in the editable region, try to select // only editable text if (getSelectionStart() >= editStart) { // however, if we already have the editable region selected, // default select all if (getSelectionStart() != editStart || getSelectionEnd() != this.getText().length()) { setSelectionStart(editStart); setSelectionEnd(this.getText().length()); // already handled, don't use default handler e.consume(); } } } else if (e.getKeyCode() == KeyEvent.VK_DOWN && !e.isShiftDown() && !e.isAltDown()) { // next in history StringBuilder temp = new StringBuilder(getText()); // remove the current command temp.delete(editStart, temp.length()); temp.append(history.getNextCommand()); setText(temp.toString(), false); e.consume(); } else if (e.getKeyCode() == KeyEvent.VK_UP && !e.isShiftDown() && !e.isAltDown()) { // prev in history StringBuilder temp = new StringBuilder(getText()); // remove the current command temp.delete(editStart, temp.length()); temp.append(history.getPrevCommand()); setText(temp.toString(), false); e.consume(); } } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { // handle script execution if (!e.isShiftDown() && !e.isAltDown()) { if (running) { // we need to put text into the input stream StringBuilder text = new StringBuilder(this.getText()); text.append(System.getProperty("line.separator")); String command = text.substring(editStart); setText(text.toString()); ((ConsoleInputStream) in).addText(command); } else { // run the engine StringBuilder text = new StringBuilder(this.getText()); String command = text.substring(editStart); text.append(System.getProperty("line.separator")); setText(text.toString()); // add to the history history.add(command); // run on a separate thread pythonThread = new Thread(new PythonRunner(command)); // so this thread can't hang JVM shutdown pythonThread.setDaemon(true); pythonThread.start(); } e.consume(); } else if (!e.isAltDown()) { // shift+enter StringBuilder text = new StringBuilder(this.getText()); if (getSelectedText() != null) { // replace text text.delete(getSelectionStart(), getSelectionEnd()); } text.insert(getSelectionStart(), System.getProperty("line.separator")); setText(text.toString(), false); } } else if (e.getKeyCode() == KeyEvent.VK_HOME) { int selectStart = getSelectionStart(); if (selectStart > editStart) { // we're after edit start, see if we're on the same line as edit // start for (int i = editStart; i < selectStart; i++) { if (this.getText().charAt(i) == '\n') { // not on the same line // use default handle return; } } if (e.isShiftDown()) { // move to edit start getCaret().moveDot(editStart); } else { // move select end, too getCaret().setDot(editStart); } e.consume(); } } }
I won't post the full code of the JConsole class here, but I will attach the source files. I'm working on getting a working Eclipse project uploaded, but currently I'm having no luck because of the file-size limit.
Usage
Since the JConsole extends a JTextArea, you can simply add the JConsole anywhere you would use a JTextArea.
Here's some example code for adding the console to a JFrame:
package console; import java.awt.GridLayout; import javax.swing.JFrame; public class JConsoleTest { public static void main(String[] args) { JFrame frame = new JFrame("Jython Interactive Console"); frame.setSize(480, 640); frame.setLayout(new GridLayout()); frame.add(new JConsole()); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }
Conclusion
I know this is a very long tutorial, but hopefully it's provided some useful information either in the individual components I used, or as a whole.
One major thing to note about using Jython:
Jython 2.5.1 unfortunately does NOT have a built-in help function, and many of the built-in functions do not have their doc strings set.
License and Disclaimer:
You are free to use this code in any way you want, it is provided "as-is" and I take no responsibility if something doesn't work correctly (please post any bugs you find, though. That would be very helpful, and I will probably update the code to fix the problem). Some credit in your final product would be nice, but I don't feel inclined to enforce/sue if you don't
Happy Coding