package com.ibm.nmon.gui.chart.summary;
import com.ibm.nmon.gui.table.ChoosableColumnTableModel;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyChangeEvent;
import java.util.BitSet;
import java.util.List;
import java.util.Map;
import com.ibm.nmon.data.DataTuple;
import com.ibm.nmon.data.DataType;
import com.ibm.nmon.data.ProcessDataType;
import com.ibm.nmon.data.SystemDataSet;
import com.ibm.nmon.gui.chart.data.*;
import com.ibm.nmon.analysis.AnalysisRecord;
import static com.ibm.nmon.analysis.Statistic.*;
import com.ibm.nmon.gui.main.NMONVisualizerGui;
/**
* Table model for {@link ChartSummaryPanel}. This model holds a single {@link DataTupleDataset}
* which is used to display various summary data about the chart. For bar charts, this model
* displays a row for each row / column combination in the chart DataSet. For line and interval line
* charts, there will be one row for each series (i.e. line).
*/
public final class ChartSummaryTableModel extends ChoosableColumnTableModel implements PropertyChangeListener {
private static final long serialVersionUID = -4224937019632087892L;
// check mark
static final String VISIBLE = "\u2713";
private static final String[] COLUMN_NAMES = new String[] { VISIBLE, "Hostname", "Data Type", "Metric",
"Series Name", MINIMUM.toString(), AVERAGE.toString(), MAXIMUM.toString(), STD_DEV.toString(), null,
MEDIAN.toString(), PERCENTILE_95.toString(), PERCENTILE_99.toString(), SUM.toString(), COUNT.toString(),
"Graph " + MINIMUM.toString(), "Graph " + AVERAGE.toString(), "Graph " + MAXIMUM.toString(),
"Graph " + STD_DEV.toString(), "Graph " + MEDIAN.toString(), "Graph " + PERCENTILE_95.toString(),
"Graph " + PERCENTILE_99.toString(), "Graph " + SUM.toString(), "Graph " + COUNT.toString() };
private static final DataTuple NULL_TUPLE = new DataTuple(new SystemDataSet("N/A"), new DataType("N/A", "N/A",
"N/A"), "N/A");
private static final AnalysisRecord NULL_ANALYSIS = new AnalysisRecord(NULL_TUPLE.getDataSet());
private final NMONVisualizerGui gui;
private boolean[] defaultColumns;
private DataTupleDataset dataset;
private boolean[] rowVisible;
// the GUI only creates a single summary table
// so, cache which rows are selected for each tuple
// note that the DataTuple itself cannot be used since it references a DataSet which could be a
// memory leak as datasets are removed
// so, use the hash code instead
private final Map<Integer, boolean[]> rowVisibleCache;
private PropertyChangeSupport propertyChangeSupport;
public ChartSummaryTableModel(NMONVisualizerGui gui, String... defaultColumnNames) {
this.gui = gui;
this.propertyChangeSupport = new PropertyChangeSupport(this);
COLUMN_NAMES[9] = GRANULARITY_MAXIMUM.getName(gui.getGranularity());
buildColumnNameMap();
// default default columns
if (defaultColumnNames == null) {
defaultColumnNames = new String[] { VISIBLE, "Data Type", "Metric", "Minimum", "Average", "Maximum",
"Std Dev" };
}
defaultColumns = new boolean[COLUMN_NAMES.length];
java.util.Arrays.fill(defaultColumns, false);
// enable/disable is always shown
defaultColumns[0] = true;
for (String columnName : defaultColumnNames) {
int idx = getColumnIndex(columnName);
if (idx != -1) {
defaultColumns[idx] = true;
}
else {
logger.warn("ignoring non-existent column '{}' for defaults", columnName);
}
}
enabledColumns = new BitSet(COLUMN_NAMES.length);
for (int i = 0; i < defaultColumns.length; i++) {
enabledColumns.set(i, defaultColumns[i]);
}
dataset = null;
rowVisibleCache = new java.util.HashMap<Integer, boolean[]>();
}
@Override
public int getRowCount() {
if (dataset == null) {
return 0;
}
else if (dataset instanceof DataTupleCategoryDataset) {
DataTupleCategoryDataset d = (DataTupleCategoryDataset) dataset;
if (d.containsIntervals()) {
// one table row per interval
return d.getRowCount();
}
else if (d.categoriesHaveDifferentStats()) {
// different stats => no need to show a row per category
return d.getColumnCount();
}
else {
return d.getRowCount() * d.getColumnCount();
}
}
else if (dataset instanceof DataTupleXYDataset) {
return ((DataTupleXYDataset) dataset).getSeriesCount();
}
else if (dataset instanceof DataTupleHistogramDataset) {
return ((DataTupleHistogramDataset) dataset).getSeriesCount();
}
else {
return 0;
}
}
@Override
public String[] getAllColumns() {
return COLUMN_NAMES;
}
@Override
public boolean getDefaultColumnState(int column) {
return defaultColumns[column];
}
@Override
public boolean canDisableColumn(int column) {
// all columns except visibility
return column != 0;
}
@Override
public boolean isCellEditable(int row, int column) {
return column == 0 && enabledColumns.get(0);
}
@Override
protected Class<?> getEnabledColumnClass(int columnIndex) {
if (columnIndex == 0) {
return Boolean.class;
}
else if (columnIndex < 5) {
return String.class;
}
else if (columnIndex == 14) {
// count
return Integer.class;
}
else if (columnIndex == 23) {
// graph count
return Integer.class;
}
else {
return Double.class;
}
}
@Override
protected String getEnabledColumnName(int column) {
return COLUMN_NAMES[column];
}
@Override
protected Object getEnabledValueAt(int row, int column) {
DataTuple tuple = null;
String seriesName = "";
boolean graphDataOnly = false;
if (dataset instanceof DataTupleCategoryDataset) {
DataTupleCategoryDataset d = (DataTupleCategoryDataset) dataset;
int columnCount = d.containsIntervals() ? d.getRowCount() : d.getColumnCount();
int datasetRow = row / columnCount;
int datasetColumn = row % columnCount;
tuple = d.getTuple(datasetRow, datasetColumn);
if (d.containsIntervals()) {
seriesName = d.getRowKey(datasetColumn).toString();
}
else {
if (d.categoriesHaveDifferentStats()) {
seriesName = d.getColumnKey(datasetColumn).toString();
}
else {
seriesName = d.getColumnKey(datasetColumn).toString() + " - " + d.getRowKey(datasetRow).toString();
}
}
graphDataOnly = d.containsIntervals();
}
else if (dataset instanceof DataTupleXYDataset) {
DataTupleXYDataset d = (DataTupleXYDataset) dataset;
tuple = d.getTuple(row, -1);
seriesName = d.getSeriesKey(row).toString();
}
else if (dataset instanceof DataTupleHistogramDataset) {
DataTupleHistogramDataset d = (DataTupleHistogramDataset) dataset;
tuple = d.getTuple(row, -1);
seriesName = d.getSeriesKey(row).toString();
}
AnalysisRecord analysis = null;
// will happen with bar charts that are not 'symmetric', i.e. all DataTypes are not
// available for all DataSets
if (tuple == null) {
tuple = NULL_TUPLE;
analysis = NULL_ANALYSIS;
}
else {
if (!graphDataOnly) {
analysis = gui.getAnalysis(tuple.getDataSet());
}
}
switch (column) {
case 0:
return rowVisible[row];
case 1:
return tuple.getDataSet().getHostname();
case 2:
return tuple.getDataType().toString();
case 3:
return tuple.getField();
case 4:
return seriesName;
case 5:
return graphDataOnly ? dataset.getMinimum(row) : analysis.getMinimum(tuple.getDataType(), tuple.getField());
case 6:
return graphDataOnly ? dataset.getAverage(row) : analysis.getAverage(tuple.getDataType(), tuple.getField());
case 7:
return graphDataOnly ? dataset.getMaximum(row) : analysis.getMaximum(tuple.getDataType(), tuple.getField());
case 8:
return graphDataOnly ? dataset.getStandardDeviation(row) : analysis.getStandardDeviation(
tuple.getDataType(), tuple.getField());
case 9:
return graphDataOnly ? Double.NaN : analysis.getGranularityMaximum(tuple.getDataType(), tuple.getField());
case 10:
return graphDataOnly ? dataset.getMedian(row) : analysis.getMedian(tuple.getDataType(), tuple.getField());
case 11:
return graphDataOnly ? dataset.get95thPercentile(row) : analysis.get95thPercentile(tuple.getDataType(),
tuple.getField());
case 12:
return graphDataOnly ? dataset.get99thPercentile(row) : analysis.get99thPercentile(tuple.getDataType(),
tuple.getField());
case 13:
return graphDataOnly ? dataset.getSum(row) : analysis.getSum(tuple.getDataType(), tuple.getField());
case 14:
return graphDataOnly ? dataset.getCount(row) : analysis.getCount(tuple.getDataType(), tuple.getField());
case 15:
return dataset.getMinimum(row);
case 16:
return dataset.getAverage(row);
case 17:
return dataset.getMaximum(row);
case 18:
return dataset.getStandardDeviation(row);
case 19:
return dataset.getMedian(row);
case 20:
return dataset.get95thPercentile(row);
case 21:
return dataset.get99thPercentile(row);
case 22:
return dataset.getSum(row);
case 23:
return dataset.getCount(row);
default:
throw new ArrayIndexOutOfBoundsException(column);
}
}
@Override
public void setValueAt(Object value, int rowIndex, int columnIndex) {
// visibility is the only editable field
boolean visible = (Boolean) value;
if (rowVisible[rowIndex] != visible) {
rowVisible[rowIndex] = visible;
Object[] values = new Object[3];
values[0] = getDatasetRow(rowIndex);
values[1] = getDatasetColumn(columnIndex);
values[2] = visible;
propertyChangeSupport.firePropertyChange("rowVisible", null, values);
}
}
// the table may represent a dataset that has both rows and columns of data
// these functions convert table rows to dataset rows and columns
//
// row column table row
// 0 0 0
// 0 1 1
// 1 0 2
// 1 1 4
//
// for single column datasets, the row mapping is 1:1
int getDatasetRow(int tableRow) {
if (dataset instanceof DataTupleCategoryDataset) {
DataTupleCategoryDataset d = (DataTupleCategoryDataset) dataset;
if (d.containsIntervals()) {
return tableRow;
}
else {
int columnCount = d.getColumnCount();
return tableRow / columnCount;
}
}
else {
return tableRow;
}
}
int getDatasetColumn(int tableRow) {
if (dataset instanceof DataTupleCategoryDataset) {
DataTupleCategoryDataset d = (DataTupleCategoryDataset) dataset;
if (d.containsIntervals()) {
return -1;
}
else {
int columnCount = d.getColumnCount();
if (columnCount == 0) {
return -1;
}
else {
return tableRow % columnCount;
}
}
}
else {
return -1;
}
}
int getTableRow(int datasetRow, int datasetColumn) {
if (dataset instanceof DataTupleCategoryDataset) {
DataTupleCategoryDataset d = (DataTupleCategoryDataset) dataset;
if (d.containsIntervals()) {
return datasetRow;
}
else {
int columnCount = d.getColumnCount();
return datasetRow * columnCount + datasetColumn;
}
}
else {
return datasetRow;
}
}
void setData(DataTupleDataset dataset) {
if (this.dataset == dataset) {
return;
}
this.dataset = dataset;
if (dataset == null) {
fireTableDataChanged();
return;
}
int rowCount = getRowCount();
// do not use DataTuple.hashCode() since we want the tuple's DataSet to be excluded
// charts with the same types and fields should 'remember' the same row visibility
int hashCode = 1;
for (DataTuple t : dataset.getAllTuples()) {
int typeHash = t.getDataType().hashCode();
if (t.getDataType().getClass() == ProcessDataType.class) {
// default process hash code uses process id
com.ibm.nmon.data.Process process = ((ProcessDataType) t.getDataType()).getProcess();
// all processes of the same name should display the same rows
// but let the aggregated process be separate
if (process.getId() != -1) {
typeHash = process.getName().hashCode();
}
}
hashCode = (hashCode * 11) + (typeHash * 31) + (t.getField().hashCode() * 57);
}
rowVisible = rowVisibleCache.get(hashCode);
if (rowVisible == null) {
rowVisible = new boolean[rowCount];
java.util.Arrays.fill(rowVisible, true);
rowVisibleCache.put(hashCode, rowVisible);
}
if (dataset instanceof DataTupleCategoryDataset) {
DataTupleCategoryDataset d = ((DataTupleCategoryDataset) dataset);
if (!d.containsIntervals()) {
// cannot change bar visibility
setEnabled(VISIBLE, false);
}
else {
setEnabled(VISIBLE, true);
}
}
else if (dataset instanceof DataTupleXYDataset) {
DataTupleXYDataset d = (DataTupleXYDataset) dataset;
// cannot changed stacked charts
if (d.isStacked()) {
setEnabled(VISIBLE, false);
}
else {
// cannot change visibility if only 1 line
if (d.getSeriesCount() > 1) {
setEnabled(VISIBLE, true);
}
else {
setEnabled(VISIBLE, false);
}
}
}
else if (dataset instanceof DataTupleHistogramDataset) {
DataTupleHistogramDataset d = (DataTupleHistogramDataset) dataset;
// cannot change visibility if only 1 line
if (d.getSeriesCount() > 1) {
setEnabled(VISIBLE, true);
}
else {
setEnabled(VISIBLE, false);
}
}
if (logger.isTraceEnabled()) {
if (dataset instanceof DataTupleXYDataset) {
DataTupleXYDataset d = ((DataTupleXYDataset) dataset);
List<String> series = new java.util.ArrayList<String>();
for (int i = 0; i < d.getSeriesCount(); i++) {
series.add(d.getSeriesKey(i).toString());
}
logger.trace("set data for XY chart with {}", series);
}
else if (dataset instanceof DataTupleXYDataset) {
DataTupleCategoryDataset d = (DataTupleCategoryDataset) dataset;
Map<String, List<String>> series = new java.util.HashMap<String, List<String>>();
for (int i = 0; i < d.getColumnCount(); i++) {
List<String> categories = new java.util.ArrayList<String>(3);
series.put(d.getColumnKey(i).toString(), categories);
for (int j = 0; j < d.getRowCount(); j++) {
categories.add(d.getRowKey(j).toString());
}
}
logger.trace("set data for category chart with {}", series);
}
else if (dataset instanceof DataTupleHistogramDataset) {
DataTupleHistogramDataset d = ((DataTupleHistogramDataset) dataset);
List<String> series = new java.util.ArrayList<String>();
for (int i = 0; i < d.getSeriesCount(); i++) {
series.add(d.getSeriesKey(i).toString());
}
logger.trace("set data for histogram chart with {}", series);
}
}
fireTableDataChanged();
Object[] values = new Object[3];
values[1] = getDatasetColumn(0);
for (int i = 0; i < rowCount; i++) {
values[0] = getDatasetRow(i);
values[2] = rowVisible[i];
propertyChangeSupport.firePropertyChange("rowVisible", null, values);
}
}
void clear() {
if (dataset != null) {
// note that rowVisibleCache has already been added to visibleRowCache in setData()
// no need to copy and store here
dataset = null;
fireTableDataChanged();
}
}
void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(listener);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("granularity".equals(evt.getPropertyName())) {
updateGranularityMax();
}
}
void updateGranularityMax() {
COLUMN_NAMES[9] = GRANULARITY_MAXIMUM.getName(gui.getGranularity());
buildColumnNameMap();
if (enabledColumns.get(9)) {
// update granularity max column name
fireTableStructureChanged();
}
}
}