/*******************************************************************************
* Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation from Oracle TopLink
******************************************************************************/
package org.eclipse.persistence.tools.workbench.uitools;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintStream;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.WindowConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.TabSet;
import javax.swing.text.TabStop;
import org.eclipse.persistence.tools.workbench.utility.io.Pipe;
/**
* A console builds a simple window that will display all the text written
* to two streams: "out" and "err". Once a console is constructed the
* standard system streams can be redirected to the console's streams:
*
* Console console = new Console();
* System.setOut(new PrintStream(console.getOutStream(), true)); // true = auto-flush
* System.setErr(new PrintStream(console.getErrStream(), true)); // true = auto-flush
*
* The console window can be opened explicitly:
*
* console.open();
*
* or it will open automatically whenever text is written to either of
* the two streams.
*
* NB: This console is primarily for development-time use only. It would
* need to be refactored for use by end-users (strings would need to
* be "externalized", behavior made more configurable, etc.).
*/
public class Console {
/** Any text written to the streams is redirected to this document. */
private StyledDocument document;
/**
* The document text is mono-spaced and color-coded
* (out -> black; err -> red).
*/
private AttributeSet outAttributeSet;
private AttributeSet errAttributeSet;
/** The standard output and error streams can be redirected to these. */
private OutputStream outStream;
private OutputStream errStream;
/**
* We start a thread for each of the streams. The threads wait for
* text to be written to the streams and then append the text to
* the document. The threads are interrupted when the console is
* closed.
*/
private Thread outSynchronizerThread;
private Thread errSynchronizerThread;
/** Hold the text pane so we can keep the "tail" of the text visible. */
private JTextPane textPane;
/** Hold the window so we can open and close it. */
private JFrame console;
// ********** static methods **********
/**
* Build a console that replaces the current System
* output and error streams.
*/
public static Console buildSystemConsole() {
Console console = new Console();
System.setOut(new PrintStream(console.getOutStream(), true)); // true = auto-flush
System.setErr(new PrintStream(console.getErrStream(), true)); // true = auto-flush
return console;
}
// ********** constructor **********
/**
* Default constructor.
*/
public Console() {
super();
this.initialize();
}
// ********** initialization **********
protected void initialize() {
this.document = this.buildDocument();
// set up the "out" stream
this.outAttributeSet = this.buildOutAttributeSet();
Pipe outPipe = new Pipe();
this.outStream = outPipe.getOutputStream();
this.outSynchronizerThread = this.buildSynchronizerThread(outPipe.getInputStream(), this.outAttributeSet, "out");
this.outSynchronizerThread.start();
// set up the "err" stream
this.errAttributeSet = this.buildErrAttributeSet();
Pipe errPipe = new Pipe();
this.errStream = errPipe.getOutputStream();
this.errSynchronizerThread = this.buildSynchronizerThread(errPipe.getInputStream(), this.errAttributeSet, "err");
this.errSynchronizerThread.start();
// set up the UI, but don't open it until the client requests
// or something is written to the console
this.textPane = this.buildTextPane();
this.console = this.buildConsole();
}
private StyledDocument buildDocument() {
StyledDocument result = new DefaultStyledDocument();
// set up tab stops for the entire document
MutableAttributeSet mas = new SimpleAttributeSet();
StyleConstants.setTabSet(mas, new TabSet(new TabStop[] {new TabStop(30)}));
result.setParagraphAttributes(0, 0, mas, false);
result.addDocumentListener(this.buildDocumentListener());
return result;
}
private DocumentListener buildDocumentListener() {
return new DocumentListener() {
public void changedUpdate(DocumentEvent e) {
Console.this.documentChanged();
}
public void insertUpdate(DocumentEvent e) {
Console.this.documentChanged();
}
public void removeUpdate(DocumentEvent e) {
Console.this.documentChanged();
}
public String toString() {
return "document listener";
}
};
}
private AttributeSet buildOutAttributeSet() {
return this.buildAttributeSet(Color.BLACK);
}
private AttributeSet buildErrAttributeSet() {
return this.buildAttributeSet(Color.RED);
}
private AttributeSet buildAttributeSet(Color color) {
MutableAttributeSet attributes = new SimpleAttributeSet();
StyleConstants.setForeground(attributes, color);
StyleConstants.setFontFamily(attributes, "Monospaced");
StyleConstants.setFontSize(attributes, 12);
return attributes;
}
private Thread buildSynchronizerThread(InputStream inputStream, AttributeSet attributeSet, String name) {
return new Thread(new Synchronizer(inputStream, this.document, attributeSet), "Console Synchronizer: " + name);
}
private JTextPane buildTextPane() {
JTextPane result = new NonWrappingTextPane(this.document);
result.setEditable(false);
return result;
}
private JFrame buildConsole() {
JFrame window = new JFrame(this.title());
window.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
window.getContentPane().add(this.buildMainPanel(), BorderLayout.CENTER);
window.setLocation(300, 300);
window.setSize(600, 400);
window.addWindowListener(this.buildWindowListener());
return window;
}
protected String title() {
return "Console";
}
protected JPanel buildMainPanel() {
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.add(this.buildScrollableTextPane(), BorderLayout.CENTER);
mainPanel.add(this.buildControlPanel(), BorderLayout.PAGE_END);
return mainPanel;
}
protected Component buildScrollableTextPane() {
return new JScrollPane(this.textPane);
}
protected Component buildControlPanel() {
JPanel controlPanel = new JPanel(new BorderLayout());
GridLayout grid = new GridLayout(1, 0);
grid.setHgap(5);
JPanel controlPanel2 = new JPanel(grid);
controlPanel2.add(this.buildCopyButton());
controlPanel2.add(this.buildClearButton());
controlPanel2.setBorder(BorderFactory.createEmptyBorder(4, 0, 2, 0));
controlPanel.add(controlPanel2, BorderLayout.LINE_END);
return controlPanel;
}
private Component buildCopyButton() {
return new JButton(this.buildCopyAction());
}
private Action buildCopyAction() {
Action action = new AbstractAction("Copy") {
public void actionPerformed(ActionEvent event) {
Console.this.copy();
}
};
action.setEnabled(true);
return action;
}
private Component buildClearButton() {
return new JButton(this.buildClearAction());
}
private Action buildClearAction() {
Action action = new AbstractAction("Clear") {
public void actionPerformed(ActionEvent event) {
Console.this.clear();
}
};
action.setEnabled(true);
return action;
}
private WindowListener buildWindowListener() {
return new WindowAdapter() {
public void windowClosed(WindowEvent e) {
super.windowClosed(e);
Console.this.shutDown();
}
public String toString() {
return "window listener";
}
};
}
// ********** queries **********
/**
* Return the "out" stream, typically corresponding to System.out.
*/
public OutputStream getOutStream() {
return this.outStream;
}
/**
* Return the "err" stream, typically corresponding to System.err.
*/
public OutputStream getErrStream() {
return this.errStream;
}
// ********** behavior **********
/**
* Open the console window. The console can be "hidden" and re-opened
* repeatedly, but once it is "closed" it can no longer be re-opened.
* @see #hide()
* @see #close()
*/
public void open() {
if ((this.outSynchronizerThread == null) || (this.errSynchronizerThread == null)) {
throw new IllegalStateException("Console cannot be re-opened once it has been closed.");
}
this.console.setVisible(true);
}
/**
* Hide the console window. The window will re-appear either when a
* client calls #open() or when something is written to either of the
* console's two output streams.
*/
public void hide() {
this.console.setVisible(false);
}
/**
* Close the console window. This also shuts down the two threads
* monitoring the "out" and "err" streams. Once this method is called
* the console can no longer be re-opened.
*/
public void close() {
this.console.dispose();
}
/**
* The document changed in some fashion, make sure the console is
* open and the last line is visible.
*/
void documentChanged() {
this.textPane.setCaretPosition(this.document.getLength());
if ( ! this.console.isVisible()) {
this.open();
}
}
/**
* Copy the entire contents of the console to the system "clipboard".
*/
void copy() {
this.textPane.selectAll();
this.textPane.copy();
this.textPane.setCaretPosition(this.document.getLength());
}
/**
* Clear out the console.
*/
public void clear() {
try {
this.document.remove(0, this.document.getLength());
} catch (BadLocationException ex) {
// should never happen...
}
}
/**
* This is called when the console is closed and disposed.
*/
void shutDown() {
this.outSynchronizerThread.interrupt();
this.outSynchronizerThread = null;
this.errSynchronizerThread.interrupt();
this.errSynchronizerThread = null;
}
// ********** nested classes **********
/**
* This task will synchronize a document with an input stream
* by reading from the stream and updating the document via the AWT
* Event Queue.
*/
private static class Synchronizer implements Runnable {
private InputStream inputStream;
private Document document;
private AttributeSet attributeSet;
Synchronizer(InputStream inputStream, Document document, AttributeSet attributeSet) {
super();
this.inputStream = inputStream;
this.document = document;
this.attributeSet = attributeSet;
}
/**
* Loop while there are more data to be read from the input stream.
* @see Runnable#run()
*/
public void run() {
byte[] buffer = new byte[2048]; // use the default "pipe" size
int length = this.read(buffer);
while (length != -1) {
this.appendDocument(new String(buffer, 0, length));
length = this.read(buffer);
}
}
/**
* Wrap any unexpected exception in a RuntimeException.
*/
private int read(byte[] buffer) {
try {
return this.inputStream.read(buffer);
} catch (InterruptedIOException ex) {
return -1; // the thread was interrupted - time to quit
} catch (IOException ex) {
// the pipe is broken
this.appendDocument("The Thread writing to the pipe is dead.");
return 0;
// throw new RuntimeException(ex);
}
}
/**
* Place a task on the AWT Event Queue that will append
* the document with the specified string.
*/
private void appendDocument(String string) {
EventQueue.invokeLater(new Appendix(this.document, string, this.attributeSet));
}
}
/**
* This is the task dispatched to the AWT Event Queue that
* will update the document model held by the console's text area.
*/
private static class Appendix implements Runnable {
private Document document;
private String string;
private AttributeSet attributeSet;
Appendix(Document document, String string, AttributeSet attributeSet) {
super();
this.document = document;
this.string = string;
this.attributeSet = attributeSet;
}
/**
* Append the string to the end of the document,
* with an optional attribute set.
* @see Runnable#run()
*/
public void run() {
try {
this.document.insertString(this.document.getLength(), this.string, this.attributeSet);
} catch (BadLocationException ex) {
// should never happen...
}
}
}
/**
* Disable line-wrap in JTextPane.
* Work-around for JDK bug/rfe 4131119.
*/
private static class NonWrappingTextPane extends JTextPane {
NonWrappingTextPane(StyledDocument document) {
super(document);
}
public boolean getScrollableTracksViewportWidth() {
return this.getSize().width < this.getParent().getSize().width;
}
public void setSize(Dimension d) {
if (d.width < this.getParent().getSize().width) {
d.width = this.getParent().getSize().width;
}
super.setSize(d);
}
}
}