Monday, August 4, 2008

Using Orson Chart Beans with JConsole Custom Plugin

In this blog entry, I will be looking at using Orson Chart Beans to generate charts in a custom tab added to JConsole. Orson Charts (also sometimes called JFreeBeans in some of the documentation) are backed by JFreeChart and present an easier approach to including JFreeChart-generated charts in Swing applications than using JFreeChart directly.

I could have generated the charts shown in this blog entry's custom JConsole tab using JFreeChart directly (see Example 3/Listing 4 in the article Visualize Your Oracle Database Data with JFreeChart article for an example of using JFreeChart directly in Swing). However, I am using Orson Charts so that I can demonstrate their use and so that more emphasis can stay on implementing the JConsole custom tab.

Java SE 6 provides new features (service provider lookup and SwingWorker) that enable custom tabs in JConsole. The Sun Java SE 6 JDK distribution includes an example custom tab implementation called JTop that demonstrates how to take advantage of the service provider lookup and SwingWorker to create custom tabs for JConsole.

For my example, I'm not dynamically changing the data (for simplicity) and so I don't need to implement as many methods as does the JTop example. Instead, I am building the Orson charts with static, hard-coded data to simplify the examples. However, in a real implementation, one would likely use dynamic data gathered from various sources to build the charts. Data might be retrieved related to databases, networks, file systems, etc., to complement the JVM data already made available in JConsole.

The next class, OrsonBasedJConsolePlugin, shows the simple code needed to create a JConsole Plugin. Most of the interesting functionality is actually in a class this class references.

OrsonBasedJConsolePlugin

package dustin.jconsole.orson;

import com.sun.tools.jconsole.JConsolePlugin;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.swing.JPanel;
import javax.swing.SwingWorker;

/**
* This is a simplistic example of extending JConsole with a custom plugin
* that takes advantage of the Orson (JFreeBeans).
*/
public class OrsonBasedJConsolePlugin
extends JConsolePlugin implements PropertyChangeListener
{
private OrsonPanel orsonPanel = null;
private Map<String, JPanel> customTabs = null;

public OrsonBasedJConsolePlugin()
{
addContextPropertyChangeListener(this);
}

@Override
public Map<String, JPanel> getTabs()
{
if ( customTabs == null )
{
// Using LinkedHashMap because it is a Map implementation that
// maintains its predictable order (usually the order of insertion
// into the LinkedHashMap. Using the LinkedHashMap ensures that the
// new custom tabs appear in the desired order in JConsole.
customTabs = new LinkedHashMap();
orsonPanel = new OrsonPanel();
}

customTabs.put("Orson-Based Chart", new OrsonPanel());
return customTabs;
}

@Override
public SwingWorker<?, ?> newSwingWorker()
{
return orsonPanel.newSwingWorker();
}

@Override
public void propertyChange(PropertyChangeEvent evt)
{
// Implement behavior upon property change.
}
}


The next class is the OrsonPanel class that actually does most of the work related to Orson Charts generation. This panel was created in NetBeans 6.1 GUI builder and some of the "fold" comments indicate this. See the Java SE 6-provided JTop demonstration for a more complete demonstration of implementation of a SwingWorker. The image before the code listing shows how it appears in NetBeans. Note that the generic images for Orson bar chart and Orson pie chart help see what will go where on the final HMI.



OrsonPanel

package dustin.jconsole.orson;

//import java.util.concurrent.ExecutionException;
import javax.swing.SwingWorker;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;

/**
* Simple JPanel using a chart generated JFreeChart-based Orson library.
*/
public class OrsonPanel extends javax.swing.JPanel
{
/** Creates new form OrsonPanel */
public OrsonPanel()
{
initComponents();
populatePieChart();
populateUsersMachinesBarChart();
}

/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {

orsonUsersMachineBarChartPanel = new javax.swing.JLabel();
orsonBarChart = new org.jfree.beans.JBarChart();
orsonPieChart = new org.jfree.beans.JPieChart();

orsonUsersMachineBarChartPanel.setText("Displaying Charts in JConsole with JFreeChart-based Orson is Easy!");

javax.swing.GroupLayout orsonBarChartLayout = new javax.swing.GroupLayout(orsonBarChart);
orsonBarChart.setLayout(orsonBarChartLayout);
orsonBarChartLayout.setHorizontalGroup(
orsonBarChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 360, Short.MAX_VALUE)
);
orsonBarChartLayout.setVerticalGroup(
orsonBarChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 230, Short.MAX_VALUE)
);

javax.swing.GroupLayout orsonPieChartLayout = new javax.swing.GroupLayout(orsonPieChart);
orsonPieChart.setLayout(orsonPieChartLayout);
orsonPieChartLayout.setHorizontalGroup(
orsonPieChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 360, Short.MAX_VALUE)
);
orsonPieChartLayout.setVerticalGroup(
orsonPieChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 230, Short.MAX_VALUE)
);

javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(orsonBarChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(orsonPieChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(orsonUsersMachineBarChartPanel)
.addGap(197, 197, 197))))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(orsonUsersMachineBarChartPanel)
.addGap(18, 18, 18)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(orsonBarChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(orsonPieChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addContainerGap(27, Short.MAX_VALUE))
);
}// </editor-fold>


