001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 * 
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.log4j.chainsaw;
018
019import java.text.DateFormat;
020import java.util.ArrayList;
021import java.util.Comparator;
022import java.util.Date;
023import java.util.Iterator;
024import java.util.List;
025import java.util.SortedSet;
026import java.util.TreeSet;
027import javax.swing.table.AbstractTableModel;
028import org.apache.log4j.Priority;
029import org.apache.log4j.Logger;
030
031/**
032 * Represents a list of <code>EventDetails</code> objects that are sorted on
033 * logging time. Methods are provided to filter the events that are visible.
034 *
035 * @author <a href="mailto:oliver@puppycrawl.com">Oliver Burn</a>
036 */
037class MyTableModel
038    extends AbstractTableModel
039{
040
041    /** used to log messages **/
042    private static final Logger LOG = Logger.getLogger(MyTableModel.class);
043
044    /** use the compare logging events **/
045    private static final Comparator MY_COMP = new Comparator()
046    {
047        /** @see Comparator **/
048        public int compare(Object aObj1, Object aObj2) {
049            if ((aObj1 == null) && (aObj2 == null)) {
050                return 0; // treat as equal
051            } else if (aObj1 == null) {
052                return -1; // null less than everything
053            } else if (aObj2 == null) {
054                return 1; // think about it. :->
055            }
056
057            // will assume only have LoggingEvent
058            final EventDetails le1 = (EventDetails) aObj1;
059            final EventDetails le2 = (EventDetails) aObj2;
060
061            if (le1.getTimeStamp() < le2.getTimeStamp()) {
062                return 1;
063            }
064            // assume not two events are logged at exactly the same time
065            return -1;
066        }
067        };
068
069    /**
070     * Helper that actually processes incoming events.
071     * @author <a href="mailto:oliver@puppycrawl.com">Oliver Burn</a>
072     */
073    private class Processor
074        implements Runnable
075    {
076        /** loops getting the events **/
077        public void run() {
078            while (true) {
079                try {
080                    Thread.sleep(1000);
081                } catch (InterruptedException e) {
082                    // ignore
083                }
084
085                synchronized (mLock) {
086                    if (mPaused) {
087                        continue;
088                    }
089
090                    boolean toHead = true; // were events added to head
091                    boolean needUpdate = false;
092                    final Iterator it = mPendingEvents.iterator();
093                    while (it.hasNext()) {
094                        final EventDetails event = (EventDetails) it.next();
095                        mAllEvents.add(event);
096                        toHead = toHead && (event == mAllEvents.first());
097                        needUpdate = needUpdate || matchFilter(event);
098                    }
099                    mPendingEvents.clear();
100
101                    if (needUpdate) {
102                        updateFilteredEvents(toHead);
103                    }
104                }
105            }
106
107        }
108    }
109
110
111    /** names of the columns in the table **/
112    private static final String[] COL_NAMES = {
113        "Time", "Priority", "Trace", "Category", "NDC", "Message"};
114
115    /** definition of an empty list **/
116    private static final EventDetails[] EMPTY_LIST =  new EventDetails[] {};
117
118    /** used to format dates **/
119    private static final DateFormat DATE_FORMATTER =
120        DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM);
121
122    /** the lock to control access **/
123    private final Object mLock = new Object();
124    /** set of all logged events - not filtered **/
125    private final SortedSet mAllEvents = new TreeSet(MY_COMP);
126    /** events that are visible after filtering **/
127    private EventDetails[] mFilteredEvents = EMPTY_LIST;
128    /** list of events that are buffered for processing **/
129    private final List mPendingEvents = new ArrayList();
130    /** indicates whether event collection is paused to the UI **/
131    private boolean mPaused = false;
132
133    /** filter for the thread **/
134    private String mThreadFilter = "";
135    /** filter for the message **/
136    private String mMessageFilter = "";
137    /** filter for the NDC **/
138    private String mNDCFilter = "";
139    /** filter for the category **/
140    private String mCategoryFilter = "";
141    /** filter for the priority **/
142    private Priority mPriorityFilter = Priority.DEBUG;
143
144
145    /**
146     * Creates a new <code>MyTableModel</code> instance.
147     *
148     */
149    MyTableModel() {
150        final Thread t = new Thread(new Processor());
151        t.setDaemon(true);
152        t.start();
153    }
154
155
156    ////////////////////////////////////////////////////////////////////////////
157    // Table Methods
158    ////////////////////////////////////////////////////////////////////////////
159
160    /** @see javax.swing.table.TableModel **/
161    public int getRowCount() {
162        synchronized (mLock) {
163            return mFilteredEvents.length;
164        }
165    }
166
167    /** @see javax.swing.table.TableModel **/
168    public int getColumnCount() {
169        // does not need to be synchronized
170        return COL_NAMES.length;
171    }
172
173    /** @see javax.swing.table.TableModel **/
174    public String getColumnName(int aCol) {
175        // does not need to be synchronized
176        return COL_NAMES[aCol];
177    }
178
179    /** @see javax.swing.table.TableModel **/
180    public Class getColumnClass(int aCol) {
181        // does not need to be synchronized
182        return (aCol == 2) ? Boolean.class : Object.class;
183    }
184
185    /** @see javax.swing.table.TableModel **/
186    public Object getValueAt(int aRow, int aCol) {
187        synchronized (mLock) {
188            final EventDetails event = mFilteredEvents[aRow];
189
190            if (aCol == 0) {
191                return DATE_FORMATTER.format(new Date(event.getTimeStamp()));
192            } else if (aCol == 1) {
193                return event.getPriority();
194            } else if (aCol == 2) {
195                return (event.getThrowableStrRep() == null)
196                    ? Boolean.FALSE : Boolean.TRUE;
197            } else if (aCol == 3) {
198                return event.getCategoryName();
199            } else if (aCol == 4) {
200                return event.getNDC();
201            }
202            return event.getMessage();
203        }
204    }
205
206    ////////////////////////////////////////////////////////////////////////////
207    // Public Methods
208    ////////////////////////////////////////////////////////////////////////////
209
210    /**
211     * Sets the priority to filter events on. Only events of equal or higher
212     * property are now displayed.
213     *
214     * @param aPriority the priority to filter on
215     */
216    public void setPriorityFilter(Priority aPriority) {
217        synchronized (mLock) {
218            mPriorityFilter = aPriority;
219            updateFilteredEvents(false);
220        }
221    }
222
223    /**
224     * Set the filter for the thread field.
225     *
226     * @param aStr the string to match
227     */
228    public void setThreadFilter(String aStr) {
229        synchronized (mLock) {
230            mThreadFilter = aStr.trim();
231            updateFilteredEvents(false);
232        }
233    }
234
235    /**
236     * Set the filter for the message field.
237     *
238     * @param aStr the string to match
239     */
240    public void setMessageFilter(String aStr) {
241        synchronized (mLock) {
242            mMessageFilter = aStr.trim();
243            updateFilteredEvents(false);
244        }
245    }
246
247    /**
248     * Set the filter for the NDC field.
249     *
250     * @param aStr the string to match
251     */
252    public void setNDCFilter(String aStr) {
253        synchronized (mLock) {
254            mNDCFilter = aStr.trim();
255            updateFilteredEvents(false);
256        }
257    }
258
259    /**
260     * Set the filter for the category field.
261     *
262     * @param aStr the string to match
263     */
264    public void setCategoryFilter(String aStr) {
265        synchronized (mLock) {
266            mCategoryFilter = aStr.trim();
267            updateFilteredEvents(false);
268        }
269    }
270
271    /**
272     * Add an event to the list.
273     *
274     * @param aEvent a <code>EventDetails</code> value
275     */
276    public void addEvent(EventDetails aEvent) {
277        synchronized (mLock) {
278            mPendingEvents.add(aEvent);
279        }
280    }
281
282    /**
283     * Clear the list of all events.
284     */
285    public void clear() {
286        synchronized (mLock) {
287            mAllEvents.clear();
288            mFilteredEvents = new EventDetails[0];
289            mPendingEvents.clear();
290            fireTableDataChanged();
291        }
292    }
293
294    /** Toggle whether collecting events **/
295    public void toggle() {
296        synchronized (mLock) {
297            mPaused = !mPaused;
298        }
299    }
300
301    /** @return whether currently paused collecting events **/
302    public boolean isPaused() {
303        synchronized (mLock) {
304            return mPaused;
305        }
306    }
307
308    /**
309     * Get the throwable information at a specified row in the filtered events.
310     *
311     * @param aRow the row index of the event
312     * @return the throwable information
313     */
314    public EventDetails getEventDetails(int aRow) {
315        synchronized (mLock) {
316            return mFilteredEvents[aRow];
317        }
318    }
319
320    ////////////////////////////////////////////////////////////////////////////
321    // Private methods
322    ////////////////////////////////////////////////////////////////////////////
323
324    /**
325     * Update the filtered events data structure.
326     * @param aInsertedToFront indicates whether events were added to front of
327     *        the events. If true, then the current first event must still exist
328     *        in the list after the filter is applied.
329     */
330    private void updateFilteredEvents(boolean aInsertedToFront) {
331        final long start = System.currentTimeMillis();
332        final List filtered = new ArrayList();
333        final int size = mAllEvents.size();
334        final Iterator it = mAllEvents.iterator();
335
336        while (it.hasNext()) {
337            final EventDetails event = (EventDetails) it.next();
338            if (matchFilter(event)) {
339                filtered.add(event);
340            }
341        }
342
343        final EventDetails lastFirst = (mFilteredEvents.length == 0)
344            ? null
345            : mFilteredEvents[0];
346        mFilteredEvents = (EventDetails[]) filtered.toArray(EMPTY_LIST);
347
348        if (aInsertedToFront && (lastFirst != null)) {
349            final int index = filtered.indexOf(lastFirst);
350            if (index < 1) {
351                LOG.warn("In strange state");
352                fireTableDataChanged();
353            } else {
354                fireTableRowsInserted(0, index - 1);
355            }
356        } else {
357            fireTableDataChanged();
358        }
359
360        final long end = System.currentTimeMillis();
361        LOG.debug("Total time [ms]: " + (end - start)
362                  + " in update, size: " + size);
363    }
364
365    /**
366     * Returns whether an event matches the filters.
367     *
368     * @param aEvent the event to check for a match
369     * @return whether the event matches
370     */
371    private boolean matchFilter(EventDetails aEvent) {
372        if (aEvent.getPriority().isGreaterOrEqual(mPriorityFilter) &&
373            (aEvent.getThreadName().indexOf(mThreadFilter) >= 0) &&
374            (aEvent.getCategoryName().indexOf(mCategoryFilter) >= 0) &&
375            ((mNDCFilter.length() == 0) ||
376             ((aEvent.getNDC() != null) &&
377              (aEvent.getNDC().indexOf(mNDCFilter) >= 0))))
378        {
379            final String rm = aEvent.getMessage();
380            if (rm == null) {
381                // only match if we have not filtering in place
382                return (mMessageFilter.length() == 0);
383            } else {
384                return (rm.indexOf(mMessageFilter) >= 0);
385            }
386        }
387
388        return false; // by default not match
389    }
390}