package org.ripple.power.ui.btc;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableRowSorter;
import org.ripple.power.Helper;
import org.ripple.power.config.LSystem;
import org.ripple.power.txns.btc.Alert;
import org.ripple.power.txns.btc.AlertListener;
import org.ripple.power.txns.btc.BTCLoader;
import org.ripple.power.txns.btc.BlockStatus;
import org.ripple.power.txns.btc.BlockStoreException;
import org.ripple.power.txns.btc.ChainListener;
import org.ripple.power.txns.btc.ConnectionListener;
import org.ripple.power.txns.btc.NetParams;
import org.ripple.power.txns.btc.Peer;
import org.ripple.power.txns.btc.Sha256Hash;
import org.ripple.power.txns.btc.StoredBlock;
import org.ripple.power.ui.UIConfig;
import org.ripple.power.ui.table.AddressTable;
public class StatusPanel extends JPanel implements AlertListener, ChainListener, ConnectionListener {
/**
*
*/
private static final long serialVersionUID = 1L;
private static final String[] serviceNames = {"Network"};
private static final Class<?>[] blockColumnClasses = {
Date.class, Integer.class, String.class, Integer.class, String.class};
private static final String[] blockColumnNames = {
"Date", "Height", "Block", "Version", "Status"};
private static final int[] blockColumnTypes = {
AddressTable.DATE, AddressTable.INTEGER, AddressTable.HASH, AddressTable.INTEGER, AddressTable.STATUS};
private BlockTableModel blockTableModel;
private JTable blockTable;
private JScrollPane blockScrollPane;
private static final Class<?>[] alertColumnClasses = {
Integer.class, Date.class, String.class, String.class};
private static final String[] alertColumnNames = {
"ID", "Expires", "Status", "Message"};
private static final int[] alertColumnTypes = {
AddressTable.INTEGER, AddressTable.DATE, AddressTable.STATUS, AddressTable.MESSAGE};
private AlertTableModel alertTableModel;
private JTable alertTable;
private JScrollPane alertScrollPane;
private static final Class<?>[] connectionColumnClasses = {
Date.class, String.class, Integer.class, String.class, String.class};
private static final String[] connectionColumnNames = {
"Connected", "Address", "Version", "Subversion", "Services"};
private static final int[] connectionColumnTypes = {
AddressTable.DATE, AddressTable.ADDRESS, AddressTable.INTEGER, AddressTable.SERVICES, AddressTable.SERVICES};
private final ConnectionTableModel connectionTableModel;
private final JTable connectionTable;
private final JScrollPane connectionScrollPane;
private final JLabel chainHeadField;
private final JLabel chainHeightField;
private final JLabel networkDifficultyField;
private final JLabel peerConnectionsField;
public StatusPanel() {
super(new BorderLayout());
setOpaque(true);
setBackground(UIConfig.background);
setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
JPanel tablePane = new JPanel();
tablePane.setLayout(new BoxLayout(tablePane, BoxLayout.Y_AXIS));
tablePane.setBackground(UIConfig.background);
try {
alertTableModel = new AlertTableModel(alertColumnNames, alertColumnClasses);
alertTable = new AddressTable(alertTableModel, alertColumnTypes);
alertTable.setRowSorter(new TableRowSorter<>(alertTableModel));
alertTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
alertTable.setPreferredScrollableViewportSize(new Dimension(
alertTable.getPreferredScrollableViewportSize().width,
alertTable.getRowHeight()*3));
alertScrollPane = new JScrollPane(alertTable);
tablePane.add(Box.createGlue());
tablePane.add(new JLabel("<html><h3><font color=white>Alerts</font></h3></html>"));
tablePane.add(alertScrollPane);
} catch (BlockStoreException exc) {
BTCLoader.error("Block store exception while creating alert table", exc);
}
connectionTableModel = new ConnectionTableModel(connectionColumnNames, connectionColumnClasses);
connectionTable = new AddressTable(connectionTableModel, connectionColumnTypes);
connectionTable.setRowSorter(new TableRowSorter<>(connectionTableModel));
connectionTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
connectionScrollPane = new JScrollPane(connectionTable);
tablePane.add(Box.createGlue());
tablePane.add(new JLabel("<html><h3><font color=white>Connections</font></h3></html>"));
tablePane.add(connectionScrollPane);
try {
blockTableModel = new BlockTableModel(blockColumnNames, blockColumnClasses);
blockTable = new AddressTable(blockTableModel, blockColumnTypes);
blockTable.setRowSorter(new TableRowSorter<>(blockTableModel));
blockTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
blockScrollPane = new JScrollPane(blockTable);
tablePane.add(Box.createGlue());
tablePane.add(new JLabel("<html><h3><font color=white>Recent Blocks</font></h3></html>"));
tablePane.add(blockScrollPane);
tablePane.add(Box.createGlue());
} catch (BlockStoreException exc) {
BTCLoader.error("Block store exception while creating block status table", exc);
}
chainHeadField = new JLabel();
JPanel chainHeadPane = new JPanel();
chainHeadPane.add(Box.createGlue());
chainHeadPane.add(chainHeadField);
chainHeadPane.add(Box.createGlue());
chainHeightField = new JLabel();
JPanel chainHeightPane = new JPanel();
chainHeightPane.add(Box.createGlue());
chainHeightPane.add(chainHeightField);
chainHeightPane.add(Box.createGlue());
networkDifficultyField = new JLabel();
JPanel networkDifficultyPane = new JPanel();
networkDifficultyPane.add(Box.createGlue());
networkDifficultyPane.add(networkDifficultyField);
networkDifficultyPane.add(Box.createGlue());
peerConnectionsField = new JLabel();
JPanel peerConnectionsPane = new JPanel();
peerConnectionsPane.add(Box.createGlue());
peerConnectionsPane.add(peerConnectionsField);
peerConnectionsPane.add(Box.createGlue());
JPanel statusPane = new JPanel();
statusPane.setLayout(new BoxLayout(statusPane, BoxLayout.Y_AXIS));
statusPane.setOpaque(true);
statusPane.setBackground(UIConfig.background);
statusPane.add(chainHeadPane);
statusPane.add(chainHeightPane);
statusPane.add(networkDifficultyPane);
statusPane.add(peerConnectionsPane);
statusPane.add(Box.createVerticalStrut(20));
add(statusPane, BorderLayout.NORTH);
add(tablePane, BorderLayout.CENTER);
BTCLoader.blockChain.addListener((ChainListener)this);
BTCLoader.networkHandler.addListener((ConnectionListener)this);
BTCLoader.networkMessageListener.addListener((AlertListener)this);
connectionTableModel.updateConnections();
updateStatus();
}
@Override
public void blockStored(StoredBlock storedBlock) {
blockTableModel.blockStored(storedBlock);
}
@Override
public void blockUpdated(StoredBlock storedBlock) {
blockTableModel.blockStored(storedBlock);
}
@Override
public void chainUpdated() {
LSystem.invokeLater(new Runnable() {
@Override
public void run() {
updateStatus();
}
});
}
@Override
public void connectionStarted(Peer peer, int count) {
connectionTableModel.addConnection(peer);
LSystem.invokeLater(new Runnable() {
@Override
public void run() {
updateStatus();
}
});
}
@Override
public void connectionEnded(Peer peer, int count) {
connectionTableModel.removeConnection(peer);
LSystem.invokeLater(new Runnable() {
@Override
public void run() {
updateStatus();
}
});
}
@Override
public void alertReceived(Alert alert) {
alertTableModel.addAlert(alert);
}
private void updateStatus() {
Sha256Hash chainHead = BTCLoader.blockStore.getChainHead();
chainHeadField.setText(String.format("<html><b>Chain head: %s</b></html>",
chainHead.toString()));
int chainHeight = BTCLoader.blockStore.getChainHeight();
chainHeightField.setText(String.format("<html><b>Chain height: %d</b></html>",
chainHeight));
BigInteger targetDifficulty = BTCLoader.blockStore.getTargetDifficulty();
BigInteger networkDifficulty = NetParams.PROOF_OF_WORK_LIMIT.divide(targetDifficulty);
String displayDifficulty = Helper.numberToShortString(networkDifficulty);
networkDifficultyField.setText(String.format("<html><b>Network difficulty: %s</b></html>",
displayDifficulty));
peerConnectionsField.setText(String.format("<html><b>Peer connections: %d</b></html>",
connectionTable.getRowCount()));
LSystem.invokeLater(new Runnable() {
@Override
public void run() {
repaint();
}
});
}
private class BlockStatusComparator implements Comparator<BlockStatus> {
public BlockStatusComparator() {
}
@Override
public int compare(BlockStatus o1, BlockStatus o2) {
long t1 = o1.getTimeStamp();
long t2 = o2.getTimeStamp();
return (t1==t2 ? 0 : (t1>t2 ? -1 : 1));
}
}
private class BlockTableModel extends AbstractTableModel {
/**
*
*/
private static final long serialVersionUID = 1L;
private final String[] columnNames;
private final Class<?>[] columnClasses;
private BlockStatus[] blocks;
private final Map<Sha256Hash, BlockStatus> blockMap = new HashMap<>(50);
private final Map<Integer, BlockStatus> heightMap = new HashMap<>(50);
private boolean refreshPending;
public BlockTableModel(String[] columnNames, Class<?>[] columnClasses) throws BlockStoreException {
super();
this.columnNames = columnNames;
this.columnClasses = columnClasses;
blocks = (BlockStatus[])BTCLoader.blockStore.getBlockStatus(150).toArray(new BlockStatus[0]);
Arrays.sort(blocks, new BlockStatusComparator());
for (BlockStatus block : blocks) {
blockMap.put(block.getHash(), block);
if (block.isOnChain()){
heightMap.put(block.getHeight(), block);
}
}
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public Class<?> getColumnClass(int column) {
return columnClasses[column];
}
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public int getRowCount() {
return blocks.length;
}
@Override
public Object getValueAt(int row, int column) {
Object value;
BlockStatus status;
synchronized(blockMap) {
status = blocks[row];
}
switch (column) {
case 0:
value = new Date(status.getTimeStamp()*1000);
break;
case 1:
value = status.isOnChain() ? status.getHeight() : 0;
break;
case 2:
value = status.getHash().toString();
break;
case 3:
value = status.getVersion();
break;
case 4:
if (status.isOnChain())
value = "On Chain";
else if (status.isOnHold())
value = "Held";
else
value = "Ready";
break;
default:
throw new IndexOutOfBoundsException("Table column "+column+" is not valid");
}
return value;
}
public void blockStored(StoredBlock storedBlock) {
Sha256Hash blockHash = storedBlock.getHash();
Integer blockHeight = storedBlock.getHeight();
synchronized(blockMap) {
BlockStatus blockStatus = blockMap.get(blockHash);
if (blockStatus == null) {
blockStatus = new BlockStatus(blockHash, storedBlock.getBlock().getTimeStamp(),
storedBlock.getHeight(), storedBlock.getBlock().getVersion(),
storedBlock.isOnChain(), storedBlock.isOnHold());
BlockStatus[] newBlocks = new BlockStatus[blocks.length+1];
System.arraycopy(blocks, 0, newBlocks, 0, blocks.length);
newBlocks[blocks.length] = blockStatus;
Arrays.sort(newBlocks, new BlockStatusComparator());
blocks = newBlocks;
blockMap.put(blockHash, blockStatus);
} else {
blockStatus.setHeight(storedBlock.getHeight());
blockStatus.setChain(storedBlock.isOnChain());
blockStatus.setHold(storedBlock.isOnHold());
}
if (storedBlock.isOnChain()) {
BlockStatus chkStatus = heightMap.get(blockHeight);
if (chkStatus == null) {
heightMap.put(blockHeight, blockStatus);
} else if (!chkStatus.getHash().equals(blockHash)) {
chkStatus.setChain(false);
chkStatus.setHeight(0);
heightMap.put(blockHeight, blockStatus);
}
}
}
if (!refreshPending) {
refreshPending = true;
LSystem.invokeLater(new Runnable() {
@Override
public void run() {
fireTableDataChanged();
refreshPending = false;
}
});
}
}
}
private class AlertTableModel extends AbstractTableModel {
/**
*
*/
private static final long serialVersionUID = 1L;
private final String[] columnNames;
private final Class<?>[] columnClasses;
private final List<Alert> alertList;
private boolean refreshPending = false;
public AlertTableModel(String[] columnNames, Class<?>[] columnClasses) throws BlockStoreException {
super();
this.columnNames = columnNames;
this.columnClasses = columnClasses;
alertList = BTCLoader.blockStore.getAlerts();
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public Class<?> getColumnClass(int column) {
return columnClasses[column];
}
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public int getRowCount() {
return alertList.size();
}
@Override
public Object getValueAt(int row, int column) {
Object value = null;
Alert alert = alertList.get(alertList.size()-1-row);
switch (column) {
case 0:
value = alert.getID();
break;
case 1:
value = new Date(alert.getExpireTime()*1000);
break;
case 2:
if (alert.isCanceled())
value = "Canceled";
else if (alert.getExpireTime() < System.currentTimeMillis()/1000)
value = "Expired";
else
value = "";
break;
case 3:
value = alert.getMessage();
break;
}
return value;
}
public void addAlert(Alert alert) {
alertList.add(alert);
if (!refreshPending) {
refreshPending = true;
LSystem.invokeLater(new Runnable() {
@Override
public void run() {
fireTableDataChanged();
refreshPending = false;
}
});
}
}
}
private class ConnectionTableModel extends AbstractTableModel {
/**
*
*/
private static final long serialVersionUID = 1L;
private final String[] columnNames;
private final Class<?>[] columnClasses;
private final List<Peer> connectionList = new ArrayList<>(128);
public ConnectionTableModel(String[] columnNames, Class<?>[] columnClasses) {
super();
this.columnNames = columnNames;
this.columnClasses = columnClasses;
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public Class<?> getColumnClass(int column) {
return columnClasses[column];
}
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public int getRowCount() {
return connectionList.size();
}
@Override
public Object getValueAt(int row, int column) {
Object value = null;
Peer peer = connectionList.get(row);
switch (column) {
case 0:
value = new Date(peer.getAddress().getTimeConnected()*1000);
break;
case 1:
value = peer.getAddress().toString();
break;
case 2:
value = peer.getVersion();
break;
case 3:
value = peer.getUserAgent();
break;
case 4:
long services = peer.getServices();
StringBuilder serviceString = new StringBuilder(32);
for (int i=0; i<serviceNames.length; i++) {
if ((services & (1<<i)) != 0)
serviceString.append(serviceNames[i]);
}
value = serviceString.toString();
break;
}
return value;
}
public void updateConnections() {
List<Peer> connections = BTCLoader.networkHandler.getConnections();
for(Peer peer:connections){
if(!connectionList.contains(peer)){
connectionList.add(peer);
}
}
}
public void addConnection(Peer peer) {
ConnectionUpdate updateTask = new ConnectionUpdate(connectionList, peer, true);
LSystem.invokeLater(updateTask);
}
public void removeConnection(Peer peer) {
ConnectionUpdate updateTask = new ConnectionUpdate(connectionList, peer, false);
LSystem.invokeLater(updateTask);
}
}
private class ConnectionUpdate implements Runnable {
private final boolean addConnection;
private final Peer peer;
private final List<Peer> connectionList;
public ConnectionUpdate(List<Peer> connectionList, Peer peer, boolean addConnection) {
this.connectionList = connectionList;
this.peer = peer;
this.addConnection = addConnection;
}
@Override
public void run() {
if (addConnection) {
if (!connectionList.contains(peer)){
connectionList.add(peer);
}
} else {
connectionList.remove(peer);
}
connectionTableModel.fireTableDataChanged();
}
}
}