/**
* Copyright 2005-2012 Akiban Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.persistit.ui;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.MouseEvent;
import java.io.IOException;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import com.persistit.Exchange;
import com.persistit.Key;
import com.persistit.Value;
import com.persistit.exception.PersistitException;
public class PersistitTableModel extends AbstractTableModel {
private final static int SCROLL_EXTRA = 100;
private final static float SCROLL_FACTOR = 1.3f;
private final static int ROW_CACHE_SIZE = 100;
private final static int INITIAL_ROW_COUNT_ESTIMATE = 10000;
private final static String[] COLUMN_HEADER_NAMES = { "Index", "Key", "Type", "Value", };
private final static int[] COLUMN_HEADER_WIDTHS = { 50, 450, 50, 450, };
private final static int COLUMN_COUNT = COLUMN_HEADER_NAMES.length;
/**
* The Exchange on which the display will be based.
*/
private Exchange _exchange;
/**
* The root key for this display.
*/
private Key _rootKey;
/**
* The Row that corresponds to the root Key.
*/
private final Row _rootRow = new Row(new byte[0]);
/**
* A list of contiguous rows. The first element in the list corresponds with
* the row index in _rowCacheOffset.
*/
private final RowCache _rowCache = new RowCache();
/**
* There are at least this many rows.
*/
private int _rowCountActual;
/**
* The last row count estimate that was returned by getRowCount().
*/
private int _rowCountEstimate;
/**
* Count of getValue operations satisfied from cache
*/
private int _rowGetValueHits;
/**
* Count of getValue operations
*/
private int _rowGetValueOperations;
/**
* Establishes the supplied Exchange as the root of a display tree.
*
* @param exchange
*/
public void setExchange(final Exchange exchange) {
_exchange = exchange;
_rootKey = new Key(_exchange.getKey());
_rowCache.clear();
_rowCountActual = -1;
_rowCountEstimate = INITIAL_ROW_COUNT_ESTIMATE;
fireTableChanged(new TableModelEvent(this));
}
public Exchange getExchange() {
return _exchange;
}
@Override
public Object getValueAt(final int rowIndex, final int columnIndex) {
if (rowIndex < 0) {
throw new IllegalArgumentException("rowIndex=" + rowIndex);
}
if (_rowCountActual >= 0 && rowIndex > _rowCountActual) {
return _rootRow;
}
_rowGetValueOperations++;
try {
Row row = _rowCache.get(rowIndex);
if (row != null) {
_rowGetValueHits++;
return row;
}
int minCachedIndex = _rowCache.getFirstIndex();
int maxCachedIndex = _rowCache.getLastIndex();
if (rowIndex < minCachedIndex) {
// (1) are we going to get the row by backing up
// or by starting at the beginning?
//
if (rowIndex > (minCachedIndex / 2)) {
row = _rowCache.get(minCachedIndex);
for (int index = minCachedIndex; index > rowIndex;) {
if (row == null)
return null;
row = row.getNextRow(false);
index--;
if (row == null) {
row = _rootRow;
}
_rowCache.put(index, row);
}
return row;
} else {
maxCachedIndex = 0;
minCachedIndex = 0;
}
}
int index;
if (maxCachedIndex == 0) {
index = 0;
row = _rootRow;
if (rowIndex == 0)
_rowCache.put(index, row);
} else {
index = maxCachedIndex - 1;
row = _rowCache.get(index);
}
for (; index < rowIndex;) {
if (row == null)
return null;
row = row.getNextRow(true);
index++;
if (row == null) {
final boolean changed = index != _rowCountEstimate;
// we are at the end!
final int previousEstimate = _rowCountEstimate;
_rowCountEstimate = index;
_rowCountActual = index;
if (changed)
fireSizeChanged(previousEstimate);
return null;
} else
_rowCache.put(index, row);
}
if (_rowCountActual < 0 && _rowCountEstimate - rowIndex < SCROLL_EXTRA) {
final int previousEstimate = _rowCountEstimate;
_rowCountEstimate = (int) ((_rowCountEstimate + SCROLL_EXTRA) * SCROLL_FACTOR);
fireSizeChanged(previousEstimate);
}
return row;
} catch (final PersistitException de) {
return de;
}
}
@Override
public int getRowCount() {
return _rowCountEstimate;
}
@Override
public int getColumnCount() {
return COLUMN_COUNT;
}
/**
* Sets up the TableColumnModel for a supplied JTable
*
* @param table
*/
public void setupColumns(final JTable table) {
final TableColumnModel tcm = table.getColumnModel();
final int count = tcm.getColumnCount();
final String[] headers = COLUMN_HEADER_NAMES;
final int[] widths = COLUMN_HEADER_WIDTHS;
final TableCellRenderer renderer = new ExchangeTableCellRenderer();
for (int i = 0; i < headers.length; i++) {
TableColumn tc;
if (i >= count) {
tc = new TableColumn();
tcm.addColumn(tc);
} else {
tc = tcm.getColumn(i);
}
tc.setHeaderValue(headers[i]);
tc.setPreferredWidth(widths[i]);
tc.setCellRenderer(renderer);
}
}
private void fireSizeChanged(final int previousEstimate) {
if (_rowCountEstimate > previousEstimate) {
fireTableRowsInserted(previousEstimate, _rowCountEstimate - 1);
} else if (_rowCountEstimate < previousEstimate) {
fireTableRowsDeleted(_rowCountEstimate, previousEstimate - 1);
}
}
//
// Helper classes
//
private class Row {
private final byte[] _keyBytes;
private int _index = -1; // TODO - debugging
private Row(final byte[] keyBytes) {
_keyBytes = keyBytes;
}
/**
* Determines whether children of the Key described by this Row should
* be displayed.
*
* @return <i>true</i> if the children of this Row should be displayed.
*/
private boolean isExpanded() {
return true;
}
private Row getNextRow(final boolean forward) throws PersistitException {
final Exchange ex = setupExchange();
final boolean expanded = (isExpanded());
if (!ex.traverse(forward ? Key.GT : Key.LT, expanded)) {
return null;
}
final Key key = ex.getKey();
if (key.compareKeyFragment(_rootKey, 0, _rootKey.getEncodedSize()) != 0) {
return null;
}
final int keyBytesSize = key.getEncodedSize() - _rootKey.getEncodedSize();
final byte[] keyBytes = new byte[keyBytesSize];
System.arraycopy(key.getEncodedBytes(), _rootKey.getEncodedSize(), keyBytes, 0, keyBytesSize);
return new Row(keyBytes);
}
private String getValueTypeString() {
try {
final Exchange ex = setupExchange();
if (ex.getKey().getEncodedSize() == 0)
return "";
ex.fetch();
final Value value = ex.getValue();
if (!value.isDefined())
return "undefined";
return value.getType().getName();
} catch (final PersistitException de) {
return de.toString();
}
}
private String getValueString() {
try {
final Exchange ex = setupExchange();
if (ex.getKey().getEncodedSize() == 0)
return "";
ex.fetch();
final Value value = ex.getValue();
if (value.getEncodedSize() > 50000) {
return "Too long to display: " + value.getEncodedSize();
} else {
return value.toString();
}
} catch (final PersistitException de) {
return de.toString();
}
}
private Key getKey() {
final Exchange ex = setupExchange();
return ex.getKey();
}
private Value getValue() throws PersistitException, IOException {
final Exchange ex = setupExchange();
ex.fetch();
return ex.getValue();
}
private String getKeyString() {
final Exchange ex = setupExchange();
ex.getKey().setIndex(0);
return ex.getKey().toString();
}
private Exchange setupExchange() {
final Exchange ex = PersistitTableModel.this.getExchange();
final Key key = ex.getKey();
System.arraycopy(_rootKey.getEncodedBytes(), 0, key.getEncodedBytes(), 0, _rootKey.getEncodedSize());
System.arraycopy(_keyBytes, 0, key.getEncodedBytes(), _rootKey.getEncodedSize(), _keyBytes.length);
key.setEncodedSize(_rootKey.getEncodedSize() + _keyBytes.length);
return ex;
}
@Override
public String toString() {
return _index + ": " + setupExchange().getKey().toString() + "-->" + getValueString();
}
}
private static class RowCache {
/**
* Array of cached rows, organized as a ring buffer.
*/
private final Row[] _cache = new Row[ROW_CACHE_SIZE];
/**
* Offset of lowest indexed Row in the cache.
*/
private int _tail = 0;
/**
* Offset one past highest indexed Row in the cache.
*/
private int _head = 0;
/**
* The index within the table of the lowest indexed Row in the cache.
*/
private int _firstCachedIndex = 0;
private void clear() {
_tail = 0;
_head = 0;
for (int index = 0; index < ROW_CACHE_SIZE; index++) {
_cache[index] = null;
}
}
private int getFirstIndex() {
return _firstCachedIndex;
}
private int getLastIndex() {
return _firstCachedIndex + size();
}
private int size() {
int size = _head - _tail;
if (size < 0)
size += ROW_CACHE_SIZE;
return size;
}
private Row get(int index) {
index -= _firstCachedIndex;
if (index < 0)
return null;
if (index >= size())
return null;
index = (index + _tail) % ROW_CACHE_SIZE;
return _cache[index];
}
private void put(int index, final Row row) {
row._index = index;
index -= _firstCachedIndex;
final int size = size();
if (index == -1) {
_tail--;
if (_tail < 0)
_tail += ROW_CACHE_SIZE;
if (_head == _tail) {
_head--;
if (_head < 0)
_head += ROW_CACHE_SIZE;
}
_firstCachedIndex--;
_cache[_tail] = row;
} else if (index == size) {
_cache[_head] = row;
_head++;
if (_head == ROW_CACHE_SIZE)
_head = 0;
if (_head == _tail) {
_cache[_tail] = null;
_tail++;
if (_tail == ROW_CACHE_SIZE)
_tail = 0;
_firstCachedIndex++;
}
} else if (index >= 0 && index < size) {
index -= _tail;
if (index < 0)
index += ROW_CACHE_SIZE;
_cache[index] = row;
} else {
clear();
_cache[0] = row;
_head++;
_firstCachedIndex += index;
}
}
}
private static class ExchangeTableCellRenderer extends DefaultTableCellRenderer {
private ExchangeTableCellRenderer() {
setFont(new Font("Lucida", Font.PLAIN, 13));
setBorder(BorderFactory.createEmptyBorder(1, 4, 1, 4));
}
@Override
public Component getTableCellRendererComponent(final JTable table, final Object value,
final boolean isSelected, final boolean hasFocus, final int rowIndex, final int columnIndex) {
final JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus,
rowIndex, columnIndex);
final Row row = (Row) value;
if (row == null) {
label.setText("");
} else
switch (columnIndex) {
case 0: {
label.setText(Integer.toString(rowIndex));
label.setHorizontalAlignment(SwingConstants.RIGHT);
break;
}
case 1: {
label.setText(row.getKeyString());
label.setHorizontalAlignment(SwingConstants.LEFT);
break;
}
case 2: {
label.setText(row.getValueTypeString());
label.setHorizontalAlignment(SwingConstants.CENTER);
break;
}
case 3: {
label.setText(row.getValueString());
label.setHorizontalAlignment(SwingConstants.LEFT);
break;
}
default: {
label.setText("Unknown column " + columnIndex);
}
}
return label;
}
@Override
protected void setValue(final Object value) {
}
}
public JTable createTreeDisplayTable() {
final TreeDisplayTable tdt = new TreeDisplayTable(this);
setupColumns(tdt);
return tdt;
}
private static class TreeDisplayTable extends JTable {
TreeDisplayTable(final PersistitTableModel dtm) {
super(dtm);
}
@Override
public String getToolTipText(final MouseEvent me) {
final int row = rowAtPoint(me.getPoint());
final int col = columnAtPoint(me.getPoint());
if (row >= 0 && col >= 0) {
final Object value = getModel().getValueAt(row, col);
if (value != null) {
final TableCellRenderer tcr = getCellRenderer(row, col);
final Component rc = tcr.getTableCellRendererComponent(this, value, false, false, row, col);
if (rc instanceof JLabel) {
final String s = ((JLabel) rc).getText();
if (s.length() > 20)
return s;
}
}
}
return null;
}
}
}