1 /*********************************************************************
5 * Purpose : Provide the central GUI for displaying Privoxy
6 * statistics. It can be contacted either by the
7 * local machine or other machines in a network and
8 * display consolidated, tabular statistics.
10 * Copyright : Written by and Copyright (C) 2003 the SourceForge
11 * Privoxy team. http://www.privoxy.org/
13 * Based on the Internet Junkbuster originally written
14 * by and Copyright (C) 1997 Anonymous Coders and
15 * Junkbusters Corporation. http://www.junkbusters.com
17 * This program is free software; you can redistribute it
18 * and/or modify it under the terms of the GNU General
19 * Public License as published by the Free Software
20 * Foundation; either version 2 of the License, or (at
21 * your option) any later version.
23 * This program is distributed in the hope that it will
24 * be useful, but WITHOUT ANY WARRANTY; without even the
25 * implied warranty of MERCHANTABILITY or FITNESS FOR A
26 * PARTICULAR PURPOSE. See the GNU General Public
27 * License for more details.
29 * The GNU General Public License should be included with
30 * this file. If not, you can view it at
31 * http://www.gnu.org/copyleft/gpl.html
32 * or write to the Free Software Foundation, Inc., 59
33 * Temple Place - Suite 330, Boston, MA 02111-1307, USA.
37 *********************************************************************/
39 package org.privoxy.activityconsole;
42 import java.awt.event.*;
46 import javax.swing.border.*;
47 import javax.swing.event.*;
48 import javax.swing.table.*;
51 * The main Activity Console GUI.
52 * @author Last Modified By: $Author$
53 * @version $Rev$-$Date$$State$
55 public final class ActivityConsoleGui extends JFrame implements ActionListener
57 private static final String
58 COPYRIGHT = org.privoxy.activityconsole.Copyright.COPYRIGHT;
60 ActivityConsoleGui parent_;
61 ServerThread _serverThread = null;
62 private ListResourceBundle resStrings = (ListResourceBundle)ListResourceBundle.getBundle("org.privoxy.activityconsole.ActivityConsoleResources");
66 SortableTableModel _model;
69 _tableColumnMap = new Vector();
72 mainPanel = new JPanel(new GridBagLayout());
74 JMenuItem _deleteItem, _quitItem, _configItem;
76 private DefaultTableCellRenderer _statRenderer = null;
81 * Constructor of the Activity Console GUI.
82 * @param arg the port to serve connections on - as an int parsed from the String
84 public ActivityConsoleGui(String arg)
88 addWindowListener(new WindowCloseMonitor());
90 JMenuBar menuBar = new JMenuBar();
92 JMenu menuFile = new JMenu(resStrings.getString("menuFile"));
93 MenuAction quitAction = new MenuAction(resStrings.getString("menuFileQuit"));
94 JMenu menuEdit = new JMenu(resStrings.getString("menuEdit"));
95 _quitItem = menuFile.add(quitAction);
96 menuBar.add(menuFile);
97 _configItem = menuEdit.add(new MenuAction(resStrings.getString("menuEditConfig")));
98 menuBar.add(menuEdit);
99 _deleteItem = menuEdit.add(new MenuAction(resStrings.getString("menuEditDelete")));
100 menuBar.add(menuEdit);
101 this.setJMenuBar(menuBar);
102 _deleteItem.setEnabled(false);
106 _port = Integer.parseInt(arg);
116 * The cell renderer for the StatWidget Component - simply returns the component
117 * itself. Additionally, it has the extra hack of telling the StatWidget where
118 * it is in the table so it can update itself again when it comes time to flash.
120 _statRenderer = new DefaultTableCellRenderer()
122 public Component getTableCellRendererComponent(JTable table,
129 /* Housekeeping: keep track of the row, column and table references as we go */
130 ((StatWidget)value).setRowColTable(row,column,table);
131 return(Component)value;
134 public void setValue(Object value)
139 color = (Color)value;
141 catch (ClassCastException e)
145 setBackground(color);
149 Vector data = new Vector();
150 _model = new SortableTableModel(data, getColumnNames());
151 _table = new JTable(_model);
152 _table.setPreferredScrollableViewportSize(new Dimension(800,50));
153 _table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
154 _table.setCellSelectionEnabled(false);
155 _table.setRowSelectionAllowed(false);
158 * The first column is normal and text-ish - the host address and
159 * port being served (i.e. 127.0.0.1:8118). The rest need to have statistic
162 SortButtonRenderer _headerRenderer = new SortButtonRenderer();
163 TableColumnModel cm = _table.getColumnModel();
164 /* Make the first column twice the width of the others. It shows bigger stuff. */
165 cm.getColumn(0).setPreferredWidth(cm.getColumn(0).getPreferredWidth() * 2);
166 cm.getColumn(0).setHeaderRenderer(_headerRenderer);
167 for (i = 1;i<_model.getColumnCount();i++)
169 cm.getColumn(i).setPreferredWidth((int)(cm.getColumn(i).getPreferredWidth() * 1));
170 cm.getColumn(i).setCellRenderer(_statRenderer);
171 cm.getColumn(i).setHeaderRenderer(_headerRenderer);
174 JTableHeader header = _table.getTableHeader();
175 header.addMouseListener(new HeaderListener(header,_headerRenderer));
177 ListSelectionModel csm = _table.getSelectionModel();
178 csm.addListSelectionListener(new SelectedListener(csm));
180 ActivityConsoleGuiUtil.constrain(mainPanel, new JScrollPane(_table),
181 1, 1, // X, Y Coordinates
182 1, 1, // Grid width, height
183 GridBagConstraints.BOTH, // Fill value
184 GridBagConstraints.WEST, // Anchor value
185 1.0,1.0, // Weight X, Y
186 0, 0, 0, 0 ); // Top, left, bottom, right insets
188 this.getContentPane().add(mainPanel, BorderLayout.CENTER);
192 _table.setPreferredScrollableViewportSize(new Dimension(_table.getWidth(),50));
197 _serverThread = new ServerThread(this, _port);
198 _serverThread.start();
201 setBounds(ActivityConsoleGuiUtil.center(this.getSize()));
206 * Updates the title bar with the port currently being served.
207 * @param port the port being served
209 public void updateTitle(int port)
211 String title = resStrings.getString("guiTitle");
213 title = StringUtil.replaceSubstring(title,"%1",""+port);
217 public void actionPerformed(ActionEvent e)
221 class MenuAction extends AbstractAction
223 public MenuAction(String text)
228 public MenuAction(String text, Icon icon)
233 public void actionPerformed(ActionEvent e)
235 if (e.getSource() == _quitItem)
237 parent_.setVisible(false);
241 else if (e.getSource() == _deleteItem)
245 else if (e.getSource() == _configItem)
247 changeServerAction();
253 * Asks the user to specify a new port to serve
255 public void changeServerAction()
258 String message = resStrings.getString("guiNewPortPrompt");
259 message = StringUtil.replaceSubstring(message,"%1",""+_port);
261 String inputValue = JOptionPane.showInputDialog(this,
263 resStrings.getString("guiNewPortTitle"),
264 JOptionPane.QUESTION_MESSAGE);
265 if (inputValue != null)
268 port = Integer.parseInt(inputValue);
275 JOptionPane.showMessageDialog(null, resStrings.getString("guiNewPortErrorPrompt"), resStrings.getString("guiNewPortErrorTitle"), JOptionPane.ERROR_MESSAGE);
280 if (_serverThread != null)
282 _serverThread.doClose();
283 _serverThread.interrupt();
284 _serverThread = null;
287 _serverThread = new ServerThread(parent_, port);
288 _serverThread.start();
295 * Deletes the "selected" row after seeking confirmation
297 public void deleteAction()
299 int numSelections = _table.getSelectedRowCount();
300 int selRow = _table.getSelectedRow();
301 if (numSelections > 0)
304 (selRow < _table.getRowCount()))
306 /* Ask for confirmation */
307 String message = resStrings.getString("guiDeleteConfirmPrompt");
308 message = StringUtil.replaceSubstring(message,"%1",(String)_model.getValueAt(selRow,0));
309 int ret = JOptionPane.showConfirmDialog(null,
311 resStrings.getString("guiDeleteConfirmTitle"),
312 JOptionPane.YES_NO_OPTION);
313 if (ret == JOptionPane.YES_OPTION)
315 _model.removeRow(selRow);
322 * Retrieves the names of the column headers. This should be made to read a properties
324 * @return Vector the set of column names. It also has the side-effect of adding
325 * entries to the global column mapping Vector where we map the staus integer identifiers
326 * to the column positions and names. Should probably fix that too.
328 public Vector getColumnNames()
330 Vector names = new Vector();
332 names.addElement(resStrings.getString("guiDefaultColumn0"));
333 names.addElement(resStrings.getString("guiDefaultColumn1"));
334 names.addElement(resStrings.getString("guiDefaultColumn2"));
335 names.addElement(resStrings.getString("guiDefaultColumn3"));
336 names.addElement(resStrings.getString("guiDefaultColumn4"));
337 names.addElement(resStrings.getString("guiDefaultColumn5"));
338 names.addElement(resStrings.getString("guiDefaultColumn6"));
339 names.addElement(resStrings.getString("guiDefaultColumn7"));
340 names.addElement(resStrings.getString("guiDefaultColumn8"));
341 names.addElement(resStrings.getString("guiDefaultColumn9"));
342 names.addElement(resStrings.getString("guiDefaultColumn10"));
344 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn1"),1));
345 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn2"),2));
346 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn3"),3));
347 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn4"),4));
348 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn5"),5));
349 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn6"),6));
350 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn7"),7));
351 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn8"),8));
352 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn9"),9));
353 _tableColumnMap.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn10"),10));
359 * Parses a String of statistics coming from Privoxy.
360 * @param line The statistics string sent from Privoxy
361 * @param from the hostname that sent the statistics
363 public void updateStats(String line, String from)
366 * An example line of data:
367 * 0:8118 1:0 2:0 3:0 4:0 5:0 6:0 7:0 8:0 9:0 10:0
370 String tableKey = "", key_str, value_str, token;
371 StringTokenizer colonToken;
372 StringTokenizer spaceTokens = new StringTokenizer(line);
373 Vector stats = new Vector();
375 while (spaceTokens.hasMoreTokens())
377 token = spaceTokens.nextToken();
378 colonToken = new StringTokenizer(token,":");
379 if (colonToken.hasMoreTokens())
381 key_str = null; value_str = null;
384 /* First token is the key */
385 key_str = colonToken.nextToken();
388 key = Integer.parseInt(key_str);
390 catch (NumberFormatException n)
395 if ((colonToken.hasMoreTokens()) && (key > -1))
397 /* Next token, if present, is the value */
398 value_str = colonToken.nextToken();
402 * The key to the table row is the concatenation of the serving
403 * IP address string, a full colon, and the port string.
405 tableKey = from + ":" + value_str;
409 value = Integer.parseInt(value_str);
410 stats.addElement((Object)(new Stat(key, value)));
412 catch (NumberFormatException n)
419 if ((tableKey.compareTo("") != 0) && (stats.size() > 0))
421 updateTable(tableKey, stats);
422 stats.removeAllElements();
428 * Updates (or creates) a line in the table representing the incoming packet of stats.
429 * @param tableKey Our key to a unique table row: the hostname concatenated with the Privoxy port being served.
430 * @param stats Vector of statistics elements
432 public void updateTable(String tableKey, Vector stats)
434 boolean found = false;
435 for (int i = 0; i < _model.getRowCount(); i++)
437 if (((String)_model.getValueAt(i,_table.convertColumnIndexToView(0))).compareTo(tableKey) == 0)
439 updateTableEntry(i, stats);
443 /* If we can't find one in the table already... */
445 createTableEntry(tableKey, stats);
449 * Creates a line in the table representing the incoming packet of stats.
450 * @param tableKey Our key to a unique table row: the hostname concatenated with the Privoxy port being served.
451 * @param stats Vector of statistics elements
453 public void createTableEntry(String tableKey, Vector stats)
456 Vector row = new Vector();
457 boolean added = false;
459 row.addElement(tableKey);
462 * If we have a key (in stats) that maps to a key in the _tableColumnMap,
463 * then we add it to the vector destined for the table.
465 for (i = 0; i < _tableColumnMap.size(); i ++)
467 for (j = 0; j < stats.size(); j++)
469 if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
471 row.addElement(new StatWidget(((Stat)stats.elementAt(j)).getValue(),500));
477 row.addElement(new StatWidget(0,500));
486 * Updates a line in the table by tweaking the StatWidgets.
487 * @param row the table row if the StatWidget
488 * @param stats The Vector of Stat elements to update the table row with
490 public void updateTableEntry(int row, Vector stats)
494 for (i = 0; i < _tableColumnMap.size(); i ++)
496 for (j = 0; j < stats.size(); j++)
498 if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
500 ((StatWidget)_model.getValueAt(row,i+1)).updateValue(((Stat)stats.elementAt(j)).getValue());
501 stats.removeElementAt(j);
509 * Worker class to offer a clickable table header for sorting.
511 class HeaderListener extends MouseAdapter
514 SortButtonRenderer renderer;
516 HeaderListener(JTableHeader header,SortButtonRenderer renderer)
518 this.header = header;
519 this.renderer = renderer;
522 public void mousePressed(MouseEvent e)
524 Point click = e.getPoint();
525 int col = header.columnAtPoint(click);
526 int margin1, margin2;
527 int sortCol = header.getTable().convertColumnIndexToModel(col);
529 /* Don't perform the sort if the user is just trying to resize the columns. */
530 margin1 = header.columnAtPoint(new Point(click.x+3,click.y));
531 margin2 = header.columnAtPoint(new Point(click.x-3,click.y));
532 if ((col == margin1) && (col == margin2))
534 renderer.setPressedColumn(col);
535 renderer.setSelectedColumn(col);
538 if (header.getTable().isEditing())
540 header.getTable().getCellEditor().stopCellEditing();
544 if (SortButtonRenderer.DOWN == renderer.getState(col))
552 ((SortableTableModel)header.getTable().getModel())
553 .sortByColumn(sortCol, isAscent);
557 public void mouseReleased(MouseEvent e)
559 int col = header.columnAtPoint(e.getPoint());
560 renderer.setPressedColumn(-1); // clear
566 * Worker class to tell the menu when it's OK to delete a row (i.e. when a row gets
567 * selected). This doesn't work reliably, but it's better than nothing.
569 public class SelectedListener implements ListSelectionListener
571 ListSelectionModel model;
573 public SelectedListener(ListSelectionModel lsm)
578 public void valueChanged(ListSelectionEvent lse)
580 // NOTE - keep this in sync with columnSelectionChanged below...
581 int numSelections = _table.getSelectedRowCount();
582 int selRow = _table.getSelectedRow();
583 if (numSelections > 0)
586 (selRow < _table.getRowCount()))
588 _deleteItem.setEnabled(true);
591 _deleteItem.setEnabled(false);
594 _deleteItem.setEnabled(false);
596 public void columnSelectionChanged(ListSelectionEvent lse)
598 // NOTE - keep this in sync with valueChanged above...
599 int numSelections = _table.getSelectedRowCount();
600 int selRow = _table.getSelectedRow();
601 if (numSelections > 0)
604 (selRow < _table.getRowCount()))
606 _deleteItem.setEnabled(true);
609 _deleteItem.setEnabled(false);
612 _deleteItem.setEnabled(false);
617 * Watch for the window closing event. Dunno why swing doesn't handle this better natively.
619 public class WindowCloseMonitor extends WindowAdapter
621 public void windowClosing(WindowEvent e)
623 Window w = e.getWindow();