Typo: inspect_jpegs, not inspect-jpegs in form
[privoxy.git] / src / java / org / privoxy / activityconsole / ActivityConsoleGui.java
1 /*********************************************************************
2  *
3  * File        :  $Source: /cvsroot/ijbswa/current/src/java/org/privoxy/activityconsole/ActivityConsoleGui.java,v $
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: ActivityConsoleGui.java,v $
37  *    Revision 1.1  2003/01/18 14:37:24  david__schmidt
38  *    Initial checkin of directory structure and source code of the java Activity
39  *    Console
40  *
41  *********************************************************************/
42
43 package org.privoxy.activityconsole;
44
45 import java.awt.*;
46 import java.awt.event.*;
47 import java.io.*;
48 import java.util.*;
49 import javax.swing.*;
50 import javax.swing.border.*;
51 import javax.swing.event.*;
52 import javax.swing.table.*;
53
54 /**
55  * The main Activity Console GUI.
56  * @author Last Modified By: $Author: david__schmidt $
57  * @version $Rev$-$Date: 2003/01/18 14:37:24 $$State: Exp $
58  */
59 public final class ActivityConsoleGui extends JFrame implements ActionListener
60 {
61   private static final String
62   COPYRIGHT = org.privoxy.activityconsole.Copyright.COPYRIGHT;
63
64   ActivityConsoleGui parent_;
65   ServerThread _serverThread = null;
66   private ListResourceBundle resStrings = (ListResourceBundle)ListResourceBundle.getBundle("org.privoxy.activityconsole.ActivityConsoleResources");
67
68   JTable _table;
69
70   JScrollPane _tableScroller = new JScrollPane();
71
72   SortableTableModel _model;
73
74   Vector _tableColumnMap = new Vector();
75
76   JPanel _mainPanel = new JPanel(new GridBagLayout());
77
78   JMenuItem _deleteItem, _quitItem, _configItem;
79   JCheckBoxMenuItem _viewWideItem;
80
81   private DefaultTableCellRenderer _statRenderer = null;
82
83   int _port = 0;
84
85   Properties _properties = null;
86
87   /**
88    * Constructor of the Activity Console GUI.
89    * @param arg the port to serve connections on - as an int parsed from the String
90    */
91   public ActivityConsoleGui(String arg)
92   {
93     int i;
94
95     addWindowListener(new WindowCloseMonitor());
96
97     JMenuBar menuBar = new JMenuBar();
98
99     JMenu menuFile = new JMenu(resStrings.getString("menuFile"));
100     MenuAction quitAction = new MenuAction(resStrings.getString("menuFileQuit"));
101     _quitItem = menuFile.add(quitAction);
102     menuBar.add(menuFile);
103
104     JMenu menuEdit = new JMenu(resStrings.getString("menuEdit"));
105     _configItem = menuEdit.add(new MenuAction(resStrings.getString("menuEditConfig")));
106     _deleteItem = menuEdit.add(new MenuAction(resStrings.getString("menuEditDelete")));
107     menuBar.add(menuEdit);
108
109     JMenu menuView = new JMenu(resStrings.getString("menuView"));
110     _viewWideItem = new JCheckBoxMenuItem(resStrings.getString("menuViewWide"));
111     _viewWideItem.addActionListener(this);
112     menuView.add(_viewWideItem);
113     menuBar.add(menuView);
114
115     this.setJMenuBar(menuBar);
116     _deleteItem.setEnabled(false);
117
118     loadProperties();
119
120     try
121     {
122       _port = Integer.parseInt(arg);
123       if (_port < 0)
124         _port = 0;
125     }
126     catch (Throwable t)
127     {
128       _port = 0;
129     }
130
131     /**
132      * The cell renderer for the StatWidget Component - simply returns the component
133      * itself.  Additionally, it has the extra hack of telling the StatWidget where
134      * it is in the table so it can update itself again when it comes time to flash.
135      */
136     _statRenderer = new DefaultTableCellRenderer()
137     {
138       public Component getTableCellRendererComponent(JTable table,
139                                                      Object value,
140                                                      boolean isSelected,
141                                                      boolean hasFocus,
142                                                      int row,
143                                                      int column)
144       {
145         /* Housekeeping: keep track of the row, column and table references as we go */
146         ((StatWidget)value).setRowColTable(row,column,table);
147         return(Component)value;
148       }
149
150       public void setValue(Object value)
151       {
152         Color color = null;
153         try
154         {
155           color = (Color)value;
156         }
157         catch (ClassCastException e)
158         {
159           color = Color.white;
160         }
161         setBackground(color);
162       }
163     };
164
165     initTable();
166
167     ActivityConsoleGuiUtil.constrain(_mainPanel, _tableScroller,
168                                      1, 1, // X, Y Coordinates
169                                      1, 1, // Grid width, height
170                                      GridBagConstraints.BOTH,  // Fill value
171                                      GridBagConstraints.WEST,  // Anchor value
172                                      1.0,1.0,  // Weight X, Y
173                                      0, 0, 0, 0 ); // Top, left, bottom, right insets
174
175     this.getContentPane().add(_mainPanel, BorderLayout.CENTER);
176
177     parent_ = this;
178     this.pack();
179     _table.setPreferredScrollableViewportSize(new Dimension(_table.getWidth(),50));
180     this.pack();
181
182     if (_port > 0)
183     {
184       _serverThread = new ServerThread(this, _port);
185       _serverThread.start();
186     }
187     updateTitle(_port);
188     setBounds(ActivityConsoleGuiUtil.center(this.getSize()));
189     this.show();
190   }
191
192   /**
193    * Updates the title bar with the port currently being served.
194    * @param port the port being served
195    */
196   public void updateTitle(int port)
197   {
198     String title = resStrings.getString("guiTitle");
199
200     title = StringUtil.replaceSubstring(title,"%1",""+port);
201     setTitle(title);
202   }
203
204   public void actionPerformed(ActionEvent e)
205   {
206     if (e.getSource() == _viewWideItem)
207     {
208       setProperty("AC.detailedColumnSet", _viewWideItem.isSelected());
209       initTable();
210       this.pack();
211       _table.setPreferredScrollableViewportSize(new Dimension(_table.getWidth(),50));
212       this.pack();
213     }
214   }
215
216   class MenuAction extends AbstractAction
217   {
218     public MenuAction(String text)
219     {
220       super(text,null);
221     }
222
223     public MenuAction(String text, Icon icon)
224     {
225       super(text,icon);
226     }
227
228     public void actionPerformed(ActionEvent e)
229     {
230       if (e.getSource() == _quitItem)
231       {
232         saveProperties();
233         parent_.setVisible(false);
234         parent_.dispose();
235         System.exit(0);
236       }
237       else if (e.getSource() == _deleteItem)
238       {
239         deleteAction();
240       }
241       else if (e.getSource() == _configItem)
242       {
243         changeServerAction();
244       }
245     }
246   }
247
248   /**
249    * Asks the user to specify a new port to serve
250    */
251   public void changeServerAction()
252   {
253     int port = -1;
254     String message = resStrings.getString("guiNewPortPrompt");
255     message = StringUtil.replaceSubstring(message,"%1",""+_port);
256
257     String inputValue = JOptionPane.showInputDialog(this,
258                                                     message,
259                                                     resStrings.getString("guiNewPortTitle"),
260                                                     JOptionPane.QUESTION_MESSAGE);
261     if (inputValue != null)
262       try
263       {
264         port = Integer.parseInt(inputValue);
265       }
266       catch (Throwable t)
267       {
268         port = -1;
269       }
270     if (port < 1)
271       JOptionPane.showMessageDialog(null, resStrings.getString("guiNewPortErrorPrompt"), resStrings.getString("guiNewPortErrorTitle"), JOptionPane.ERROR_MESSAGE);
272     else
273     {
274       if (_port != port)
275       {
276         if (_serverThread != null)
277         {
278           _serverThread.doClose();
279           _serverThread.interrupt();
280           _serverThread = null;
281         }
282         _port = port;
283         _serverThread = new ServerThread(parent_, port);
284         _serverThread.start();
285         updateTitle(_port);
286       }
287     }
288   }
289
290   /**
291    * Deletes the "selected" row after seeking confirmation
292    */
293   public void deleteAction()
294   {
295     int numSelections = _table.getSelectedRowCount();
296     int selRow = _table.getSelectedRow();
297     if (numSelections > 0)
298     {
299       if ((selRow > -1) &&
300           (selRow < _table.getRowCount()))
301       {
302         /* Ask for confirmation */
303         String message = resStrings.getString("guiDeleteConfirmPrompt");
304         message = StringUtil.replaceSubstring(message,"%1",(String)_model.getValueAt(selRow,0));
305         int ret = JOptionPane.showConfirmDialog(null,
306                                                 message,
307                                                 resStrings.getString("guiDeleteConfirmTitle"),
308                                                 JOptionPane.YES_NO_OPTION);
309         if (ret == JOptionPane.YES_OPTION)
310         {
311           _model.removeRow(selRow);
312         }
313       }
314     }
315   }
316
317   /**
318    * Builds a new table with the requested columns.
319    */
320   public void initTable()
321   {
322     _model = new SortableTableModel(new Vector(), getColumnNames());
323     _table = new JTable(_model);
324     _table.setPreferredScrollableViewportSize(new Dimension(800,50));
325     _table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
326     _table.setCellSelectionEnabled(false);
327     _table.setRowSelectionAllowed(false);
328     SortButtonRenderer headerRenderer = new SortButtonRenderer();
329     TableColumnModel cm = _table.getColumnModel();
330     /* Make the first column twice the width of the others. It shows bigger stuff. */
331     cm.getColumn(0).setPreferredWidth(cm.getColumn(0).getPreferredWidth() * 2);
332     cm.getColumn(0).setHeaderRenderer(headerRenderer);
333     for (int i = 1;i<_model.getColumnCount();i++)
334     {
335       cm.getColumn(i).setPreferredWidth((int)(cm.getColumn(i).getPreferredWidth() * 1));
336       cm.getColumn(i).setCellRenderer(_statRenderer);
337       cm.getColumn(i).setHeaderRenderer(headerRenderer);
338     }
339
340     JTableHeader header = _table.getTableHeader();
341     header.addMouseListener(new HeaderListener(header,headerRenderer));
342
343     ListSelectionModel csm = _table.getSelectionModel();
344     csm.addListSelectionListener(new SelectedListener(csm));
345
346     _tableScroller.setViewportView(_table);
347
348   }
349
350   /**
351    * Retrieves the names of the column headers.
352    * @return Vector the set of column names.  It also has the side-effect of adding
353    * entries to the global column mapping Vector where we map the staus integer identifiers
354    * to the column positions and names.  Should probably fix that too.
355    */
356   public Vector getColumnNames()
357   {
358     Vector names = new Vector();
359     _tableColumnMap = getUserColumnNames();
360
361     names.addElement(resStrings.getString("guiDefaultColumn0"));
362     for (int i = 0; i < _tableColumnMap.size(); i ++)
363     {
364       names.addElement(((ColumnRef)_tableColumnMap.elementAt(i)).getDescription());
365     }
366
367     return names;
368   }
369
370   /**
371    * Builds a map of columns based on the properties file.
372    * If it is somehow unsuitable, the default table will be built.
373    * This should be made to read a properties file. FIXME.
374    * @return Vector The vector of column name-to-stat-ID mappings
375    */
376   public Vector getUserColumnNames()
377   {
378     Vector map;
379
380     map = getDefaultColumnNames();
381
382     return map;
383   }
384
385   /**
386    * Builds a default map of columns.
387    * @return Vector The vector of column name-to-stat-ID mappings
388    */
389   public Vector getDefaultColumnNames()
390   {
391     Vector map = new Vector();
392     boolean detailedList = getProperty("AC.detailedColumnSet", false);
393     // In case they didn't have the preference set... set it.
394     setProperty("AC.detailedColumnSet", detailedList);
395     _viewWideItem.setSelected(detailedList);
396
397     map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn1"),1));
398     map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn2"),2));
399     map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn3"),3));
400     if (detailedList)
401     {
402       map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn4"),4));
403       map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn5"),5));
404       map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn6"),6));
405       map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn7"),7));
406       map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn8"),8));
407       map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn9"),9));
408       map.addElement(new ColumnRef(resStrings.getString("guiDefaultColumn10"),10));
409     }
410     return map;
411   }
412
413   /**
414    * Parses a String of statistics coming from Privoxy.
415    * @param line The statistics string sent from Privoxy
416    * @param from the hostname that sent the statistics
417    */
418   public void updateStats(String line, String from)
419   {
420     /*
421      * An example line of data:
422      * 0:8118 1:0 2:0 3:0 4:0 5:0 6:0 7:0 8:0 9:0 10:0
423      */
424     int key, value;
425     String tableKey = "", key_str, value_str, token;
426     StringTokenizer colonToken;
427     StringTokenizer spaceTokens = new StringTokenizer(line);
428     Vector stats = new Vector();
429
430     while (spaceTokens.hasMoreTokens())
431     {
432       token = spaceTokens.nextToken();
433       colonToken = new StringTokenizer(token,":");
434       if (colonToken.hasMoreTokens())
435       {
436         key_str = null; value_str = null;
437         key = -1; value = 0;
438
439         /* First token is the key */
440         key_str = colonToken.nextToken();
441         try
442         {
443           key = Integer.parseInt(key_str);
444         }
445         catch (NumberFormatException n)
446         {
447           key = -1;
448         }
449
450         if ((colonToken.hasMoreTokens()) && (key > -1))
451         {
452           /* Next token, if present, is the value */
453           value_str = colonToken.nextToken();
454           if (key == 0)
455           {
456             /*
457              * The key to the table row is the concatenation of the serving
458              * IP address string, a full colon, and the port string.
459              */
460             tableKey = from + ":" + value_str;
461           }
462           try
463           {
464             value = Integer.parseInt(value_str);
465             stats.addElement((Object)(new Stat(key, value)));
466           }
467           catch (NumberFormatException n)
468           {
469             value = 0;
470           }
471         }
472       }
473     }
474     if ((tableKey.compareTo("") != 0) && (stats.size() > 0))
475     {
476       updateTable(tableKey, stats);
477       stats.removeAllElements();
478     }
479     stats = null;
480   }
481
482   /**
483    * Updates (or creates) a line in the table representing the incoming packet of stats.
484    * @param tableKey Our key to a unique table row: the hostname concatenated with the Privoxy port being served.
485    * @param stats Vector of statistics elements
486    */
487   public void updateTable(String tableKey, Vector stats)
488   {
489     boolean found = false;
490     for (int i = 0; i < _model.getRowCount(); i++)
491     {
492       if (((String)_model.getValueAt(i,_table.convertColumnIndexToView(0))).compareTo(tableKey) == 0)
493       {
494         updateTableEntry(i, stats);
495         found = true;
496       }
497     }
498     /* If we can't find one in the table already... */
499     if (found == false)
500       createTableEntry(tableKey, stats);
501   }
502
503   /**
504    * Creates a line in the table representing the incoming packet of stats.
505    * @param tableKey Our key to a unique table row: the hostname concatenated with the Privoxy port being served.
506    * @param stats Vector of statistics elements
507    */
508   public void createTableEntry(String tableKey, Vector stats)
509   {
510     int i, j;
511     Vector row = new Vector();
512     boolean added = false;
513
514     row.addElement(tableKey);
515
516     /*
517      * If we have a key (in stats) that maps to a key in the _tableColumnMap,
518      * then we add it to the vector destined for the table.
519      */
520     for (i = 0; i < _tableColumnMap.size(); i ++)
521     {
522       for (j = 0; j < stats.size(); j++)
523       {
524         if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
525         {
526           row.addElement(new StatWidget(((Stat)stats.elementAt(j)).getValue(),500));
527           added = true;
528         }
529       }
530       if (added == false)
531       {
532         row.addElement(new StatWidget(0,500));
533       }
534       else
535         added = false;
536     }
537     _model.addRow(row);
538   }
539
540   /**
541    * Updates a line in the table by tweaking the StatWidgets.
542    * @param row the table row if the StatWidget
543    * @param stats The Vector of Stat elements to update the table row with
544    */
545   public void updateTableEntry(int row, Vector stats)
546   {
547     int i, j;
548
549     for (i = 0; i < _tableColumnMap.size(); i ++)
550     {
551       for (j = 0; j < stats.size(); j++)
552       {
553         if (((Stat)stats.elementAt(j)).getKey() == ((ColumnRef)_tableColumnMap.elementAt(i)).getKey())
554         {
555           ((StatWidget)_model.getValueAt(row,i+1)).updateValue(((Stat)stats.elementAt(j)).getValue());
556           stats.removeElementAt(j);
557           break;
558         }
559       }
560     }
561   }
562
563   /**
564    * Load up the properties
565    *
566    */
567   private void loadProperties()
568   {
569     _properties = new Properties();
570     try
571     {
572       _properties.load(new FileInputStream("ActivityConsole.properties"));
573     }
574     catch (Throwable t)
575     {
576       // System.out.println(t);
577       // No properties file... use hardcoded defaults.
578     }
579   }
580
581   /**
582    * Save the properties
583    *
584    */
585   private void saveProperties()
586   {
587     try
588     {
589       _properties.store(new FileOutputStream("ActivityConsole.properties"),resStrings.getString("guiPropertiesFileHeader"));
590     }
591     catch (Throwable t)
592     {
593       System.out.println(t);
594     }
595   }
596
597   /**
598    * Set a property
599    */
600   public void setProperty(String key, String value)
601   {
602     _properties.setProperty(key,value);
603   }
604
605   /**
606    * Set a boolean property
607    */
608   public void setProperty(String key, boolean value)
609   {
610     Boolean bVal = new Boolean(value);
611     _properties.setProperty(key,(String)bVal.toString());
612   }
613
614   /**
615    * Get a property
616    */
617   public String getProperty(String key, String defaultValue)
618   {
619     return _properties.getProperty(key,defaultValue);
620   }
621
622   /**
623    * Get a boolean property
624    */
625   public boolean getProperty(String key, boolean defaultValue)
626   {
627     String sDefaultValue;
628     String property;
629     if (defaultValue == false)
630       sDefaultValue = "false";
631     else
632       sDefaultValue = "true";
633     property = _properties.getProperty(key,sDefaultValue);
634     if (property.compareToIgnoreCase("true") == 0)
635       return true;
636     else
637       return false;
638   }
639
640   /**
641    * Remove a property
642    */
643   public void removeProperty(String key)
644   {
645     _properties.remove(key);
646   }
647
648   /**
649    * Worker class to offer a clickable table header for sorting.
650    */
651   class HeaderListener extends MouseAdapter
652   {
653     JTableHeader   header;
654     SortButtonRenderer renderer;
655
656     HeaderListener(JTableHeader header,SortButtonRenderer renderer)
657     {
658       this.header   = header;
659       this.renderer = renderer;
660     }
661
662     public void mousePressed(MouseEvent e)
663     {
664       Point click = e.getPoint();
665       int col = header.columnAtPoint(click);
666       int margin1, margin2;
667       int sortCol = header.getTable().convertColumnIndexToModel(col);
668
669       /* Don't perform the sort if the user is just trying to resize the columns. */
670       margin1 = header.columnAtPoint(new Point(click.x+3,click.y));
671       margin2 = header.columnAtPoint(new Point(click.x-3,click.y));
672       if ((col == margin1) && (col == margin2))
673       {
674         renderer.setPressedColumn(col);
675         renderer.setSelectedColumn(col);
676         header.repaint();
677
678         if (header.getTable().isEditing())
679         {
680           header.getTable().getCellEditor().stopCellEditing();
681         }
682
683         boolean isAscent;
684         if (SortButtonRenderer.DOWN == renderer.getState(col))
685         {
686           isAscent = true;
687         }
688         else
689         {
690           isAscent = false;
691         }
692         ((SortableTableModel)header.getTable().getModel())
693         .sortByColumn(sortCol, isAscent);    
694       }
695     }
696
697     public void mouseReleased(MouseEvent e)
698     {
699       int col = header.columnAtPoint(e.getPoint());
700       renderer.setPressedColumn(-1);
701       header.repaint();
702     }
703   }
704
705   /**
706    * Worker class to tell the menu when it's OK to delete a row (i.e. when a row gets
707    * selected).  This doesn't work reliably, but it's better than nothing.
708    */
709   public class SelectedListener implements ListSelectionListener
710   {
711     ListSelectionModel model;
712
713     public SelectedListener(ListSelectionModel lsm)
714     {
715       model = lsm;
716     }
717
718     public void valueChanged(ListSelectionEvent lse)
719     {
720       // NOTE - keep this in sync with columnSelectionChanged below...
721       int numSelections = _table.getSelectedRowCount();
722       int selRow = _table.getSelectedRow();
723       if (numSelections > 0)
724       {
725         if ((selRow > -1) &&
726             (selRow < _table.getRowCount()))
727         {
728           _deleteItem.setEnabled(true);
729         }
730         else
731           _deleteItem.setEnabled(false);
732       }
733       else
734         _deleteItem.setEnabled(false);
735     }
736     public void columnSelectionChanged(ListSelectionEvent lse)
737     {
738       // NOTE - keep this in sync with valueChanged above...
739       int numSelections = _table.getSelectedRowCount();
740       int selRow = _table.getSelectedRow();
741       if (numSelections > 0)
742       {
743         if ((selRow > -1) &&
744             (selRow < _table.getRowCount()))
745         {
746           _deleteItem.setEnabled(true);
747         }
748         else
749           _deleteItem.setEnabled(false);
750       }
751       else
752         _deleteItem.setEnabled(false);
753     }
754   }
755
756   /**
757    * Watch for the window closing event.  Dunno why swing doesn't handle this better natively.
758    */
759   public class WindowCloseMonitor extends WindowAdapter
760   {
761     public void windowClosing(WindowEvent e)
762     {
763       saveProperties();
764       Window w = e.getWindow();
765       w.setVisible(false);
766       w.dispose();
767       System.exit(0);
768     }
769   }
770 }