/* * 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(); } } }