package org.limewire.ui.swing.advanced;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.WriterAppender;
import org.apache.log4j.spi.LoggerRepository;
import org.jdesktop.application.Resource;
import org.limewire.concurrent.ManagedThread;
import org.limewire.core.api.mojito.MojitoManager;
import org.limewire.core.api.support.LocalClientInfo;
import org.limewire.core.api.support.LocalClientInfoFactory;
import org.limewire.i18n.I18nMarker;
import org.limewire.service.ErrorService;
import org.limewire.ui.swing.components.NumericTextField;
import org.limewire.ui.swing.components.TextFieldClipboardControl;
import org.limewire.ui.swing.settings.ConsoleSettings;
import org.limewire.ui.swing.util.FileChooser;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.util.ThreadUtils;
import com.google.inject.Inject;
/**
* A Console for log/any output.
*/
public class Console extends JPanel {
@Resource
private Color tableColor;
@Resource
private Font outputFont;
/** Factory for LocalClientInfo object. */
private volatile LocalClientInfoFactory localClientInfoFactory;
/** Manager for Mojito DHT. */
private final MojitoManager mojitoManager;
private final int idealSize;
private final int maxExcess;
private JScrollPane scrollPane;
private JTextArea output;
private JButton apply;
private JButton clear;
private JButton save;
private JComboBox loggerComboBox;
private JComboBox levelComboBox;
/**
* Text field into which the delay time (in seconds) can be input.
*/
private NumericTextField delayTxt;
private boolean scroll = true;
private boolean altCtrlDown = false;
private List<ConsoleListener> listeners = null;
/**
* Delay time (in seconds) for updating the console text area.
*/
private int delay;
/**
* Buffer for text to be appended to the console text
* area. Uses a StringBuffer because it is
* synchronized.
*/
private StringBuffer delayBuf;
/**
* The timer we use to schedule updates to the console
* text area.
*/
private Timer delayTimer;
/** Appender for the log. */
private Appender logAppender;
/**
* Constructs the Console for displaying messages.
*/
@Inject
public Console(MojitoManager mojitoManager,
LocalClientInfoFactory localClientInfoFactory) {
this.mojitoManager = mojitoManager;
this.localClientInfoFactory = localClientInfoFactory;
idealSize = ConsoleSettings.CONSOLE_IDEAL_SIZE.getValue();
maxExcess = ConsoleSettings.CONSOLE_MAX_EXCESS.getValue();
output = new JTextArea();
output.setEditable(false);
scrollPane = new JScrollPane(output);
scrollPane.setBorder(BorderFactory.createMatteBorder(0,0,1,0, Color.black));
scrollPane.getVerticalScrollBar().addAdjustmentListener(
new AdjustmentListener() {
public void adjustmentValueChanged(AdjustmentEvent e) {
if (e.getValueIsAdjusting()) {
scroll = false;
} else {
scroll = true;
}
}
});
loggerComboBox = new JComboBox(new LoggerComboBoxModel());
levelComboBox = new JComboBox(new LevelComboBoxModel());
loggerComboBox.setAutoscrolls(true);
loggerComboBox.setMaximumRowCount(20);
loggerComboBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent evt) {
selectLoggerLevel();
}
});
loggerComboBox.addPopupMenuListener(new PopupMenuListener() {
public void popupMenuWillBecomeVisible(PopupMenuEvent evt) {
refreshLoggers();
}
public void popupMenuCanceled(PopupMenuEvent evt) {}
public void popupMenuWillBecomeInvisible(PopupMenuEvent evt) {}
});
levelComboBox.setAutoscrolls(true);
apply = new JButton(I18n.tr("Apply"));
apply.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
applyLevel();
}
});
clear = new JButton(I18n.tr("Clear"));
clear.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
clear();
}
});
save = new JButton(I18n.tr("Save"));
save.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
save();
}
});
// default the delay time to zero (ie, live
// updates).
JLabel delayLabel = new JLabel(I18n.tr("Delay: "));
delayLabel.setMinimumSize(new Dimension(50, 23));
delayLabel.setHorizontalAlignment(SwingConstants.RIGHT);
delay = 0;
delayBuf = new StringBuffer();
delayTimer = null;
// Create text field for numeric value.
delayTxt = new NumericTextField(3);
delayTxt.setValue(0);
delayTxt.setHorizontalAlignment(JTextField.RIGHT);
delayTxt.setMinimumSize(new Dimension(50, 23));
delayTxt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
setDelay();
}
});
// Install clipboard actions on text field.
TextFieldClipboardControl.install(delayTxt);
// Developers can press and hold Alt+Ctrl while clicking
// on Save to get the current stack traces.
KeyListener keyListener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
altCtrlDown = e.isAltDown() && e.isControlDown();
}
// Note: if the user is holding Alt+Ctrl while
// switching to a different Tab this will never
// get called! We need a second Listener!
@Override
public void keyReleased(KeyEvent e) {
altCtrlDown = false;
}
};
// Install the listener on all components
addKeyListener(keyListener);
scrollPane.addKeyListener(keyListener);
output.addKeyListener(keyListener);
loggerComboBox.addKeyListener(keyListener);
levelComboBox.addKeyListener(keyListener);
apply.addKeyListener(keyListener);
clear.addKeyListener(keyListener);
save.addKeyListener(keyListener);
// Reset the flag if this Tab gets invisible
addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
altCtrlDown = false;
}
});
setLayout(new BorderLayout());
add(BorderLayout.CENTER, scrollPane);
JPanel controlsPanel = new JPanel();
controlsPanel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 1;
gbc.gridheight = 1;
gbc.weightx = 1;
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(loggerComboBox, gbc);
gbc.gridx = 1;
gbc.gridy = 0;
gbc.gridwidth = 1;
gbc.gridheight = 1;
gbc.weightx = 0;
gbc.weighty = 0;
gbc.insets = new Insets(0, 5, 0, 0);
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(levelComboBox, gbc);
gbc.gridx = 2;
gbc.gridy = 0;
gbc.gridwidth = 1;
gbc.gridheight = 1;
gbc.weightx = 0;
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(delayLabel, gbc);
gbc.gridx = 3;
gbc.gridy = 0;
gbc.gridwidth = 1;
gbc.gridheight = 1;
gbc.weightx = 0;
gbc.weighty = 0;
gbc.insets = new Insets(0, 0, 0, 0);
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(delayTxt, gbc);
gbc.gridx = 4;
gbc.gridy = 0;
gbc.gridwidth = 1;
gbc.gridheight = 1;
gbc.weightx = 0;
gbc.weighty = 0;
gbc.insets = new Insets(0, 5, 0, 0);
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(apply, gbc);
gbc.gridx = 5;
gbc.gridy = 0;
gbc.gridwidth = 1;
gbc.gridheight = 1;
gbc.weightx = 0;
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(clear, gbc);
gbc.gridx = 6;
gbc.gridy = 0;
gbc.gridwidth = 1;
gbc.gridheight = 1;
gbc.weightx = 0;
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(save, gbc);
if (ConsoleSettings.SHOW_INPUT_FIELD.getValue()) {
listeners = new ArrayList<ConsoleListener>();
JTextField inputField = new JTextField();
inputField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
JTextField textField = (JTextField)evt.getSource();
String command = textField.getText().trim();
if (command.length() == 0) {
return;
}
textField.setText("");
try {
ConsoleWriter writer = new ConsoleWriter();
PrintWriter out = new PrintWriter(writer);
for(ConsoleListener l : listeners) {
if (l.handleCommand(command, out)) {
return;
}
}
appendText("Unknown command: " + command + "\n");
} catch (IOException err) {
appendText(err.getMessage());
}
}
});
addConsoleListener(new ConsoleListener() {
public boolean handleCommand(final String command, final PrintWriter out) throws IOException {
Runnable task = new Runnable() {
public void run() {
try {
// Invoke method to pass command to DHT.
Console.this.mojitoManager.handle(command, out);
} catch (SecurityException e) {
e.printStackTrace(out);
} catch (IllegalArgumentException e) {
e.printStackTrace(out);
} finally {
out.flush();
}
}
};
new ManagedThread(task).start();
return true;
}
});
gbc.gridx = 0;
gbc.gridy = 1;
gbc.gridwidth = 5;
gbc.gridheight = 1;
gbc.weightx = 0;
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
controlsPanel.add(inputField, gbc);
}
add(BorderLayout.SOUTH, controlsPanel);
// Update resource values.
GuiUtils.assignResources(this);
scrollPane.getViewport().setBackground(this.tableColor);
output.setFont(this.outputFont);
refreshLoggers();
}
/**
* Adds the specified listener to the list that is notified when a command
* is entered in the input field.
*/
public void addConsoleListener(ConsoleListener l) {
if (listeners != null && l != null) {
listeners.add(l);
}
}
/**
* Adds console appender to logger.
*/
public void attachLogs() {
logAppender = new WriterAppender(new PatternLayout(
ConsoleSettings.CONSOLE_PATTERN_LAYOUT.get()), new ConsoleWriter());
LogManager.getRootLogger().addAppender(logAppender);
}
/**
* Removes appender from logger.
*/
public void removeLogs() {
LogManager.getRootLogger().removeAppender(logAppender);
}
/**
* Rebuilds the Logger ComboBox.
*/
private void refreshLoggers() {
LoggerRepository repository = LogManager.getLoggerRepository();
Enumeration currentLoggers = repository.getCurrentLoggers();
LoggerComboBoxModel loggerModel = (LoggerComboBoxModel) loggerComboBox.getModel();
int loggerIndex = loggerComboBox.getSelectedIndex();
LoggerNode currentLogger = (loggerIndex >= 0) ? loggerModel.getLogger(loggerIndex) : null;
/*
* Step 1: Create a Tree of Packages and Classes
*/
List<PackageNode> pkgList = new ArrayList<PackageNode>();
Map<String, PackageNode> pkgMap = new HashMap<String, PackageNode>();
while (currentLoggers.hasMoreElements()) {
Logger lggr = (Logger)currentLoggers.nextElement();
String pkg = PackageNode.getPackage(lggr);
PackageNode node = pkgMap.get(pkg);
if (node == null) {
node = new PackageNode(pkg);
pkgMap.put(pkg, node);
pkgList.add(node);
}
node.add(lggr);
}
/*
* Step 2: Sort the Packages by name
*/
Collections.sort(pkgList, new Comparator<PackageNode>() {
public int compare(PackageNode o1, PackageNode o2) {
return o1.getName().compareTo(o2.getName());
}
});
/*
* Step 3: Turn the Tree into a flat List of
*
* Package
* Class
* Class
* Package
* Class
* ...
*/
loggerIndex = -1;
List<LoggerNode> nodes = new ArrayList<LoggerNode>();
for(PackageNode pkgNode : pkgList) {
pkgNode.sort();
nodes.add(pkgNode);
if (loggerIndex == -1 && pkgNode.equals(currentLogger)) {
loggerIndex = nodes.size()-1;
}
for (ClassNode classNode : pkgNode.getNodes()) {
nodes.add(classNode);
if (loggerIndex == -1
&& classNode.equals(currentLogger)) {
loggerIndex = nodes.size()-1;
}
}
}
loggerModel.refreshLoggers(nodes);
boolean empty = nodes.isEmpty();
loggerComboBox.setEnabled(!empty);
levelComboBox.setEnabled(!empty);
apply.setEnabled(!empty);
if (!empty) {
loggerComboBox.setSelectedIndex(loggerIndex >= 0 ? loggerIndex : 0);
selectLoggerLevel();
}
}
/**
* Selects the Level of the currently selected Logger.
*/
private void selectLoggerLevel() {
LoggerComboBoxModel loggerModel = (LoggerComboBoxModel) loggerComboBox.getModel();
LevelComboBoxModel levelModel = (LevelComboBoxModel) levelComboBox.getModel();
int loggerIndex = loggerComboBox.getSelectedIndex();
if (loggerIndex < 0)
return;
Level level = getLevel(loggerModel.getLogger(loggerIndex));
levelModel.setSelectedItem(level);
}
/**
* Applies the currently selected logging level.
*/
private void applyLevel() {
// because the user might not hit enter after
// typing a delay, and then subsequently hit apply,
// also set the delay here.
//
setDelay();
LoggerComboBoxModel loggerModel = (LoggerComboBoxModel) loggerComboBox.getModel();
LevelComboBoxModel levelModel = (LevelComboBoxModel) levelComboBox.getModel();
int loggerIndex = loggerComboBox.getSelectedIndex();
if (loggerIndex < 0)
return;
LoggerNode logger = loggerModel.getLogger(loggerIndex);
Level currentLevel = getLevel(logger);
int levelIndex = levelComboBox.getSelectedIndex();
Level newLevel = (levelIndex > 0) ? levelModel.getLevel(levelIndex) : null;
if (!currentLevel.equals(newLevel)) {
logger.setLevel(newLevel);
loggerComboBox.setSelectedIndex(loggerIndex); // update the ComboxBox (the text)
loggerModel.updateIndex(loggerIndex);
}
}
/**
* Appends text to the console.
*
* @param text the text to be appended
*/
public void appendText(final String text) {
if (!output.isEnabled()) {
return;
}
// if there is a non-zero delay value, then append
// the text to the buffer instead of immediately
// scheduling it to be added to the console text
// area.
//
if (0 != delay) {
delayBuf.append(text);
return;
}
invokeLaterConsoleAppend(text);
}
/**
* Clears the console.
*/
public void clear() {
output.setText(null);
}
/**
* Saves the current Console output and the stack traces of
* all active Threads if available.
*/
public void save() {
try {
output.setEnabled(altCtrlDown);
String log = output.getText().trim();
String traces = ThreadUtils.getAllStackTraces();
if (log.length() == 0
&& traces.length() == 0) {
return;
}
if (altCtrlDown) {
StringBuilder buffer = new StringBuilder();
buffer.append("-- BEGIN STACK TRACES --\n");
buffer.append(traces.length() > 0 ? traces : "NONE");
buffer.append("\n-- END STACK TRACES --\n");
appendText(buffer.toString());
} else {
StringBuilder buffer = new StringBuilder();
buffer.append(new Date()).append("\n\n");
Exception e = new Exception() {
@Override
public void printStackTrace(PrintWriter out) {
/* PRINT NOTHING */
}
};
LocalClientInfo info = this.localClientInfoFactory.createLocalClientInfo(
e, Thread.currentThread().getName(), "Console Log", false);
buffer.append(info.toBugReport());
buffer.append("-- BEGIN STACK TRACES --\n");
buffer.append(traces.length() > 0 ? traces : "NONE");
buffer.append("\n-- END STACK TRACES --\n");
buffer.append("\n-- BEGIN LOG --\n");
buffer.append(log.length() > 0 ? log : "NONE");
buffer.append("\n-- END LOG --\n");
File file = FileChooser.getSaveAsFile(this,
I18nMarker.marktr("Save As"),
new File(FileChooser.getLastInputDirectory(), "limewire-log.txt"));
if (file == null) {
return;
}
BufferedWriter out = new BufferedWriter(new FileWriter(file));
out.write(buffer.toString());
out.close();
}
} catch (IOException err) {
ErrorService.error(err);
} finally {
output.setEnabled(true);
}
}
/**
* Appends consoleTxt to the console text area in the
* swing thread.
*
* @param consoleTxt string to append to the console text area
*/
public void invokeLaterConsoleAppend (final String consoleTxt) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
_appendText(consoleTxt);
}
});
}
/**
* Appends the specified text to the console text area. This should be
* called from within the Swing thread.
*/
public void _appendText (String consoleTxt) {
output.append(consoleTxt);
int excess = output.getDocument().getLength() - idealSize;
if (excess >= maxExcess) {
output.replaceRange("", 0, excess);
}
if (scroll)
output.setCaretPosition(output.getText().length());
}
/**
* Called when the value of the delay text field is
* changed. Parse the value as an integer, understood
* to be seconds, and set the update delay to that
* value.
*/
public void setDelay() {
delay = delayTxt.getValue(0);
// if the delay is set to zero, flush the buffer to
// the console text area, since it is possible that
// some text was dumped in there, and we don't want
// to lose it.
//
if (0 == delay) {
synchronized (delayBuf) {
if (0 != delayBuf.length()) {
String strbuf = delayBuf.toString();
delayBuf.delete(0, strbuf.length());
invokeLaterConsoleAppend(strbuf);
}
}
if (delayTimer != null) {
delayTimer.stop();
delayTimer = null;
}
}
// a non-zero delay value. schedule a timer to
// update the console text area every 'delay'
// seconds.
//
else {
if (null == delayTimer) {
delayTimer = new Timer(delay*1000, new ActionListener() {
public void actionPerformed (ActionEvent evt) {
if (0 == delayBuf.length())
return;
synchronized(delayBuf) {
String strbuf = delayBuf.toString();
delayBuf.delete(0, strbuf.length());
_appendText(strbuf);
}
}
});
delayTimer.start();
}
else
delayTimer.setDelay(delay * 1000);
}
}
/**
* Returns Level.OFF instead of null if logging is turned off.
*/
private static final Level getLevel(LoggerNode logger) {
Level level = logger.getLevel();
if (level == null)
level = Level.OFF;
return level;
}
private final class ConsoleWriter extends Writer {
private StringBuilder buffer = new StringBuilder();
@Override
public void write(char[] cbuf, int off, int len) {
buffer.append(cbuf, off, len);
}
@Override
public void close() {
buffer = null;
}
@Override
public void flush() {
Console.this.appendText(buffer.toString());
buffer.setLength(0);
}
}
/**
* Logger ComboBox model.
*/
private static class LoggerComboBoxModel extends DefaultComboBoxModel {
private static final String SPACER = " ";
private List<LoggerNode> nodes = Collections.emptyList();
private void updateIndex(int index) {
fireContentsChanged(this, index, index);
}
private void refreshLoggers(List<LoggerNode> nodes) {
this.nodes = nodes;
fireContentsChanged(this, 0, nodes.size());
}
@Override
public int getSize() {
return nodes.size();
}
private LoggerNode getLogger(int index) {
return nodes.get(index);
}
@Override
public Object getElementAt(int index) {
LoggerNode logger = getLogger(index);
Level level = getLevel(logger);
if (level.equals(Level.OFF)) {
if (logger.isLeaf()) {
return SPACER + logger.getName();
} else {
return logger.getName();
}
} else {
if (logger.isLeaf()) {
return SPACER + logger.getName() + " [" + level + "]";
} else {
return logger.getName();
}
}
}
}
/**
* Logging level ComboBox model.
*/
private static class LevelComboBoxModel extends DefaultComboBoxModel {
private final Level[] levels = new Level[] {
Level.OFF,
Level.ALL,
Level.DEBUG,
Level.ERROR,
Level.FATAL,
Level.INFO,
Level.WARN
};
@Override
public int getSize() {
return levels.length;
}
private Level getLevel(int index) {
return levels[index];
}
@Override
public Object getElementAt(int index) {
return getLevel(index).toString();
}
}
/**
* A interface to build a very simple Tree of
* Packages and Classes.
*/
private interface LoggerNode {
boolean isLeaf();
Level getLevel();
void setLevel(Level level);
String getName();
}
private static class PackageNode implements LoggerNode {
private String pkg;
private List<ClassNode> classNodes = new ArrayList<ClassNode>();
private PackageNode(String pkg) {
this.pkg = pkg;
}
public void add(Logger logger) {
classNodes.add(new ClassNode(this, logger));
}
public Level getLevel() {
return Level.OFF;
}
public void setLevel(Level level) {
for(int i = classNodes.size()-1; i >= 0; i--) {
classNodes.get(i).setLevel(level);
}
}
public boolean isLeaf() {
return false;
}
public void sort() {
Collections.sort(classNodes, new Comparator<ClassNode>() {
public int compare(ClassNode o1, ClassNode o2) {
return o1.getName().compareTo(o2.getName());
}
});
}
public List<ClassNode> getNodes() {
return classNodes;
}
@Override
public int hashCode() {
return pkg.hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof PackageNode)) {
return false;
}
return pkg.equals(((PackageNode)o).pkg);
}
public String getName() {
return pkg;
}
@Override
public String toString() {
return getName();
}
private static String getPackage(Logger logger) {
String name = logger.getName();
int i = name.lastIndexOf('.');
return (i != -1) ? name.substring(0, i) + ".*" : name + ".*";
}
}
private static class ClassNode implements LoggerNode {
private PackageNode parent;
private Logger logger;
private ClassNode(PackageNode parent, Logger logger) {
this.parent = parent;
this.logger = logger;
}
public PackageNode getParent() {
return parent;
}
public Logger getLogger() {
return logger;
}
public Level getLevel() {
return logger.getLevel();
}
public void setLevel(Level level) {
logger.setLevel(level);
}
public boolean isLeaf() {
return true;
}
public String getName() {
return logger.getName();
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ClassNode)) {
return false;
}
return getName().equals(((ClassNode)o).getName());
}
@Override
public String toString() {
return getName();
}
}
/**
* Defines a listener to handle commands entered in the input field.
*/
public static interface ConsoleListener {
public boolean handleCommand(String command, PrintWriter out) throws IOException;
}
}