/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2002-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.gui.swing;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import java.util.Date;
import java.text.DateFormat;
import java.awt.EventQueue;
import javax.swing.JTable;
import javax.swing.table.TableModel;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.event.EventListenerList;
import javax.swing.BoundedRangeModel;
import org.apache.sis.util.CharSequences;
import org.geotoolkit.resources.Vocabulary;
/**
* 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}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.02
*
* @since 2.0
* @module
*/
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 #getValueAt(int,int)} and
* {@link LoggingPanel} must be updated.
*/
private static final short[] COLUMN_NAMES = {
Vocabulary.Keys.Logger,
Vocabulary.Keys.Class,
Vocabulary.Keys.Method,
Vocabulary.Keys.TimeOfDay,
Vocabulary.Keys.Level,
Vocabulary.Keys.Message
};
/**
* Holds a log record. If the message is multi-lines, then a {@code Record} instance
* will be repeated for each line with a reference to the same {@link LogRecord} but
* a different line of the message.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.01
*
* @since 3.01
* @module
*/
private static final class Record {
/**
* Special value for {@link #time} meaning that this entry is for additional
* lines after the first one.
*/
private static final String ADDITIONAL_LINES = "ADDITIONAL_LINES";
/**
* The log record.
*/
final LogRecord log;
/**
* The message. If there is log message has more than one line, then this
* will contains only one line of that message.
*/
private final String line;
/**
* The time. Will be created when first needed. The {@value #ADDITIONAL_LINES}
* string is used as a special value meaning that nothing should be formatted.
*/
private String time;
/**
* Creates a new record for the given log.
*/
private Record(final LogRecord log, final String line) {
this.log = log;
this.line = line;
}
/**
* Creates the records for the given log and its message.
*/
static Record[] create(final LogRecord log, final String message) {
final CharSequence[] lines = CharSequences.splitOnEOL(message);
final Record[] records = new Record[lines.length];
for (int i=0; i<lines.length; i++) {
records[i] = new Record(log, lines[i].toString());
}
for (int i=1; i<lines.length; i++) {
records[i].time = ADDITIONAL_LINES;
}
return records;
}
/**
* Returns the value at the given column.
*
* @param owner The model which is invoking this method.
* @param columnIndex The column for which to get the value.
* @return The value at the given column.
*/
public Object getValueAt(final LoggingTableModel owner, final int columnIndex) {
if (time == ADDITIONAL_LINES) { // NOSONAR (Intentional identity comparisons, not String.equals)
switch (columnIndex) {
// The Level as Integer will be handled specially by
// LoggingPanel.Highlighter.isHighlighted(...).
case 4: return Integer.valueOf(log.getLevel().intValue());
case 5: return line;
default: return null;
}
}
/*
* General case (the logging message holds on a single line,
* or we are asking for the first line of a multi-lines log).
*/
switch (columnIndex) {
default: throw new AssertionError(columnIndex);
case 0: return log.getLoggerName();
case 1: return getShortClassName(log.getSourceClassName());
case 2: return log.getSourceMethodName();
case 4: return log.getLevel();
case 5: return line;
case 3: {
if (time == null) {
time = owner.dateFormat.format(new Date(log.getMillis()));
}
return time;
}
}
}
}
/**
* The last {@link Record}s stored. This array will grows as needed up to
* {@link #capacity}. Once the maximal capacity is reached, early records
* are discarded.
*/
private Record[] records = new Record[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 discarded.
*/
private int capacity = 500;
/**
* The total number of logging messages published by this panel. This number may be
* greater than the amount of {@link Record} actually memorized, since early records
* may have been discarded. The slot in {@code records} where to write the next
* message is defined by {@code recordCount % capacity}.
*/
private int recordCount;
/**
* 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);
/**
* Constructs the handler.
*/
public LoggingTableModel() {
setLevel(Level.ALL);
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 discarded.
*/
public int getCapacity() {
return capacity;
}
/**
* Sets 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 discarded.
*/
public synchronized void setCapacity(final int capacity) {
if (recordCount != 0) {
throw new IllegalStateException("Not yet implemented.");
}
this.capacity = capacity;
}
/**
* Publishes a {@link LogRecord}. If the maximal capacity has been reached,
* then the oldest record will be discarded.
*/
@Override
public synchronized void publish(final LogRecord record) {
if (!isLoggable(record)) {
return;
}
/*
* Wraps the LogRecord in exactly one Record instances (typicaly case), or more
* Record instances if the LogRecord message spans more than one line.
*/
final Record[] toAdd = Record.create(record, getFormatter().formatMessage(record));
for (final Record item : toAdd) {
final int nextSlot = recordCount % capacity;
if (nextSlot >= records.length) {
records = Arrays.copyOf(records, Math.min(records.length*2, capacity));
}
records[nextSlot] = item;
recordCount++;
}
/*
* Notify all listeners that one (or more) records have been added.
*/
final int upper = Math.min(recordCount, capacity);
final int removed = Math.min(recordCount - capacity, toAdd.length);
final TableModelEvent remove, insert;
if (removed > 0) {
remove = new TableModelEvent(this, 0, removed-1,
TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE);
} else {
remove = null;
}
insert = new TableModelEvent(this, upper - toAdd.length, upper - 1,
TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT);
EventQueue.invokeLater(new Runnable() {
@Override public void run() {
if (remove != null) {
fireTableChanged(remove);
}
fireTableChanged(insert);
}
});
}
/**
* Controls the scrolling of {@link LoggingPanel}. This class scroll down if the view port
* was already at the bottom of the scroll area and a new record is inserted, or scroll up
* if the view port is <em>not</em> at the bottom of the scroll area and the first record
* has been removed.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.02
*
* @since 3.02
* @module
*/
@SuppressWarnings("serial")
static final class Scroll extends AutoScroll implements TableModelListener {
/**
* The table on which to control the scrolling.
*/
private final JTable table;
/**
* Constructs a new {@code AutoScroll} for the specified model.
*/
Scroll(final JTable table, final BoundedRangeModel model) {
super(model);
this.table = table;
}
/**
* Invoked when a new record has been added. If the oldest record has been removed
* and the viewport is not at the bottom of the scroll area, scroll up in order to
* keep the viewport on the same records.
*/
@Override
public void tableChanged(final TableModelEvent event) {
if (event.getType() == TableModelEvent.DELETE && !isViewBottom()) {
final int n = event.getFirstRow() - (event.getLastRow() + 1); // Intentionally negative.
conditionalScroll(n * table.getRowHeight());
}
}
}
/**
* Returns the 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.
*/
private Record getLogRecord(int row) {
// Above check is performed with an assertion instead than an API contract check
// because this method is invoked indirectly only from the table inside LoggingPanel.
// This is pretty far from a public access (while not inacessible).
assert row < getRowCount();
if (recordCount > capacity) {
row += (recordCount % capacity);
row %= capacity;
}
return records[row];
}
/**
* Returns the number of columns in the model.
*/
@Override
public int getColumnCount() {
return COLUMN_NAMES.length;
}
/**
* Returns the number of rows in the model.
*/
@Override
public synchronized int getRowCount() {
return Math.min(recordCount, capacity);
}
/**
* Returns the most specific superclass for all the cell values in the column.
*/
@Override
public Class<?> getColumnClass(final int columnIndex) {
switch (columnIndex) {
case 4: return Level .class;
default: return String.class;
}
}
/**
* Returns the name of the column at {@code columnIndex}.
*/
@Override
public String getColumnName(final int columnIndex) {
return Vocabulary.format(COLUMN_NAMES[columnIndex]);
}
/**
* Returns the value for the cell at {@code columnIndex} and {@code rowIndex}.
*/
@Override
public synchronized Object getValueAt(final int rowIndex, final int columnIndex) {
return getLogRecord(rowIndex).getValueAt(this, 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;
}
/**
* Does nothing since cells are not editable.
*/
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
}
/**
* Returns {@code false} since cells are not editable.
*/
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return false;
}
/**
* Adds a listener that is notified each time a change to the data model occurs.
*/
@Override
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.
*/
@Override
public void removeTableModelListener(final TableModelListener listener) {
listenerList.remove(TableModelListener.class, listener);
}
/**
* Forwards the given notification event to all {@link TableModelListeners}.
* Listeners are notified in registration order. This order is necessary,
* because the last listener is the {@link Scroll} object and we want it
* to be notified last.
*/
private void fireTableChanged(final TableModelEvent event) {
final Object[] listeners = listenerList.getListenerList();
for (int i=0; i<listeners.length; i+=2) {
if (listeners[i] == TableModelListener.class) {
((TableModelListener) listeners[i+1]).tableChanged(event);
}
}
}
/**
* Flushes any buffered output.
*/
@Override
public void flush() {
}
/**
* Closes the {@code Handler} and free all associated resources.
*/
@Override
public void close() {
}
}