Initial checkin of directory structure and source code of the java Activity
[privoxy.git] / src / java / org / privoxy / activityconsole / ActivityConsoleGui.java
diff --git a/src/java/org/privoxy/activityconsole/ActivityConsoleGui.java b/src/java/org/privoxy/activityconsole/ActivityConsoleGui.java
new file mode 100644 (file)
index 0000000..2f85dc3
--- /dev/null
@@ -0,0 +1,629 @@
+/*********************************************************************
+ *
+ * File        :  $Source$
+ *
+ * Purpose     :  Provide the central GUI for displaying Privoxy
+ *                statistics.  It can be contacted either by the
+ *                local machine or other machines in a network and
+ *                display consolidated, tabular statistics.
+ *
+ * Copyright   :  Written by and Copyright (C) 2003 the SourceForge
+ *                Privoxy team. http://www.privoxy.org/
+ *
+ *                Based on the Internet Junkbuster originally written
+ *                by and Copyright (C) 1997 Anonymous Coders and
+ *                Junkbusters Corporation.  http://www.junkbusters.com
+ *
+ *                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.
+ *
+ *                The GNU General Public License should be included with
+ *                this file.  If not, you can view it at
+ *                http://www.gnu.org/copyleft/gpl.html
+ *                or write to the Free Software Foundation, Inc., 59
+ *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ *
+ * Revisions   :
+ *    $Log$
+ *********************************************************************/
+
+package org.privoxy.activityconsole;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+
+/**
+ * The main Activity Console GUI.
+ * @author Last Modified By: $Author$
+ * @version $Rev$-$Date$$State$
+ */
+public final class ActivityConsoleGui extends JFrame implements ActionListener
+{
+  private static final String
+  COPYRIGHT = org.privoxy.activityconsole.Copyright.COPYRIGHT;
+
+  ActivityConsoleGui parent_;
+  ServerThread _serverThread = null;
+  private ListResourceBundle resStrings = (ListResourceBundle)ListResourceBundle.getBundle("org.privoxy.activityconsole.ActivityConsoleResources");
+
+  JTable _table;
+
+  SortableTableModel _model;
+
+  Vector
+  _tableColumnMap = new Vector();
+
+  JPanel
+  mainPanel = new JPanel(new GridBagLayout());
+
+  JMenuItem _deleteItem, _quitItem, _configItem;
+
+  private DefaultTableCellRenderer _statRenderer = null;
+
+  int _port = 0;
+
+  /**
+   * Constructor of the Activity Console GUI.
+   * @param arg the port to serve connections on - as an int parsed from the String
+   */
+  public ActivityConsoleGui(String arg)
+  {
+    int i;
+
+    addWindowListener(new WindowCloseMonitor());
+
+    JMenuBar menuBar = new JMenuBar();
+
+    JMenu menuFile = new JMenu(resStrings.getString("menuFile"));
+    MenuAction quitAction = new MenuAction(resStrings.getString("menuFileQuit"));
+    JMenu menuEdit = new JMenu(resStrings.getString("menuEdit"));
+    _quitItem = menuFile.add(quitAction);
+    menuBar.add(menuFile);
+    _configItem = menuEdit.add(new MenuAction(resStrings.getString("menuEditConfig")));
+    menuBar.add(menuEdit);
+    _deleteItem = menuEdit.add(new MenuAction(resStrings.getString("menuEditDelete")));
+    menuBar.add(menuEdit);
+    this.setJMenuBar(menuBar);
+    _deleteItem.setEnabled(false);
+
+    try
+    {
+      _port = Integer.parseInt(arg);
+      if (_port < 0)
+        _port = 0;
+    }
+    catch (Throwable t)
+    {
+      _port = 0;
+    }
+
+    /**
+     * The cell renderer for the StatWidget Component - simply returns the component
+     * itself.  Additionally, it has the extra hack of telling the StatWidget where
+     * it is in the table so it can update itself again when it comes time to flash.
+     */
+    _statRenderer = new DefaultTableCellRenderer()
+    {
+      public Component getTableCellRendererComponent(JTable table,
+                                                     Object value,
+                                                     boolean isSelected,
+                                                     boolean hasFocus,
+                                                     int row,
+                                                     int column)
+      {
+        /* Housekeeping: keep track of the row, column and table references as we go */
+        ((StatWidget)value).setRowColTable(row,column,table);
+        return(Component)value;
+      }
+
+      public void setValue(Object value)
+      {
+        Color color = null;
+        try
+        {
+          color = (Color)value;
+        }
+        catch (ClassCastException e)
+        {
+          color = Color.white;
+        }
+        setBackground(color);
+      }
+    };
+
+    Vector data = new Vector();
+    _model = new SortableTableModel(data, getColumnNames());
+    _table = new JTable(_model);
+    _table.setPreferredScrollableViewportSize(new Dimension(800,50));
+    _table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+    _table.setCellSelectionEnabled(false);
+    _table.setRowSelectionAllowed(false);
+
+    /*
+     * The first column is normal and text-ish - the host address and
+     * port being served (i.e. 127.0.0.1:8118).  The rest need to have statistic
+     * renderers defined.
+     */
+    SortButtonRenderer _headerRenderer = new SortButtonRenderer();
+    TableColumnModel cm = _table.getColumnModel();
+    /* Make the first column twice the width of the others. It shows bigger stuff. */
+    cm.getColumn(0).setPreferredWidth(cm.getColumn(0).getPreferredWidth() * 2);
+    cm.getColumn(0).setHeaderRenderer(_headerRenderer);
+    for (i = 1;i<_model.getColumnCount();i++)
+    {
+      cm.getColumn(i).setPreferredWidth((int)(cm.getColumn(i).getPreferredWidth() * 1));
+      cm.getColumn(i).setCellRenderer(_statRenderer);
+      cm.getColumn(i).setHeaderRenderer(_headerRenderer);
+    }
+
+    JTableHeader header = _table.getTableHeader();
+    header.addMouseListener(new HeaderListener(header,_headerRenderer));
+
+    ListSelectionModel csm = _table.getSelectionModel();
+    csm.addListSelectionListener(new SelectedListener(csm));
+
+    ActivityConsoleGuiUtil.constrain(mainPanel, new JScrollPane(_table),
+                                     1, 1, // X, Y Coordinates
+                                     1, 1, // Grid width, height
+                                     GridBagConstraints.BOTH,  // Fill value
+                                     GridBagConstraints.WEST,  // Anchor value
+                                     1.0,1.0,  // Weight X, Y
+                                     0, 0, 0, 0 ); // Top, left, bottom, right insets
+
+    this.getContentPane().add(mainPanel, BorderLayout.CENTER);
+
+    parent_ = this;
+    this.pack();
+    _table.setPreferredScrollableViewportSize(new Dimension(_table.getWidth(),50));
+    this.pack();
+
+    if (_port > 0)
+    {
+      _serverThread = new ServerThread(this, _port);
+      _serverThread.start();
+    }
+    updateTitle(_port);
+    setBounds(ActivityConsoleGuiUtil.center(this.getSize()));
+    this.show();
+  }
+
+  /**
+   * Updates the title bar with the port currently being served.
+   * @param port the port being served
+   */
+  public void updateTitle(int port)
+  {
+    String title = resStrings.getString("guiTitle");
+
+    title = StringUtil.replaceSubstring(title,"%1",""+port);
+    setTitle(title);
+  }
+
+  public void actionPerformed(ActionEvent e)
+  {
+  }
+
+  class MenuAction extends AbstractAction
+  {
+    public MenuAction(String text)
+    {
+      super(text,null);
+    }
+
+    public MenuAction(String text, Icon icon)
+    {
+      super(text,icon);
+    }
+
+    public void actionPerformed(ActionEvent e)
+    {
+      if (e.getSource() == _quitItem)
+      {
+        parent_.setVisible(false);
+        parent_.dispose();
+        System.exit(0);
+      }
+      else if (e.getSource() == _deleteItem)
+      {
+        deleteAction();
+      }
+      else if (e.getSource() == _configItem)
+      {
+        changeServerAction();
+      }
+    }
+  }
+
+  /**
+   * Asks the user to specify a new port to serve
+   */
+  public void changeServerAction()
+  {
+    int port = -1;
+    String message = resStrings.getString("guiNewPortPrompt");
+    message = StringUtil.replaceSubstring(message,"%1",""+_port);
+
+    String inputValue = JOptionPane.showInputDialog(this,
+                                                    message,
+                                                    resStrings.getString("guiNewPortTitle"),
+                                                    JOptionPane.QUESTION_MESSAGE);
+    if (inputValue != null)
+      try
+      {
+        port = Integer.parseInt(inputValue);
+      }
+      catch (Throwable t)
+      {
+        port = -1;
+      }
+    if (port < 1)
+      JOptionPane.showMessageDialog(null, resStrings.getString("guiNewPortErrorPrompt"), resStrings.getString("guiNewPortErrorTitle"), JOptionPane.ERROR_MESSAGE);
+    else
+    {
+      if (_port != port)
+      {
+        if (_serverThread != null)
+        {
+          _serverThread.doClose();
+          _serverThread.interrupt();
+          _serverThread = null;
+        }
+        _port = port;
+        _serverThread = new ServerThread(parent_, port);
+        _serverThread.start();
+        updateTitle(_port);
+      }
+    }
+  }
+
+  /**
+   * Deletes the "selected" row after seeking confirmation
+   */
+  public void deleteAction()
+  {
+    int numSelections = _table.getSelectedRowCount();
+    int selRow = _table.getSelectedRow();
+    if (numSelections > 0)
+    {
+      if ((selRow > -1) &&
+          (selRow < _table.getRowCount()))
+      {
+        /* Ask for confirmation */
+        String message = resStrings.getString("guiDeleteConfirmPrompt");
+        message = StringUtil.replaceSubstring(message,"%1",(String)_model.getValueAt(selRow,0));
+        int ret = JOptionPane.showConfirmDialog(null,
+                                                message,
+                                                resStrings.getString("guiDeleteConfirmTitle"),
+                                                JOptionPane.YES_NO_OPTION);
+        if (ret == JOptionPane.YES_OPTION)
+        {
+          _model.removeRow(selRow);
+        }
+      }
+    }
+  }
+
+  /**
+   * Retrieves the names of the column headers.  This should be made to read a properties
+   * file. FIXME.
+   * @return Vector the set of column names.  It also has the side-effect of adding
+   * entries to the global column mapping Vector where we map the staus integer identifiers
+   * to the column positions and names.  Should probably fix that too.
+   */
+  public Vector getColumnNames()
+  {
+    Vector names = new Vector();
+
+    names.addElement(resStrings.getString("guiDefaultColumn0"));
+    names.addElement(resStrings.getString("guiDefaultColumn1"));
+    names.addElement(resStrings.getString("guiDefaultColumn2"));
+    names.addElement(resStrings.getString("guiDefaultColumn3"));
+    names.addElement(resStrings.getString("guiDefaultColumn4"));
+    names.addElement(resStrings.getString("guiDefaultColumn5"));
+    names.addElement(resStrings.getString("guiDefaultColumn6"));
+    names.addElement(resStrings.getString("guiDefaultColumn7"));
+    names.addElement(resStrings.getString("guiDefaultColumn8"));
+    names.addElement(resStrings.getString("guiDefaultColumn9"));
+    names.addElement(resStrings.getString("guiDefaultColumn10"));
+
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn1"),1));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn2"),2));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn3"),3));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn4"),4));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn5"),5));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn6"),6));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn7"),7));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn8"),8));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn9"),9));
+    _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn10"),10));
+
+    return names;
+  }
+
+  /**
+   * Parses a String of statistics coming from Privoxy.
+   * @param line The statistics string sent from Privoxy
+   * @param from the hostname that sent the statistics
+   */
+  public void updateStats(String line, String from)
+  {
+    /*
+     * An example line of data:
+     * 0:8118 1:0 2:0 3:0 4:0 5:0 6:0 7:0 8:0 9:0 10:0
+     */
+    int key, value;
+    String tableKey = "", key_str, value_str, token;
+    StringTokenizer colonToken;
+    StringTokenizer spaceTokens = new StringTokenizer(line);
+    Vector stats = new Vector();
+
+    while (spaceTokens.hasMoreTokens())
+    {
+      token = spaceTokens.nextToken();
+      colonToken = new StringTokenizer(token,":");
+      if (colonToken.hasMoreTokens())
+      {
+        key_str = null; value_str = null;
+        key = -1; value = 0;
+
+        /* First token is the key */
+        key_str = colonToken.nextToken();
+        try
+        {
+          key = Integer.parseInt(key_str);
+        }
+        catch (NumberFormatException n)
+        {
+          key = -1;
+        }
+
+        if ((colonToken.hasMoreTokens()) && (key > -1))
+        {
+          /* Next token, if present, is the value */
+          value_str = colonToken.nextToken();
+          if (key == 0)
+          {
+            /*
+             * The key to the table row is the concatenation of the serving
+             * IP address string, a full colon, and the port string.
+             */
+            tableKey = from + ":" + value_str;
+          }
+          try
+          {
+            value = Integer.parseInt(value_str);
+            stats.addElement((Object)(new Stat(key, value)));
+          }
+          catch (NumberFormatException n)
+          {
+            value = 0;
+          }
+        }
+      }
+    }
+    if ((tableKey.compareTo("") != 0) && (stats.size() > 0))
+    {
+      updateTable(tableKey, stats);
+      stats.removeAllElements();
+    }
+    stats = null;
+  }
+
+  /**
+   * Updates (or creates) a line in the table representing the incoming packet of stats.
+   * @param tableKey Our key to a unique table row: the hostname concatenated with the Privoxy port being served.
+   * @param stats Vector of statistics elements
+   */
+  public void updateTable(String tableKey, Vector stats)
+  {
+    boolean found = false;
+    for (int i = 0; i < _model.getRowCount(); i++)
+    {
+      if (((String)_model.getValueAt(i,_table.convertColumnIndexToView(0))).compareTo(tableKey) == 0)
+      {
+        updateTableEntry(i, stats);
+        found = true;
+      }
+    }
+    /* If we can't find one in the table already... */
+    if (found == false)
+      createTableEntry(tableKey, stats);
+  }
+
+  /**
+   * Creates a line in the table representing the incoming packet of stats.
+   * @param tableKey Our key to a unique table row: the hostname concatenated with the Privoxy port being served.
+   * @param stats Vector of statistics elements
+   */
+  public void createTableEntry(String tableKey, Vector stats)
+  {
+    int i, j;
+    Vector row = new Vector();
+    boolean added = false;
+
+    row.addElement(tableKey);
+
+    /*
+     * If we have a key (in stats) that maps to a key in the _tableColumnMap,
+     * then we add it to the vector destined for the table.
+     */
+    for (i = 0; i < _tableColumnMap.size(); i ++)
+    {
+      for (j = 0; j < stats.size(); j++)
+      {
+        if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
+        {
+          row.addElement(new StatWidget(((Stat)stats.elementAt(j)).getValue(),500));
+          added = true;
+        }
+      }
+      if (added == false)
+      {
+        row.addElement(new StatWidget(0,500));
+      }
+      else
+        added = false;
+    }
+    _model.addRow(row);
+  }
+
+  /**
+   * Updates a line in the table by tweaking the StatWidgets.
+   * @param row the table row if the StatWidget
+   * @param stats The Vector of Stat elements to update the table row with
+   */
+  public void updateTableEntry(int row, Vector stats)
+  {
+    int i, j;
+
+    for (i = 0; i < _tableColumnMap.size(); i ++)
+    {
+      for (j = 0; j < stats.size(); j++)
+      {
+        if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
+        {
+          ((StatWidget)_model.getValueAt(row,i+1)).updateValue(((Stat)stats.elementAt(j)).getValue());
+          stats.removeElementAt(j);
+          break;
+        }
+      }
+    }
+  }
+
+  /**
+   * Worker class to offer a clickable table header for sorting.
+   */
+  class HeaderListener extends MouseAdapter
+  {
+    JTableHeader   header;
+    SortButtonRenderer renderer;
+
+    HeaderListener(JTableHeader header,SortButtonRenderer renderer)
+    {
+      this.header   = header;
+      this.renderer = renderer;
+    }
+
+    public void mousePressed(MouseEvent e)
+    {
+      Point click = e.getPoint();
+      int col = header.columnAtPoint(click);
+      int margin1, margin2;
+      int sortCol = header.getTable().convertColumnIndexToModel(col);
+
+      /* Don't perform the sort if the user is just trying to resize the columns. */
+      margin1 = header.columnAtPoint(new Point(click.x+3,click.y));
+      margin2 = header.columnAtPoint(new Point(click.x-3,click.y));
+      if ((col == margin1) && (col == margin2))
+      {
+        renderer.setPressedColumn(col);
+        renderer.setSelectedColumn(col);
+        header.repaint();
+
+        if (header.getTable().isEditing())
+        {
+          header.getTable().getCellEditor().stopCellEditing();
+        }
+
+        boolean isAscent;
+        if (SortButtonRenderer.DOWN == renderer.getState(col))
+        {
+          isAscent = true;
+        }
+        else
+        {
+          isAscent = false;
+        }
+        ((SortableTableModel)header.getTable().getModel())
+        .sortByColumn(sortCol, isAscent);    
+      }
+    }
+
+    public void mouseReleased(MouseEvent e)
+    {
+      int col = header.columnAtPoint(e.getPoint());
+      renderer.setPressedColumn(-1);                // clear
+      header.repaint();
+    }
+  }
+
+  /**
+   * Worker class to tell the menu when it's OK to delete a row (i.e. when a row gets
+   * selected).  This doesn't work reliably, but it's better than nothing.
+   */
+  public class SelectedListener implements ListSelectionListener
+  {
+    ListSelectionModel model;
+
+    public SelectedListener(ListSelectionModel lsm)
+    {
+      model = lsm;
+    }
+
+    public void valueChanged(ListSelectionEvent lse)
+    {
+      // NOTE - keep this in sync with columnSelectionChanged below...
+      int numSelections = _table.getSelectedRowCount();
+      int selRow = _table.getSelectedRow();
+      if (numSelections > 0)
+      {
+        if ((selRow > -1) &&
+            (selRow < _table.getRowCount()))
+        {
+          _deleteItem.setEnabled(true);
+        }
+        else
+          _deleteItem.setEnabled(false);
+      }
+      else
+        _deleteItem.setEnabled(false);
+    }
+    public void columnSelectionChanged(ListSelectionEvent lse)
+    {
+      // NOTE - keep this in sync with valueChanged above...
+      int numSelections = _table.getSelectedRowCount();
+      int selRow = _table.getSelectedRow();
+      if (numSelections > 0)
+      {
+        if ((selRow > -1) &&
+            (selRow < _table.getRowCount()))
+        {
+          _deleteItem.setEnabled(true);
+        }
+        else
+          _deleteItem.setEnabled(false);
+      }
+      else
+        _deleteItem.setEnabled(false);
+    }
+  }
+
+  /**
+   * Watch for the window closing event.  Dunno why swing doesn't handle this better natively.
+   */
+  public class WindowCloseMonitor extends WindowAdapter
+  {
+    public void windowClosing(WindowEvent e)
+    {
+      Window w = e.getWindow();
+      w.setVisible(false);
+      w.dispose();
+      System.exit(0);
+    }
+  }
+}
\ No newline at end of file