View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.log4j.chainsaw;
19  
20  import java.awt.BorderLayout;
21  import java.awt.Component;
22  import java.awt.Container;
23  import java.awt.Dimension;
24  import java.awt.FlowLayout;
25  import java.awt.Font;
26  import java.awt.FontMetrics;
27  import java.awt.Point;
28  import java.awt.Toolkit;
29  import java.awt.event.ActionEvent;
30  import java.awt.event.ActionListener;
31  import java.awt.event.FocusEvent;
32  import java.awt.event.FocusListener;
33  import java.awt.event.InputEvent;
34  import java.awt.event.KeyEvent;
35  import java.awt.event.KeyListener;
36  import java.awt.event.MouseAdapter;
37  import java.awt.event.MouseEvent;
38  import java.awt.event.MouseMotionAdapter;
39  import java.awt.event.WindowAdapter;
40  import java.awt.event.WindowEvent;
41  import java.beans.PropertyChangeEvent;
42  import java.beans.PropertyChangeListener;
43  import java.io.BufferedInputStream;
44  import java.io.BufferedOutputStream;
45  import java.io.EOFException;
46  import java.io.File;
47  import java.io.FileInputStream;
48  import java.io.FileNotFoundException;
49  import java.io.FileOutputStream;
50  import java.io.FileReader;
51  import java.io.FileWriter;
52  import java.io.IOException;
53  import java.io.ObjectInputStream;
54  import java.io.ObjectOutputStream;
55  import java.io.StringReader;
56  import java.net.URLEncoder;
57  import java.text.DateFormat;
58  import java.text.NumberFormat;
59  import java.text.SimpleDateFormat;
60  import java.util.ArrayList;
61  import java.util.Date;
62  import java.util.Enumeration;
63  import java.util.HashMap;
64  import java.util.HashSet;
65  import java.util.Iterator;
66  import java.util.List;
67  import java.util.Map;
68  import java.util.StringTokenizer;
69  import java.util.Vector;
70  
71  import javax.swing.AbstractAction;
72  import javax.swing.Action;
73  import javax.swing.BorderFactory;
74  import javax.swing.Box;
75  import javax.swing.BoxLayout;
76  import javax.swing.ButtonGroup;
77  import javax.swing.ImageIcon;
78  import javax.swing.JButton;
79  import javax.swing.JCheckBoxMenuItem;
80  import javax.swing.JComboBox;
81  import javax.swing.JComponent;
82  import javax.swing.JDialog;
83  import javax.swing.JEditorPane;
84  import javax.swing.JFrame;
85  import javax.swing.JLabel;
86  import javax.swing.JMenuItem;
87  import javax.swing.JPanel;
88  import javax.swing.JPopupMenu;
89  import javax.swing.JRadioButtonMenuItem;
90  import javax.swing.JScrollPane;
91  import javax.swing.JSeparator;
92  import javax.swing.JSplitPane;
93  import javax.swing.JTable;
94  import javax.swing.JTextArea;
95  import javax.swing.JTextField;
96  import javax.swing.JToolBar;
97  import javax.swing.KeyStroke;
98  import javax.swing.ListSelectionModel;
99  import javax.swing.SwingConstants;
100 import javax.swing.SwingUtilities;
101 import javax.swing.WindowConstants;
102 import javax.swing.event.ChangeEvent;
103 import javax.swing.event.DocumentEvent;
104 import javax.swing.event.DocumentListener;
105 import javax.swing.event.ListSelectionEvent;
106 import javax.swing.event.ListSelectionListener;
107 import javax.swing.event.TableColumnModelEvent;
108 import javax.swing.event.TableColumnModelListener;
109 import javax.swing.event.TableModelEvent;
110 import javax.swing.event.TableModelListener;
111 import javax.swing.table.TableColumn;
112 import javax.swing.table.TableColumnModel;
113 import javax.swing.text.Document;
114 
115 import org.apache.log4j.LogManager;
116 import org.apache.log4j.Logger;
117 import org.apache.log4j.PatternLayout;
118 import org.apache.log4j.chainsaw.color.ColorPanel;
119 import org.apache.log4j.chainsaw.color.RuleColorizer;
120 import org.apache.log4j.chainsaw.filter.FilterModel;
121 import org.apache.log4j.chainsaw.icons.ChainsawIcons;
122 import org.apache.log4j.chainsaw.icons.LineIconFactory;
123 import org.apache.log4j.chainsaw.layout.DefaultLayoutFactory;
124 import org.apache.log4j.chainsaw.layout.EventDetailLayout;
125 import org.apache.log4j.chainsaw.layout.LayoutEditorPane;
126 import org.apache.log4j.chainsaw.messages.MessageCenter;
127 import org.apache.log4j.chainsaw.prefs.LoadSettingsEvent;
128 import org.apache.log4j.chainsaw.prefs.Profileable;
129 import org.apache.log4j.chainsaw.prefs.SaveSettingsEvent;
130 import org.apache.log4j.chainsaw.prefs.SettingsManager;
131 import org.apache.log4j.chainsaw.xstream.TableColumnConverter;
132 import org.apache.log4j.helpers.Constants;
133 import org.apache.log4j.rule.ExpressionRule;
134 import org.apache.log4j.rule.Rule;
135 import org.apache.log4j.spi.LoggingEvent;
136 import org.apache.log4j.spi.LoggingEventFieldResolver;
137 
138 import com.thoughtworks.xstream.XStream;
139 import com.thoughtworks.xstream.io.xml.DomDriver;
140 
141 
142 /***
143  * A LogPanel provides a view to a collection of LoggingEvents.<br>
144  * <br>
145  * As events are received, the keywords in the 'tab identifier' application
146  * preference  are replaced with the values from the received event.  The
147  * main application uses  this expression to route received LoggingEvents to
148  * individual LogPanels which  match each event's resolved expression.<br>
149  * <br>
150  * The LogPanel's capabilities can be broken up into four areas:<br>
151  * <ul><li> toolbar - provides 'find' and 'refine focus' features
152  * <li> logger tree - displays a tree of the logger hierarchy, which can be used
153  * to filter the display
154  * <li> table - displays the events which pass the filtering rules
155  * <li>detail panel - displays information about the currently selected event
156  * </ul>
157  * Here is a complete list of LogPanel's capabilities:<br>
158  * <ul><li>display selected LoggingEvent row number and total LoggingEvent count
159  * <li>pause or unpause reception of LoggingEvents
160  * <li>configure, load and save column settings (displayed columns, order, width)
161  * <li>configure, load and save color rules
162  * filter displayed LoggingEvents based on the logger tree settings
163  * <li>filter displayed LoggingEvents based on a 'refine focus' expression
164  * (evaluates only those LoggingEvents which pass the logger tree filter
165  * <li>colorize LoggingEvents based on expressions
166  * <li>hide, show and configure the detail pane and tooltip
167  * <li>configure the formatting of the logger, level and timestamp fields
168  * <li>dock or undock
169  * <li>table displays first line of exception, but when cell is clicked, a
170  * popup opens to display the full stack trace
171  * <li>find
172  * <li>scroll to bottom
173  * <li>sort
174  * <li>provide a context menu which can be used to build color or display expressions
175  * <li>hide or show the logger tree
176  * <li>toggle the container storing the LoggingEvents to use either a
177  * CyclicBuffer (defaults to max size of 5000,  but configurable  through
178  * CHAINSAW_CAPACITY system property) or ArrayList (no max size)
179  * <li>use the mouse context menu to 'best-fit' columns, define display
180  * expression filters based on mouse location and access other capabilities
181  *</ul>
182  *
183  *@see org.apache.log4j.chainsaw.color.ColorPanel
184  *@see org.apache.log4j.rule.ExpressionRule
185  *@see org.apache.log4j.spi.LoggingEventFieldResolver
186  *
187  *@author Scott Deboy (sdeboy at apache.org)
188  *@author Paul Smith (psmith at apache.org)
189  *@author Stephen Pain
190  *@author Isuru Suriarachchi
191  *
192  */
193 public class LogPanel extends DockablePanel implements EventBatchListener,
194   Profileable {
195   private static final double DEFAULT_DETAIL_SPLIT_LOCATION = .5;
196   private static final double DEFAULT_LOG_TREE_SPLIT_LOCATION = .25;
197   private final String identifier;
198   private final ChainsawStatusBar statusBar;
199   private final JFrame preferencesFrame = new JFrame();
200   private final JFrame colorFrame = new JFrame();
201   private final JFrame undockedFrame;
202   private final DockablePanel externalPanel;
203   private final Action dockingAction;
204   private final JToolBar undockedToolbar;
205   private final JSortTable table;
206   private final TableColorizingRenderer renderer;
207   private final EventContainer tableModel;
208   private final ThrowableRenderPanel throwableRenderPanel;
209   private final JEditorPane detail;
210   private final JSplitPane lowerPanel;
211   private final DetailPaneUpdater detailPaneUpdater;
212   private final JPanel detailPanel = new JPanel(new BorderLayout());
213   private final JSplitPane nameTreeAndMainPanelSplit;
214   private final LoggerNameTreePanel logTreePanel;
215   private final LogPanelPreferenceModel preferenceModel =
216     new LogPanelPreferenceModel();
217   private final LogPanelPreferencePanel preferencesPanel =
218     new LogPanelPreferencePanel(preferenceModel);
219   private final FilterModel filterModel = new FilterModel();
220   private final RuleColorizer colorizer = new RuleColorizer();
221   private final RuleMediator ruleMediator = new RuleMediator();
222   private EventDetailLayout detailLayout = new EventDetailLayout();
223   private double lastDetailPanelSplitLocation = DEFAULT_DETAIL_SPLIT_LOCATION;
224   private double lastLogTreePanelSplitLocation =
225     DEFAULT_LOG_TREE_SPLIT_LOCATION;
226   private Point currentPoint;
227   private boolean paused = false;
228   private Rule findRule;
229   private final JPanel findPanel;
230   private JTextField findField;
231   private int dividerSize;
232   static final String TABLE_COLUMN_ORDER = "table.columns.order";
233   static final String TABLE_COLUMN_WIDTHS = "table.columns.widths";
234   static final String COLUMNS_EXTENSION = ".columns";
235   static final String COLORS_EXTENSION = ".colors";
236   private static final int LOG_PANEL_SERIALIZATION_VERSION_NUMBER = 1;
237   private int previousLastIndex = -1;
238   private final DateFormat timestampExpressionFormat = new SimpleDateFormat(Constants.TIMESTAMP_RULE_FORMAT);
239   private final Logger logger = LogManager.getLogger(LogPanel.class);
240   private final Vector filterExpressionVector;
241 
242   /***
243    * Creates a new LogPanel object.  If a LogPanel with this identifier has
244    * been loaded previously, reload settings saved on last exit.
245    *
246    * @param statusBar shared status bar, provided by main application
247    * @param identifier used to load and save settings
248    */
249   public LogPanel(final ChainsawStatusBar statusBar, final String identifier, int cyclicBufferSize) {
250     this.identifier = identifier;
251     this.statusBar = statusBar;
252     logger.debug("creating logpanel for " + identifier);
253 
254     setLayout(new BorderLayout());
255     findPanel = new JPanel();
256 
257     final Map columnNameKeywordMap = new HashMap();
258     columnNameKeywordMap.put(
259       ChainsawConstants.CLASS_COL_NAME, LoggingEventFieldResolver.CLASS_FIELD);
260     columnNameKeywordMap.put(
261       ChainsawConstants.FILE_COL_NAME, LoggingEventFieldResolver.FILE_FIELD);
262     columnNameKeywordMap.put(
263       ChainsawConstants.LEVEL_COL_NAME, LoggingEventFieldResolver.LEVEL_FIELD);
264     columnNameKeywordMap.put(
265       ChainsawConstants.LINE_COL_NAME, LoggingEventFieldResolver.LINE_FIELD);
266     columnNameKeywordMap.put(
267       ChainsawConstants.LOGGER_COL_NAME, LoggingEventFieldResolver.LOGGER_FIELD);
268     columnNameKeywordMap.put(
269       ChainsawConstants.NDC_COL_NAME, LoggingEventFieldResolver.NDC_FIELD);
270     columnNameKeywordMap.put(
271       ChainsawConstants.MESSAGE_COL_NAME, LoggingEventFieldResolver.MSG_FIELD);
272     columnNameKeywordMap.put(
273       ChainsawConstants.THREAD_COL_NAME, LoggingEventFieldResolver.THREAD_FIELD);
274     columnNameKeywordMap.put(
275       ChainsawConstants.THROWABLE_COL_NAME,
276       LoggingEventFieldResolver.EXCEPTION_FIELD);
277     columnNameKeywordMap.put(
278       ChainsawConstants.TIMESTAMP_COL_NAME,
279       LoggingEventFieldResolver.TIMESTAMP_FIELD);
280 
281     preferencesFrame.setTitle("'" + identifier + "' Log Panel Preferences");
282     preferencesFrame.setIconImage(
283       ((ImageIcon) ChainsawIcons.ICON_PREFERENCES).getImage());
284     preferencesFrame.getContentPane().add(preferencesPanel);
285 
286     preferencesFrame.setSize(640, 480);
287 
288     preferencesPanel.setOkCancelActionListener(
289       new ActionListener() {
290         public void actionPerformed(ActionEvent e) {
291           preferencesFrame.setVisible(false);
292         }
293       });
294 
295     setDetailPaneConversionPattern(
296       DefaultLayoutFactory.getDefaultPatternLayout());
297       detailLayout.setConversionPattern(
298       DefaultLayoutFactory.getDefaultPatternLayout());
299 
300     undockedFrame = new JFrame(identifier);
301     undockedFrame.setDefaultCloseOperation(
302       WindowConstants.DO_NOTHING_ON_CLOSE);
303 
304     if (ChainsawIcons.UNDOCKED_ICON != null) {
305       undockedFrame.setIconImage(
306         new ImageIcon(ChainsawIcons.UNDOCKED_ICON).getImage());
307     }
308 
309     externalPanel = new DockablePanel();
310     externalPanel.setLayout(new BorderLayout());
311     undockedFrame.getContentPane().add(externalPanel);
312 
313     undockedFrame.addWindowListener(
314       new WindowAdapter() {
315         public void windowClosing(WindowEvent e) {
316           dock();
317         }
318       });
319 
320     undockedToolbar = createDockwindowToolbar();
321     externalPanel.add(undockedToolbar, BorderLayout.NORTH);
322     undockedFrame.pack();
323 
324     /*
325      * Menus on which the preferencemodels rely
326      */
327 
328     /***
329      * Setup a popup menu triggered for Timestamp column to allow time stamp
330      * format changes
331      */
332     final JPopupMenu dateFormatChangePopup = new JPopupMenu();
333     final JRadioButtonMenuItem isoButton =
334       new JRadioButtonMenuItem(
335         new AbstractAction("Use ISO8601Format") {
336           public void actionPerformed(ActionEvent e) {
337             preferenceModel.setDateFormatPattern("ISO8601");
338           }
339         });
340     final JRadioButtonMenuItem simpleTimeButton =
341       new JRadioButtonMenuItem(
342         new AbstractAction("Use simple time") {
343           public void actionPerformed(ActionEvent e) {
344             preferenceModel.setDateFormatPattern("HH:mm:ss");
345           }
346         });
347 
348     ButtonGroup dfBG = new ButtonGroup();
349     dfBG.add(isoButton);
350     dfBG.add(simpleTimeButton);
351     isoButton.setSelected(true);
352     dateFormatChangePopup.add(isoButton);
353     dateFormatChangePopup.add(simpleTimeButton);
354 
355     final JCheckBoxMenuItem menuItemToggleToolTips =
356       new JCheckBoxMenuItem("Show ToolTips");
357     menuItemToggleToolTips.addActionListener(
358       new ActionListener() {
359         public void actionPerformed(ActionEvent evt) {
360           preferenceModel.setToolTips(menuItemToggleToolTips.isSelected());
361         }
362       });
363     menuItemToggleToolTips.setIcon(new ImageIcon(ChainsawIcons.TOOL_TIP));
364 
365     final JCheckBoxMenuItem menuItemLoggerTree =
366       new JCheckBoxMenuItem("Show Logger Tree panel");
367     menuItemLoggerTree.addActionListener(
368       new ActionListener() {
369         public void actionPerformed(ActionEvent e) {
370           preferenceModel.setLogTreePanelVisible(
371             menuItemLoggerTree.isSelected());
372         }
373       });
374     menuItemLoggerTree.setIcon(new ImageIcon(ChainsawIcons.WINDOW_ICON));
375 
376     final JCheckBoxMenuItem menuItemScrollBottom =
377       new JCheckBoxMenuItem("Scroll to bottom");
378     menuItemScrollBottom.addActionListener(
379       new ActionListener() {
380         public void actionPerformed(ActionEvent evt) {
381           preferenceModel.setScrollToBottom(menuItemScrollBottom.isSelected());
382         }
383       });
384     menuItemScrollBottom.setSelected(isScrollToBottom());
385 
386     menuItemScrollBottom.setIcon(
387       new ImageIcon(ChainsawIcons.SCROLL_TO_BOTTOM));
388 
389     final JCheckBoxMenuItem menuItemToggleDetails =
390       new JCheckBoxMenuItem("Show Detail Pane");
391     menuItemToggleDetails.addActionListener(
392       new ActionListener() {
393         public void actionPerformed(ActionEvent e) {
394           preferenceModel.setDetailPaneVisible(
395             menuItemToggleDetails.isSelected());
396         }
397       });
398 
399     menuItemToggleDetails.setIcon(new ImageIcon(ChainsawIcons.INFO));
400 
401     /*
402      * add preferencemodel listeners
403      */
404     preferenceModel.addPropertyChangeListener(
405       "levelIcons",
406       new PropertyChangeListener() {
407         public void propertyChange(PropertyChangeEvent evt) {
408           renderer.setLevelUseIcons(
409             ((Boolean) evt.getNewValue()).booleanValue());
410           table.tableChanged(new TableModelEvent(tableModel));
411         }
412       });
413 
414     preferenceModel.addPropertyChangeListener(
415       "detailPaneVisible",
416       new PropertyChangeListener() {
417         public void propertyChange(PropertyChangeEvent evt) {
418           boolean newValue = ((Boolean) evt.getNewValue()).booleanValue();
419 
420           if (newValue) {
421             showDetailPane();
422           } else {
423             hideDetailPane();
424           }
425         }
426       });
427 
428     preferenceModel.addPropertyChangeListener(
429       "logTreePanelVisible",
430       new PropertyChangeListener() {
431         public void propertyChange(PropertyChangeEvent evt) {
432           boolean newValue = ((Boolean) evt.getNewValue()).booleanValue();
433 
434           if (newValue) {
435             showLogTreePanel();
436           } else {
437             hideLogTreePanel();
438           }
439         }
440       });
441     
442     preferenceModel.addPropertyChangeListener(
443       "toolTips",
444       new PropertyChangeListener() {
445         public void propertyChange(PropertyChangeEvent evt) {
446           renderer.setToolTipsVisible(
447             ((Boolean) evt.getNewValue()).booleanValue());
448         }
449       });
450 
451     preferenceModel.addPropertyChangeListener(
452       "visibleColumns",
453       new PropertyChangeListener() {
454     	public void propertyChange(PropertyChangeEvent evt) {
455     		//remove all columns and re-add visible
456             TableColumnModel columnModel = table.getColumnModel();
457             while (columnModel.getColumnCount() > 0) {
458                 columnModel.removeColumn(columnModel.getColumn(0)); 
459     		}
460             for (Iterator iter = preferenceModel.getVisibleColumnOrder().iterator();iter.hasNext();) {
461     			TableColumn c = (TableColumn)iter.next();
462     			columnModel.addColumn(c);
463     		}
464     	}
465       });
466 
467     PropertyChangeListener datePrefsChangeListener =
468       new PropertyChangeListener() {
469         public void propertyChange(PropertyChangeEvent evt) {
470           LogPanelPreferenceModel model =
471             (LogPanelPreferenceModel) evt.getSource();
472 
473           isoButton.setSelected(model.isUseISO8601Format());
474           simpleTimeButton.setSelected(
475             !model.isUseISO8601Format() && !model.isCustomDateFormat());
476 
477           if (model.isUseISO8601Format()) {
478             renderer.setDateFormatter(new SimpleDateFormat(Constants.ISO8601_PATTERN));
479           } else {
480       		try {
481             renderer.setDateFormatter(
482               new SimpleDateFormat(model.getDateFormatPattern()));
483             		} catch (IllegalArgumentException iae) {
484             			model.setDefaultDatePatternFormat();
485                         renderer.setDateFormatter(new SimpleDateFormat(Constants.ISO8601_PATTERN));
486             		}
487           }
488 
489           table.tableChanged(new TableModelEvent(tableModel));
490         }
491       };
492 
493     preferenceModel.addPropertyChangeListener(
494       "dateFormatPattern", datePrefsChangeListener);
495     preferenceModel.addPropertyChangeListener(
496       "dateFormatPattern", datePrefsChangeListener);
497 
498     preferenceModel.addPropertyChangeListener(
499       "loggerPrecision",
500       new PropertyChangeListener() {
501         public void propertyChange(PropertyChangeEvent evt) {
502           LogPanelPreferenceModel model =
503             (LogPanelPreferenceModel) evt.getSource();
504 
505           renderer.setLoggerPrecision(model.getLoggerPrecision());
506 
507           table.tableChanged(new TableModelEvent(tableModel));
508         }
509       });
510 
511     preferenceModel.addPropertyChangeListener(
512       "toolTips",
513       new PropertyChangeListener() {
514         public void propertyChange(PropertyChangeEvent evt) {
515           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
516           menuItemToggleToolTips.setSelected(value);
517         }
518       });
519 
520     preferenceModel.addPropertyChangeListener(
521       "logTreePanelVisible",
522       new PropertyChangeListener() {
523         public void propertyChange(PropertyChangeEvent evt) {
524           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
525           menuItemLoggerTree.setSelected(value);
526         }
527       });
528 
529     preferenceModel.addPropertyChangeListener(
530       "scrollToBottom",
531       new PropertyChangeListener() {
532         public void propertyChange(PropertyChangeEvent evt) {
533           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
534           menuItemScrollBottom.setSelected(value);
535           if (value) {
536           	table.scrollToBottom(table.columnAtPoint(table.getVisibleRect().getLocation()));
537           }
538         }
539       });
540 
541     preferenceModel.addPropertyChangeListener(
542       "detailPaneVisible",
543       new PropertyChangeListener() {
544         public void propertyChange(PropertyChangeEvent evt) {
545           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
546           menuItemToggleDetails.setSelected(value);
547         }
548       });
549 
550     /*
551      *End of preferenceModel listeners
552      */
553     tableModel = new ChainsawCyclicBufferTableModel(cyclicBufferSize);
554     table = new JSortTable(tableModel);
555     //add a listener to update the 'refine focus'
556     tableModel.addNewKeyListener(new NewKeyListener() {
557 		public void newKeyAdded(NewKeyEvent e) {
558             columnNameKeywordMap.put(e.getKey(), "PROP." + e.getKey());
559 		}
560     });
561 
562     /*
563      * Set the Display rule to use the mediator, the model will add itself as
564      * a property change listener and update itself when the rule changes.
565      */
566     tableModel.setDisplayRule(ruleMediator);
567 
568     tableModel.addEventCountListener(
569       new EventCountListener() {
570         public void eventCountChanged(int currentCount, int totalCount) {
571           if (LogPanel.this.isVisible()) {
572             statusBar.setSelectedLine(
573               table.getSelectedRow() + 1, currentCount, totalCount);
574           }
575         }
576       });
577 
578     tableModel.addEventCountListener(
579       new EventCountListener() {
580         final NumberFormat formatter = NumberFormat.getPercentInstance();
581         boolean warning75 = false;
582         boolean warning100 = false;
583 
584         public void eventCountChanged(int currentCount, int totalCount) {
585           if (tableModel.isCyclic()) {
586             double percent =
587               ((double) totalCount) / ((ChainsawCyclicBufferTableModel) tableModel)
588               .getMaxSize();
589             String msg = null;
590 
591             if ((percent > 0.75) && (percent < 1.0) && !warning75) {
592               msg =
593                 "Warning :: " + formatter.format(percent) + " of the '"
594                 + getIdentifier() + "' buffer has been used";
595               warning75 = true;
596             } else if ((percent >= 1.0) && !warning100) {
597               msg =
598                 "Warning :: " + formatter.format(percent) + " of the '"
599                 + getIdentifier()
600                 + "' buffer has been used.  Older events are being discarded.";
601               warning100 = true;
602             }
603 
604             if (msg != null) {
605               MessageCenter.getInstance().getLogger().info(msg);
606             }
607           }
608         }
609       });
610 
611     /*
612      * Logger tree panel
613      *
614      */
615     LogPanelLoggerTreeModel logTreeModel = new LogPanelLoggerTreeModel();
616     logTreePanel = new LoggerNameTreePanel(logTreeModel, preferenceModel);
617     tableModel.addLoggerNameListener(logTreeModel);
618 
619     /***
620      * Set the LoggerRule to be the LoggerTreePanel, as this visual component
621      * is a rule itself, and the RuleMediator will automatically listen when
622      * it's rule state changes.
623      */
624     ruleMediator.setLoggerRule(logTreePanel);
625     colorizer.setLoggerRule(logTreePanel.getLoggerColorRule());
626 
627     /*
628      * Color rule frame and panel
629      */
630     colorFrame.setTitle("'" + identifier + "' Color Filter");
631     colorFrame.setIconImage(
632       ((ImageIcon) ChainsawIcons.ICON_PREFERENCES).getImage());
633 
634     final ColorPanel colorPanel = new ColorPanel(colorizer, filterModel);
635 
636     colorFrame.getContentPane().add(colorPanel);
637 
638     colorPanel.setCloseActionListener(
639       new ActionListener() {
640         public void actionPerformed(ActionEvent e) {
641           colorFrame.setVisible(false);
642         }
643       });
644 
645     colorizer.addPropertyChangeListener(
646       "colorrule",
647       new PropertyChangeListener() {
648         public void propertyChange(PropertyChangeEvent evt) {
649           if (table != null) {
650             table.repaint();
651           }
652         }
653       });
654 
655     /*
656      * Table definition.  Actual construction is above (next to tablemodel)
657      */
658     table.setRowHeight(20);
659     table.setShowGrid(false);
660 
661     table.getColumnModel().addColumnModelListener(
662       new ChainsawTableColumnModelListener());
663 
664     table.setAutoCreateColumnsFromModel(false);
665 
666     table.addMouseMotionListener(new TableColumnDetailMouseListener());
667 
668     table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
669 
670     //set valueisadjusting if holding down a key - don't process setdetail events
671     table.addKeyListener(
672       new KeyListener() {
673         public void keyTyped(KeyEvent e) {
674         }
675 
676         public void keyPressed(KeyEvent e) {
677           synchronized (detail) {
678             table.getSelectionModel().setValueIsAdjusting(true);
679             detail.notify();
680           }
681         }
682 
683         public void keyReleased(KeyEvent e) {
684           synchronized (detail) {
685             table.getSelectionModel().setValueIsAdjusting(false);
686             detail.notify();
687           }
688         }
689       });
690 
691     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
692 
693     table.getSelectionModel().addListSelectionListener(
694       new ListSelectionListener() {
695         public void valueChanged(ListSelectionEvent evt) {
696           if (
697             ((evt.getFirstIndex() == evt.getLastIndex())
698               && (evt.getFirstIndex() > 0)) || (evt.getValueIsAdjusting())) {
699             return;
700           }
701           boolean lastIndexOnLastRow = (evt.getLastIndex() == (table.getRowCount() - 1));
702           boolean lastIndexSame = (previousLastIndex == evt.getLastIndex());
703 
704           /*
705            * when scroll-to-bottom is active, here is what events look like:
706            * rowcount-1: 227, last: 227, previous last: 191..first: 191
707            * 
708            * when the user has unselected the bottom row, here is what the events look like:
709            * rowcount-1: 227, last: 227, previous last: 227..first: 222
710            * 
711            * note: previouslast is set after it is evaluated in the bypass scroll check
712           */
713          //System.out.println("rowcount: " + (table.getRowCount() - 1) + ", last: " + evt.getLastIndex() +", previous last: " + previousLastIndex + "..first: " + evt.getFirstIndex() + ", isadjusting: " + evt.getValueIsAdjusting());
714           
715           boolean disableScrollToBottom = (lastIndexOnLastRow && lastIndexSame && previousLastIndex != evt.getFirstIndex());
716           if (disableScrollToBottom && isScrollToBottom() && table.getRowCount() > 0) {
717           	preferenceModel.setScrollToBottom(false);
718           }
719           previousLastIndex = evt.getLastIndex();
720 
721           final ListSelectionModel lsm = (ListSelectionModel) evt.getSource();
722 
723           if (lsm.isSelectionEmpty()) {
724             if (isVisible()) {
725               statusBar.setNothingSelected();
726             }
727 
728             if (detail.getDocument().getDefaultRootElement() != null) {
729               detailPaneUpdater.setSelectedRow(-1);
730             }
731           } else {
732             if (table.getSelectedRow() > -1) {
733               int selectedRow = table.getSelectedRow();
734 
735               if (isVisible()) {
736                 updateStatusBar();
737               }
738 
739               try {
740                 if (tableModel.getRowCount() >= selectedRow) {
741                   detailPaneUpdater.setSelectedRow(table.getSelectedRow());
742                 } else {
743                   detailPaneUpdater.setSelectedRow(-1);
744                 }
745               } catch (Exception e) {
746                 e.printStackTrace();
747                 detailPaneUpdater.setSelectedRow(-1);
748               }
749             }
750           }
751         }
752       });
753 
754     renderer = new TableColorizingRenderer(colorizer);
755     renderer.setToolTipsVisible(preferenceModel.isToolTips());
756 
757     table.setDefaultRenderer(Object.class, renderer);
758 
759     /*
760      * Throwable popup
761      */
762     throwableRenderPanel = new ThrowableRenderPanel();
763 
764     final JDialog detailDialog = new JDialog((JFrame) null, true);
765     Container container = detailDialog.getContentPane();
766     final JTextArea detailArea = new JTextArea(10, 40);
767     detailArea.setEditable(false);
768     container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
769     container.add(new JScrollPane(detailArea));
770 
771     detailDialog.pack();
772 
773     throwableRenderPanel.addActionListener(
774       new ActionListener() {
775         public void actionPerformed(ActionEvent e) {
776           Object o = table.getValueAt(
777             table.getSelectedRow(), table.getSelectedColumn());
778           if (o == null) {
779             //no row selected - ignore
780           	logger.debug("no row selected - unable to display throwable popup");
781             return;
782           }
783           detailDialog.setTitle(
784             table.getColumnName(table.getSelectedColumn()) + " detail...");
785 
786           if (o instanceof String[]) {
787             StringBuffer buf = new StringBuffer();
788             String[] ti = (String[]) o;
789             buf.append(ti[0]).append("\n");
790 
791             for (int i = 1; i < ti.length; i++) {
792               buf.append(ti[i]).append("\n    ");
793             }
794 
795             detailArea.setText(buf.toString());
796           } else {
797             detailArea.setText((o == null) ? "" : o.toString());
798           }
799 
800           detailDialog.setLocation(lowerPanel.getLocationOnScreen());
801           SwingUtilities.invokeLater(
802             new Runnable() {
803               public void run() {
804                 detailDialog.setVisible(true);
805               }
806             });
807         }
808       });
809 
810     /*
811      * We listen for new Key's coming in so we can get them automatically
812      * added as columns
813      */
814     tableModel.addNewKeyListener(
815       new NewKeyListener() {
816         public void newKeyAdded(final NewKeyEvent e) {
817         	SwingUtilities.invokeLater(new Runnable() {
818         		public void run() {
819            // don't add the column if we already know about it, this could be if we've seen it before and saved the column preferences
820             //this may throw an illegalargexception - ignore it because we need to add only if not already added
821         	//if the column is already added, don't add again
822         	
823         	try {
824         	if(table.getColumn(e.getKey())!=null){
825                 return;
826             } 
827         	} catch (IllegalArgumentException iae) {}
828           TableColumn col = new TableColumn(e.getNewModelIndex());
829           col.setHeaderValue(e.getKey());
830 
831           if (preferenceModel.addColumn(col)) {
832         	  table.addColumn(col);
833         	  preferenceModel.setColumnVisible(e.getKey().toString(), true);
834           }
835         		}
836         	});
837         }
838       });
839 
840     tableModel.addPropertyChangeListener(
841       "cyclic",
842       new PropertyChangeListener() {
843         public void propertyChange(PropertyChangeEvent arg0) {
844           if (tableModel.isCyclic()) {
845             MessageCenter.getInstance().getLogger().warn(
846               "Changed to Cyclic Mode. Maximum # events kept: "
847               + tableModel.getMaxSize());
848           } else {
849             MessageCenter.getInstance().getLogger().warn(
850               "Changed to Unlimited Mode. Warning, you may run out of memory.");
851           }
852         }
853       });
854 
855     table.getTableHeader().addMouseListener(
856       new MouseAdapter() {
857         public void mouseClicked(MouseEvent e) {
858           checkEvent(e);
859         }
860 
861         public void mousePressed(MouseEvent e) {
862           checkEvent(e);
863         }
864 
865         public void mouseReleased(MouseEvent e) {
866           checkEvent(e);
867         }
868 
869         private void checkEvent(MouseEvent e) {
870           if (e.isPopupTrigger()) {
871             TableColumnModel colModel = table.getColumnModel();
872             int index = colModel.getColumnIndexAtX(e.getX());
873             int modelIndex = colModel.getColumn(index).getModelIndex();
874 
875             if ((modelIndex + 1) == ChainsawColumns.INDEX_TIMESTAMP_COL_NAME) {
876               dateFormatChangePopup.show(e.getComponent(), e.getX(), e.getY());
877             }
878           }
879         }
880       });
881 
882     /*
883      * Upper panel definition
884      */
885     JPanel upperPanel = new JPanel(new BorderLayout());
886     upperPanel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 0));
887 
888     final JLabel filterLabel = new JLabel("Refine focus on: ");
889     filterLabel.setFont(filterLabel.getFont().deriveFont(Font.BOLD));
890 
891     JPanel upperLeftPanel =
892       new JPanel(new FlowLayout(FlowLayout.CENTER, 3, 0));
893     upperLeftPanel.add(filterLabel);
894 
895     //hold a reference to the combobox model so that we can check to prevent duplicates
896     //final Vector filterExpressionVector = new Vector();
897     filterExpressionVector = new Vector();
898     //add (hopefully useful) default filters
899     filterExpressionVector.add("LEVEL == TRACE");
900     filterExpressionVector.add("LEVEL >= DEBUG");
901     filterExpressionVector.add("LEVEL >= INFO");
902     filterExpressionVector.add("LEVEL >= WARN");
903     filterExpressionVector.add("LEVEL >= ERROR");
904     filterExpressionVector.add("LEVEL == FATAL");
905     
906     final JComboBox filterCombo = new JComboBox(filterExpressionVector);
907     filterCombo.setSelectedIndex(-1);
908     final JTextField filterText;
909 
910     if (filterCombo.getEditor().getEditorComponent() instanceof JTextField) {
911       String comboToolTipText =
912         "Enter an expression, press enter to add to list";
913       filterText = (JTextField) filterCombo.getEditor().getEditorComponent();
914       filterText.setToolTipText(comboToolTipText);
915       filterText.addKeyListener(
916         new ExpressionRuleContext(filterModel, filterText));
917       filterText.getDocument().addDocumentListener(
918         new DelayedFilterTextDocumentListener(filterText));
919       filterCombo.setEditable(true);
920       filterCombo.addActionListener(
921         new AbstractAction() {
922           public void actionPerformed(ActionEvent e) {
923             if (e.getActionCommand().equals("comboBoxEdited")) {
924               try {
925                 //verify the expression is valid
926                 ExpressionRule.getRule(
927                   filterCombo.getSelectedItem().toString());
928               } catch (IllegalArgumentException iae) {
929                 //don't add expressions that aren't valid
930                 return;
931               }
932 
933               //should be 'valid expression' check
934               if (!(filterExpressionVector.contains(filterCombo.getSelectedItem()))) {
935                 filterCombo.addItem(filterCombo.getSelectedItem());
936               }
937             }
938           }
939         });
940       upperPanel.add(filterCombo, BorderLayout.CENTER);
941     } else {
942       filterText = new JTextField();
943       filterText.setToolTipText("Enter an expression");
944       filterText.addKeyListener(
945         new ExpressionRuleContext(filterModel, filterText));
946       filterText.getDocument().addDocumentListener(
947         new DelayedFilterTextDocumentListener(filterText));
948       upperPanel.add(filterText, BorderLayout.CENTER);
949     }
950 
951     upperPanel.add(upperLeftPanel, BorderLayout.WEST);
952 
953     JPanel upperRightPanel =
954       new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
955 
956     //Adding a button to clear filter expressions which are currently remembered by Chainsaw...
957     final JButton clearButton = new JButton("Clear expression");
958     clearButton.setToolTipText("Click here to remove the selected expression from the list");
959     clearButton.addActionListener(
960             new AbstractAction() {
961                 public void actionPerformed(ActionEvent e){
962                 	Object selectedItem = filterCombo.getSelectedItem();
963                     if (e.getSource() == clearButton && selectedItem != null && !selectedItem.toString().equals("")){
964                         if (filterExpressionVector.contains(selectedItem.toString())){
965                             filterExpressionVector.remove(selectedItem.toString());
966                         }
967                         filterCombo.setSelectedIndex(-1);
968                     }
969                     //don't just remove the entry from the store, clear the refine focus field
970                     filterText.setText(null);
971                 }
972             }
973     );
974 
975     upperRightPanel.add(clearButton);
976 
977     upperPanel.add(upperRightPanel, BorderLayout.EAST);
978 
979     /*
980      * Detail pane definition
981      */
982     detail = new JEditorPane(ChainsawConstants.DETAIL_CONTENT_TYPE, "");
983     detail.setEditable(false);
984 
985     detailPaneUpdater = new DetailPaneUpdater();
986 
987     addFocusListener(new FocusListener() {
988 
989         public void focusGained(FocusEvent e) {
990             detailPaneUpdater.updateDetailPane();
991         }
992 
993         public void focusLost(FocusEvent e) {
994             
995         }
996     });
997 
998     tableModel.addTableModelListener(new TableModelListener() {
999 		public void tableChanged(TableModelEvent e) {
1000 			detailPaneUpdater.setSelectedRow(table.getSelectedRow());
1001 		}
1002     });
1003     
1004     addPropertyChangeListener(
1005       "detailPaneConversionPattern", detailPaneUpdater);
1006 
1007     final JScrollPane detailPane = new JScrollPane(detail);
1008 
1009     detailPane.setPreferredSize(new Dimension(900, 50));
1010 
1011     detailPanel.add(detailPane, BorderLayout.CENTER);
1012 
1013     JPanel eventsAndStatusPanel = new JPanel(new BorderLayout());
1014 
1015     final JScrollPane eventsPane = new JScrollPane(table);
1016 
1017     eventsAndStatusPanel.add(eventsPane, BorderLayout.CENTER);
1018 
1019     final JPanel statusLabelPanel = new JPanel();
1020     statusLabelPanel.setLayout(new BorderLayout());
1021 
1022     statusLabelPanel.add(upperPanel, BorderLayout.CENTER);
1023     eventsAndStatusPanel.add(statusLabelPanel, BorderLayout.NORTH);
1024 
1025     lowerPanel =
1026       new JSplitPane(
1027         JSplitPane.VERTICAL_SPLIT, eventsAndStatusPanel, detailPanel);
1028 
1029     dividerSize = lowerPanel.getDividerSize();
1030     lowerPanel.setDividerLocation(-1);
1031 
1032     lowerPanel.setResizeWeight(1.0);
1033     lowerPanel.setBorder(null);
1034     lowerPanel.setContinuousLayout(true);
1035 
1036     if (preferenceModel.isDetailPaneVisible()) {
1037       showDetailPane();
1038     } else {
1039       hideDetailPane();
1040     }
1041     
1042     /*
1043      * Detail panel layout editor
1044      */
1045     final JToolBar detailToolbar = new JToolBar(SwingConstants.HORIZONTAL);
1046     detailToolbar.setFloatable(false);
1047 
1048     final LayoutEditorPane layoutEditorPane = new LayoutEditorPane();
1049     final JDialog layoutEditorDialog =
1050       new JDialog((JFrame) null, "Pattern Editor");
1051     layoutEditorDialog.getContentPane().add(layoutEditorPane);
1052     layoutEditorDialog.setSize(640, 480);
1053 
1054     layoutEditorPane.addCancelActionListener(
1055       new ActionListener() {
1056         public void actionPerformed(ActionEvent e) {
1057           layoutEditorDialog.setVisible(false);
1058         }
1059       });
1060 
1061     layoutEditorPane.addOkActionListener(
1062       new ActionListener() {
1063         public void actionPerformed(ActionEvent e) {
1064           setDetailPaneConversionPattern(
1065             layoutEditorPane.getConversionPattern());
1066           layoutEditorDialog.setVisible(false);
1067         }
1068       });
1069 
1070     Action editDetailAction =
1071       new AbstractAction(
1072         "Edit...", new ImageIcon(ChainsawIcons.ICON_EDIT_RECEIVER)) {
1073         public void actionPerformed(ActionEvent e) {
1074           layoutEditorPane.setConversionPattern(
1075             getDetailPaneConversionPattern());
1076 
1077           Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
1078           Point p =
1079             new Point(
1080               ((int) ((size.getWidth() / 2)
1081               - (layoutEditorDialog.getSize().getWidth() / 2))),
1082               ((int) ((size.getHeight() / 2)
1083               - (layoutEditorDialog.getSize().getHeight() / 2))));
1084           layoutEditorDialog.setLocation(p);
1085 
1086           layoutEditorDialog.setVisible(true);
1087         }
1088       };
1089 
1090     editDetailAction.putValue(
1091       Action.SHORT_DESCRIPTION,
1092       "opens a Dialog window to Edit the Pattern Layout text");
1093 
1094     final SmallButton editDetailButton = new SmallButton(editDetailAction);
1095     editDetailButton.setText(null);
1096     detailToolbar.add(Box.createHorizontalGlue());
1097     detailToolbar.add(editDetailButton);
1098     detailToolbar.addSeparator();
1099     detailToolbar.add(Box.createHorizontalStrut(5));
1100 
1101     Action closeDetailAction =
1102       new AbstractAction(null, LineIconFactory.createCloseIcon()) {
1103         public void actionPerformed(ActionEvent arg0) {
1104           preferenceModel.setDetailPaneVisible(false);
1105         }
1106       };
1107 
1108     closeDetailAction.putValue(
1109       Action.SHORT_DESCRIPTION, "Hides the Detail Panel");
1110 
1111     SmallButton closeDetailButton = new SmallButton(closeDetailAction);
1112     detailToolbar.add(closeDetailButton);
1113 
1114     detailPanel.add(detailToolbar, BorderLayout.NORTH);
1115 
1116     JPopupMenu editDetailPopupMenu = new JPopupMenu();
1117     editDetailPopupMenu.add(editDetailAction);
1118     editDetailPopupMenu.addSeparator();
1119 
1120     final ButtonGroup layoutGroup = new ButtonGroup();
1121 
1122     JRadioButtonMenuItem defaultLayoutRadio =
1123       new JRadioButtonMenuItem(
1124         new AbstractAction("Set to Default Layout") {
1125           public void actionPerformed(ActionEvent e) {
1126             setDetailPaneConversionPattern(
1127               DefaultLayoutFactory.getDefaultPatternLayout());
1128           }
1129         });
1130     editDetailPopupMenu.add(defaultLayoutRadio);
1131     layoutGroup.add(defaultLayoutRadio);
1132     defaultLayoutRadio.setSelected(true);
1133 
1134     JRadioButtonMenuItem tccLayoutRadio =
1135       new JRadioButtonMenuItem(
1136         new AbstractAction("Set to TCCLayout") {
1137           public void actionPerformed(ActionEvent e) {
1138             setDetailPaneConversionPattern(
1139               PatternLayout.TTCC_CONVERSION_PATTERN);
1140           }
1141         });
1142     editDetailPopupMenu.add(tccLayoutRadio);
1143     layoutGroup.add(tccLayoutRadio);
1144 
1145     PopupListener editDetailPopupListener =
1146       new PopupListener(editDetailPopupMenu);
1147     detail.addMouseListener(editDetailPopupListener);
1148 
1149     /*
1150      * Logger tree splitpane definition
1151      */
1152     nameTreeAndMainPanelSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, logTreePanel, lowerPanel);
1153     
1154     nameTreeAndMainPanelSplit.setToolTipText("Still under development....");
1155     nameTreeAndMainPanelSplit.setDividerLocation(-1);
1156 
1157     add(nameTreeAndMainPanelSplit, BorderLayout.CENTER);
1158 
1159     if (isLogTreeVisible()) {
1160         showLogTreePanel();
1161     } else {
1162         hideLogTreePanel();
1163     }
1164 
1165     /*
1166      * Other menu items
1167      */
1168     final JMenuItem menuItemBestFit = new JMenuItem("Best fit column");
1169     menuItemBestFit.addActionListener(
1170       new ActionListener() {
1171         public void actionPerformed(ActionEvent evt) {
1172           if (currentPoint != null) {
1173             int column = table.columnAtPoint(currentPoint);
1174             int maxWidth = getMaxColumnWidth(column);
1175             table.getColumnModel().getColumn(column).setPreferredWidth(
1176               maxWidth);
1177           }
1178         }
1179       });
1180 
1181     JMenuItem menuItemColorPanel = new JMenuItem("LogPanel Color Filter...");
1182     menuItemColorPanel.addActionListener(
1183       new ActionListener() {
1184         public void actionPerformed(ActionEvent evt) {
1185           showColorPreferences();
1186         }
1187       });
1188     menuItemColorPanel.setIcon(ChainsawIcons.ICON_PREFERENCES);
1189 
1190     JMenuItem menuItemLogPanelPreferences =
1191       new JMenuItem("LogPanel Preferences...");
1192     menuItemLogPanelPreferences.addActionListener(
1193       new ActionListener() {
1194         public void actionPerformed(ActionEvent evt) {
1195           showPreferences();
1196         }
1197       });
1198     menuItemLogPanelPreferences.setIcon(ChainsawIcons.ICON_PREFERENCES);
1199 
1200     final JMenuItem menuItemFocusOn =
1201       new JMenuItem("Set 'refine focus' field");
1202     menuItemFocusOn.addActionListener(
1203       new ActionListener() {
1204         public void actionPerformed(ActionEvent evt) {
1205           if (currentPoint != null) {
1206             String operator = "==";
1207             int column = table.columnAtPoint(currentPoint);
1208             int row = table.rowAtPoint(currentPoint);
1209             String colName = table.getColumnName(column);
1210             String value = "";
1211 
1212             if (colName.equalsIgnoreCase(ChainsawConstants.TIMESTAMP_COL_NAME)) {
1213             	value = timestampExpressionFormat.format(new Date(table.getValueAt(row, column).toString()));
1214             } else {
1215               Object o = table.getValueAt(row, column);
1216 
1217               if (o != null) {
1218                 if (o instanceof String[]) {
1219                   value = ((String[]) o)[0];
1220                   operator = "~=";
1221                 } else {
1222                   value = o.toString();
1223                 }
1224               }
1225             }
1226 
1227             if (columnNameKeywordMap.containsKey(colName)) {
1228               filterText.setText(
1229                 columnNameKeywordMap.get(colName).toString() + " " + operator
1230                 + " '" + value + "'");
1231             }
1232           }
1233         }
1234       });
1235 
1236     final JMenuItem menuDefineAddCustomFilter =
1237       new JMenuItem("Add to 'refine focus' field");
1238     menuDefineAddCustomFilter.addActionListener(
1239       new ActionListener() {
1240         public void actionPerformed(ActionEvent evt) {
1241           if (currentPoint != null) {
1242             String operator = "==";
1243             int column = table.columnAtPoint(currentPoint);
1244             int row = table.rowAtPoint(currentPoint);
1245             String colName = table.getColumnName(column);
1246             String value = "";
1247 
1248             if (colName.equalsIgnoreCase(ChainsawConstants.TIMESTAMP_COL_NAME)) {
1249               JComponent comp =
1250                 (JComponent) table.getCellRenderer(row, column);
1251 
1252               if (comp instanceof JLabel) {
1253                 value = ((JLabel) comp).getText();
1254               }
1255             } else {
1256               Object o = table.getValueAt(row, column).toString();
1257 
1258               if (o instanceof String[]) {
1259                 value = ((String[]) o)[0];
1260                 operator = "~=";
1261               } else {
1262                 value = o.toString();
1263               }
1264             }
1265 
1266             if (columnNameKeywordMap.containsKey(colName)) {
1267               filterText.setText(
1268                 filterText.getText() + " && "
1269                 + columnNameKeywordMap.get(colName).toString() + " "
1270                 + operator + " '" + value + "'");
1271             }
1272           }
1273         }
1274       });
1275 
1276     final JPopupMenu p = new JPopupMenu();
1277 
1278     final Action clearFocusAction =
1279       new AbstractAction("Clear 'refine focus' field") {
1280         public void actionPerformed(ActionEvent e) {
1281           filterText.setText(null);
1282           ruleMediator.setRefinementRule(null);
1283         }
1284       };
1285 
1286     final JMenuItem menuItemToggleDock = new JMenuItem("Undock/dock");
1287 
1288     dockingAction =
1289       new AbstractAction("Undock") {
1290           public void actionPerformed(ActionEvent evt) {
1291             if (isDocked()) {
1292               undock();
1293             } else {
1294               dock();
1295             }
1296           }
1297         };
1298     dockingAction.putValue(
1299       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.UNDOCK));
1300     menuItemToggleDock.setAction(dockingAction);
1301 
1302     /*
1303      * Popup definition
1304      */
1305     p.add(clearFocusAction);
1306     p.add(menuItemFocusOn);
1307     p.add(menuDefineAddCustomFilter);
1308     p.add(new JSeparator());
1309 
1310     p.add(menuItemBestFit);
1311     p.add(new JSeparator());
1312 
1313     p.add(menuItemToggleDetails);
1314     p.add(menuItemLoggerTree);
1315     p.add(menuItemToggleToolTips);
1316     p.add(new JSeparator());
1317     p.add(menuItemScrollBottom);
1318 
1319     p.add(new JSeparator());
1320     p.add(menuItemToggleDock);
1321 
1322     p.add(new JSeparator());
1323     p.add(menuItemColorPanel);
1324     p.add(menuItemLogPanelPreferences);
1325 
1326     final PopupListener popupListener = new PopupListener(p);
1327 
1328     eventsPane.addMouseListener(popupListener);
1329     table.addMouseListener(popupListener);
1330   }
1331   
1332   /***
1333    * Accessor
1334    *
1335    * @return scrollToBottom
1336    *
1337    */
1338   public boolean isScrollToBottom() {
1339   	return preferenceModel.isScrollToBottom();
1340   }
1341 
1342   /***
1343    * Mutator
1344    *
1345    */
1346   public void toggleScrollToBottom() {
1347   	preferenceModel.setScrollToBottom(!preferenceModel.isScrollToBottom());
1348   }
1349   
1350   /***
1351    * Accessor
1352    *
1353    * @return namespace
1354    *
1355    * @see Profileable
1356    */
1357   public String getNamespace() {
1358     return getIdentifier();
1359   }
1360 
1361   /***
1362    * Accessor
1363    *
1364    * @return identifier
1365    *
1366    * @see EventBatchListener
1367    */
1368   public String getInterestedIdentifier() {
1369     return getIdentifier();
1370   }
1371 
1372   /***
1373    * Process events associated with the identifier.  Currently assumes it only
1374    * receives events which share this LogPanel's identifier
1375    *
1376    * @param ident identifier shared by events
1377    * @param events list of LoggingEvent objects
1378    */
1379   public void receiveEventBatch(String ident, List events) {
1380     /*
1381      * if this panel is paused, we totally ignore events
1382      */
1383     if (isPaused()) {
1384       return;
1385     }
1386 
1387     //table.getSelectionModel().setValueIsAdjusting(true);
1388     boolean rowAdded = false;
1389 
1390     int first = tableModel.getLastAdded() + 1;
1391 
1392     for (Iterator iter = events.iterator(); iter.hasNext();) {
1393       LoggingEvent event = (LoggingEvent) iter.next();
1394 
1395       updateOtherModels(event);
1396 
1397       boolean isCurrentRowAdded = tableModel.isAddRow(event, true);
1398       rowAdded = rowAdded ? true : isCurrentRowAdded;
1399     }
1400 
1401     table.getSelectionModel().setValueIsAdjusting(false);
1402 
1403     //tell the model to notify the count listeners
1404     tableModel.notifyCountListeners();
1405 
1406     if (rowAdded) {
1407       if (tableModel.isSortEnabled()) {
1408         tableModel.sort();
1409       }
1410 
1411       tableModel.fireTableEvent(
1412         first, tableModel.getLastAdded(), events.size());
1413 
1414       if (isScrollToBottom()) {
1415         table.scrollToBottom(
1416           table.columnAtPoint(table.getVisibleRect().getLocation()));
1417       }
1418 
1419       //always update detail pane (since we may be using a cyclic buffer which is full)
1420       detailPaneUpdater.setSelectedRow(table.getSelectedRow());
1421     }
1422   }
1423   
1424   /***
1425    * Load settings from the panel preference model
1426    *
1427    * @param event
1428    *
1429    * @see LogPanelPreferenceModel
1430    */
1431   public void loadSettings(LoadSettingsEvent event) {
1432 
1433     File xmlFile = new File(SettingsManager.getInstance()
1434                 .getSettingsDirectory(), URLEncoder.encode(identifier) + ".xml");
1435 
1436         if (xmlFile.exists()) {
1437             XStream stream = buildXStreamForLogPanelPreference();
1438             ObjectInputStream in = null;
1439             try {
1440             	FileReader r = new FileReader(xmlFile);
1441             	in = stream.createObjectInputStream(r);
1442             	
1443                 LogPanelPreferenceModel storedPrefs = (LogPanelPreferenceModel)in.readObject();
1444                 preferenceModel.apply(storedPrefs);
1445                 TableColumnModel columnModel = table.getColumnModel();
1446                 //remove previous columns
1447                 while (columnModel.getColumnCount() > 0) {
1448                 	columnModel.removeColumn(columnModel.getColumn(0));
1449                 }
1450                 //add visible column order columns
1451                 for (Iterator iter = preferenceModel.getVisibleColumnOrder().iterator();iter.hasNext();) {
1452                 	TableColumn col = (TableColumn)iter.next();
1453                 	columnModel.addColumn(col);
1454                 }
1455 
1456                 try {
1457                 	//may be panel configs that don't have these values
1458                 lowerPanel.setDividerLocation(in.readInt());
1459                 nameTreeAndMainPanelSplit.setDividerLocation(in.readInt());
1460                 detailLayout.setConversionPattern(in.readObject().toString());
1461                 Point p = (Point)in.readObject();
1462                 undockedFrame.setLocation(p.x, p.y);
1463                 undockedFrame.setSize(((Dimension)in.readObject()));
1464 
1465                 int versionNumber = 0;
1466                 Vector savedVector;
1467 
1468                 //this version number is checked to identify whether there is a Vector comming next
1469                 try {
1470                     versionNumber = in.readInt();
1471                 } catch (EOFException eof){
1472                 }
1473 
1474                 //read the vector only if the version number is greater than 0. higher version numbers can be
1475                 //used in the future to save more data structures
1476                 if (versionNumber > 0){
1477                     savedVector = (Vector) in.readObject();
1478                     for(int i = 0 ; i < savedVector.size() ; i++){
1479                         Object item = savedVector.get(i);
1480                         if(!filterExpressionVector.contains(item)){
1481                             filterExpressionVector.add(item);
1482                         }
1483                     }
1484                 }
1485 
1486                 } catch (EOFException eof){
1487                 }
1488             } catch (Exception e) {
1489                 e.printStackTrace();
1490                 // TODO need to log this..
1491             } finally {
1492             	if (in != null) {
1493             		try {
1494             			in.close();
1495             		} catch (IOException ioe) {}
1496             	}
1497             }
1498         } else {
1499             loadDefaultColumnSettings(event);
1500 		}
1501         
1502     logTreePanel.ignore(preferenceModel.getHiddenLoggers());
1503 
1504     //first attempt to load encoded file
1505     File f2 =
1506       new File(
1507         SettingsManager.getInstance().getSettingsDirectory(), URLEncoder.encode(identifier) + COLORS_EXTENSION);
1508 
1509     if (f2.exists()) {
1510         loadColorSettings(f2);
1511     } else {
1512         f2 =
1513             new File(
1514               SettingsManager.getInstance().getSettingsDirectory(), identifier + COLORS_EXTENSION);
1515     }
1516   }
1517 
1518   /***
1519    * Save preferences to the panel preference model
1520    *
1521    * @param event
1522    *
1523    * @see LogPanelPreferenceModel
1524    */
1525   public void saveSettings(SaveSettingsEvent event) {
1526       File xmlFile = new File(SettingsManager.getInstance()
1527               .getSettingsDirectory(), URLEncoder.encode(identifier) + ".xml");
1528 
1529     preferenceModel.setHiddenLoggers(new HashSet(logTreePanel.getHiddenSet()));
1530     List visibleOrder = new ArrayList();
1531     Enumeration cols = table.getColumnModel().getColumns();
1532     while (cols.hasMoreElements()) {
1533     	TableColumn c = (TableColumn)cols.nextElement();
1534     	visibleOrder.add(c);
1535     }
1536     preferenceModel.setVisibleColumnOrder(visibleOrder);
1537     
1538     XStream stream = buildXStreamForLogPanelPreference();
1539     ObjectOutputStream s = null;
1540     try {
1541     	FileWriter w = new FileWriter(xmlFile);
1542     	s = stream.createObjectOutputStream(w);
1543     	s.writeObject(preferenceModel);
1544         s.writeInt(lowerPanel.getDividerLocation());
1545     	s.writeInt(nameTreeAndMainPanelSplit.getDividerLocation());
1546     	s.writeObject(detailLayout.getConversionPattern());
1547     	s.writeObject(undockedFrame.getLocation());
1548     	s.writeObject(undockedFrame.getSize());
1549         //this is a version number written to the file to identify that there is a Vector serialized after this
1550         s.writeInt(LOG_PANEL_SERIALIZATION_VERSION_NUMBER);
1551         s.writeObject(filterExpressionVector);
1552     } catch (Exception ex) {
1553         ex.printStackTrace();
1554         // TODO need to log this..
1555     } finally {
1556     	if (s != null) {
1557     		try {
1558     			s.close();
1559     		} catch (IOException ioe) {}
1560     	}
1561     }
1562 
1563 //    TODO colour settings need to be saved
1564     saveColorSettings();
1565   }
1566 
1567     private XStream buildXStreamForLogPanelPreference() {
1568         XStream stream = new XStream(new DomDriver());
1569         stream.registerConverter(new TableColumnConverter());
1570         return stream;
1571     }
1572 
1573   /***
1574      * Display the panel preferences frame
1575      */
1576   void showPreferences() {
1577     preferencesFrame.setVisible(true);
1578   }
1579 
1580   /***
1581    * Display the color rule frame
1582    */
1583   void showColorPreferences() {
1584     colorFrame.pack();
1585     colorFrame.setVisible(true);
1586   }
1587 
1588   /***
1589    * Toggle panel preference for detail visibility on or off
1590    */
1591   void toggleDetailVisible() {
1592     preferenceModel.setDetailPaneVisible(
1593       !preferenceModel.isDetailPaneVisible());
1594   }
1595 
1596   /***
1597    * Accessor
1598    *
1599    * @return detail visibility flag
1600    */
1601   boolean isDetailVisible() {
1602     return preferenceModel.isDetailPaneVisible();
1603   }
1604 
1605   /***
1606    * Toggle panel preference for logger tree visibility on or off
1607    */
1608   void toggleLogTreeVisible() {
1609     preferenceModel.setLogTreePanelVisible(
1610       !preferenceModel.isLogTreePanelVisible());
1611   }
1612 
1613   /***
1614    * Accessor
1615    *
1616    * @return logger tree visibility flag
1617    */
1618   boolean isLogTreeVisible() {
1619     return preferenceModel.isLogTreePanelVisible();
1620   }
1621 
1622   /***
1623    * Return all events
1624    *
1625    * @return list of LoggingEvents
1626    */
1627   List getEvents() {
1628     return tableModel.getAllEvents();
1629   }
1630 
1631   /***
1632    * Return the events that are visible with the current filter applied
1633    *
1634    * @return list of LoggingEvents
1635    */
1636   List getFilteredEvents() {
1637   	return tableModel.getFilteredEvents();  
1638   }
1639   
1640   List getMatchingEvents(Rule rule) {
1641     return tableModel.getMatchingEvents(rule);
1642   }
1643 
1644   /***
1645    * Remove all events
1646    */
1647   void clearEvents() {
1648     clearModel();
1649   }
1650 
1651   /***
1652    * Accessor
1653    *
1654    * @return identifier
1655    */
1656   String getIdentifier() {
1657     return identifier;
1658   }
1659 
1660   /***
1661    * Undocks this DockablePanel by removing the panel from the LogUI window
1662    * and placing it inside it's own JFrame.
1663    */
1664   void undock() {
1665   	int row = table.getSelectedRow();
1666     setDocked(false);
1667     externalPanel.removeAll();
1668     findPanel.removeAll();
1669     findPanel.add(findField);
1670 
1671     externalPanel.add(undockedToolbar, BorderLayout.NORTH);
1672     externalPanel.add(nameTreeAndMainPanelSplit, BorderLayout.CENTER);
1673     externalPanel.setDocked(false);
1674 
1675     undockedFrame.setVisible(true);
1676     dockingAction.putValue(Action.NAME, "Dock");
1677     dockingAction.putValue(Action.SMALL_ICON, ChainsawIcons.ICON_DOCK);
1678     if (row > -1) {
1679     	table.scrollToRow(row, table.columnAtPoint(table.getVisibleRect().getLocation()));
1680     }
1681   }
1682 
1683   /***
1684    * Add an eventCountListener
1685    *
1686    * @param l
1687    */
1688   void addEventCountListener(EventCountListener l) {
1689     tableModel.addEventCountListener(l);
1690   }
1691 
1692   /***
1693    * Accessor
1694    *
1695    * @return paused flag
1696    */
1697   boolean isPaused() {
1698     return paused;
1699   }
1700 
1701   /***
1702    * Modifies the Paused property and notifies the listeners
1703    *
1704    * @param paused
1705    */
1706   void setPaused(boolean paused) {
1707     boolean oldValue = this.paused;
1708     this.paused = paused;
1709     firePropertyChange("paused", oldValue, paused);
1710   }
1711 
1712   /***
1713    * Change the selected event on the log panel
1714    *
1715    * @param eventNumber
1716    */
1717   void setSelectedEvent(int eventNumber){
1718       table.scrollToRow(eventNumber - 1, 0);
1719   }
1720 
1721   /***
1722    * Add a preference propertyChangeListener
1723    *
1724    * @param listener
1725    */
1726   void addPreferencePropertyChangeListener(PropertyChangeListener listener) {
1727     preferenceModel.addPropertyChangeListener(listener);
1728   }
1729 
1730   /***
1731    * Toggle the LoggingEvent container from either managing a cyclic buffer of
1732    * events or an ArrayList of events
1733    */
1734   void toggleCyclic() {
1735     tableModel.setCyclic(!tableModel.isCyclic());
1736   }
1737 
1738   /***
1739    * Accessor
1740    *
1741    * @return flag answering if LoggingEvent container is a cyclic buffer
1742    */
1743   boolean isCyclic() {
1744     return tableModel.isCyclic();
1745   }
1746 
1747   public boolean updateRule(String ruleText) {
1748     if ((ruleText == null) || (ruleText.equals(""))) {
1749       findRule = null;
1750       colorizer.setFindRule(null);
1751       findField.setToolTipText(
1752         "Enter expression - right click or ctrl-space for menu");
1753       return false;
1754     } else {
1755       //only turn off scroltobottom when finding something (find not empty)
1756       preferenceModel.setScrollToBottom(false);
1757       try {
1758         findField.setToolTipText(
1759           "Enter expression - right click or ctrl-space for menu");
1760         findRule = ExpressionRule.getRule(ruleText);
1761         colorizer.setFindRule(findRule);
1762 
1763         return true;
1764       } catch (IllegalArgumentException re) {
1765         findField.setToolTipText(re.getMessage());
1766         colorizer.setFindRule(null);
1767 
1768         return false;
1769       }
1770     }
1771   }
1772 
1773   /***
1774    * Display the detail pane, using the last known divider location
1775    */
1776   private void showDetailPane() {
1777     lowerPanel.setDividerSize(dividerSize);
1778     lowerPanel.setDividerLocation(lastDetailPanelSplitLocation);
1779     detailPanel.setVisible(true);
1780     lowerPanel.repaint();
1781   }
1782 
1783   /***
1784    * Hide the detail pane, holding the current divider location for later use
1785    */
1786   private void hideDetailPane() {
1787     int currentSize = lowerPanel.getHeight() - lowerPanel.getDividerSize();
1788 
1789     if (currentSize > 0) {
1790       lastDetailPanelSplitLocation =
1791         (double) lowerPanel.getDividerLocation() / currentSize;
1792      }
1793 
1794     lowerPanel.setDividerSize(0);
1795     detailPanel.setVisible(false);
1796     lowerPanel.repaint();
1797   }
1798 
1799   /***
1800    * Display the log tree pane, using the last known divider location
1801    */
1802   private void showLogTreePanel() {
1803     nameTreeAndMainPanelSplit.setDividerSize(dividerSize);
1804     nameTreeAndMainPanelSplit.setDividerLocation(
1805       lastLogTreePanelSplitLocation);
1806     logTreePanel.setVisible(true);
1807     nameTreeAndMainPanelSplit.repaint();
1808   }
1809 
1810   /***
1811    * Hide the log tree pane, holding the current divider location for later use
1812    */
1813   private void hideLogTreePanel() {
1814     //subtract one to make sizes match
1815     int currentSize = nameTreeAndMainPanelSplit.getWidth() - nameTreeAndMainPanelSplit.getDividerSize() - 1;
1816 
1817     if (currentSize > 0) {
1818       lastLogTreePanelSplitLocation =
1819         (double) nameTreeAndMainPanelSplit.getDividerLocation() / currentSize;
1820     }
1821     nameTreeAndMainPanelSplit.setDividerSize(0);
1822     logTreePanel.setVisible(false);
1823     nameTreeAndMainPanelSplit.repaint();
1824   }
1825 
1826   /***
1827    * Return a toolbar used by the undocked LogPanel's frame
1828    *
1829    * @return toolbar
1830    */
1831   private JToolBar createDockwindowToolbar() {
1832     final JToolBar toolbar = new JToolBar();
1833     toolbar.setFloatable(false);
1834 
1835     final Action dockPauseAction =
1836       new AbstractAction("Pause") {
1837         public void actionPerformed(ActionEvent evt) {
1838           setPaused(!isPaused());
1839         }
1840       };
1841 
1842     dockPauseAction.putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_P));
1843     dockPauseAction.putValue(
1844       Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("F12"));
1845     dockPauseAction.putValue(
1846       Action.SHORT_DESCRIPTION,
1847       "Halts the display, while still allowing events to stream in the background");
1848     dockPauseAction.putValue(
1849       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.PAUSE));
1850 
1851     final SmallToggleButton dockPauseButton =
1852       new SmallToggleButton(dockPauseAction);
1853     dockPauseButton.setText("");
1854 
1855     dockPauseButton.getModel().setSelected(isPaused());
1856 
1857     addPropertyChangeListener(
1858       "paused",
1859       new PropertyChangeListener() {
1860         public void propertyChange(PropertyChangeEvent evt) {
1861           dockPauseButton.getModel().setSelected(isPaused());
1862         }
1863       });
1864     toolbar.add(dockPauseButton);
1865 
1866     Action dockShowPrefsAction =
1867       new AbstractAction("") {
1868         public void actionPerformed(ActionEvent arg0) {
1869           showPreferences();
1870         }
1871       };
1872 
1873     dockShowPrefsAction.putValue(
1874       Action.SHORT_DESCRIPTION, "Define preferences...");
1875     dockShowPrefsAction.putValue(
1876       Action.SMALL_ICON, ChainsawIcons.ICON_PREFERENCES);
1877 
1878     toolbar.add(new SmallButton(dockShowPrefsAction));
1879 
1880     Action dockToggleLogTreeAction =
1881       new AbstractAction() {
1882         public void actionPerformed(ActionEvent e) {
1883           toggleLogTreeVisible();
1884         }
1885       };
1886 
1887       dockToggleLogTreeAction.putValue(Action.SHORT_DESCRIPTION, "Toggles the Logger Tree Pane");
1888       dockToggleLogTreeAction.putValue("enabled", Boolean.TRUE);
1889       dockToggleLogTreeAction.putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_T));
1890       dockToggleLogTreeAction.putValue(
1891         Action.ACCELERATOR_KEY,
1892         KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.ALT_MASK));
1893       dockToggleLogTreeAction.putValue(
1894         Action.SMALL_ICON, new ImageIcon(ChainsawIcons.WINDOW_ICON));
1895 
1896     final SmallToggleButton toggleLogTreeButton =
1897       new SmallToggleButton(dockToggleLogTreeAction);
1898     preferenceModel.addPropertyChangeListener("logTreePanelVisible", new PropertyChangeListener() {
1899     	public void propertyChange(PropertyChangeEvent evt) {
1900     	    toggleLogTreeButton.setSelected(preferenceModel.isLogTreePanelVisible());    		
1901     	}
1902     });
1903     		
1904     toggleLogTreeButton.setSelected(isLogTreeVisible());
1905     toolbar.add(toggleLogTreeButton);
1906     toolbar.addSeparator();
1907 
1908     final Action undockedClearAction =
1909       new AbstractAction("Clear") {
1910         public void actionPerformed(ActionEvent arg0) {
1911           clearModel();
1912         }
1913       };
1914 
1915     undockedClearAction.putValue(
1916       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.DELETE));
1917     undockedClearAction.putValue(
1918       Action.SHORT_DESCRIPTION, "Removes all the events from the current view");
1919 
1920     final SmallButton dockClearButton = new SmallButton(undockedClearAction);
1921     dockClearButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
1922       KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, InputEvent.CTRL_MASK),
1923       undockedClearAction.getValue(Action.NAME));
1924     dockClearButton.getActionMap().put(
1925       undockedClearAction.getValue(Action.NAME), undockedClearAction);
1926 
1927     dockClearButton.setText("");
1928     toolbar.add(dockClearButton);
1929     toolbar.addSeparator();
1930 
1931     Action dockToggleScrollToBottomAction =
1932         new AbstractAction("Toggles Scroll to Bottom") {
1933           public void actionPerformed(ActionEvent e) {
1934             toggleScrollToBottom();
1935           }
1936         };
1937 
1938         dockToggleScrollToBottomAction.putValue(Action.SHORT_DESCRIPTION, "Toggles Scroll to Bottom");
1939         dockToggleScrollToBottomAction.putValue("enabled", Boolean.TRUE);
1940         dockToggleScrollToBottomAction.putValue(
1941           Action.SMALL_ICON, new ImageIcon(ChainsawIcons.SCROLL_TO_BOTTOM));
1942 
1943       final SmallToggleButton toggleScrollToBottomButton =
1944         new SmallToggleButton(dockToggleScrollToBottomAction);
1945       preferenceModel.addPropertyChangeListener("scrollToBottom", new PropertyChangeListener() {
1946       	public void propertyChange(PropertyChangeEvent evt) {
1947       	    toggleScrollToBottomButton.setSelected(isScrollToBottom());    		
1948       	}
1949       });
1950 
1951       toggleScrollToBottomButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
1952   	      KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.CTRL_MASK),
1953   	      dockToggleScrollToBottomAction.getValue(Action.NAME));
1954   	    toggleScrollToBottomButton.getActionMap().put(
1955   	      dockToggleScrollToBottomAction.getValue(Action.NAME), dockToggleScrollToBottomAction);
1956       
1957       toggleScrollToBottomButton.setSelected(isScrollToBottom());
1958       toggleScrollToBottomButton.setText("");
1959       toolbar.add(toggleScrollToBottomButton);
1960       toolbar.addSeparator();
1961     
1962     findField = new JTextField();
1963     findField.addKeyListener(
1964       new ExpressionRuleContext(filterModel, findField));
1965 
1966     final Action undockedFindNextAction =
1967       new AbstractAction() {
1968         public void actionPerformed(ActionEvent e) {
1969           findNext();
1970         }
1971       };
1972 
1973     undockedFindNextAction.putValue(Action.NAME, "Find next");
1974     undockedFindNextAction.putValue(
1975       Action.SHORT_DESCRIPTION,
1976       "Find the next occurrence of the rule from the current row");
1977     undockedFindNextAction.putValue(
1978       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.DOWN));
1979 
1980     SmallButton undockedFindNextButton =
1981       new SmallButton(undockedFindNextAction);
1982 
1983     undockedFindNextButton.setAction(undockedFindNextAction);
1984     undockedFindNextButton.setText("");
1985     undockedFindNextButton.getActionMap().put(
1986       undockedFindNextAction.getValue(Action.NAME), undockedFindNextAction);
1987     undockedFindNextButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
1988       KeyStroke.getKeyStroke("F3"),
1989       undockedFindNextAction.getValue(Action.NAME));
1990 
1991     final Action undockedFindPreviousAction =
1992       new AbstractAction() {
1993         public void actionPerformed(ActionEvent e) {
1994           findPrevious();
1995         }
1996       };
1997 
1998     undockedFindPreviousAction.putValue(Action.NAME, "Find previous");
1999     undockedFindPreviousAction.putValue(
2000       Action.SHORT_DESCRIPTION,
2001       "Find the previous occurrence of the rule from the current row");
2002     undockedFindPreviousAction.putValue(
2003       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.UP));
2004 
2005     SmallButton undockedFindPreviousButton =
2006       new SmallButton(undockedFindPreviousAction);
2007 
2008     undockedFindPreviousButton.setAction(undockedFindPreviousAction);
2009     undockedFindPreviousButton.setText("");
2010     undockedFindPreviousButton.getActionMap().put(
2011       undockedFindPreviousAction.getValue(Action.NAME),
2012       undockedFindPreviousAction);
2013     undockedFindPreviousButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
2014                               .put(
2015       KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.SHIFT_MASK),
2016       undockedFindPreviousAction.getValue(Action.NAME));
2017 
2018     Dimension findSize = new Dimension(170, 22);
2019     Dimension findPanelSize = new Dimension(175, 30);
2020     findPanel.setPreferredSize(findPanelSize);
2021     findPanel.setMaximumSize(findPanelSize);
2022     findPanel.setMinimumSize(findPanelSize);
2023     findField.setPreferredSize(findSize);
2024     findField.setMaximumSize(findSize);
2025     findField.setMinimumSize(findSize);
2026     findPanel.setAlignmentY(Component.CENTER_ALIGNMENT);
2027     findField.setAlignmentY(Component.CENTER_ALIGNMENT);
2028     
2029     toolbar.add(findPanel);
2030     toolbar.add(undockedFindNextButton);
2031     toolbar.add(undockedFindPreviousButton);
2032 
2033     toolbar.addSeparator();
2034 
2035     Action redockAction =
2036       new AbstractAction("", ChainsawIcons.ICON_DOCK) {
2037         public void actionPerformed(ActionEvent arg0) {
2038           dock();
2039         }
2040       };
2041 
2042     redockAction.putValue(
2043       Action.SHORT_DESCRIPTION,
2044       "Docks this window back with the main Chainsaw window");
2045 
2046     SmallButton redockButton = new SmallButton(redockAction);
2047     toolbar.add(redockButton);
2048 
2049     return toolbar;
2050   }
2051 
2052   /***
2053    * Update the status bar with current selected row and row count
2054    */
2055   private void updateStatusBar() {
2056     SwingUtilities.invokeLater(
2057       new Runnable() {
2058         public void run() {
2059           statusBar.setSelectedLine(
2060             table.getSelectedRow() + 1, tableModel.getRowCount(),
2061             tableModel.size());
2062         }
2063       });
2064   }
2065 
2066   /***
2067    * Update the detail pane layout text
2068    *
2069    * @param conversionPattern layout text
2070    */
2071   private void setDetailPaneConversionPattern(String conversionPattern) {
2072     String oldPattern = getDetailPaneConversionPattern();
2073     ((EventDetailLayout) detailLayout).setConversionPattern(conversionPattern);
2074     firePropertyChange(
2075       "detailPaneConversionPattern", oldPattern,
2076       getDetailPaneConversionPattern());
2077   }
2078 
2079   /***
2080    * Accessor
2081    *
2082    * @return conversionPattern layout text
2083    */
2084   private String getDetailPaneConversionPattern() {
2085     return ((EventDetailLayout) detailLayout).getConversionPattern();
2086   }
2087 
2088   /***
2089    * Reset the LoggingEvent container, detail panel and status bar
2090    */
2091   private void clearModel() {
2092     tableModel.clearModel();
2093 
2094     synchronized (detail) {
2095       detailPaneUpdater.setSelectedRow(-1);
2096       detail.notify();
2097     }
2098 
2099     statusBar.setNothingSelected();
2100   }
2101 
2102   /***
2103    * Finds the next row matching the current find rule, and ensures it is made
2104    * visible
2105    *
2106    */
2107   public void findNext() {
2108     updateRule(findField.getText());
2109 
2110     if (findRule != null) {
2111       try {
2112         final int nextRow =
2113           tableModel.find(findRule, table.getSelectedRow() + 1, true);
2114 
2115         if (nextRow > -1) {
2116           table.scrollToRow(
2117             nextRow, table.columnAtPoint(table.getVisibleRect().getLocation()));
2118           findField.setToolTipText("Enter an expression");
2119         }
2120       } catch (IllegalArgumentException iae) {
2121         findField.setToolTipText(iae.getMessage());
2122         colorizer.setFindRule(null);
2123       }
2124     }
2125   }
2126 
2127   /***
2128    * Finds the previous row matching the current find rule, and ensures it is made
2129    * visible
2130    *
2131    */
2132   public void findPrevious() {
2133     updateRule(findField.getText());
2134 
2135     if (findRule != null) {
2136       try {
2137         final int previousRow =
2138           tableModel.find(findRule, table.getSelectedRow() - 1, false);
2139 
2140         if (previousRow > -1) {
2141           table.scrollToRow(
2142             previousRow,
2143             table.columnAtPoint(table.getVisibleRect().getLocation()));
2144           findField.setToolTipText("Enter an expression");
2145         }
2146       } catch (IllegalArgumentException iae) {
2147         findField.setToolTipText(iae.getMessage());
2148       }
2149     }
2150   }
2151 
2152   /***
2153    * Docks this DockablePanel by hiding the JFrame and placing the Panel back
2154    * inside the LogUI window.
2155    */
2156   private void dock() {
2157   	
2158   	int row = table.getSelectedRow();
2159     setDocked(true);
2160     undockedFrame.setVisible(false);
2161     removeAll();
2162 
2163     add(nameTreeAndMainPanelSplit, BorderLayout.CENTER);
2164     externalPanel.setDocked(true);
2165     dockingAction.putValue(Action.NAME, "Undock");
2166     dockingAction.putValue(Action.SMALL_ICON, ChainsawIcons.ICON_UNDOCK);
2167     if (row > -1) {
2168     	table.scrollToRow(row, table.columnAtPoint(table.getVisibleRect().getLocation()));
2169     }
2170   }
2171 
2172   /***
2173    * Save panel color settings
2174    */
2175   private void saveColorSettings() {
2176     ObjectOutputStream o = null;
2177 
2178     try {
2179       File f = new File(SettingsManager.getInstance().getSettingsDirectory(), 
2180       		URLEncoder.encode(getIdentifier() + COLORS_EXTENSION));
2181       logger.debug("writing colors to file: " + f);
2182       
2183       o = new ObjectOutputStream(
2184           new BufferedOutputStream(new FileOutputStream(f)));
2185 
2186       o.writeObject(colorizer.getRules());
2187       o.flush();
2188     } catch (FileNotFoundException fnfe) {
2189       fnfe.printStackTrace();
2190     } catch (IOException ioe) {
2191       ioe.printStackTrace();
2192     } finally {
2193       try {
2194         if (o != null) {
2195           o.close();
2196         }
2197       } catch (IOException ioe) {
2198         ioe.printStackTrace();
2199       }
2200     }
2201   }
2202 
2203   /***
2204    * Load default column settings if no settings exist for this identifier
2205    *
2206    * @param event
2207    */
2208   private void loadDefaultColumnSettings(LoadSettingsEvent event) {
2209     String columnOrder = event.getSetting(TABLE_COLUMN_ORDER);
2210 
2211     TableColumnModel columnModel = table.getColumnModel();
2212 
2213     Map columnNameMap = new HashMap();
2214 
2215     for (int i = 0; i < columnModel.getColumnCount(); i++) {
2216       columnNameMap.put(table.getColumnName(i), columnModel.getColumn(i));
2217     }
2218 
2219     int index = 0;
2220     StringTokenizer tok = new StringTokenizer(columnOrder, ",");
2221     List sortedColumnList = new ArrayList();
2222 
2223     /*
2224        remove all columns from the table that exist in the model
2225        and add in the correct order to a new arraylist
2226        (may be a subset of possible columns)
2227      **/
2228     while (tok.hasMoreElements()) {
2229       String element = (String) tok.nextElement();
2230       TableColumn column = (TableColumn) columnNameMap.get(element);
2231 
2232       if (column != null) {
2233         sortedColumnList.add(column);
2234         table.removeColumn(column);
2235       }
2236     }
2237     preferenceModel.setDetailPaneVisible(event.asBoolean("detailPaneVisible"));
2238     preferenceModel.setLogTreePanelVisible(event.asBoolean("logTreePanelVisible"));
2239     //re-add columns to the table in the order provided from the list
2240     for (Iterator iter = sortedColumnList.iterator(); iter.hasNext();) {
2241       TableColumn element = (TableColumn) iter.next();
2242       if (preferenceModel.addColumn(element)) {
2243           table.addColumn(element);
2244     	  preferenceModel.setColumnVisible(element.getHeaderValue().toString(), true);
2245       }
2246     }
2247 
2248     String columnWidths = event.getSetting(TABLE_COLUMN_WIDTHS);
2249 
2250     tok = new StringTokenizer(columnWidths, ",");
2251     index = 0;
2252 
2253     while (tok.hasMoreElements()) {
2254       String element = (String) tok.nextElement();
2255 
2256       try {
2257         int width = Integer.parseInt(element);
2258 
2259         if (index > (columnModel.getColumnCount() - 1)) {
2260           logger.warn(
2261             "loadsettings - failed attempt to set width for index " + index
2262             + ", width " + element);
2263         } else {
2264           columnModel.getColumn(index).setPreferredWidth(width);
2265         }
2266 
2267         index++;
2268       } catch (NumberFormatException e) {
2269         logger.error("Error decoding a Table width", e);
2270       }
2271     }
2272     undockedFrame.setSize(getSize());
2273     undockedFrame.setLocation(getBounds().x, getBounds().y);
2274 
2275       repaint();
2276     }
2277 
2278   public JTextField getFindTextField() {
2279     return findField;
2280   }
2281 
2282   /***
2283    * Load panel color settings
2284    */
2285   private void loadColorSettings(File f) {
2286     if (f.exists()) {
2287       ObjectInputStream s = null;
2288 
2289       try {
2290         s = new ObjectInputStream(
2291             new BufferedInputStream(new FileInputStream(f)));
2292 
2293         Map map = (Map) s.readObject();
2294         colorizer.setRules(map);
2295       } catch (EOFException eof) { //end of file - ignore..
2296       }catch (IOException ioe) {
2297         ioe.printStackTrace();
2298         //unable to load file - delete it
2299         f.delete();
2300       } catch (ClassNotFoundException cnfe) {
2301         cnfe.printStackTrace();
2302       } finally {
2303         if (s != null) {
2304           try {
2305             s.close();
2306           } catch (IOException ioe) {
2307             ioe.printStackTrace();
2308           }
2309         }
2310       }
2311     }
2312   }
2313 
2314   /***
2315    * Iterate over all values in the column and return the longest width
2316    *
2317    * @param index column index
2318    *
2319    * @return longest width - relies on FontMetrics.stringWidth for calculation
2320    */
2321   private int getMaxColumnWidth(int index) {
2322     FontMetrics metrics = getGraphics().getFontMetrics();
2323     int longestWidth =
2324       metrics.stringWidth("  " + table.getColumnName(index) + "  ")
2325       + (2 * table.getColumnModel().getColumnMargin());
2326 
2327     for (int i = 0, j = tableModel.getRowCount(); i < j; i++) {
2328       Component c =
2329         renderer.getTableCellRendererComponent(
2330           table, table.getValueAt(i, index), false, false, i, index);
2331 
2332       if (c instanceof JLabel) {
2333         longestWidth =
2334           Math.max(longestWidth, metrics.stringWidth(((JLabel) c).getText()));
2335       }
2336     }
2337 
2338     return longestWidth + 5;
2339   }
2340 
2341   /***
2342    * ensures the Entry map of all the unque logger names etc, that is used for
2343    * the Filter panel is updated with any new information from the event
2344    *
2345    * @param event
2346    */
2347   private void updateOtherModels(LoggingEvent event) {
2348 
2349     /*
2350      * EventContainer is a LoggerNameModel imp, use that for notifing
2351      */
2352     tableModel.addLoggerName(event.getLoggerName());
2353 
2354     filterModel.processNewLoggingEvent(event);
2355   }
2356 
2357   /***
2358    * This class receives notification when the Refine focus text field is
2359    * updated, where a backgrounh thread periodically wakes up and checks if
2360    * they have stopped typing yet. This ensures that the filtering of the
2361    * model is not done for every single character typed.
2362    *
2363    * @author Paul Smith psmith
2364    */
2365   private final class DelayedFilterTextDocumentListener
2366     implements DocumentListener {
2367     private static final long CHECK_PERIOD = 1000;
2368     private final JTextField filterText;
2369     private long lastTimeStamp = System.currentTimeMillis();
2370     private final Thread delayThread;
2371     private final String defaultToolTip;
2372     private String lastFilterText = null;
2373 
2374     private DelayedFilterTextDocumentListener(final JTextField filterText) {
2375       super();
2376       this.filterText = filterText;
2377       this.defaultToolTip = filterText.getToolTipText();
2378 
2379       this.delayThread =
2380         new Thread(
2381           new Runnable() {
2382             public void run() {
2383               while (true) {
2384                 try {
2385                   Thread.sleep(CHECK_PERIOD);
2386                 } catch (InterruptedException e) {
2387                 }
2388 
2389                 if (
2390                   (System.currentTimeMillis() - lastTimeStamp) < CHECK_PERIOD) {
2391                   // They typed something since the last check. we ignor
2392                   // this for a sample period
2393                   //                logger.debug("Typed something since the last check");
2394                 } else if (
2395                   (System.currentTimeMillis() - lastTimeStamp) < (2 * CHECK_PERIOD)) {
2396                   // they stopped typing recently, but have stopped for at least
2397                   // 1 sample period. lets apply the filter
2398                   //                logger.debug("Typed something recently applying filter");
2399                   if (filterText != null && (!(filterText.getText().equals(lastFilterText)))) {
2400                     lastFilterText = filterText.getText();
2401                     setFilter();
2402                   }
2403                 } else {
2404                   // they stopped typing a while ago, let's forget about it
2405                   //                logger.debug(
2406                   //                  "They stoppped typing a while ago, assuming filter has been applied");
2407                 }
2408               }
2409             }
2410           });
2411 
2412       delayThread.setPriority(Thread.MIN_PRIORITY);
2413       delayThread.start();
2414     }
2415 
2416     /***
2417      * Update timestamp
2418      *
2419      * @param e
2420      */
2421     public void insertUpdate(DocumentEvent e) {
2422       notifyChange();
2423     }
2424 
2425     /***
2426      * Update timestamp
2427      *
2428      * @param e
2429      */
2430     public void removeUpdate(DocumentEvent e) {
2431       notifyChange();
2432     }
2433 
2434     /***
2435      * Update timestamp
2436      *
2437      * @param e
2438      */
2439     public void changedUpdate(DocumentEvent e) {
2440       notifyChange();
2441     }
2442 
2443     /***
2444      * Update timestamp
2445      */
2446     private void notifyChange() {
2447       this.lastTimeStamp = System.currentTimeMillis();
2448     }
2449 
2450     /***
2451      * Update refinement rule based on the entered expression.
2452      */
2453     private void setFilter() {
2454       if (filterText.getText().equals("")) {
2455         ruleMediator.setRefinementRule(null);
2456         filterText.setToolTipText(defaultToolTip);
2457       } else {
2458         try {
2459           ruleMediator.setRefinementRule(
2460             ExpressionRule.getRule(filterText.getText()));
2461           filterText.setToolTipText(defaultToolTip);
2462         } catch (IllegalArgumentException iae) {
2463           filterText.setToolTipText(iae.getMessage());
2464         }
2465       }
2466     }
2467   }
2468 
2469   /***
2470    * Update active tooltip
2471    */
2472   private final class TableColumnDetailMouseListener extends MouseMotionAdapter {
2473     private int currentRow = -1;
2474 
2475     private TableColumnDetailMouseListener() {
2476     }
2477 
2478     /***
2479      * Update tooltip based on mouse position
2480      *
2481      * @param evt
2482      */
2483     public void mouseMoved(MouseEvent evt) {
2484       currentPoint = evt.getPoint();
2485 
2486       if (preferenceModel.isToolTips()) {
2487         int row = table.rowAtPoint(evt.getPoint());
2488 
2489         if ((row == currentRow) || (row == -1)) {
2490           return;
2491         }
2492 
2493         currentRow = row;
2494 
2495         LoggingEvent event = tableModel.getRow(currentRow);
2496 
2497         if (event != null) {
2498           StringBuffer buf = new StringBuffer();
2499           buf.append(detailLayout.getHeader())
2500              .append(detailLayout.format(event)).append(
2501             detailLayout.getFooter());
2502           table.setToolTipText(buf.toString());
2503         }
2504       } else {
2505         table.setToolTipText(null);
2506       }
2507     }
2508   }
2509 
2510   //if columnmoved or columnremoved callback received, re-apply table's sort index based
2511   //sort column name
2512   private class ChainsawTableColumnModelListener
2513     implements TableColumnModelListener {
2514     private ChainsawTableColumnModelListener() {
2515     }
2516 
2517     /***
2518      * If a new column was added to the display and that column was the exception column,
2519      * set the cell editor to the throwablerenderer
2520      *
2521      * @param e
2522      */
2523     public void columnAdded(TableColumnModelEvent e) {
2524       Enumeration enumeration = table.getColumnModel().getColumns();
2525 
2526       while (enumeration.hasMoreElements()) {
2527         TableColumn column = (TableColumn) enumeration.nextElement();
2528 
2529         if (
2530           (column.getModelIndex() + 1) == ChainsawColumns.INDEX_THROWABLE_COL_NAME) {
2531           column.setCellEditor(throwableRenderPanel);
2532         }
2533       }
2534     }
2535 
2536     /***
2537      * Update sorted column
2538      *
2539      * @param e
2540      */
2541     public void columnRemoved(TableColumnModelEvent e) {
2542       table.updateSortedColumn();
2543     }
2544 
2545     /***
2546      * Update sorted column
2547      *
2548      * @param e
2549      */
2550     public void columnMoved(TableColumnModelEvent e) {
2551       table.updateSortedColumn();
2552     }
2553 
2554     /***
2555      * Ignore margin changed
2556      *
2557      * @param e
2558      */
2559     public void columnMarginChanged(ChangeEvent e) {
2560     }
2561 
2562     /***
2563      * Ignore selection changed
2564      *
2565      * @param e
2566      */
2567     public void columnSelectionChanged(ListSelectionEvent e) {
2568     }
2569   }
2570 
2571   /***
2572    * Thread that periodically checks if the selected row has changed, and if
2573    * it was, updates the Detail Panel with the detailed Logging information
2574    */
2575   private class DetailPaneUpdater implements PropertyChangeListener {
2576     private int selectedRow = -1;
2577 
2578     private DetailPaneUpdater() {
2579     }
2580 
2581     /***
2582      * Update detail pane to display information about the LoggingEvent at index row
2583      *
2584      * @param row
2585      */
2586     private void setSelectedRow(int row) {
2587       selectedRow = row;
2588       updateDetailPane();
2589     }
2590 
2591     /***
2592      * Update detail pane
2593      */
2594     private void updateDetailPane() {
2595       /*
2596        * Don't bother doing anything if it's not visible
2597        */
2598       if (!detail.isVisible()) {
2599         return;
2600       }
2601 
2602 	      LoggingEvent event = null;
2603 	      if (selectedRow != -1) {
2604 	        event = tableModel.getRow(selectedRow);
2605 	
2606 	        if (event != null) {
2607 	          final StringBuffer buf = new StringBuffer();
2608 	          buf.append(detailLayout.getHeader())
2609 	             .append(detailLayout.format(event)).append(
2610 	            detailLayout.getFooter());
2611 	          if (buf.length() > 0) {
2612 		          	try {
2613 		          		final Document doc = detail.getEditorKit().createDefaultDocument();
2614 		          		detail.getEditorKit().read(new StringReader(buf.toString()), doc, 0);
2615 				      	SwingUtilities.invokeLater(new Runnable() {
2616 				      		public void run() {
2617 				      			detail.setDocument(doc);
2618 				      			detail.setCaretPosition(0);
2619 				      		}
2620 				      	});
2621 		          	} catch (Exception e) {}
2622 	      		}
2623 	        }
2624 	      }
2625 	
2626 	      if (event == null) {
2627           	try {
2628           		final Document doc = detail.getEditorKit().createDefaultDocument();
2629           		detail.getEditorKit().read(new StringReader("<html>Nothing selected</html>"), doc, 0);
2630 		      	SwingUtilities.invokeLater(new Runnable() {
2631 		      		public void run() {
2632 		      			detail.setDocument(doc);
2633 		      			detail.setCaretPosition(0);
2634 		      		}
2635 		      	});
2636           	} catch (Exception e) {}
2637   		}
2638     }
2639 
2640     /***
2641      * Update detail pane layout if it's changed
2642      *
2643      * @param arg0
2644      */
2645     public void propertyChange(PropertyChangeEvent arg0) {
2646       SwingUtilities.invokeLater(
2647         new Runnable() {
2648           public void run() {
2649             updateDetailPane();
2650           }
2651         });
2652     }
2653   }
2654 }