/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
* LogPanel
* Copyright (C) 2008 University of Waikato, Hamilton, New Zealand
*
*/
package weka.gui.beans;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.Iterator;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import weka.gui.Logger;
/**
* Class for displaying a status area (made up of a variable number of
* lines) and a log area.
*
* @author mhall (mhall{[at]}pentaho{[dot]}com)
* @version $Revision: 7251 $
*/
public class LogPanel extends JPanel implements Logger {
/**
* Holds the index (line number) in the JTable of each component
* being tracked.
*/
protected HashMap<String,Integer> m_tableIndexes =
new HashMap<String, Integer>();
/**
* Holds the timers associated with each component being tracked.
*/
private HashMap<String, Timer> m_timers =
new HashMap<String, Timer>();
/**
* The table model for the JTable used in the status area
*/
private final DefaultTableModel m_tableModel;
/**
* The table for the status area
*/
private JTable m_table;
/**
* Tabbed pane to hold both the status and the log
*/
private JTabbedPane m_tabs = new JTabbedPane();
/**
* The log panel to delegate log messages to.
*/
private weka.gui.LogPanel m_logPanel =
new weka.gui.LogPanel(null, false, true, false);
public LogPanel() {
String[] columnNames = {"Component", "Parameters", "Time", "Status"};
m_tableModel = new DefaultTableModel(columnNames, 0);
// JTable with error/warning indication for rows.
m_table = new JTable() {
public Class getColumnClass(int column) {
return getValueAt(0, column).getClass();
}
public Component prepareRenderer(TableCellRenderer renderer,
int row, int column) {
Component c = super.prepareRenderer(renderer, row, column);
if (!c.getBackground().equals(getSelectionBackground()))
{
String type = (String)getModel().getValueAt(row, 3);
Color backgroundIndicator = null;
if (type.startsWith("ERROR")) {
backgroundIndicator = Color.RED;
} else if (type.startsWith("WARNING")) {
backgroundIndicator = Color.YELLOW;
} else if (type.startsWith("INTERRUPTED")) {
backgroundIndicator = Color.MAGENTA;
}
c.setBackground(backgroundIndicator);
}
return c;
}
};
m_table.setModel(m_tableModel);
m_table.getColumnModel().getColumn(0).setPreferredWidth(100);
m_table.getColumnModel().getColumn(1).setPreferredWidth(150);
m_table.getColumnModel().getColumn(2).setPreferredWidth(30);
m_table.getColumnModel().getColumn(3).setPreferredWidth(400);
m_table.setShowVerticalLines(true);
JPanel statusPan = new JPanel();
statusPan.setLayout(new BorderLayout());
statusPan.add(new JScrollPane(m_table), BorderLayout.CENTER);
m_tabs.addTab("Status", statusPan);
m_tabs.addTab("Log", m_logPanel);
setLayout(new BorderLayout());
add(m_tabs, BorderLayout.CENTER);
}
/**
* Clear the status area.
*/
public void clearStatus() {
// stop any running timers
Iterator<Timer> i = m_timers.values().iterator();
while (i.hasNext()) {
i.next().stop();
}
// clear the map entries
m_timers.clear();
m_tableIndexes.clear();
// clear the rows from the table
while (m_tableModel.getRowCount() > 0) {
m_tableModel.removeRow(0);
}
}
/**
* The JTable used for the status messages (in case clients
* want to attach listeners etc.)
*
* @return the JTable used for the status messages.
*/
public JTable getStatusTable() {
return m_table;
}
/**
* Sends the supplied message to the log area. These message will typically
* have the current timestamp prepended, and be viewable as a history.
*
* @param message the log message
*/
public synchronized void logMessage(String message) {
// delegate to the weka.gui.LogPanel
m_logPanel.logMessage(message);
}
/**
* Sends the supplied message to the status area. These messages are
* typically one-line status messages to inform the user of progress
* during processing (i.e. it doesn't matter if the user doesn't happen
* to look at each message). These messages have the following format:
*
* <Component name (needs to be unique)>|<Parameter string (optional)|<Status message>
*
* @param message the status message.
*/
public synchronized void statusMessage(String message) {
boolean hasDelimiters = (message.indexOf('|') > 0);
String stepName = "";
String stepHash = "";
String stepParameters = "";
String stepStatus = "";
if (!hasDelimiters) {
stepName = "Unknown";
stepHash = "Unknown";
stepStatus = message;
} else {
// Extract the fields of the status message
stepHash = message.substring(0, message.indexOf('|'));
message = message.substring(message.indexOf('|') + 1,
message.length());
// See if there is a unique object ID in the stepHash string
if (stepHash.indexOf('$') > 0) {
// Extract the step name
stepName = stepHash.substring(0, stepHash.indexOf('$'));
} else {
stepName = stepHash;
}
// See if there are any step parameters to extract
if (message.indexOf('|') > 0) {
stepParameters = message.substring(0, message.indexOf('|'));
stepStatus = message.substring(message.indexOf('|') + 1,
message.length());
} else {
// set the status message to the remainder
stepStatus = message;
}
}
// Now see if this step is in the hashmap
if (m_tableIndexes.containsKey(stepHash)) {
// Get the row number and update the table model...
final Integer rowNum = m_tableIndexes.get(stepHash);
if (stepStatus.equalsIgnoreCase("remove") ||
stepStatus.equalsIgnoreCase("remove.")) {
//m_tableModel.fireTableDataChanged();
m_tableIndexes.remove(stepHash);
m_timers.get(stepHash).stop();
m_timers.remove(stepHash);
// now need to decrement all the row indexes of
// any rows greater than this one
Iterator<String> i = m_tableIndexes.keySet().iterator();
while (i.hasNext()) {
String nextKey = i.next();
int index = m_tableIndexes.get(nextKey).intValue();
if (index > rowNum.intValue()) {
index--;
//System.err.println("*** " + nextKey + " needs decrementing to " + index);
m_tableIndexes.put(nextKey, index);
// System.err.println("new index " + m_rows.get(nextKey).intValue());
}
}
// Remove the entry...
if (!SwingUtilities.isEventDispatchThread()) {
try {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
m_tableModel.removeRow(rowNum);
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
m_tableModel.removeRow(rowNum);
}
} else {
final String stepNameCopy = stepName;
final String stepStatusCopy = stepStatus;
final String stepParametersCopy = stepParameters;
if (!SwingUtilities.isEventDispatchThread()) {
try {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// ERROR overrides INTERRUPTED
if (!(stepStatusCopy.startsWith("INTERRUPTED") &&
((String)m_tableModel.getValueAt(rowNum.intValue(), 3)).startsWith("ERROR"))) {
m_tableModel.setValueAt(stepNameCopy, rowNum.intValue(), 0);
m_tableModel.setValueAt(stepParametersCopy, rowNum.intValue(), 1);
m_tableModel.setValueAt(m_table.getValueAt(rowNum.intValue(), 2), rowNum.intValue(), 2);
m_tableModel.setValueAt(stepStatusCopy, rowNum.intValue(), 3);
}
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
if (!(stepStatusCopy.startsWith("INTERRUPTED") &&
((String)m_tableModel.getValueAt(rowNum.intValue(), 3)).startsWith("ERROR"))) {
m_tableModel.setValueAt(stepNameCopy, rowNum.intValue(), 0);
m_tableModel.setValueAt(stepParametersCopy, rowNum.intValue(), 1);
m_tableModel.setValueAt(m_table.getValueAt(rowNum.intValue(), 2), rowNum.intValue(), 2);
m_tableModel.setValueAt(stepStatusCopy, rowNum.intValue(), 3);
}
}
if (stepStatus.startsWith("ERROR") ||
stepStatus.startsWith("INTERRUPTED") ||
stepStatus.equalsIgnoreCase("finished") ||
stepStatus.equalsIgnoreCase("finished.") ||
stepStatus.equalsIgnoreCase("done") ||
stepStatus.equalsIgnoreCase("done.") ||
stepStatus.equalsIgnoreCase("stopped") ||
stepStatus.equalsIgnoreCase("stopped.")) {
// stop the timer.
m_timers.get(stepHash).stop();
} else if (!m_timers.get(stepHash).isRunning()) {
// need to create a new one in order to reset the
// elapsed time.
installTimer(stepHash);
}
// m_tableModel.fireTableCellUpdated(rowNum.intValue(), 3);
}
} else if (!stepStatus.equalsIgnoreCase("Remove") &&
!stepStatus.equalsIgnoreCase("Remove.")) {
// Add this one to the hash map
int numKeys = m_tableIndexes.keySet().size();
m_tableIndexes.put(stepHash, numKeys);
// Now add a row to the table model
final Object[] newRow = new Object[4];
newRow[0] = stepName;
newRow[1] = stepParameters;
newRow[2] = "-";
newRow[3] = stepStatus;
final String stepHashCopy = stepHash;
try {
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
m_tableModel.addRow(newRow);
//m_tableModel.fireTableDataChanged();
}
});
} else {
m_tableModel.addRow(newRow);
}
installTimer(stepHashCopy);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
private void installTimer(final String stepHash) {
final long startTime = System.currentTimeMillis();
Timer newTimer = new Timer(1000, new ActionListener() {
public void actionPerformed(ActionEvent e) {
synchronized(LogPanel.this) {
if (m_tableIndexes.containsKey(stepHash)) {
final Integer rn = m_tableIndexes.get(stepHash);
long elapsed = System.currentTimeMillis() - startTime;
long seconds = elapsed / 1000;
long minutes = seconds / 60;
final long hours = minutes / 60;
seconds = seconds - (minutes * 60);
minutes = minutes - (hours * 60);
final long seconds2 = seconds;
final long minutes2 = minutes;
if (!SwingUtilities.isEventDispatchThread()) {
try {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
m_tableModel.
setValueAt("" + hours + ":" + minutes2 + ":" + seconds2, rn.intValue(), 2);
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
m_tableModel.
setValueAt("" + hours + ":" + minutes2 + ":" + seconds2, rn.intValue(), 2);
}
}
}
}
});
m_timers.put(stepHash, newTimer);
newTimer.start();
}
/**
* Main method to test this class.
*
* @param args any arguments (unused)
*/
public static void main(String[] args) {
try {
final javax.swing.JFrame jf = new javax.swing.JFrame("Status/Log Panel");
jf.getContentPane().setLayout(new BorderLayout());
final LogPanel lp = new LogPanel();
jf.getContentPane().add(lp, BorderLayout.CENTER);
jf.getContentPane().add(lp, BorderLayout.CENTER);
jf.addWindowListener(new java.awt.event.WindowAdapter() {
public void windowClosing(java.awt.event.WindowEvent e) {
jf.dispose();
System.exit(0);
}
});
jf.pack();
jf.setVisible(true);
lp.statusMessage("Step 1|Some options here|A status message");
lp.statusMessage("Step 2$hashkey|Status message: no options");
Thread.sleep(3000);
lp.statusMessage("Step 2$hashkey|Funky Chickens!!!");
Thread.sleep(3000);
lp.statusMessage("Step 1|Some options here|finished");
//lp.statusMessage("Step 1|Some options here|back again!");
Thread.sleep(3000);
lp.statusMessage("Step 2$hashkey|ERROR! More Funky Chickens!!!");
Thread.sleep(3000);
lp.statusMessage("Step 2$hashkey|WARNING - now a warning...");
Thread.sleep(3000);
lp.statusMessage("Step 2$hashkey|Back to normal.");
Thread.sleep(3000);
lp.statusMessage("Step 2$hashkey|INTERRUPTED.");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}