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