Initial checkin of directory structure and source code of the java Activity
[privoxy.git] / src / java / org / privoxy / activityconsole / ActivityConsoleGui.java
1 /*********************************************************************
2  *
3  * File        :  $Source$
4  *
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.
9  *
10  * Copyright   :  Written by and Copyright (C) 2003 the SourceForge
11  *                Privoxy team. http://www.privoxy.org/
12  *
13  *                Based on the Internet Junkbuster originally written
14  *                by and Copyright (C) 1997 Anonymous Coders and
15  *                Junkbusters Corporation.  http://www.junkbusters.com
16  *
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.
22  *
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.
28  *
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.
34  *
35  * Revisions   :
36  *    $Log$
37  *********************************************************************/
38
39 package org.privoxy.activityconsole;
40
41 import java.awt.*;
42 import java.awt.event.*;
43 import java.io.*;
44 import java.util.*;
45 import javax.swing.*;
46 import javax.swing.border.*;
47 import javax.swing.event.*;
48 import javax.swing.table.*;
49
50 /**
51  * The main Activity Console GUI.
52  * @author Last Modified By: $Author$
53  * @version $Rev$-$Date$$State$
54  */
55 public final class ActivityConsoleGui extends JFrame implements ActionListener
56 {
57   private static final String
58   COPYRIGHT = org.privoxy.activityconsole.Copyright.COPYRIGHT;
59
60   ActivityConsoleGui parent_;
61   ServerThread _serverThread = null;
62   private ListResourceBundle resStrings = (ListResourceBundle)ListResourceBundle.getBundle("org.privoxy.activityconsole.ActivityConsoleResources");
63
64   JTable _table;
65
66   SortableTableModel _model;
67
68   Vector
69   _tableColumnMap = new Vector();
70
71   JPanel
72   mainPanel = new JPanel(new GridBagLayout());
73
74   JMenuItem _deleteItem, _quitItem, _configItem;
75
76   private DefaultTableCellRenderer _statRenderer = null;
77
78   int _port = 0;
79
80   /**
81    * Constructor of the Activity Console GUI.
82    * @param arg the port to serve connections on - as an int parsed from the String
83    */
84   public ActivityConsoleGui(String arg)
85   {
86     int i;
87
88     addWindowListener(new WindowCloseMonitor());
89
90     JMenuBar menuBar = new JMenuBar();
91
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);
103
104     try
105     {
106       _port = Integer.parseInt(arg);
107       if (_port < 0)
108         _port = 0;
109     }
110     catch (Throwable t)
111     {
112       _port = 0;
113     }
114
115     /**
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.
119      */
120     _statRenderer = new DefaultTableCellRenderer()
121     {
122       public Component getTableCellRendererComponent(JTable table,
123                                                      Object value,
124                                                      boolean isSelected,
125                                                      boolean hasFocus,
126                                                      int row,
127                                                      int column)
128       {
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;
132       }
133
134       public void setValue(Object value)
135       {
136         Color color = null;
137         try
138         {
139           color = (Color)value;
140         }
141         catch (ClassCastException e)
142         {
143           color = Color.white;
144         }
145         setBackground(color);
146       }
147     };
148
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);
156
157     /*
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
160      * renderers defined.
161      */
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++)
168     {
169       cm.getColumn(i).setPreferredWidth((int)(cm.getColumn(i).getPreferredWidth() * 1));
170       cm.getColumn(i).setCellRenderer(_statRenderer);
171       cm.getColumn(i).setHeaderRenderer(_headerRenderer);
172     }
173
174     JTableHeader header = _table.getTableHeader();
175     header.addMouseListener(new HeaderListener(header,_headerRenderer));
176
177     ListSelectionModel csm = _table.getSelectionModel();
178     csm.addListSelectionListener(new SelectedListener(csm));
179
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
187
188     this.getContentPane().add(mainPanel, BorderLayout.CENTER);
189
190     parent_ = this;
191     this.pack();
192     _table.setPreferredScrollableViewportSize(new Dimension(_table.getWidth(),50));
193     this.pack();
194
195     if (_port > 0)
196     {
197       _serverThread = new ServerThread(this, _port);
198       _serverThread.start();
199     }
200     updateTitle(_port);
201     setBounds(ActivityConsoleGuiUtil.center(this.getSize()));
202     this.show();
203   }
204
205   /**
206    * Updates the title bar with the port currently being served.
207    * @param port the port being served
208    */
209   public void updateTitle(int port)
210   {
211     String title = resStrings.getString("guiTitle");
212
213     title = StringUtil.replaceSubstring(title,"%1",""+port);
214     setTitle(title);
215   }
216
217   public void actionPerformed(ActionEvent e)
218   {
219   }
220
221   class MenuAction extends AbstractAction
222   {
223     public MenuAction(String text)
224     {
225       super(text,null);
226     }
227
228     public MenuAction(String text, Icon icon)
229     {
230       super(text,icon);
231     }
232
233     public void actionPerformed(ActionEvent e)
234     {
235       if (e.getSource() == _quitItem)
236       {
237         parent_.setVisible(false);
238         parent_.dispose();
239         System.exit(0);
240       }
241       else if (e.getSource() == _deleteItem)
242       {
243         deleteAction();
244       }
245       else if (e.getSource() == _configItem)
246       {
247         changeServerAction();
248       }
249     }
250   }
251
252   /**
253    * Asks the user to specify a new port to serve
254    */
255   public void changeServerAction()
256   {
257     int port = -1;
258     String message = resStrings.getString("guiNewPortPrompt");
259     message = StringUtil.replaceSubstring(message,"%1",""+_port);
260
261     String inputValue = JOptionPane.showInputDialog(this,
262                                                     message,
263                                                     resStrings.getString("guiNewPortTitle"),
264                                                     JOptionPane.QUESTION_MESSAGE);
265     if (inputValue != null)
266       try
267       {
268         port = Integer.parseInt(inputValue);
269       }
270       catch (Throwable t)
271       {
272         port = -1;
273       }
274     if (port < 1)
275       JOptionPane.showMessageDialog(null, resStrings.getString("guiNewPortErrorPrompt"), resStrings.getString("guiNewPortErrorTitle"), JOptionPane.ERROR_MESSAGE);
276     else
277     {
278       if (_port != port)
279       {
280         if (_serverThread != null)
281         {
282           _serverThread.doClose();
283           _serverThread.interrupt();
284           _serverThread = null;
285         }
286         _port = port;
287         _serverThread = new ServerThread(parent_, port);
288         _serverThread.start();
289         updateTitle(_port);
290       }
291     }
292   }
293
294   /**
295    * Deletes the "selected" row after seeking confirmation
296    */
297   public void deleteAction()
298   {
299     int numSelections = _table.getSelectedRowCount();
300     int selRow = _table.getSelectedRow();
301     if (numSelections > 0)
302     {
303       if ((selRow > -1) &&
304           (selRow < _table.getRowCount()))
305       {
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,
310                                                 message,
311                                                 resStrings.getString("guiDeleteConfirmTitle"),
312                                                 JOptionPane.YES_NO_OPTION);
313         if (ret == JOptionPane.YES_OPTION)
314         {
315           _model.removeRow(selRow);
316         }
317       }
318     }
319   }
320
321   /**
322    * Retrieves the names of the column headers.  This should be made to read a properties
323    * file. FIXME.
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.
327    */
328   public Vector getColumnNames()
329   {
330     Vector names = new Vector();
331
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"));
343
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));
354
355     return names;
356   }
357
358   /**
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
362    */
363   public void updateStats(String line, String from)
364   {
365     /*
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
368      */
369     int key, value;
370     String tableKey = "", key_str, value_str, token;
371     StringTokenizer colonToken;
372     StringTokenizer spaceTokens = new StringTokenizer(line);
373     Vector stats = new Vector();
374
375     while (spaceTokens.hasMoreTokens())
376     {
377       token = spaceTokens.nextToken();
378       colonToken = new StringTokenizer(token,":");
379       if (colonToken.hasMoreTokens())
380       {
381         key_str = null; value_str = null;
382         key = -1; value = 0;
383
384         /* First token is the key */
385         key_str = colonToken.nextToken();
386         try
387         {
388           key = Integer.parseInt(key_str);
389         }
390         catch (NumberFormatException n)
391         {
392           key = -1;
393         }
394
395         if ((colonToken.hasMoreTokens()) && (key > -1))
396         {
397           /* Next token, if present, is the value */
398           value_str = colonToken.nextToken();
399           if (key == 0)
400           {
401             /*
402              * The key to the table row is the concatenation of the serving
403              * IP address string, a full colon, and the port string.
404              */
405             tableKey = from + ":" + value_str;
406           }
407           try
408           {
409             value = Integer.parseInt(value_str);
410             stats.addElement((Object)(new Stat(key, value)));
411           }
412           catch (NumberFormatException n)
413           {
414             value = 0;
415           }
416         }
417       }
418     }
419     if ((tableKey.compareTo("") != 0) && (stats.size() > 0))
420     {
421       updateTable(tableKey, stats);
422       stats.removeAllElements();
423     }
424     stats = null;
425   }
426
427   /**
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
431    */
432   public void updateTable(String tableKey, Vector stats)
433   {
434     boolean found = false;
435     for (int i = 0; i < _model.getRowCount(); i++)
436     {
437       if (((String)_model.getValueAt(i,_table.convertColumnIndexToView(0))).compareTo(tableKey) == 0)
438       {
439         updateTableEntry(i, stats);
440         found = true;
441       }
442     }
443     /* If we can't find one in the table already... */
444     if (found == false)
445       createTableEntry(tableKey, stats);
446   }
447
448   /**
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
452    */
453   public void createTableEntry(String tableKey, Vector stats)
454   {
455     int i, j;
456     Vector row = new Vector();
457     boolean added = false;
458
459     row.addElement(tableKey);
460
461     /*
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.
464      */
465     for (i = 0; i < _tableColumnMap.size(); i ++)
466     {
467       for (j = 0; j < stats.size(); j++)
468       {
469         if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
470         {
471           row.addElement(new StatWidget(((Stat)stats.elementAt(j)).getValue(),500));
472           added = true;
473         }
474       }
475       if (added == false)
476       {
477         row.addElement(new StatWidget(0,500));
478       }
479       else
480         added = false;
481     }
482     _model.addRow(row);
483   }
484
485   /**
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
489    */
490   public void updateTableEntry(int row, Vector stats)
491   {
492     int i, j;
493
494     for (i = 0; i < _tableColumnMap.size(); i ++)
495     {
496       for (j = 0; j < stats.size(); j++)
497       {
498         if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
499         {
500           ((StatWidget)_model.getValueAt(row,i+1)).updateValue(((Stat)stats.elementAt(j)).getValue());
501           stats.removeElementAt(j);
502           break;
503         }
504       }
505     }
506   }
507
508   /**
509    * Worker class to offer a clickable table header for sorting.
510    */
511   class HeaderListener extends MouseAdapter
512   {
513     JTableHeader   header;
514     SortButtonRenderer renderer;
515
516     HeaderListener(JTableHeader header,SortButtonRenderer renderer)
517     {
518       this.header   = header;
519       this.renderer = renderer;
520     }
521
522     public void mousePressed(MouseEvent e)
523     {
524       Point click = e.getPoint();
525       int col = header.columnAtPoint(click);
526       int margin1, margin2;
527       int sortCol = header.getTable().convertColumnIndexToModel(col);
528
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))
533       {
534         renderer.setPressedColumn(col);
535         renderer.setSelectedColumn(col);
536         header.repaint();
537
538         if (header.getTable().isEditing())
539         {
540           header.getTable().getCellEditor().stopCellEditing();
541         }
542
543         boolean isAscent;
544         if (SortButtonRenderer.DOWN == renderer.getState(col))
545         {
546           isAscent = true;
547         }
548         else
549         {
550           isAscent = false;
551         }
552         ((SortableTableModel)header.getTable().getModel())
553         .sortByColumn(sortCol, isAscent);    
554       }
555     }
556
557     public void mouseReleased(MouseEvent e)
558     {
559       int col = header.columnAtPoint(e.getPoint());
560       renderer.setPressedColumn(-1);                // clear
561       header.repaint();
562     }
563   }
564
565   /**
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.
568    */
569   public class SelectedListener implements ListSelectionListener
570   {
571     ListSelectionModel model;
572
573     public SelectedListener(ListSelectionModel lsm)
574     {
575       model = lsm;
576     }
577
578     public void valueChanged(ListSelectionEvent lse)
579     {
580       // NOTE - keep this in sync with columnSelectionChanged below...
581       int numSelections = _table.getSelectedRowCount();
582       int selRow = _table.getSelectedRow();
583       if (numSelections > 0)
584       {
585         if ((selRow > -1) &&
586             (selRow < _table.getRowCount()))
587         {
588           _deleteItem.setEnabled(true);
589         }
590         else
591           _deleteItem.setEnabled(false);
592       }
593       else
594         _deleteItem.setEnabled(false);
595     }
596     public void columnSelectionChanged(ListSelectionEvent lse)
597     {
598       // NOTE - keep this in sync with valueChanged above...
599       int numSelections = _table.getSelectedRowCount();
600       int selRow = _table.getSelectedRow();
601       if (numSelections > 0)
602       {
603         if ((selRow > -1) &&
604             (selRow < _table.getRowCount()))
605         {
606           _deleteItem.setEnabled(true);
607         }
608         else
609           _deleteItem.setEnabled(false);
610       }
611       else
612         _deleteItem.setEnabled(false);
613     }
614   }
615
616   /**
617    * Watch for the window closing event.  Dunno why swing doesn't handle this better natively.
618    */
619   public class WindowCloseMonitor extends WindowAdapter
620   {
621     public void windowClosing(WindowEvent e)
622     {
623       Window w = e.getWindow();
624       w.setVisible(false);
625       w.dispose();
626       System.exit(0);
627     }
628   }
629 }