/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.gui.swing;
// Logging
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
// Table model
import javax.swing.table.TableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.event.EventListenerList;
import java.awt.EventQueue;
// Collections
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashMap;
// Formatting
import java.util.Date;
import java.text.DateFormat;
// Resources
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
/**
* A logging {@link Handler} storing {@link LogRecords} as a {@link TableModel}.
* This model is used by {@link LoggingPanel} for displaying logging messages in
* a {@link javax.swing.JTable}.
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
final class LoggingTableModel extends Handler implements TableModel {
/**
* Resource keys for default column names. <STRONG>NOTE: Order is significant.</STRONG>
* If the order is changed, then the constants in {@link LoggingPanel} must be updated.
*/
private static final int[] COLUMN_NAMES = new int[] {
VocabularyKeys.LOGGER,
VocabularyKeys.CLASS,
VocabularyKeys.METHOD,
VocabularyKeys.TIME_OF_DAY,
VocabularyKeys.LEVEL,
VocabularyKeys.MESSAGE
};
/**
* Resource keys for column names. This is usuall the same array than {@code COLUMN_NAMES}.
* However, method {@link #setColumnVisible} may add or remove column in this list.
*/
private int[] columnNames = COLUMN_NAMES;
/**
* The last {@link LogRecord}s stored. This array will grows as needed up to
* {@link #capacity}. Once the maximal capacity is reached, early records
* are discarted.
*/
private LogRecord[] records = new LogRecord[16];
/**
* The maximum amount of records that can be stored in this logging panel.
* If more than {@link #capacity} messages are logged, early messages will
* be discarted.
*/
private int capacity = 500;
/**
* The total number of logging messages published by this panel. This number may be
* greater than the amount of {@link LogRecord} actually memorized, since early records
* may have been discarted. The slot in {@code records} where to write the next
* message can be computed by <code>recordCount % capacity</code>.
*/
private int recordCount;
/**
* String representations of latest required records. Keys are {@link LogRecord} objects
* and values are {@code String[]}. This is a cache for faster rendering.
*/
private final Map cache = new LinkedHashMap() {
protected boolean removeEldestEntry(final Map.Entry eldest) {
return size() >= Math.min(capacity, 80);
}
};
/**
* The list of registered listeners.
*/
private final EventListenerList listenerList = new EventListenerList();
/**
* The format to use for formatting time.
*/
private final DateFormat dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM);
/**
* Construct the handler.
*/
public LoggingTableModel() {
setLevel(Level.CONFIG);
setFormatter(new SimpleFormatter());
}
/**
* Returns the capacity. This is the maximum number of {@link LogRecord}s this handler
* can memorize. If more messages are logged, then the oldiest messages will be discarted.
*/
public int getCapacity() {
return capacity;
}
/**
* Set the capacity. This is the maximum number of {@link LogRecord}s this handler can
* memorize. If more messages are logged, then the oldiest messages will be discarted.
*/
public synchronized void setCapacity(final int capacity) {
if (recordCount != 0) {
throw new IllegalStateException("Not yet implemented.");
}
this.capacity = capacity;
}
/**
* Returns {@code true} if the given column is visible.
*
* @param index One of {@link LoggingPanel} constants, which maps to entries in
* {@link COLUMN_NAMES}. For example {@code 0} for the logger,
* {@code 1} for the class, etc.
*/
final boolean isColumnVisible(int index) {
final int key = COLUMN_NAMES[index];
for (int i=0; i<columnNames.length; i++) {
if (columnNames[i] == key) {
return true;
}
}
return false;
}
/**
* Show or hide the given column.
*
* @param index One of {@link LoggingPanel} constants, which maps to entries in
* {@link COLUMN_NAMES}. For example {@code 0} for the logger,
* {@code 1} for the class, etc.
* @param visible The visible state for the specified column.
*/
final void setColumnVisible(final int index, final boolean visible) {
final int key = COLUMN_NAMES[index];
int[] names = new int[COLUMN_NAMES.length];
int count = 0;
for (int i=0; i<COLUMN_NAMES.length; i++) {
final int toTest = COLUMN_NAMES[i];
if (toTest == key) {
if (visible) {
names[count++] = toTest;
}
continue;
}
for (int j=0; j<columnNames.length; j++) {
if (columnNames[j] == toTest) {
names[count++] = toTest;
break;
}
}
}
columnNames = names = XArray.resize(names, count);
cache.clear();
fireTableChanged(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
assert isColumnVisible(index) == visible : visible;
}
/**
* Publish a {@link LogRecord}. If the maximal capacity has been reached,
* the oldiest record will be discarted.
*/
public synchronized void publish(final LogRecord record) {
if (!isLoggable(record)) {
return;
}
final int nextSlot = recordCount % capacity;
if (nextSlot >= records.length) {
records = (LogRecord[]) XArray.resize(records, Math.min(records.length*2, capacity));
}
records[nextSlot] = record;
final TableModelEvent event;
if (++recordCount <= capacity) {
event = new TableModelEvent(this, nextSlot, nextSlot,
TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT);
} else {
event = new TableModelEvent(this, 0, capacity-1,
TableModelEvent.ALL_COLUMNS, TableModelEvent.UPDATE);
}
//
// Notify all listeners that a record has been added.
//
EventQueue.invokeLater(new Runnable() {
public void run() {
fireTableChanged(event);
}
});
}
/**
* Returns the log record for the specified row.
*
* @param row The row in the table. This is the visible row,
* not the record number from the first record.
*/
public synchronized LogRecord getLogRecord(int row) {
assert row < getRowCount();
if (recordCount > capacity) {
row += (recordCount % capacity);
row %= capacity;
}
return records[row];
}
/**
* Returns the number of columns in the model.
*/
public int getColumnCount() {
return columnNames.length;
}
/**
* Returns the number of rows in the model.
*/
public synchronized int getRowCount() {
return Math.min(recordCount, capacity);
}
/**
* Returns the most specific superclass for all the cell values in the column.
*/
public Class getColumnClass(final int columnIndex) {
return String.class;
}
/**
* Returns the name of the column at {@code columnIndex}.
*/
public String getColumnName(final int columnIndex) {
return Vocabulary.format(columnNames[columnIndex]);
}
/**
* Returns the value for the cell at {@code columnIndex} and {@code rowIndex}.
*/
public synchronized Object getValueAt(final int rowIndex, final int columnIndex) {
final LogRecord record = getLogRecord(rowIndex);
String[] row = (String[]) cache.get(record);
if (row == null) {
row = new String[getColumnCount()];
for (int i=0; i<row.length; i++) {
final String value;
switch (columnNames[i]) {
case VocabularyKeys.LOGGER: value=record.getLoggerName(); break;
case VocabularyKeys.CLASS: value=getShortClassName(record.getSourceClassName()); break;
case VocabularyKeys.METHOD: value=record.getSourceMethodName(); break;
case VocabularyKeys.TIME_OF_DAY: value=dateFormat.format(new Date(record.getMillis())); break;
case VocabularyKeys.LEVEL: value=record.getLevel().getLocalizedName(); break;
case VocabularyKeys.MESSAGE: value=getFormatter().formatMessage(record); break;
default: throw new AssertionError(i);
}
row[i] = value;
}
cache.put(record, row);
assert cache.size() <= capacity;
}
return row[columnIndex];
}
/**
* Returns the class name in a shorter form (without package).
*/
private static String getShortClassName(String name) {
if (name != null) {
final int dot = name.lastIndexOf('.');
if (dot >= 0) {
name = name.substring(dot+1);
}
name = name.replace('$','.');
}
return name;
}
/**
* Do nothing since cells are not editable.
*/
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
}
/**
* Returns {@code false} since cells are not editable.
*/
public boolean isCellEditable(int rowIndex, int columnIndex) {
return false;
}
/**
* Adds a listener that is notified each time a change to the data model occurs.
*/
public void addTableModelListener(final TableModelListener listener) {
listenerList.add(TableModelListener.class, listener);
}
/**
* Removes a listener from the list that is notified each time a change occurs.
*/
public void removeTableModelListener(final TableModelListener listener) {
listenerList.remove(TableModelListener.class, listener);
}
/**
* Forwards the given notification event to all {@link TableModelListeners}.
*/
private void fireTableChanged(final TableModelEvent event) {
final Object[] listeners = listenerList.getListenerList();
for (int i=listeners.length-2; i>=0; i-=2) {
if (listeners[i]==TableModelListener.class) {
((TableModelListener)listeners[i+1]).tableChanged(event);
}
}
}
/**
* Flush any buffered output.
*/
public void flush() {
}
/**
* Close the {@code Handler} and free all associated resources.
*/
public void close() {
}
}