package com.ibm.nmon.gui.report;
import org.slf4j.Logger;
import java.util.BitSet;
import java.util.List;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import org.jfree.chart.JFreeChart;
import com.ibm.nmon.analysis.Statistic;
import com.ibm.nmon.chart.definition.*;
import com.ibm.nmon.data.DataSet;
import com.ibm.nmon.data.definition.DataDefinition;
import com.ibm.nmon.gui.Styles;
import com.ibm.nmon.gui.chart.*;
import com.ibm.nmon.gui.chart.builder.ChartBuilderPlugin;
import com.ibm.nmon.gui.main.NMONVisualizerGui;
import com.ibm.nmon.gui.util.ItemProgressDialog;
import com.ibm.nmon.interval.IntervalListener;
import com.ibm.nmon.interval.Interval;
/**
* <p>
* JTabbed pane for displaying a set of related charts. Charts are defined using
* {@link BaseChartDefinition}. Each chart will appear on a tab named by
* {@link BaseChartDefinition#getShortName() getShortName()}.
* </p>
*
* <p>
* This class listens for {@link IntervalListener interval} events as well as time zone and
* granularity changes. New {@link DataSet data} can be added to or removed from reports at run time
* (if the given chart definitions should display values from more than one data set).
* </p>
*
* <p>
* This class also ensures that tabs will only be created for charts that are supported by the given
* data sets. This allows multiple chart definitions with the same short name to be passed in if
* they match different data sets (i.e. different hostnames or operating systems).
* </p>
*/
public final class ReportPanel extends JTabbedPane implements PropertyChangeListener, IntervalListener {
private static final long serialVersionUID = 6377401207979477789L;
private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(ReportPanel.class);
public static enum MultiplexMode {
NONE, BY_TYPE, BY_FIELD
};
private final NMONVisualizerGui gui;
private final JFrame parent;
private final List<DataSet> dataSets;
private final String reportCacheKey;
private MultiplexMode multiplexMode;
private List<BaseChartDefinition> chartsInUse;
private BitSet chartNeedsUpdate;
private final ChartFactory chartFactory;
// ignore chart updates when tabs are being built
private boolean buildingTabs;
private int previousTab = -1;
public ReportPanel(NMONVisualizerGui gui, String reportCacheKey, DataSet data) {
this(gui, gui.getMainFrame(), reportCacheKey, java.util.Collections.singletonList(data), MultiplexMode.NONE);
}
public ReportPanel(NMONVisualizerGui gui, String reportCacheKey) {
this(gui, gui.getMainFrame(), reportCacheKey, new java.util.ArrayList<DataSet>(), MultiplexMode.NONE);
}
public ReportPanel(NMONVisualizerGui gui, JFrame parent, String reportCacheKey, List<DataSet> dataSets,
MultiplexMode multiplexMode) {
super();
setTabLayoutPolicy(SCROLL_TAB_LAYOUT);
this.chartFactory = new ChartFactory(gui);
this.chartFactory.setGranularity(gui.getGranularity());
this.gui = gui;
this.parent = parent;
this.dataSets = dataSets;
this.reportCacheKey = reportCacheKey;
this.multiplexMode = multiplexMode;
this.chartsInUse = java.util.Collections.emptyList();
buildTabs(gui);
addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
// no need to update the chart if the tabs are still being built
if (!buildingTabs) {
int idx = getSelectedIndex();
if (idx != -1) {
if (!updateChart()) {
// still need to notify listeners that the chart is now showing
firePropertyChange("chart", null, getChartPanel(idx));
}
if ((previousTab != -1) && (previousTab < getTabCount())) {
getChartPanel(previousTab).setEnabled(false);
}
getChartPanel(idx).setEnabled(true);
previousTab = idx;
}
}
}
});
setEnabled(false);
gui.getIntervalManager().addListener(this);
gui.addPropertyChangeListener("granularity", this);
gui.addPropertyChangeListener("timeZone", this);
}
@Override
public void setEnabled(boolean enabled) {
if (enabled != isEnabled()) {
super.setEnabled(enabled);
if ((chartsInUse != null) && !chartsInUse.isEmpty()) {
int idx = getSelectedIndex();
if (enabled) {
if (idx != -1) {
if (!updateChart()) {
// still need to notify listeners that the chart is now showing
firePropertyChange("chart", null, getChartPanel(idx));
}
}
}
else {
if (idx != -1) {
// notify listeners that the chart is not showing
getChartPanel().clearChart();
// ensure chart is recreated when re-enabled
chartNeedsUpdate.set(idx);
// chart panel will already have fired a PropertyChange event
}
}
if (idx != -1) {
getChartPanel(idx).setEnabled(enabled);
previousTab = idx;
}
// leave the listeners enabled
// the heavy work is in updateChart() which checks for enabled too
}
}
}
public MultiplexMode getMultiplexMode() {
return multiplexMode;
}
public void setMultiplexMode(MultiplexMode multiplexMode) {
if (multiplexMode == null) {
multiplexMode = MultiplexMode.NONE;
}
if (this.multiplexMode != multiplexMode) {
this.multiplexMode = multiplexMode;
buildTabs(gui);
resetReport();
}
}
public void setData(Iterable<? extends DataSet> dataSets) {
this.dataSets.clear();
for (DataSet data : dataSets) {
this.dataSets.add(data);
}
java.util.Collections.sort(this.dataSets);
buildTabs(gui);
resetReport();
}
public void addData(DataSet data) {
if (!dataSets.contains(data)) {
dataSets.add(data);
java.util.Collections.sort(dataSets);
buildTabs(gui);
resetReport();
}
}
public void removeData(DataSet data) {
if (dataSets.remove(data)) {
java.util.Collections.sort(dataSets);
buildTabs(gui);
resetReport();
}
}
public void clearData() {
dataSets.clear();
buildTabs(gui);
resetReport();
}
// mark all charts as invalid; update the current one
public void resetReport() {
if (chartNeedsUpdate != null) {
chartNeedsUpdate.set(0, chartNeedsUpdate.size(), true);
}
updateChart();
}
// update the current chart if enabled
// note that clearing / setting the chart fires a property change event
// return false if this did not happen so callers can fire the event regardless
private boolean updateChart() {
if (isEnabled() && (getTabCount() != 0)) {
int index = getSelectedIndex();
if ((index >= 0) && !chartsInUse.isEmpty()) {
BaseChartPanel chartPanel = getChartPanel(index);
if (chartNeedsUpdate.get(index)) {
if (dataSets.isEmpty()) {
chartPanel.clearChart();
}
else {
createChart(index);
}
chartNeedsUpdate.clear(index);
return true;
}
}
}
return false;
}
public BaseChartPanel getChartPanel() {
int index = getSelectedIndex();
if (index == -1) {
return null;
}
else {
return getChartPanel(index);
}
}
private BaseChartPanel getChartPanel(int index) {
return (BaseChartPanel) getComponentAt(index);
}
public int getPreviousTab() {
return previousTab;
}
public void addPlugin(ChartBuilderPlugin plugin) {
chartFactory.addPlugin(plugin);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("chart".equals(evt.getPropertyName())) {
// called by chart panels when the chart changes
// propagate chart events to listeners
firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
else if ("granularity".equals(evt.getPropertyName())) {
int newGranularity = (Integer) evt.getNewValue();
chartFactory.setGranularity(newGranularity);
// always update line charts on granularity changes
// bar charts and interval line charts do not need to be updated unless the stat is
// granularity max
for (int i = 0; i < chartsInUse.size(); i++) {
BaseChartDefinition chartDefinition = chartsInUse.get(i);
if (chartDefinition.getClass().equals(IntervalChartDefinition.class)
|| chartDefinition.getClass().equals(BarChartDefinition.class)) {
for (DataDefinition definition : chartDefinition.getData()) {
if (definition.getStatistic() == Statistic.GRANULARITY_MAXIMUM) {
chartNeedsUpdate.set(i);
break;
}
}
}
else {
chartNeedsUpdate.set(i);
}
updateChart();
}
}
else if ("annotation".equals(evt.getPropertyName())) {
// called by chart panels when annotations / markers are added
// all charts except the current need to be updated to show the new annotation
if (chartNeedsUpdate != null) {
chartNeedsUpdate.set(0, chartNeedsUpdate.size(), true);
chartNeedsUpdate.flip(getSelectedIndex());
}
firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
else if (evt.getPropertyName().startsWith("highlighted")) {
// called by chart panels when an element is highlighted
// propagate chart events to listeners
firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
else if ("timeZone".equals(evt.getPropertyName())) {
// assume bar charts do not need to be updated and linecharts handle this internally
// interval charts may have unnamed intervals that need to be re-displayed
// just recreate the chart
updateIntervalCharts();
}
}
@Override
public void intervalAdded(Interval interval) {
updateIntervalCharts();
}
@Override
public void intervalRemoved(Interval interval) {
updateIntervalCharts();
}
@Override
public void intervalsCleared() {
updateIntervalCharts();
}
@Override
public void currentIntervalChanged(Interval interval) {
chartFactory.setInterval(interval);
// update non-interval charts
for (int i = 0; i < chartsInUse.size(); i++) {
if (!chartsInUse.get(i).getClass().equals(IntervalChartDefinition.class)) {
chartNeedsUpdate.set(i);
}
}
updateChart();
}
@Override
public void intervalRenamed(Interval interval) {
updateIntervalCharts();
}
@Override
public void removeAll() {
if (!chartsInUse.isEmpty()) {
for (int i = 0; i < getTabCount(); i++) {
// buildTabs() adds each chart as a listener, so remove it when the tabs change
BaseChartPanel chartPanel = getChartPanel(i);
chartPanel.setEnabled(false);
chartPanel.clearChart();
chartPanel.removePropertyChangeListener(this);
}
}
super.removeAll();
chartsInUse = java.util.Collections.emptyList();
}
public void dispose() {
gui.getIntervalManager().removeListener(this);
gui.removePropertyChangeListener("granularity", this);
gui.removePropertyChangeListener("timeZone", this);
// clean up references to charts
removeAll();
}
private void updateIntervalCharts() {
// interval charts need to be updated
for (int i = 0; i < chartsInUse.size(); i++) {
if (chartsInUse.get(i).getClass().equals(IntervalChartDefinition.class)) {
chartNeedsUpdate.set(i);
}
}
updateChart();
}
// no need to recalculate granularity on data changes because NMONVisualizerApp recalculates min
// and max system times on data changes, which also changes the interval if it is the default
private void buildTabs(NMONVisualizerGui gui) {
buildingTabs = true;
// note remove all needs to know if charts existed previously, order matters here
removeAll();
if (gui.getReportCache().getReport(reportCacheKey).isEmpty()) {
addTab("No Charts", createNoReportsLabel("No Charts Defined!"));
}
else {
if (dataSets.isEmpty()) {
addTab("No Charts", createNoReportsLabel("No Parsed Data!"));
buildingTabs = false;
return;
}
if (multiplexMode == MultiplexMode.NONE) {
chartsInUse = gui.getReportCache().getReport(reportCacheKey, dataSets);
}
else {
if (dataSets.size() > 1) {
LOGGER.warn("not multiplexing charts when there is more than one dataset");
chartsInUse = gui.getReportCache().getReport(reportCacheKey, dataSets);
}
else if (multiplexMode == MultiplexMode.BY_TYPE) {
chartsInUse = gui.getReportCache()
.multiplexChartsAcrossTypes(reportCacheKey, dataSets.get(0), true);
}
else if (multiplexMode == MultiplexMode.BY_FIELD) {
chartsInUse = gui.getReportCache().multiplexChartsAcrossFields(reportCacheKey, dataSets.get(0),
true);
}
}
if (chartsInUse.isEmpty()) {
addTab("No Charts", createNoReportsLabel("No Charts for Currently Parsed Data!"));
chartsInUse = java.util.Collections.emptyList();
chartNeedsUpdate = null;
}
else {
chartNeedsUpdate = new BitSet(chartsInUse.size());
chartNeedsUpdate.set(0, chartNeedsUpdate.size(), true);
for (BaseChartDefinition report : chartsInUse) {
BaseChartPanel chartPanel = null;
if (report.getClass() == LineChartDefinition.class) {
// handle special case when not scaling CPUs
// need to allow the axis to scale past 100, up to maximum CPU value
if (report.getTitle().equals("CPU Utilization by Process")
&& !gui.getBooleanProperty("scaleProcessesByCPUs")) {
((LineChartDefinition) report).setUsePercentYAxis(false);
}
chartPanel = new LineChartPanel(gui, parent);
}
else if (report.getClass() == IntervalChartDefinition.class) {
chartPanel = new IntervalChartPanel(gui, parent);
}
else if (report.getClass() == BarChartDefinition.class) {
chartPanel = new BarChartPanel(gui, parent);
}
else if (report.getClass() == HistogramChartDefinition.class) {
chartPanel = new LineChartPanel(gui, parent);
}
else {
LOGGER.error("cannot create chart panel for {} ({})", report.getShortName(), report.getClass()
.getSimpleName());
}
// this class will receive each chart's change events and forward them rather
// than expose each chart as a separate listener
chartPanel.addPropertyChangeListener(this);
addTab(report.getShortName(), chartPanel);
}
}
}
if (previousTab > getTabCount()) {
previousTab = -1;
}
buildingTabs = false;
}
private void createChart(int index) {
BaseChartDefinition definition = chartsInUse.get(index);
JFreeChart chart = chartFactory.createChart(definition, dataSets);
// setChart will fire the event that updates the data table
BaseChartPanel chartPanel = getChartPanel(index);
chartPanel.setSaveSize(definition.getWidth(), definition.getHeight());
chartPanel.setChart(chart);
}
public void saveAllCharts(final String directory) {
final ItemProgressDialog progress = new ItemProgressDialog(parent, "Saving Charts...", getTabCount());
if (getTabCount() == 1) {
if (getComponentAt(0) instanceof JLabel) {
return;
}
}
// This code is a mess of things running in and out of the Swing Event Thread mostly to
// allow the progress dialog to be modal. If it was not modal, other issues would arise if
// users could continue to click on the UI while this code is trying to change tabs, etc.
// The current implementation seems to work correctly WRT updating the progress bar and not
// causing any exceptions due to events firing when tabs / charts are changed. Why this is,
// however, is somewhat of a multi-threaded mystery.
Thread saver = new Thread(new Runnable() {
@Override
public void run() {
final int originalTab = getSelectedIndex();
// start modal dialog must not happen directly in the Swing thread or no other code
// will run until it is closed
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
progress.setVisible(true);
}
});
for (int i = 0; i < getTabCount(); i++) {
final String finalName = chartsInUse.get(i).getShortName();
// invokeLater() ensures the name is set before the progress is updated
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
progress.setCurrentItem(finalName);
}
});
final int n = i;
// wait here to ensure the chart actually exists before trying to save it
// save is in the event thread too since the chart object is manipulated when
// saved
// blocking the event thread is OK since there is a modal dialog anyway
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
setSelectedIndex(n);
getChartPanel(n).saveChart(directory, finalName);
}
});
}
catch (Exception e) {
LOGGER.warn("error saving chart " + finalName, e);
continue;
}
// updating the progress does not work correctly when put in the previous
// invokeLater() call
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
progress.updateProgress();
}
});
} // end for
// wait here so that the progress dialog finishes updating before disappearing
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
setSelectedIndex(originalTab);
progress.dispose();
}
});
}
catch (Exception e) {
LOGGER.warn("error closing progress dialog", e);
}
}
});
saver.start();
}
private JLabel createNoReportsLabel(String toDisplay) {
JLabel label = new JLabel(toDisplay);
label.setFont(Styles.LABEL_ERROR.deriveFont(Styles.LABEL_ERROR.getSize() * 1.5f));
label.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
label.setBackground(java.awt.Color.WHITE);
label.setForeground(Styles.ERROR_COLOR);
label.setOpaque(true);
return label;
}
}