/**
* Populate Orson/JFreeChart-based pie chart.
*/
private void populatePieChart()
{
final DefaultPieDataset data = new DefaultPieDataset();
data.setValue("Opened", 23);
data.setValue("Closed", 10);
this.orsonPieChart.setDataset(data);
this.orsonPieChart.setTitle("Database Connections");
this.orsonPieChart.setSubtitle("Opened and Closed Database Connections");
}

/**
* Populate Orson/JFreeChart-based bar chart that displays numbers of types
* of users on each machine.
*/
private void populateUsersMachinesBarChart()
{
final DefaultCategoryDataset data = new DefaultCategoryDataset();
final String PRIVILEGED_USERS = "Privileged Users";
final String REGULAR_USERS = "Regular Users";
data.setValue(7, PRIVILEGED_USERS, "host1" );
data.setValue(5, PRIVILEGED_USERS, "host2" );
data.setValue(50, REGULAR_USERS, "host1" );
data.setValue(38, REGULAR_USERS, "host2");
this.orsonBarChart.setDataset(data);
this.orsonBarChart.setTitle("Types of Users Using Machines");
this.orsonBarChart.setSubtitle("Privileged and Regular Users");
this.orsonBarChart.setCategoryAxisLabel("Machine Hosts");
this.orsonBarChart.setValueAxisLabel("Number of Users");
}


/** SwingWorker used exclusively for JConsolePlugin. */
class OrsonWorker extends SwingWorker
{
OrsonWorker() {}

public CategoryDataset doInBackground()
{
final DefaultCategoryDataset data = new DefaultCategoryDataset();
return data;
}

@Override
protected void done()
{
}
}

// Return a new SwingWorker for UI update
public SwingWorker<?,?> newSwingWorker()
{
return new OrsonWorker();
}


// Variables declaration - do not modify
private org.jfree.beans.JBarChart orsonBarChart;
private org.jfree.beans.JPieChart orsonPieChart;
private javax.swing.JLabel orsonUsersMachineBarChartPanel;
// End of variables declaration
}


To use the service provider mechanism to allow JConsole to detect the custom tab implemented above, a special file must be included in the JAR with the two above classes. This special file must have the name >com.sun.tools.jconsole.JConsolePlugin and must contain the fully qualified name of the plugin class. In other words, for our example with only a single plugin, it should contain the following line:

com.sun.tools.jconsole.JConsolePlugin

dustin.jconsole.orson.OrsonBasedJConsolePlugin


A single JAR with the compiled versions of the above classes and the just-mentioned file should be built. This will be provided to JConsole when it is run. The JAR contents look like this in my example:

Contents of JConsoleWithOrson.jar

0 Sun Aug 03 21:39:48 MDT 2008 META-INF/
162 Sun Aug 03 21:39:46 MDT 2008 META-INF/MANIFEST.MF
0 Sun Aug 03 21:17:56 MDT 2008 META-INF/services/
0 Sun Aug 03 21:17:14 MDT 2008 dustin/
0 Sun Aug 03 21:17:14 MDT 2008 dustin/jconsole/
0 Sun Aug 03 21:17:14 MDT 2008 dustin/jconsole/orson/
46 Sun Aug 03 21:17:56 MDT 2008 META-INF/services/com.sun.tools.jconsole.JConsolePlugin
1458 Sun Aug 03 21:17:14 MDT 2008 dustin/jconsole/orson/OrsonBasedJConsolePlugin.class
1095 Sun Aug 03 21:39:48 MDT 2008 dustin/jconsole/orson/OrsonPanel$OrsonWorker.class
4863 Sun Aug 03 21:39:48 MDT 2008 dustin/jconsole/orson/OrsonPanel.class


While JConsole can normally be run by simply typing "jconsole" at the command-line, we now need to provide this JAR file, JConsoleWithOrson.jar, to the "jconsole" command. In addition, we also need to include Orson and JFreeChart libraries used to JConsole as well to avoid exceptions related to not finding these classes. The run command to run JConsole with this custom plugin JAR and associated Orson and JFreeChart libraries is:


jconsole -pluginpath JConsoleWithOrson.jar -J-Djava.class.path="C:\Program Files\Java\jdk1.6.0_07\lib\jconsole.jar";"C:\Program Files\Java\jdk1.6.0_07\lib\tools.jar";C:\orson-0.5.0\orson-0.5.0.jar;C:\orson-0.5.0\lib\jfreechart-1.0.6.jar;C:\orson-0.5.0\lib\jcommon-1.0.10.jar


You can see from the command for running JConsole above that I am using the latest version of Orson Charts (0.5). I am also using the version of JFreeChart and JCommon that come with the Orson distribution. There are newer versions of JFreeChart (1.0.10) available, but I wanted to use the version that came with the Orson download. Note also that if you did use JDBC database connections in a real situation, you'd need JDBC drivers and other database accessing classes as well.

When the JAR is compiled as described above and run as shown above, a new tab appears on JConsole as shown here:



Even with all the detail I tried to include in this blog entry, I hope that it is evident how easy it is to use Orson to add charts to your custom JConsole plug-in tabs.

No comments: