001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2002, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.gui.swing;
018:
019: // Logging
020: import java.util.logging.Level;
021: import java.util.logging.Logger;
022: import java.util.logging.Handler;
023: import java.util.logging.LogRecord;
024: import java.util.logging.SimpleFormatter;
025:
026: // Table model
027: import javax.swing.table.TableModel;
028: import javax.swing.table.TableColumn;
029: import javax.swing.table.TableColumnModel;
030: import javax.swing.event.TableModelEvent;
031: import javax.swing.event.TableModelListener;
032: import javax.swing.event.EventListenerList;
033: import java.awt.EventQueue;
034:
035: // Collections
036: import java.util.Map;
037: import java.util.List;
038: import java.util.ArrayList;
039: import java.util.LinkedHashMap;
040:
041: // Formatting
042: import java.util.Date;
043: import java.text.DateFormat;
044:
045: // Resources
046: import org.geotools.resources.XArray;
047: import org.geotools.resources.i18n.Vocabulary;
048: import org.geotools.resources.i18n.VocabularyKeys;
049:
050: /**
051: * A logging {@link Handler} storing {@link LogRecords} as a {@link TableModel}.
052: * This model is used by {@link LoggingPanel} for displaying logging messages in
053: * a {@link javax.swing.JTable}.
054: *
055: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/LoggingTableModel.java $
056: * @version $Id: LoggingTableModel.java 20883 2006-08-07 13:48:09Z jgarnett $
057: * @author Martin Desruisseaux
058: */
059: final class LoggingTableModel extends Handler implements TableModel {
060: /**
061: * Resource keys for default column names. <STRONG>NOTE: Order is significant.</STRONG>
062: * If the order is changed, then the constants in {@link LoggingPanel} must be updated.
063: */
064: private static final int[] COLUMN_NAMES = new int[] {
065: VocabularyKeys.LOGGER, VocabularyKeys.CLASS,
066: VocabularyKeys.METHOD, VocabularyKeys.TIME_OF_DAY,
067: VocabularyKeys.LEVEL, VocabularyKeys.MESSAGE };
068:
069: /**
070: * Resource keys for column names. This is usuall the same array than {@code COLUMN_NAMES}.
071: * However, method {@link #setColumnVisible} may add or remove column in this list.
072: */
073: private int[] columnNames = COLUMN_NAMES;
074:
075: /**
076: * The last {@link LogRecord}s stored. This array will grows as needed up to
077: * {@link #capacity}. Once the maximal capacity is reached, early records
078: * are discarted.
079: */
080: private LogRecord[] records = new LogRecord[16];
081:
082: /**
083: * The maximum amount of records that can be stored in this logging panel.
084: * If more than {@link #capacity} messages are logged, early messages will
085: * be discarted.
086: */
087: private int capacity = 500;
088:
089: /**
090: * The total number of logging messages published by this panel. This number may be
091: * greater than the amount of {@link LogRecord} actually memorized, since early records
092: * may have been discarted. The slot in {@code records} where to write the next
093: * message can be computed by <code>recordCount % capacity</code>.
094: */
095: private int recordCount;
096:
097: /**
098: * String representations of latest required records. Keys are {@link LogRecord} objects
099: * and values are {@code String[]}. This is a cache for faster rendering.
100: */
101: private final Map cache = new LinkedHashMap() {
102: protected boolean removeEldestEntry(final Map.Entry eldest) {
103: return size() >= Math.min(capacity, 80);
104: }
105: };
106:
107: /**
108: * The list of registered listeners.
109: */
110: private final EventListenerList listenerList = new EventListenerList();
111:
112: /**
113: * The format to use for formatting time.
114: */
115: private final DateFormat dateFormat = DateFormat
116: .getTimeInstance(DateFormat.MEDIUM);
117:
118: /**
119: * Construct the handler.
120: */
121: public LoggingTableModel() {
122: setLevel(Level.CONFIG);
123: setFormatter(new SimpleFormatter());
124: }
125:
126: /**
127: * Returns the capacity. This is the maximum number of {@link LogRecord}s this handler
128: * can memorize. If more messages are logged, then the oldiest messages will be discarted.
129: */
130: public int getCapacity() {
131: return capacity;
132: }
133:
134: /**
135: * Set the capacity. This is the maximum number of {@link LogRecord}s this handler can
136: * memorize. If more messages are logged, then the oldiest messages will be discarted.
137: */
138: public synchronized void setCapacity(final int capacity) {
139: if (recordCount != 0) {
140: throw new IllegalStateException("Not yet implemented.");
141: }
142: this .capacity = capacity;
143: }
144:
145: /**
146: * Returns {@code true} if the given column is visible.
147: *
148: * @param index One of {@link LoggingPanel} constants, which maps to entries in
149: * {@link COLUMN_NAMES}. For example {@code 0} for the logger,
150: * {@code 1} for the class, etc.
151: */
152: final boolean isColumnVisible(int index) {
153: final int key = COLUMN_NAMES[index];
154: for (int i = 0; i < columnNames.length; i++) {
155: if (columnNames[i] == key) {
156: return true;
157: }
158: }
159: return false;
160: }
161:
162: /**
163: * Show or hide the given column.
164: *
165: * @param index One of {@link LoggingPanel} constants, which maps to entries in
166: * {@link COLUMN_NAMES}. For example {@code 0} for the logger,
167: * {@code 1} for the class, etc.
168: * @param visible The visible state for the specified column.
169: */
170: final void setColumnVisible(final int index, final boolean visible) {
171: final int key = COLUMN_NAMES[index];
172: int[] names = new int[COLUMN_NAMES.length];
173: int count = 0;
174: for (int i = 0; i < COLUMN_NAMES.length; i++) {
175: final int toTest = COLUMN_NAMES[i];
176: if (toTest == key) {
177: if (visible) {
178: names[count++] = toTest;
179: }
180: continue;
181: }
182: for (int j = 0; j < columnNames.length; j++) {
183: if (columnNames[j] == toTest) {
184: names[count++] = toTest;
185: break;
186: }
187: }
188: }
189: columnNames = names = XArray.resize(names, count);
190: cache.clear();
191: fireTableChanged(new TableModelEvent(this ,
192: TableModelEvent.HEADER_ROW));
193: assert isColumnVisible(index) == visible : visible;
194: }
195:
196: /**
197: * Publish a {@link LogRecord}. If the maximal capacity has been reached,
198: * the oldiest record will be discarted.
199: */
200: public synchronized void publish(final LogRecord record) {
201: if (!isLoggable(record)) {
202: return;
203: }
204: final int nextSlot = recordCount % capacity;
205: if (nextSlot >= records.length) {
206: records = (LogRecord[]) XArray.resize(records, Math.min(
207: records.length * 2, capacity));
208: }
209: records[nextSlot] = record;
210: final TableModelEvent event;
211: if (++recordCount <= capacity) {
212: event = new TableModelEvent(this , nextSlot, nextSlot,
213: TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT);
214: } else {
215: event = new TableModelEvent(this , 0, capacity - 1,
216: TableModelEvent.ALL_COLUMNS, TableModelEvent.UPDATE);
217: }
218: //
219: // Notify all listeners that a record has been added.
220: //
221: EventQueue.invokeLater(new Runnable() {
222: public void run() {
223: fireTableChanged(event);
224: }
225: });
226: }
227:
228: /**
229: * Returns the log record for the specified row.
230: *
231: * @param row The row in the table. This is the visible row,
232: * not the record number from the first record.
233: */
234: public synchronized LogRecord getLogRecord(int row) {
235: assert row < getRowCount();
236: if (recordCount > capacity) {
237: row += (recordCount % capacity);
238: row %= capacity;
239: }
240: return records[row];
241: }
242:
243: /**
244: * Returns the number of columns in the model.
245: */
246: public int getColumnCount() {
247: return columnNames.length;
248: }
249:
250: /**
251: * Returns the number of rows in the model.
252: */
253: public synchronized int getRowCount() {
254: return Math.min(recordCount, capacity);
255: }
256:
257: /**
258: * Returns the most specific superclass for all the cell values in the column.
259: */
260: public Class getColumnClass(final int columnIndex) {
261: return String.class;
262: }
263:
264: /**
265: * Returns the name of the column at {@code columnIndex}.
266: */
267: public String getColumnName(final int columnIndex) {
268: return Vocabulary.format(columnNames[columnIndex]);
269: }
270:
271: /**
272: * Returns the value for the cell at {@code columnIndex} and {@code rowIndex}.
273: */
274: public synchronized Object getValueAt(final int rowIndex,
275: final int columnIndex) {
276: final LogRecord record = getLogRecord(rowIndex);
277: String[] row = (String[]) cache.get(record);
278: if (row == null) {
279: row = new String[getColumnCount()];
280: for (int i = 0; i < row.length; i++) {
281: final String value;
282: switch (columnNames[i]) {
283: case VocabularyKeys.LOGGER:
284: value = record.getLoggerName();
285: break;
286: case VocabularyKeys.CLASS:
287: value = getShortClassName(record
288: .getSourceClassName());
289: break;
290: case VocabularyKeys.METHOD:
291: value = record.getSourceMethodName();
292: break;
293: case VocabularyKeys.TIME_OF_DAY:
294: value = dateFormat.format(new Date(record
295: .getMillis()));
296: break;
297: case VocabularyKeys.LEVEL:
298: value = record.getLevel().getLocalizedName();
299: break;
300: case VocabularyKeys.MESSAGE:
301: value = getFormatter().formatMessage(record);
302: break;
303: default:
304: throw new AssertionError(i);
305: }
306: row[i] = value;
307: }
308: cache.put(record, row);
309: assert cache.size() <= capacity;
310: }
311: return row[columnIndex];
312: }
313:
314: /**
315: * Returns the class name in a shorter form (without package).
316: */
317: private static String getShortClassName(String name) {
318: if (name != null) {
319: final int dot = name.lastIndexOf('.');
320: if (dot >= 0) {
321: name = name.substring(dot + 1);
322: }
323: name = name.replace('$', '.');
324: }
325: return name;
326: }
327:
328: /**
329: * Do nothing since cells are not editable.
330: */
331: public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
332: }
333:
334: /**
335: * Returns {@code false} since cells are not editable.
336: */
337: public boolean isCellEditable(int rowIndex, int columnIndex) {
338: return false;
339: }
340:
341: /**
342: * Adds a listener that is notified each time a change to the data model occurs.
343: */
344: public void addTableModelListener(final TableModelListener listener) {
345: listenerList.add(TableModelListener.class, listener);
346: }
347:
348: /**
349: * Removes a listener from the list that is notified each time a change occurs.
350: */
351: public void removeTableModelListener(
352: final TableModelListener listener) {
353: listenerList.remove(TableModelListener.class, listener);
354: }
355:
356: /**
357: * Forwards the given notification event to all {@link TableModelListeners}.
358: */
359: private void fireTableChanged(final TableModelEvent event) {
360: final Object[] listeners = listenerList.getListenerList();
361: for (int i = listeners.length - 2; i >= 0; i -= 2) {
362: if (listeners[i] == TableModelListener.class) {
363: ((TableModelListener) listeners[i + 1])
364: .tableChanged(event);
365: }
366: }
367: }
368:
369: /**
370: * Flush any buffered output.
371: */
372: public void flush() {
373: }
374:
375: /**
376: * Close the {@code Handler} and free all associated resources.
377: */
378: public void close() {
379: }
380: }
|