package org.jboss.cache.demo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Cache;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.CacheStatus;
import static org.jboss.cache.CacheStatus.*;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.notifications.annotation.BuddyGroupChanged;
import org.jboss.cache.notifications.annotation.NodeCreated;
import org.jboss.cache.notifications.annotation.NodeModified;
import org.jboss.cache.notifications.annotation.NodeMoved;
import org.jboss.cache.notifications.annotation.NodeRemoved;
import org.jboss.cache.notifications.annotation.ViewChanged;
import org.jboss.cache.notifications.event.BuddyGroupChangedEvent;
import org.jboss.cache.notifications.event.NodeCreatedEvent;
import org.jboss.cache.notifications.event.NodeModifiedEvent;
import org.jboss.cache.notifications.event.NodeMovedEvent;
import org.jboss.cache.notifications.event.NodeRemovedEvent;
import org.jboss.cache.notifications.event.ViewChangedEvent;
import org.jboss.cache.util.CachePrinter;
import org.jgroups.Address;
import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.tree.TreePath;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
*/
public class JBossCacheDemo
{
private static Log log = LogFactory.getLog(JBossCacheDemo.class);
private static JFrame frame;
private JTabbedPane mainPane;
private JPanel panel1;
private JLabel cacheStatus;
private JPanel dataGeneratorTab;
private JPanel statisticsTab;
private JPanel clusterViewTab;
private JPanel dataViewTab;
private JPanel controlPanelTab;
private JTree dataTree;
private JTable clusterTable;
private JButton actionButton;
private JLabel configFileName;
private JProgressBar cacheStatusProgressBar;
private JTextField fqnTextField;
private JTextField keyTextField;
private JTextField valueTextField;
private JRadioButton createNodeRadioButton;
private JRadioButton removeNodeRadioButton;
private JRadioButton addKeyRadioButton;
private JRadioButton removeKeyRadioButton;
private JButton goButton;
private JTable nodeDataTable;
private JScrollPane nodeDataScrollPane;
private JButton randomGeneratorButton;
private JTextField maxNodesTextField;
private JTextField maxDepthTextField;
private JTextField numberOfKeysPerTextField;
private JButton cacheClearButton;
private JButton updateStatsButton;
private JLabel statsNumberOfNodes;
private JLabel statsNumberOfKeys;
private JLabel statsSizeOfCachedData;
private JLabel statsLastUpdated;
private JTextArea configFileContents;
private JRadioButton getNodeRadioButton;
private JScrollPane treeScrollPane;
private JPanel debugTab;
private JButton cacheDetailsButton;
private JButton cacheLockInfoButton;
private JTextArea debugTextArea;
private String cacheConfigFile;
private Cache<String, String> cache;
private String startCacheButtonLabel = "Start Cache", stopCacheButtonLabel = "Stop Cache";
private String statusStarting = "Starting Cache ... ", statusStarted = "Cache Running.", statusStopping = "Stopping Cache ...", statusStopped = "Cache Stopped.";
private ExecutorService asyncExecutor;
private BlockingQueue<Runnable> asyncTaskQueue;
private ClusterTableModel clusterDataModel;
private NodeDataTableModel nodeDataTableModel;
private DataTreeRefresher treeRefresher;
private Random r = new Random();
private boolean isUsingBuddyReplication;
public static void main(String[] args)
{
String cfgFileName = "demo-cache-config.xml";
if (args.length == 1 && args[0] != null && args[0].toLowerCase().endsWith(".xml"))
{
// the first arg is the name of the config file.
cfgFileName = args[0];
}
frame = new JFrame("JBoss Cache GUI Demo (STOPPED)");
frame.setContentPane(new JBossCacheDemo(cfgFileName).panel1);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
frame.setResizable(true);
}
public JBossCacheDemo(String cfgFileName)
{
asyncExecutor = Executors.newFixedThreadPool(1);
asyncTaskQueue = ((ThreadPoolExecutor) asyncExecutor).getQueue();
cacheConfigFile = cfgFileName;
cacheStatusProgressBar.setVisible(false);
cacheStatusProgressBar.setEnabled(false);
configFileName.setText(cacheConfigFile);
// default state of the action button should be unstarted.
actionButton.setText(startCacheButtonLabel);
cacheStatus.setText(statusStopped);
clusterDataModel = new ClusterTableModel();
clusterTable.setModel(clusterDataModel);
nodeDataTableModel = new NodeDataTableModel();
nodeDataTable.setModel(nodeDataTableModel);
// when we start up scan the classpath for a file named
actionButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
if (actionButton.getText().equals(startCacheButtonLabel))
{
// start cache
startCache();
}
else if (actionButton.getText().equals(stopCacheButtonLabel))
{
// stop cache
stopCache();
}
}
});
goButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
processAction(goButton, true);
// do this in a separate thread
asyncExecutor.execute(new Runnable()
{
public void run()
{
// based on the value of the radio button:
if (createNodeRadioButton.isSelected())
{
cache.put(fqnTextField.getText(), keyTextField.getText(), valueTextField.getText());
}
else if (removeNodeRadioButton.isSelected())
{
cache.removeNode(fqnTextField.getText());
}
else if (addKeyRadioButton.isSelected())
{
cache.put(fqnTextField.getText(), keyTextField.getText(), valueTextField.getText());
}
else if (removeKeyRadioButton.isSelected())
{
cache.remove(fqnTextField.getText(), keyTextField.getText());
}
else if (getNodeRadioButton.isSelected())
{
// do a cache.get on the Fqn first, this may involve a data gravitation
// only do this if BR is enabled as it may involve a data gravitation event
if (isUsingBuddyReplication)
cache.getInvocationContext().getOptionOverrides().setForceDataGravitation(true);
cache.getNode(fqnTextField.getText());
}
treeRefresher.repaint();
dataViewTab.repaint();
processAction(goButton, false);
// now switch to the data pane
mainPane.setSelectedIndex(1);
}
});
}
});
removeNodeRadioButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
fqnTextField.setEnabled(true);
keyTextField.setEnabled(false);
valueTextField.setEnabled(false);
}
});
removeKeyRadioButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
fqnTextField.setEnabled(true);
keyTextField.setEnabled(true);
valueTextField.setEnabled(false);
}
});
createNodeRadioButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
fqnTextField.setEnabled(true);
keyTextField.setEnabled(true);
valueTextField.setEnabled(true);
}
});
addKeyRadioButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
fqnTextField.setEnabled(true);
keyTextField.setEnabled(true);
valueTextField.setEnabled(true);
}
});
getNodeRadioButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
fqnTextField.setEnabled(true);
keyTextField.setEnabled(false);
valueTextField.setEnabled(false);
}
});
dataTree.addTreeSelectionListener(new TreeSelectionListener()
{
public void valueChanged(TreeSelectionEvent e)
{
TreePath path = e.getPath();
DataTreeRefresher.FqnTreeNode node = (DataTreeRefresher.FqnTreeNode) path.getLastPathComponent();
Fqn f = node.getFqn();
if (!f.equals(nodeDataTableModel.getCurrentFqn()))
{
nodeDataTableModel.setCurrentFqn(f);
Node n = cache.getNode(f);
if (n != null) nodeDataTableModel.setData(n.getData());
}
}
});
randomGeneratorButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
processAction(randomGeneratorButton, true);
// process this asynchronously
asyncExecutor.execute(new Runnable()
{
public void run()
{
int depth = 1;
try
{
depth = Integer.parseInt(maxDepthTextField.getText());
}
catch (NumberFormatException nfe)
{
log.warn("Entered a non-integer for depth. Using 1.", nfe);
}
int maxNodes = 1;
try
{
maxNodes = Integer.parseInt(maxNodesTextField.getText());
}
catch (NumberFormatException nfe)
{
log.warn("Entered a non-integer for max nodes. Using 1.", nfe);
}
int attribsPerNode = 1;
try
{
attribsPerNode = Integer.parseInt(numberOfKeysPerTextField.getText());
}
catch (NumberFormatException nfe)
{
log.warn("Entered a non-integer for keys per node. Using 1.", nfe);
}
Set<Fqn> fqns = new HashSet<Fqn>();
for (int i = 0; i < maxNodes; i++)
{
Fqn fqn = createRandomFqn(depth);
while (fqns.contains(fqn)) fqn = createRandomFqn(depth);
fqns.add(fqn);
}
for (Fqn f : fqns)
{
Map m = new HashMap();
for (int i = 0; i < attribsPerNode; i++) m.put(randomString(), randomString());
cache.put(f, m);
}
processAction(randomGeneratorButton, false);
// now switch to the data pane
mainPane.setSelectedIndex(1);
}
});
}
private Fqn createRandomFqn(int depth)
{
String s = "/";
for (int i = 0; i < r.nextInt(depth); i++)
{
s += randomString() + "/";
}
return Fqn.fromString(s);
}
private String randomString()
{
return Integer.toHexString(r.nextInt(Integer.MAX_VALUE)).toUpperCase();
}
});
cacheClearButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
processAction(cacheClearButton, true);
asyncExecutor.execute(new Runnable()
{
public void run()
{
cache.removeNode(Fqn.ROOT);
cache.getRoot().clearData();
processAction(cacheClearButton, false);
// now switch to the data pane
mainPane.setSelectedIndex(1);
}
});
}
});
updateStatsButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
processAction(updateStatsButton, true);
asyncExecutor.execute(new Runnable()
{
public void run()
{
updateStats();
processAction(updateStatsButton, false);
}
});
}
});
cacheDetailsButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
if (cache != null) debugTextArea.setText(CachePrinter.printCacheDetails(cache));
}
});
cacheLockInfoButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
if (cache != null) debugTextArea.setText(CachePrinter.printCacheLockingInfo(cache));
}
});
}
private void updateStats()
{
int numNodes = ((CacheSPI) cache).getNumberOfNodes();
statsNumberOfNodes.setText(numNodes + " nodes");
Map<String, Long> values = new HashMap<String, Long>();
values.put("NumKeys", 0l);
values.put("Size", 0l);
processRecursively(cache.getRoot(), values);
statsNumberOfKeys.setText(values.get("NumKeys") + " keys");
NumberFormat format = NumberFormat.getIntegerInstance();
statsSizeOfCachedData.setText(format.format(values.get("Size")) + " bytes");
statsLastUpdated.setText(new Date().toString());
}
private void processRecursively(Node<String, String> node, Map<String, Long> values)
{
// process children first
for (Node child : node.getChildren())
{
processRecursively(child, values);
}
// now process current node
Map data = node.getData();
values.put("NumKeys", values.get("NumKeys") + data.size());
values.put("Size", values.get("Size") + calculateSize(data));
}
private int calculateSize(Map<String, String> data)
{
// since all we have are strings:
int size = 0;
for (String key : data.keySet())
{
size += key.length();
size += data.get(key).length();
}
return size;
}
private void moveCacheToState(CacheStatus state)
{
switch (state)
{
case STARTING:
cacheStatus.setText(statusStarting);
processAction(actionButton, true);
break;
case STARTED:
setCacheTabsStatus(true);
actionButton.setText(stopCacheButtonLabel);
processAction(actionButton, false);
cacheStatus.setText(statusStarted);
updateTitleBar();
break;
case STOPPING:
cacheStatus.setText(statusStopping);
processAction(actionButton, true);
break;
case STOPPED:
setCacheTabsStatus(false);
actionButton.setText(startCacheButtonLabel);
processAction(actionButton, false);
cacheStatus.setText(statusStopped);
updateTitleBar();
}
controlPanelTab.repaint();
}
private void processAction(JButton button, boolean start)
{
button.setEnabled(!start);
cacheStatusProgressBar.setVisible(start);
cacheStatusProgressBar.setEnabled(start);
}
private String readContents(InputStream is) throws IOException
{
BufferedReader r = new BufferedReader(new InputStreamReader(is));
String s;
StringBuilder sb = new StringBuilder();
while ((s = r.readLine()) != null)
{
sb.append(s);
sb.append("\n");
}
return sb.toString();
}
private void startCache()
{
moveCacheToState(STARTING);
// actually start the cache asynchronously.
asyncExecutor.execute(new Runnable()
{
public void run()
{
if (cache == null)
{
URL resource = getClass().getClassLoader().getResource(cacheConfigFile);
String contents;
// update config file display
if (resource != null)
{
configFileName.setText(resource.toString());
}
else
{
configFileName.setText(cacheConfigFile);
}
configFileName.repaint();
try
{
configFileContents.setText(readContents(resource == null ? new FileInputStream(cacheConfigFile) : resource.openStream()));
configFileContents.setEditable(false);
}
catch (Exception e)
{
log.warn("Unable to open config file for display", e);
}
configFileContents.repaint();
cache = new DefaultCacheFactory().createCache(cacheConfigFile);
isUsingBuddyReplication = cache.getConfiguration().getBuddyReplicationConfig() != null && cache.getConfiguration().getBuddyReplicationConfig().isEnabled();
}
else
{
cache.start();
}
updateClusterTable(cache.getMembers());
treeRefresher = new DataTreeRefresher(dataTree, cache, nodeDataTableModel);
cache.addCacheListener(new CacheListener());
moveCacheToState(STARTED);
}
});
}
private void stopCache()
{
moveCacheToState(CacheStatus.STOPPING);
// actually stop the cache asynchronously
asyncExecutor.execute(new Runnable()
{
public void run()
{
if (cache != null) cache.stop();
moveCacheToState(STOPPED);
}
});
}
private void setCacheTabsStatus(boolean enabled)
{
int numTabs = mainPane.getTabCount();
for (int i = 1; i < numTabs; i++) mainPane.setEnabledAt(i, enabled);
panel1.repaint();
}
private void updateClusterTable(List<Address> members)
{
log.debug("Updating cluster table with new member list " + members);
clusterDataModel.setMembers(members);
updateTitleBar();
}
private void updateTitleBar()
{
String title = "JBoss Cache GUI Demo";
if (cache != null && cache.getCacheStatus() == STARTED)
{
title += " (STARTED) " + cache.getLocalAddress() + " Cluster size: " + cache.getMembers().size();
}
else
{
title += " (STOPPED)";
}
frame.setTitle(title);
}
@org.jboss.cache.notifications.annotation.CacheListener
public class CacheListener
{
@ViewChanged
public void viewChangeEvent(ViewChangedEvent e)
{
updateClusterTable(e.getNewView().getMembers());
}
@BuddyGroupChanged
public void buddyGroupChanged(BuddyGroupChangedEvent e)
{
clusterDataModel.setBuddies();
treeRefresher.repaint();
}
@NodeModified
public void nodeModified(NodeModifiedEvent e)
{
if (!e.isPre())
{
// only if this is the current node selected in the tree do we bother refreshing it
if (nodeDataTableModel.getCurrentFqn() != null && nodeDataTableModel.getCurrentFqn().equals(e.getFqn()))
{
nodeDataTableModel.updateCurrentNode();
}
}
}
@NodeCreated
public void nodeCreated(NodeCreatedEvent e)
{
if (!e.isPre())
{
final Fqn fqn = e.getFqn();
asyncExecutor.execute(new Runnable()
{
public void run()
{
treeRefresher.addNode(fqn);
// only refresh if there are no more tasks queued up
if (asyncTaskQueue.isEmpty()) treeRefresher.repaint();
}
});
}
}
@NodeMoved
public void nodeMoved(NodeMovedEvent e)
{
if (!e.isPre())
{
final Fqn fqn = e.getTargetFqn();
asyncExecutor.execute(new Runnable()
{
public void run()
{
// get all kids and add to the tree as well.
recursivelyAddNode(((CacheSPI) cache).peek(fqn, false));
// only refresh if there are no more tasks queued up
if (asyncTaskQueue.isEmpty()) treeRefresher.repaint();
}
private void recursivelyAddNode(NodeSPI<String, String> n)
{
treeRefresher.addNode(n.getFqn());
for (NodeSPI<String, String> child : n.getChildrenDirect())
recursivelyAddNode(child);
}
});
}
}
@NodeRemoved
public void nodeRemoved(NodeRemovedEvent e)
{
if (!e.isPre())
{
final Fqn fqn = e.getFqn();
asyncExecutor.execute(new Runnable()
{
public void run()
{
treeRefresher.removeNode(fqn);
// only refresh if there are no more tasks queued up
if (asyncTaskQueue.isEmpty()) treeRefresher.repaint();
}
});
}
}
// dont bother with node modified events since the tree GUI widget will refresh each node when it is selected.
}
public class ClusterTableModel extends AbstractTableModel
{
List<Address> members = new ArrayList<Address>();
List<String> memberStates = new ArrayList<String>();
public void setMembers(List<Address> members)
{
if (this.members != members)
{
this.members.clear();
this.members.addAll(members);
}
List<Address> buddies = Collections.emptyList();
if (isUsingBuddyReplication)
{
buddies = ((CacheSPI) cache).getBuddyManager().getBuddyAddresses();
log.debug("Buddy addresses: " + buddies);
}
memberStates = new ArrayList<String>(members.size());
for (Address a : members)
{
String extraInfo = "Member";
// if this is the first member then this is the coordinator
if (memberStates.isEmpty()) extraInfo += " (coord)";
if (a.equals(cache.getLocalAddress()))
extraInfo += " (me)";
else if (buddies.contains(a))
extraInfo += " (buddy)";
memberStates.add(extraInfo);
}
fireTableDataChanged();
}
public void setBuddies()
{
setMembers(members);
}
public int getRowCount()
{
return members.size();
}
public int getColumnCount()
{
return 2;
}
public Object getValueAt(int rowIndex, int columnIndex)
{
switch (columnIndex)
{
case 0:
return members.get(rowIndex);
case 1:
return memberStates.get(rowIndex);
}
return "NULL!";
}
public String getColumnName(int c)
{
if (c == 0) return "Member Address";
if (c == 1) return "Member Info";
return "NULL!";
}
}
public class NodeDataTableModel extends AbstractTableModel
{
String[] keys = {};
String[] values = {};
private Fqn currentFqn;
public void setData(Map<String, String> data)
{
keys = new String[data.size()];
values = new String[data.size()];
int i = 0;
for (String key : data.keySet())
{
keys[i] = key;
values[i] = data.get(key);
i++;
}
fireTableDataChanged();
}
public int getRowCount()
{
return keys.length;
}
public int getColumnCount()
{
return 2;
}
public Object getValueAt(int rowIndex, int columnIndex)
{
switch (columnIndex)
{
case 0:
return keys[rowIndex];
case 1:
return values[rowIndex];
}
return "NULL!";
}
public String getColumnName(int c)
{
if (c == 0) return "Key";
if (c == 1) return "Value";
return "NULL!";
}
public Fqn getCurrentFqn()
{
return currentFqn;
}
public void setCurrentFqn(Fqn currentFqn)
{
this.currentFqn = currentFqn;
}
public void updateCurrentNode()
{
setData(cache.getData(currentFqn));
}
}
}