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.beans.PropertyChangeEvent;
21  import java.beans.PropertyChangeListener;
22  import java.beans.PropertyChangeSupport;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.Date;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.Set;
33  
34  import javax.swing.ProgressMonitor;
35  import javax.swing.event.EventListenerList;
36  import javax.swing.table.AbstractTableModel;
37  
38  import org.apache.log4j.LogManager;
39  import org.apache.log4j.Logger;
40  import org.apache.log4j.chainsaw.color.RuleColorizer;
41  import org.apache.log4j.chainsaw.helper.SwingHelper;
42  import org.apache.log4j.helpers.Constants;
43  import org.apache.log4j.rule.Rule;
44  import org.apache.log4j.spi.LocationInfo;
45  import org.apache.log4j.spi.LoggingEvent;
46  
47  
48  /**
49   * A CyclicBuffer implementation of the EventContainer.
50   *
51   * NOTE:  This implementation prevents duplicate rows from being added to the model.
52   *
53   * Ignoring duplicates was added to support receivers which may attempt to deliver the same
54   * event more than once but can be safely ignored (for example, the database receiver
55   * when set to retrieve in a loop).
56   *
57   * @author Paul Smith <psmith@apache.org>
58   * @author Scott Deboy <sdeboy@apache.org>
59   * @author Stephen Pain
60   *
61   */
62  class ChainsawCyclicBufferTableModel extends AbstractTableModel
63    implements EventContainer, PropertyChangeListener {
64  
65    private static final int DEFAULT_CAPACITY = 5000;
66    //cyclic field used internally in this class, but not exposed via the eventcontainer
67    private boolean cyclic = true;
68    private int cyclicBufferSize = DEFAULT_CAPACITY;
69    //original list of LoggingEventWrapper instances
70    List unfilteredList;
71    //filtered list of LoggingEventWrapper instances
72    List filteredList;
73    private boolean currentSortAscending;
74    private int currentSortColumn;
75    private final EventListenerList eventListenerList = new EventListenerList();
76    private final List columnNames = new ArrayList(ChainsawColumns.getColumnsNames());
77    private boolean sortEnabled = false;
78    private boolean reachedCapacity = false;
79    private final Logger logger = LogManager.getLogger(ChainsawCyclicBufferTableModel.class);
80  
81    //  protected final Object syncLock = new Object();
82    private final LoggerNameModel loggerNameModelDelegate = new LoggerNameModelSupport();
83    private final Object mutex = new Object();
84  
85    //because we may be using a cyclic buffer, if an ID is not provided in the property,
86    //use and increment this row counter as the ID for each received row
87    int uniqueRow;
88    private final Set uniquePropertyKeys = new HashSet();
89    private Rule ruleMediator;
90    private final PropertyChangeSupport propertySupport = new PropertyChangeSupport(this);
91    private RuleColorizer colorizer;
92    private final String tableModelName;
93  
94    public ChainsawCyclicBufferTableModel(int cyclicBufferSize, RuleColorizer colorizer, String tableModelName) {
95      propertySupport.addPropertyChangeListener("cyclic", new ModelChanger());
96      this.cyclicBufferSize = cyclicBufferSize;
97      this.colorizer = colorizer;
98      this.tableModelName = tableModelName;
99  
100     unfilteredList = new CyclicBufferList(cyclicBufferSize);
101     filteredList = new CyclicBufferList(cyclicBufferSize);
102   }
103 
104   /* (non-Javadoc)
105    * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
106    */
107   public void propertyChange(PropertyChangeEvent evt) {
108     if (evt.getSource() instanceof Rule) {
109       if (evt.getSource() == ruleMediator && evt.getPropertyName().equals("findRule")) {
110         if (((RuleMediator) evt.getSource()).isFindRuleRequired()) {
111           //only refilter if find rule is required
112           reFilter();
113         }
114       } else {
115         reFilter();
116       }
117     }
118   }
119 
120   public List getMatchingEvents(Rule rule) {
121     List list = new ArrayList();
122     List unfilteredCopy;
123     synchronized (mutex) {
124         unfilteredCopy = new ArrayList(unfilteredList);
125     }
126     Iterator iter = unfilteredCopy.iterator();
127 
128     while (iter.hasNext()) {
129       LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper) iter.next();
130 
131       if (rule.evaluate(loggingEventWrapper.getLoggingEvent(), null)) {
132         list.add(loggingEventWrapper);
133       }
134     }
135 
136     return list;
137   }
138 
139   public void reFilter() {
140     final int previousSize;
141     final int newSize;
142           synchronized (mutex) {
143             //post refilter with newValue of TRUE (filtering is about to begin)
144             propertySupport.firePropertyChange("refilter", Boolean.FALSE, Boolean.TRUE);
145             previousSize = filteredList.size();
146             filteredList.clear();
147             if (ruleMediator == null) {
148                 LoggingEventWrapper lastEvent = null;
149                 for (Iterator iter = unfilteredList.iterator();iter.hasNext();) {
150                     LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper)iter.next();
151                     loggingEventWrapper.setDisplayed(true);
152                     updateEventMillisDelta(loggingEventWrapper, lastEvent);
153                     filteredList.add(loggingEventWrapper);
154                     lastEvent = loggingEventWrapper;
155                 }
156             } else {
157                 Iterator iter = unfilteredList.iterator();
158                 LoggingEventWrapper lastEvent = null;
159                 while (iter.hasNext()) {
160                   LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper) iter.next();
161 
162                   if (ruleMediator.evaluate(loggingEventWrapper.getLoggingEvent(), null)) {
163                     loggingEventWrapper.setDisplayed(true);
164                     filteredList.add(loggingEventWrapper);
165                     updateEventMillisDelta(loggingEventWrapper, lastEvent);
166                     lastEvent = loggingEventWrapper;
167                   } else {
168                     loggingEventWrapper.setDisplayed(false);
169                   }
170                 }
171             }
172             newSize = filteredList.size();
173           }
174       	SwingHelper.invokeOnEDT(new Runnable() {
175       		public void run() {
176       			if (newSize > 0) {
177 	      			if (previousSize == newSize) {
178 	      				//same - update all
179 	      				fireTableRowsUpdated(0, newSize - 1);
180 	      			} else if (previousSize > newSize) {
181 	      				//less now..update and delete difference
182 	      				fireTableRowsUpdated(0, newSize - 1);
183                         //swing bug exposed by variable height rows when calling fireTableRowsDeleted..use tabledatacchanged
184                         fireTableDataChanged();
185 	      			} else if (previousSize < newSize) {
186 	      				//more now..update and insert difference
187                         if (previousSize > 0) {
188 	      				    fireTableRowsUpdated(0, previousSize - 1);
189                         }
190 	      				fireTableRowsInserted(Math.max(0, previousSize), newSize - 1);
191 	      			}
192       			} else {
193       				//no rows to show
194       				fireTableDataChanged();
195       			}
196 	      	notifyCountListeners();
197             //post refilter with newValue of FALSE (filtering is complete)
198             SwingHelper.invokeOnEDT(new Runnable() {
199                 public void run() {
200                     propertySupport.firePropertyChange("refilter", Boolean.TRUE, Boolean.FALSE);
201                 }
202             });
203       	}});
204   }
205 
206   public int locate(Rule rule, int startLocation, boolean searchForward) {
207     List filteredListCopy;
208     synchronized (mutex) {
209       filteredListCopy = new ArrayList(filteredList);
210     }
211       if (searchForward) {
212         for (int i = startLocation; i < filteredListCopy.size(); i++) {
213           if (rule.evaluate(((LoggingEventWrapper) filteredListCopy.get(i)).getLoggingEvent(), null)) {
214             return i;
215           }
216         }
217         //if there was no match, start at row zero and go to startLocation
218         for (int i = 0; i < startLocation; i++) {
219           if (rule.evaluate(((LoggingEventWrapper) filteredListCopy.get(i)).getLoggingEvent(), null)) {
220             return i;
221           }
222         }
223       } else {
224         for (int i = startLocation; i > -1; i--) {
225           if (rule.evaluate(((LoggingEventWrapper) filteredListCopy.get(i)).getLoggingEvent(), null)) {
226             return i;
227           }
228         }
229         //if there was no match, start at row list.size() - 1 and go to startLocation
230         for (int i = filteredListCopy.size() - 1; i > startLocation; i--) {
231           if (rule.evaluate(((LoggingEventWrapper) filteredListCopy.get(i)).getLoggingEvent(), null)) {
232             return i;
233           }
234         }
235       }
236 
237     return -1;
238   }
239 
240   /**
241    * @param l
242    */
243   public void removeLoggerNameListener(LoggerNameListener l) {
244     loggerNameModelDelegate.removeLoggerNameListener(l);
245   }
246 
247   /**
248    * @param loggerName
249    * @return
250    */
251   public boolean addLoggerName(String loggerName) {
252     return loggerNameModelDelegate.addLoggerName(loggerName);
253   }
254 
255   public String toString() {
256     return "ChainsawCyclicBufferTableModel{" +
257         "name='" + tableModelName + '\'' +
258         '}';
259   }
260 
261   public void reset() {
262       loggerNameModelDelegate.reset();
263   }
264 
265   /**
266    * @param l
267    */
268   public void addLoggerNameListener(LoggerNameListener l) {
269     loggerNameModelDelegate.addLoggerNameListener(l);
270   }
271 
272   /**
273    * @return
274    */
275   public Collection getLoggerNames() {
276     return loggerNameModelDelegate.getLoggerNames();
277   }
278 
279   public void addEventCountListener(EventCountListener listener) {
280     eventListenerList.add(EventCountListener.class, listener);
281   }
282 
283   public boolean isSortable(int col) {
284     return true;
285   }
286 
287   public void notifyCountListeners() {
288     EventCountListener[] listeners =
289       (EventCountListener[]) eventListenerList.getListeners(
290         EventCountListener.class);
291 
292     int filteredListSize;
293     int unfilteredListSize;
294     synchronized (mutex) {
295         filteredListSize = filteredList.size();
296         unfilteredListSize = unfilteredList.size();
297     }
298     for (int i = 0; i < listeners.length; i++) {
299       listeners[i].eventCountChanged(
300         filteredListSize, unfilteredListSize);
301     }
302   }
303 
304   /**
305    * Changes the underlying display rule in use.  If there was
306    * a previous Rule defined, this Model removes itself as a listener
307    * from the old rule, and adds itself to the new rule (if the new Rule is not Null).
308    *
309    * In any case, the model ensures the Filtered list is made up to date in a separate thread.
310    */
311   public void setRuleMediator(RuleMediator ruleMediator) {
312     if (this.ruleMediator != null) {
313       this.ruleMediator.removePropertyChangeListener(this);
314     }
315 
316     this.ruleMediator = ruleMediator;
317 
318     if (this.ruleMediator != null) {
319       this.ruleMediator.addPropertyChangeListener(this);
320     }
321     reFilter();
322   }
323 
324   /* (non-Javadoc)
325      * @see org.apache.log4j.chainsaw.EventContainer#sort()
326      */
327   public void sort() {
328       boolean sort;
329       final int filteredListSize;
330       synchronized (mutex) {
331           filteredListSize = filteredList.size();
332           sort = (sortEnabled && filteredListSize > 0);
333         if (sort) {
334             //reset display (used to ensure row height is updated)
335             LoggingEventWrapper lastEvent = null;
336             for (Iterator iter = filteredList.iterator();iter.hasNext();) {
337                 LoggingEventWrapper e = (LoggingEventWrapper)iter.next();
338                 e.setDisplayed(true);
339                 updateEventMillisDelta(e, lastEvent);
340                 lastEvent = e;
341             }
342             Collections.sort(
343               filteredList,
344               new ColumnComparator(
345                 getColumnName(currentSortColumn), currentSortColumn,
346                 currentSortAscending));
347         }
348       }
349       if (sort) {
350         SwingHelper.invokeOnEDT(new Runnable() {
351             public void run() {
352                 fireTableRowsUpdated(0, Math.max(filteredListSize - 1, 0));
353             }
354         });
355       }
356   }
357 
358   public boolean isSortEnabled() {
359     return sortEnabled;
360   }
361 
362   public void sortColumn(int col, boolean ascending) {
363     logger.debug("request to sort col=" + col);
364     currentSortAscending = ascending;
365     currentSortColumn = col;
366     sortEnabled = true;
367     sort();
368   }
369 
370   /* (non-Javadoc)
371    * @see org.apache.log4j.chainsaw.EventContainer#clear()
372    */
373   public void clearModel() {
374     reachedCapacity = false;
375 
376     synchronized (mutex) {
377       unfilteredList.clear();
378       filteredList.clear();
379       uniqueRow = 0;
380     }
381 
382     SwingHelper.invokeOnEDT(new Runnable() {
383     	public void run() {
384     	    fireTableDataChanged();
385     	}
386     });
387 
388     notifyCountListeners();
389     loggerNameModelDelegate.reset();
390   }
391 
392   public List getAllEvents() {
393       synchronized (mutex) {
394           return new ArrayList(unfilteredList);
395       }
396   }
397   
398   
399   public List getFilteredEvents() {
400 
401   	synchronized (mutex) {
402   		return new ArrayList(filteredList);
403   	}
404   }
405   
406   public int getRowIndex(LoggingEventWrapper loggingEventWrapper) {
407     synchronized (mutex) {
408       return filteredList.indexOf(loggingEventWrapper);
409     }
410   }
411 
412     public void removePropertyFromEvents(String propName) {
413         //first remove the event from any displayed events, so we can fire row updated event
414         List filteredListCopy;
415         List unfilteredListCopy;
416         synchronized(mutex) {
417             filteredListCopy = new ArrayList(filteredList);
418             unfilteredListCopy = new ArrayList(unfilteredList);
419         }
420         for (int i=0;i<filteredListCopy.size();i++) {
421             LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper)filteredListCopy.get(i);
422             Object result = loggingEventWrapper.removeProperty(propName);
423             if (result != null) {
424                 fireRowUpdated(i, false);
425             }
426         }
427         //now remove the event from all events
428         for (Iterator iter = unfilteredListCopy.iterator();iter.hasNext();) {
429             LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper)iter.next();
430             loggingEventWrapper.removeProperty(propName);
431         }
432     }
433 
434     public int updateEventsWithFindRule(Rule findRule) {
435         int count = 0;
436         List unfilteredListCopy;
437         synchronized(mutex) {
438             unfilteredListCopy = new ArrayList(unfilteredList);
439         }
440         for (Iterator iter = unfilteredListCopy.iterator();iter.hasNext();) {
441             LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper) iter.next();
442             loggingEventWrapper.evaluateSearchRule(findRule);
443             //return the count of visible search matches
444             if (loggingEventWrapper.isSearchMatch() && loggingEventWrapper.isDisplayed()) {
445                 count++;
446             }
447         }
448         return count;
449     }
450 
451     public int findColoredRow(int startLocation, boolean searchForward) {
452         List filteredListCopy;
453         synchronized (mutex) {
454             filteredListCopy = new ArrayList(filteredList);
455         }
456         if (searchForward) {
457           for (int i = startLocation; i < filteredListCopy.size(); i++) {
458             LoggingEventWrapper event = (LoggingEventWrapper)filteredListCopy.get(i);
459             if (!event.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND) ||
460                     !event.getColorRuleForeground().equals(ChainsawConstants.COLOR_DEFAULT_FOREGROUND)) {
461                 return i;
462             }
463           }
464           //searching forward, no colorized event was found - now start at row zero and go to startLocation
465           for (int i = 0; i < startLocation; i++) {
466             LoggingEventWrapper event = (LoggingEventWrapper)filteredListCopy.get(i);
467             if (!event.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND) ||
468                     !event.getColorRuleForeground().equals(ChainsawConstants.COLOR_DEFAULT_FOREGROUND)) {
469                 return i;
470             }
471           }
472         } else {
473           for (int i = startLocation; i > -1; i--) {
474               LoggingEventWrapper event = (LoggingEventWrapper)filteredListCopy.get(i);
475               if (!event.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND) ||
476                       !event.getColorRuleForeground().equals(ChainsawConstants.COLOR_DEFAULT_FOREGROUND)) {
477                   return i;
478             }
479           }
480           //searching backward, no colorized event was found - now start at list.size() - 1 and go to startLocation
481           for (int i = filteredListCopy.size() - 1; i > startLocation; i--) {
482               LoggingEventWrapper event = (LoggingEventWrapper)filteredListCopy.get(i);
483               if (!event.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND) ||
484                       !event.getColorRuleForeground().equals(ChainsawConstants.COLOR_DEFAULT_FOREGROUND)) {
485                   return i;
486             }
487           }
488         }
489 
490       return -1;
491     }
492 
493   public int getSearchMatchCount() {
494     int searchMatchCount = 0;
495     synchronized(mutex) {
496       for (Iterator iter = filteredList.iterator();iter.hasNext();) {
497         LoggingEventWrapper wrapper = (LoggingEventWrapper) iter.next();
498         if (wrapper.isSearchMatch() && wrapper.isDisplayed()) {
499           searchMatchCount++;
500         }
501       }
502     }
503     return searchMatchCount;
504   }
505 
506   public int getColumnCount() {
507     return columnNames.size();
508   }
509 
510   public String getColumnName(int column) {
511       return (String) columnNames.get(column);
512   }
513 
514   public LoggingEventWrapper getRow(int row) {
515     synchronized (mutex) {
516       if (row < filteredList.size() && row > -1) {
517         return (LoggingEventWrapper) filteredList.get(row);
518       }
519     }
520 
521     return null;
522   }
523 
524   public int getRowCount() {
525     synchronized (mutex) {
526       return filteredList.size();
527     }
528   }
529 
530   public Object getValueAt(int rowIndex, int columnIndex) {
531     LoggingEvent event = null;
532 
533     synchronized (mutex) {
534       if (rowIndex < filteredList.size() && rowIndex > -1) {
535         event = ((LoggingEventWrapper) filteredList.get(rowIndex)).getLoggingEvent();
536       }
537     }
538 
539     if (event == null) {
540       return null;
541     }
542 
543     LocationInfo info = null;
544 
545     if (event.locationInformationExists()) {
546       info = event.getLocationInformation();
547     }
548 
549     switch (columnIndex + 1) {
550     case ChainsawColumns.INDEX_ID_COL_NAME:
551 
552       Object id = event.getProperty(Constants.LOG4J_ID_KEY);
553 
554       if (id != null) {
555         return id;
556       }
557 
558       return new Integer(rowIndex);
559 
560     case ChainsawColumns.INDEX_LEVEL_COL_NAME:
561       return event.getLevel();
562 
563     case ChainsawColumns.INDEX_LOG4J_MARKER_COL_NAME:
564       return event.getProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
565 
566     case ChainsawColumns.INDEX_MILLIS_DELTA_COL_NAME:
567       return event.getProperty(ChainsawConstants.MILLIS_DELTA_COL_NAME_LOWERCASE);
568 
569     case ChainsawColumns.INDEX_LOGGER_COL_NAME:
570       return event.getLoggerName();
571 
572     case ChainsawColumns.INDEX_TIMESTAMP_COL_NAME:
573       return new Date(event.getTimeStamp());
574 
575     case ChainsawColumns.INDEX_MESSAGE_COL_NAME:
576       return event.getRenderedMessage();
577 
578     case ChainsawColumns.INDEX_NDC_COL_NAME:
579       return event.getNDC();
580 
581     case ChainsawColumns.INDEX_THREAD_COL_NAME:
582       return event.getThreadName();
583 
584     case ChainsawColumns.INDEX_THROWABLE_COL_NAME:
585       return event.getThrowableStrRep();
586 
587     case ChainsawColumns.INDEX_CLASS_COL_NAME:
588       return ((info == null) || ("?".equals(info.getClassName()))) ? "" : info.getClassName();
589 
590         case ChainsawColumns.INDEX_FILE_COL_NAME:
591       return ((info == null) || ("?".equals(info.getFileName()))) ? "" : info.getFileName();
592 
593         case ChainsawColumns.INDEX_LINE_COL_NAME:
594       return ((info == null) || ("?".equals(info.getLineNumber()))) ? "" : info.getLineNumber();
595 
596         case ChainsawColumns.INDEX_METHOD_COL_NAME:
597       return ((info == null) || ("?".equals(info.getMethodName()))) ? "" : info.getMethodName();
598 
599         default:
600 
601             if (columnIndex < columnNames.size()) {
602         //case may not match..try case sensitive and fall back to case-insensitive
603         String result = event.getProperty(columnNames.get(columnIndex).toString());
604         if (result == null) {
605             String lowerColName = columnNames.get(columnIndex).toString().toLowerCase(Locale.ENGLISH);
606             Set entrySet = event.getProperties().entrySet();
607             for (Iterator iter = entrySet.iterator();iter.hasNext();) {
608                 Map.Entry thisEntry = (Map.Entry) iter.next();
609                 if (thisEntry.getKey().toString().equalsIgnoreCase(lowerColName)) {
610                     result = thisEntry.getValue().toString();
611                 }
612             }
613         }
614         if (result != null) {
615             return result;
616         }
617       }
618     }
619     return "";
620   }
621 
622   public boolean isAddRow(LoggingEventWrapper loggingEventWrapper) {
623     Object id = loggingEventWrapper.getLoggingEvent().getProperty(Constants.LOG4J_ID_KEY);
624 
625     //only set the property if it doesn't already exist
626     if (id == null) {
627       id = new Integer(++uniqueRow);
628       loggingEventWrapper.setProperty(Constants.LOG4J_ID_KEY, id.toString());
629     }
630 
631     loggingEventWrapper.updateColorRuleColors(colorizer.getBackgroundColor(loggingEventWrapper.getLoggingEvent()), colorizer.getForegroundColor(loggingEventWrapper.getLoggingEvent()));
632     Rule findRule = colorizer.getFindRule();
633     if (findRule != null) {
634       loggingEventWrapper.evaluateSearchRule(colorizer.getFindRule());
635     }
636 
637     boolean rowAdded = false;
638 
639     /**
640          * If we're in cyclic mode and over budget on the size, the addition of a new event will
641          * cause the oldest event to fall off the cliff. We need to remove that events ID from the
642          * Set so we are not keeping track of IDs for all events ever received (we'd run out of
643          * memory...)
644          */
645     synchronized(mutex) {
646         if (cyclic) {
647             CyclicBufferList bufferList = (CyclicBufferList) unfilteredList;
648             if (bufferList.size() == bufferList.getMaxSize()) {
649                 reachedCapacity = true;
650             }
651         }
652         int unfilteredSize = unfilteredList.size();
653         LoggingEventWrapper lastLoggingEventWrapper = null;
654         if (unfilteredSize > 0) {
655             lastLoggingEventWrapper = (LoggingEventWrapper) unfilteredList.get(unfilteredSize - 1);
656         }
657         unfilteredList.add(loggingEventWrapper);
658         if ((ruleMediator == null) || (ruleMediator.evaluate(loggingEventWrapper.getLoggingEvent(), null))) {
659             loggingEventWrapper.setDisplayed(true);
660             updateEventMillisDelta(loggingEventWrapper, lastLoggingEventWrapper);
661             filteredList.add(loggingEventWrapper);
662             rowAdded = true;
663         } else {
664             loggingEventWrapper.setDisplayed(false);
665         }
666     }
667 
668     checkForNewColumn(loggingEventWrapper);
669 
670     return rowAdded;
671   }
672 
673     private void updateEventMillisDelta(LoggingEventWrapper loggingEventWrapper, LoggingEventWrapper lastLoggingEventWrapper) {
674       if (lastLoggingEventWrapper != null) {
675         loggingEventWrapper.setPreviousDisplayedEventTimestamp(lastLoggingEventWrapper.getLoggingEvent().getTimeStamp());
676       } else {
677         //delta to same event = 0
678         loggingEventWrapper.setPreviousDisplayedEventTimestamp(loggingEventWrapper.getLoggingEvent().getTimeStamp());
679       }
680     }
681 
682    private void checkForNewColumn(LoggingEventWrapper loggingEventWrapper)
683    {
684       /**
685        * Is this a new Property key we haven't seen before?  Remember that now MDC has been merged
686        * into the Properties collection
687        */
688       boolean newColumn = uniquePropertyKeys.addAll(loggingEventWrapper.getPropertyKeySet());
689 
690       if (newColumn) {
691         /**
692          * If so, we should add them as columns and notify listeners.
693          */
694         for (Iterator iter = loggingEventWrapper.getPropertyKeySet().iterator(); iter.hasNext();) {
695           String key = iter.next().toString().toUpperCase();
696 
697           //add all keys except the 'log4jid' key (columnNames is all-caps)
698           if (!columnNames.contains(key) && !(Constants.LOG4J_ID_KEY.equalsIgnoreCase(key))) {
699             columnNames.add(key);
700             logger.debug("Adding col '" + key + "', columnNames=" + columnNames);
701             fireNewKeyColumnAdded(
702               new NewKeyEvent(
703                 this, columnNames.indexOf(key), key, loggingEventWrapper.getLoggingEvent().getProperty(key)));
704           }
705         }
706       }
707    }
708 
709   public void fireTableEvent(final int begin, final int end, final int count) {
710   	SwingHelper.invokeOnEDT(new Runnable() {
711   		public void run() {
712     if (cyclic) {
713       if (!reachedCapacity) {
714         //if we didn't loop and it's the 1st time, insert
715         if ((begin + count) < cyclicBufferSize) {
716           fireTableRowsInserted(begin, end);
717         } else {
718           //we did loop - insert and then update rows
719           //rows are zero-indexed, subtract 1 from cyclicbuffersize for the event notification
720           fireTableRowsInserted(begin, cyclicBufferSize - 1);
721           fireTableRowsUpdated(0, cyclicBufferSize - 1);
722           reachedCapacity = true;
723         }
724       } else {
725         fireTableRowsUpdated(0, cyclicBufferSize - 1);
726       }
727     } else {
728       fireTableRowsInserted(begin, end);
729     }
730   }});
731   }
732 
733     public void fireRowUpdated(int row, boolean checkForNewColumns) {
734         LoggingEventWrapper loggingEventWrapper = getRow(row);
735         if (loggingEventWrapper != null)
736         {
737             loggingEventWrapper.updateColorRuleColors(colorizer.getBackgroundColor(loggingEventWrapper.getLoggingEvent()), colorizer.getForegroundColor(loggingEventWrapper.getLoggingEvent()));
738             Rule findRule = colorizer.getFindRule();
739             if (findRule != null) {
740               loggingEventWrapper.evaluateSearchRule(colorizer.getFindRule());
741             }
742 
743             fireTableRowsUpdated(row, row);
744             if (checkForNewColumns) {
745                 //row may have had a column added..if so, make sure a column is added
746                 checkForNewColumn(loggingEventWrapper);
747             }
748         }
749     }
750 
751     /**
752   * @param e
753   */
754   private void fireNewKeyColumnAdded(NewKeyEvent e) {
755     NewKeyListener[] listeners =
756       (NewKeyListener[]) eventListenerList.getListeners(NewKeyListener.class);
757 
758     for (int i = 0; i < listeners.length; i++) {
759       NewKeyListener listener = listeners[i];
760       listener.newKeyAdded(e);
761     }
762   }
763 
764   /**
765    * @return
766    */
767   public int getMaxSize() {
768     return cyclicBufferSize;
769   }
770 
771   /* (non-Javadoc)
772    * @see org.apache.log4j.chainsaw.EventContainer#addNewKeyListener(org.apache.log4j.chainsaw.NewKeyListener)
773    */
774   public void addNewKeyListener(NewKeyListener l) {
775     eventListenerList.add(NewKeyListener.class, l);
776   }
777 
778   /* (non-Javadoc)
779    * @see org.apache.log4j.chainsaw.EventContainer#removeNewKeyListener(org.apache.log4j.chainsaw.NewKeyListener)
780    */
781   public void removeNewKeyListener(NewKeyListener l) {
782     eventListenerList.remove(NewKeyListener.class, l);
783   }
784 
785   /* (non-Javadoc)
786    * @see javax.swing.table.TableModel#isCellEditable(int, int)
787    */
788   public boolean isCellEditable(int rowIndex, int columnIndex) {
789     if (getColumnName(columnIndex).equalsIgnoreCase(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE)) {
790       return true;
791     }
792 
793     if (columnIndex >= columnNames.size()) {
794         return false;
795     }
796 
797     return super.isCellEditable(rowIndex, columnIndex);
798   }
799 
800   /* (non-Javadoc)
801    * @see org.apache.log4j.chainsaw.EventContainer#setCyclic(boolean)
802    */
803   public void setCyclic(final boolean cyclic) {
804     if (this.cyclic == cyclic) {
805       return;
806     }
807 
808     final boolean old = this.cyclic;
809     this.cyclic = cyclic;
810     propertySupport.firePropertyChange("cyclic", old, cyclic);
811   }
812 
813   /* (non-Javadoc)
814    * @see org.apache.log4j.chainsaw.EventContainer#addPropertyChangeListener(java.beans.PropertyChangeListener)
815    */
816   public void addPropertyChangeListener(PropertyChangeListener l) {
817     propertySupport.addPropertyChangeListener(l);
818   }
819 
820   /* (non-Javadoc)
821    * @see org.apache.log4j.chainsaw.EventContainer#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
822    */
823   public void addPropertyChangeListener(
824     String propertyName, PropertyChangeListener l) {
825     propertySupport.addPropertyChangeListener(propertyName, l);
826   }
827 
828   /* (non-Javadoc)
829    * @see org.apache.log4j.chainsaw.EventContainer#size()
830    */
831   public int size() {
832     synchronized(mutex) {
833       return unfilteredList.size();
834     }
835   }
836 
837   private class ModelChanger implements PropertyChangeListener {
838     /* (non-Javadoc)
839      * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
840      */
841     public void propertyChange(PropertyChangeEvent arg0) {
842       Thread thread =
843         new Thread(
844           new Runnable() {
845             public void run() {
846               ProgressMonitor monitor = null;
847 
848               int index = 0;
849 
850               try {
851                 synchronized (mutex) {
852                   monitor =
853                     new ProgressMonitor(
854                       null, "Switching models...",
855                       "Transferring between data structures, please wait...", 0,
856                       unfilteredList.size() + 1);
857                   monitor.setMillisToDecideToPopup(250);
858                   monitor.setMillisToPopup(100);
859                   logger.debug(
860                     "Changing Model, isCyclic is now " + cyclic);
861 
862                   List newUnfilteredList = null;
863                   List newFilteredList = null;
864 
865                   if (cyclic) {
866                     newUnfilteredList = new CyclicBufferList(cyclicBufferSize);
867                     newFilteredList = new CyclicBufferList(cyclicBufferSize);
868                   } else {
869                     newUnfilteredList = new ArrayList(cyclicBufferSize);
870                     newFilteredList = new ArrayList(cyclicBufferSize);
871                   }
872 
873                   int increment = 0;
874 
875                   for (Iterator iter = unfilteredList.iterator();
876                       iter.hasNext();) {
877                     LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper) iter.next();
878                     newUnfilteredList.add(loggingEventWrapper);
879                     monitor.setProgress(index++);
880                   }
881 
882                   unfilteredList = newUnfilteredList;
883                   filteredList = newFilteredList;
884                 }
885 
886                 monitor.setNote("Refiltering...");
887                 reFilter();
888 
889                 monitor.setProgress(index++);
890               } finally {
891                 monitor.close();
892               }
893 
894               logger.debug("Model Change completed");
895             }
896           });
897       thread.setPriority(Thread.MIN_PRIORITY + 1);
898       thread.start();
899     }
900   }
901 }