/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.demo;

import bsh.EvalError;
import bsh.Interpreter;
import bsh.util.JConsole;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Cache;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.lock.TimeoutException;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.NodeCreated;
import org.jboss.cache.notifications.annotation.NodeEvicted;
import org.jboss.cache.notifications.annotation.NodeLoaded;
import org.jboss.cache.notifications.annotation.NodeModified;
import org.jboss.cache.notifications.annotation.NodeRemoved;
import org.jboss.cache.notifications.annotation.ViewChanged;
import org.jboss.cache.notifications.event.NodeEvent;
import org.jboss.cache.notifications.event.NodeModifiedEvent;
import org.jboss.cache.notifications.event.ViewChangedEvent;
import org.jboss.cache.util.CachePrinter;
import org.jgroups.Address;

import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * JBossCache GUI for the demo
 *
 * @author <a href="mailto:manik AT jboss DOT org">Manik Surtani (manik AT jboss DOT org)</a>
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 */
@CacheListener
public class JBossCacheGUI extends JFrame implements WindowListener, TreeSelectionListener, TableModelListener
{
   private static final long serialVersionUID = -1242167331988194987L;

   private transient CacheModelDelegate cacheModelDelegate;
   private transient Cache cache;
   private DefaultTreeModel tree_model = null;
   private transient Log log = LogFactory.getLog(getClass());
   private JTree jtree = null;
   private DefaultTableModel tableModel = new DefaultTableModel();
   private JTable table = new JTable(tableModel);
   private JBossCacheGUI.DisplayNode myNodeRoot = new JBossCacheGUI.DisplayNode(Fqn.SEPARATOR);
   private transient Node root;
   private transient Node selected_node = null;
   private JPanel tablePanel = null;
   private JPopupMenu operationsPopup = null, dataModificationsPopup = null;
   private static final int KEY_COL_WIDTH = 20;
   private static final int VAL_COL_WIDTH = 300;
   private TransactionManager tx_mgr = null;
   private transient Transaction tx = null;
   private JPanel mainPanel;
   private List<Address> membership = new LinkedList<Address>();
   private Address coordinator = null;
   private boolean useConsole = false;
   JConsole bshConsole;

   /**
    * Run any work that happens in this interface in a separate thread.  This is good practise. Unless there's a
    * resource-managemenet requirement, such as in an application server, newCachedThreadPool is a good option as it
    * provides better queuing performance.
    */
   private transient Executor executor;

   public JBossCacheGUI(CacheModelDelegate cacheDelegate, boolean useConsole) throws Exception
   {
      executor = Executors.newCachedThreadPool();

      addNotify();
      this.useConsole = useConsole;
      tree_model = new DefaultTreeModel(new JBossCacheGUI.DisplayNode(Fqn.ROOT.toString()));
      jtree = new JTree(tree_model);
      jtree.setDoubleBuffered(true);
      jtree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

      JScrollPane scroll_pane = new JScrollPane(jtree);
      mainPanel = new JPanel();
      mainPanel.setLayout(new BorderLayout());
      mainPanel.add(scroll_pane, BorderLayout.CENTER);

      addWindowListener(this);

      tableModel.setColumnIdentifiers(new String[]{"Name", "Value"});
      tableModel.addTableModelListener(this);

      // add a mouse listener  to the table model for delete and insert rows.
      MouseListener dataMouseListener = new MouseAdapter()
      {
         public void mouseClicked(MouseEvent e)
         {
            if (rightClick(e.getModifiers()))
            {
               dataModificationsPopup.show(e.getComponent(), e.getX(), e.getY());
            }
         }
      };

      setTableColumnWidths();

      tablePanel = new JPanel();
      tablePanel.setLayout(new BorderLayout());
      tablePanel.add(table.getTableHeader(), BorderLayout.NORTH);
      tablePanel.add(table, BorderLayout.CENTER);

      table.addMouseListener(dataMouseListener);

      mainPanel.add(tablePanel, BorderLayout.SOUTH);
      JSplitPane contentPanel = null;
      if (useConsole)
      {
         String welcomeMessage = getWelcomeMessage();

         bshConsole = new JConsole();
         Interpreter interpreter = new Interpreter(bshConsole);

         configureInterpreter(interpreter, cacheDelegate);

         interpreter.println(welcomeMessage);
         interpreter.setShowResults(!interpreter.getShowResults());// show() in beanShell
         System.setOut(bshConsole.getOut());
         System.setErr(bshConsole.getErr());
         Thread t = new Thread(interpreter);
         t.start();

         contentPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mainPanel, bshConsole);
         getContentPane().add(contentPanel);
      }
      else
      {
         getContentPane().add(mainPanel);
      }

      jtree.addTreeSelectionListener(this);// REVISIT

      MouseListener ml = new MouseAdapter()
      {
         public void mouseClicked(final MouseEvent e)
         {
            if (log.isTraceEnabled())
            {
               log.trace("clicked GUI");
            }

            final int selRow = jtree.getRowForLocation(e.getX(), e.getY());
            final TreePath selPath = jtree.getPathForLocation(e.getX(), e.getY());
            if (selRow != -1)
            {
               if (log.isTraceEnabled())
               {
                  log.trace("clicked on node in GUI");
               }

               Runnable r = new Runnable()
               {
                  public void run()
                  {
                     try
                     {
                        selected_node = getNode(selPath.getPath());
                        jtree.setSelectionPath(selPath);

                        if (rightClick(e.getModifiers()))
                        {
                           operationsPopup.show(e.getComponent(), e.getX(), e.getY());
                        }
                     }
                     catch (TimeoutException te)
                     {
                        String message = "Unable to update GUI due to a timeout trying to acquire lock on a node." +
                              "This might be due to clicking on a node which is currently locked by an ongoing " +
                              "transaction: ";
                        printAndLogStacktrace(message, te);
                     }
                     catch (Exception e)
                     {
                        printAndLogStacktrace("Updating GUI failed: ", e);
                     }
                  }
               };
               executor.execute(r);
            }
         }
      };

      jtree.addMouseListener(ml);

      createMenus();
      setLocation(50, 50);
      setSize(getInsets().left + getInsets().right + 800, getInsets().top + getInsets().bottom + 800);

      // make sure we set the cache BEFORE initialising this!!
      setCacheModelDelegate(cacheDelegate);
      init();
      setVisible(true);
      tree_model.setRoot(myNodeRoot);
      tree_model.reload();

      if (useConsole)
      {
         //has to be called after setVisible() otherwise no effect
         contentPanel.setDividerLocation(0.65);
      }
   }

   public void setCacheModelDelegate(final CacheModelDelegate cacheModelDelegate)
   {
      this.cacheModelDelegate = cacheModelDelegate;
      if (this.cacheModelDelegate != null)
      {
         this.cache = this.cacheModelDelegate.getGenericCache();
         Runtime.getRuntime().addShutdownHook(
               new Thread()
               {
                  public void run()
                  {
                     cache.stop();
                  }
               });

         cache.addCacheListener(this);
         root = cache.getRoot();
         setTitle("JBoss Cache GUI: Local Address=" + getLocalAddress());
         tx_mgr = cache.getConfiguration().getRuntimeConfig().getTransactionManager();
         populateTree();
      }
      else
      {
         setTitle("Cache undefined");
         if (tree_model != null)
         {
            myNodeRoot = new JBossCacheGUI.DisplayNode(Fqn.SEPARATOR);
            tree_model.setRoot(myNodeRoot);
            tree_model.reload();

         }
         if (tableModel != null)
         {
            clearTable();
         }
      }
   }

   /**
    * Checks whether a click event is considered a "right-click"
    *
    * @param modifiers mouse event mods
    * @return true if deemed a right click
    */
   private boolean rightClick(int modifiers)
   {
      // the simple right click case
      if (modifiers == InputEvent.BUTTON3_MASK) return true;
      // more complex on Mac OS X where a ctrl-click is deemed the same as a right click
      return modifiers == InputEvent.BUTTON1_MASK + InputEvent.CTRL_MASK;
   }

   public void windowClosed(WindowEvent event)
   {
   }

   public void windowDeiconified(WindowEvent event)
   {
   }

   public void windowIconified(WindowEvent event)
   {
   }

   public void windowActivated(WindowEvent event)
   {
   }

   public void windowDeactivated(WindowEvent event)
   {
   }

   public void windowOpened(WindowEvent event)
   {
   }

   public void windowClosing(WindowEvent event)
   {
      stopGui();
   }


   public void tableChanged(TableModelEvent evt)
   {
      if (log.isTraceEnabled())
      {
         log.trace("table contents changed, event type is: " + evt.getType());
      }

      int row, col;
      String key, val;

      if (evt.getType() == TableModelEvent.UPDATE)
      {
         row = evt.getFirstRow();
         col = evt.getColumn();

         key = (String) tableModel.getValueAt(row, col == 0 ? 0 : col - 1);
         val = (String) tableModel.getValueAt(row, col == 0 ? 1 : col);

         if (key != null && val != null && !isPlaceHolder(key) && !isPlaceHolder(val))
         {
            try
            {
               if (log.isTraceEnabled())
               {
                  log.trace("updating node: " + selected_node + " with new values [k=" + key + ",v=" + val + "]");
               }

               selected_node.put(key, val);
               Map<String, String> data = selected_node.getData();
               populateTable(data);
            }
            catch (Exception e)
            {
               printAndLogStacktrace("Changing table failed: ", e);
            }
         }
      }
   }


   public void valueChanged(final TreeSelectionEvent evt)
   {
      if (log.isTraceEnabled())
      {
         log.trace("node was selected in GUI: " + evt.getPath());
      }

      Runnable r = new Runnable()
      {
         public void run()
         {
            try
            {
               Map<String, String> data;
               selected_node = getNode(evt.getPath().getPath());
               if (selected_node != null)
               {
                  data = selected_node.getData();
                  if (data != null)
                  {
                     mainPanel.add(tablePanel, BorderLayout.SOUTH);
                     populateTable(data);
                     validate();
                  }
                  else
                  {
                     clearTable();
                     mainPanel.remove(tablePanel);
                     validate();
                  }
               }
            }
            catch (TimeoutException te)
            {
               String message = "Unable to update GUI due to a timeout trying to acquire lock on a node." +
                     "This might be due to clicking on a node which is currently locked by an ongoing " +
                     "transaction: ";
               printAndLogStacktrace(message, te);
            }
            catch (Exception e)
            {
               printAndLogStacktrace("Updating GUI failed: ", e);
            }
         }
      };
      executor.execute(r);
   }

   protected DefaultTableModel getTableModel()
   {
      return tableModel;
   }

   protected String getWelcomeMessage()
   {
      return "Welcome to the BeanShell console.\n\n" +
            "This console gives you a direct shell interface to the GUI above and allows you to manipulate the cache directly. " +
            "Some of the variables initialised in this shell session are:\n\n" +
            "// an instance of org.jboss.cache\n" +
            "    Cache<String, String> cache;\n" +
            "// a reference to the root node\n" +
            "    Node<String, String> root;\n" +
            "// the transaction manager registered with the cache\n" +
            "    TransactionManager transactionManager;\n";
   }

   protected void configureInterpreter(Interpreter interpreter, CacheModelDelegate cacheDelegate) throws EvalError
   {
      interpreter.getNameSpace().importPackage("org.jboss.cache");
      interpreter.getNameSpace().importPackage("org.jboss.cache.transaction");
      interpreter.set("cache", cacheDelegate.getGenericCache());
      interpreter.set("root", cacheDelegate.getGenericCache().getRoot());
      interpreter.set("transactionManager", cacheDelegate.getGenericCache().getConfiguration().getRuntimeConfig().getTransactionManager());
   }

   /* ------------------ CacheListener interface ------------ */

   @NodeCreated
   @NodeLoaded
   public void nodeCreated(NodeEvent e)
   {
      /* Updating the GUI upon node creation should be done sequentially to avoid concurrency issues when
      adding multiple nodes at the same time, for example for PojoCache, where attaching a pojo creates several nodes.
      Creating several visual nodes in paralell and trying to scroll to them is not a thread safe operation */
      if (e.isPre())
      {
         JBossCacheGUI.DisplayNode n;

         n = myNodeRoot.add(e.getFqn());
         if (n != null)
         {
            tree_model.setRoot(myNodeRoot);
            tree_model.reload();
            jtree.scrollPathToVisible(new TreePath(n.getPath()));
         }
      }
   }

   @NodeModified
   public void nodeModified(final NodeModifiedEvent e)
   {
      Runnable r = new Runnable()
      {
         public void run()
         {
            if (e.isPre() && selected_node != null && selected_node.getFqn().equals(e.getFqn()))//&& !isLocal)
            {
               clearTable();
               Node n = root.getChild(e.getFqn());
               if (n != null)
               {
                  populateTable(n.getData());
               }
            }
         }
      };
      executor.execute(r);
   }

   @NodeRemoved
   @NodeEvicted
   public void nodeRemoved(final NodeEvent e)
   {
      if (log.isTraceEnabled())
      {
         log.trace("node removed, updating GUI");
      }

      Runnable r = new Runnable()
      {
         public void run()
         {
            if (e.isPre())
            {
               JBossCacheGUI.DisplayNode n;

               n = myNodeRoot.findNode(e.getFqn());
               if (n != null)
               {
                  n.removeAllChildren();
                  n.removeFromParent();
                  tree_model.setRoot(myNodeRoot);
                  tree_model.reload();
               }
            }
         }
      };
      executor.execute(r);
   }

   @ViewChanged
   public void viewChange(final ViewChangedEvent e)
   {
      Runnable r = new Runnable()
      {
         public void run()
         {
            List<Address> mbrship;
            if (e.getNewView() != null && (mbrship = e.getNewView().getMembers()) != null)
            {
               membership.clear();
               membership.addAll(mbrship);
               coordinator = mbrship.get(0);
            }
         }
      };
      executor.execute(r);
   }

   /* ---------------- End of CacheListener interface -------- */

   /*----------------- Runnable implementation to make View change calles in AWT Thread ---*/

   public void run()
   {

   }

   /* ----------------------------- Private Methods ---------------------------------- */

   /**
    * Fetches all data from underlying cache model and display it graphically
    */
   private void init()
   {
      List<Address> mbrship;

      mbrship = getMembers();
      if (mbrship != null && mbrship.size() > 0)
      {
         membership.clear();
         membership.addAll(mbrship);
         coordinator = mbrship.get(0);
      }
   }


   /**
    * Fetches all data from underlying cache model and display it graphically
    */
   private void populateTree()
   {
      addGuiNode(Fqn.ROOT);
   }


   /**
    * Recursively adds GUI nodes starting from fqn
    */
   private void addGuiNode(Fqn fqn)
   {
      Set<Object> children;

      if (fqn == null) return;

      // 1 . Add myself
      myNodeRoot.add(fqn);

      // 2. Then add my children
      children = cache.getRoot().getChild(fqn).getChildrenNames();
      if (children != null)
      {
         for (Object child_name : children)
         {
            addGuiNode(Fqn.fromRelativeElements(fqn, (String) child_name));
         }
      }
   }


   private Node getNode(Object[] path)
   {
      Fqn fqnToPath;
      if (path.length == 0) fqnToPath = Fqn.root();
      List<Object> elements = Arrays.asList(path);
      fqnToPath = Fqn.fromList(elements);
      if (root.hasChild(fqnToPath))
      {
         return root.getChild(fqnToPath);
      }
      else
      {
         /* No implicit creation, otherwise removing GUI selected nodes will be recreated */
         return null;
      }
   }

   protected void clearTable()
   {
      int num_rows = table.getRowCount();

      if (num_rows > 0)
      {
         for (int i = 0; i < num_rows; i++)
         {
            tableModel.removeRow(0);
         }
         tableModel.fireTableRowsDeleted(0, num_rows - 1);
         repaint();
      }
   }


   protected void populateTable(Map<String, String> data)
   {
      String key;
      String val;
      int num_rows;
      clearTable();
      if (data == null) return;
      num_rows = data.size();

      if (num_rows > 0)
      {
         // sort this according to the natural sort order of keys
         TreeMap<String, String> sortedMap = new TreeMap<String, String>(data);

         for (Map.Entry<String, String> entry : sortedMap.entrySet())
         {
            key = entry.getKey();
            val = entry.getValue();
            tableModel.addRow(new Object[]{key, val});
         }
         tableModel.fireTableRowsInserted(0, num_rows - 1);
         validate();
      }
   }

   private void setTableColumnWidths()
   {
      table.sizeColumnsToFit(JTable.AUTO_RESIZE_NEXT_COLUMN);
      TableColumn column;
      column = table.getColumnModel().getColumn(0);
      column.setMinWidth(KEY_COL_WIDTH);
      column.setPreferredWidth(KEY_COL_WIDTH);
      column = table.getColumnModel().getColumn(1);
      column.setPreferredWidth(VAL_COL_WIDTH);
   }

   private void createMenus()
   {
      JMenuBar menubar = new JMenuBar();
      JMenu operationsMenu = new JMenu("Operations");
      JBossCacheGUI.AddNodeAction addNode = new JBossCacheGUI.AddNodeAction();
      addNode.putValue(AbstractAction.NAME, "Add to this node");
      JBossCacheGUI.LoadAction load_action = new JBossCacheGUI.LoadAction();
      load_action.putValue(AbstractAction.NAME, "Load from the CacheLoader");
      JBossCacheGUI.RemoveNodeAction removeNode = new JBossCacheGUI.RemoveNodeAction();
      removeNode.putValue(AbstractAction.NAME, "Remove this node");
      JBossCacheGUI.EvictAction evict_action = new JBossCacheGUI.EvictAction();
      evict_action.putValue(AbstractAction.NAME, "Evict from the Cache");
      JBossCacheGUI.AddModifyDataForNodeAction addModAction = new JBossCacheGUI.AddModifyDataForNodeAction();
      addModAction.putValue(AbstractAction.NAME, "Add/Modify data");
      JBossCacheGUI.PrintLockInfoAction print_locks = new JBossCacheGUI.PrintLockInfoAction();
      print_locks.putValue(AbstractAction.NAME, "Print lock information" + (useConsole ? "" : " (stdout)"));
      JBossCacheGUI.ExitAction exitAction = new JBossCacheGUI.ExitAction();
      exitAction.putValue(AbstractAction.NAME, "Exit");
      JBossCacheGUI.StartTransaction start_tx = new JBossCacheGUI.StartTransaction();
      start_tx.putValue(AbstractAction.NAME, "Start TX");
      JBossCacheGUI.CommitTransaction commit_tx = new JBossCacheGUI.CommitTransaction();
      commit_tx.putValue(AbstractAction.NAME, "Commit TX");
      JBossCacheGUI.RollbackTransaction rollback_tx = new JBossCacheGUI.RollbackTransaction();
      rollback_tx.putValue(AbstractAction.NAME, "Rollback TX");

      operationsMenu.add(addNode);
      operationsMenu.add(load_action);
      operationsMenu.add(removeNode);
      operationsMenu.add(evict_action);
      operationsMenu.add(addModAction);
      operationsMenu.add(print_locks);
      operationsMenu.add(start_tx);
      operationsMenu.add(commit_tx);
      operationsMenu.add(rollback_tx);
      operationsMenu.add(exitAction);
      menubar.add(operationsMenu);
      setJMenuBar(menubar);

      operationsPopup = new JPopupMenu();
      operationsPopup.add(addNode);
      operationsPopup.add(load_action);
      operationsPopup.add(evict_action);
      operationsPopup.add(removeNode);
      operationsPopup.add(addModAction);

      JBossCacheGUI.InsertRowAction insertRow = new JBossCacheGUI.InsertRowAction();
      insertRow.putValue(AbstractAction.NAME, "Insert New Row");
      JBossCacheGUI.RemoveLastRowAction removeLastRow = new JBossCacheGUI.RemoveLastRowAction();
      removeLastRow.putValue(AbstractAction.NAME, "Remove Last Row");

      dataModificationsPopup = new JPopupMenu();
      dataModificationsPopup.add(insertRow);
      dataModificationsPopup.add(removeLastRow);
   }

   private Object getLocalAddress()
   {
      try
      {
         return cache.getLocalAddress();
      }
      catch (Throwable t)
      {
         log.error("JBossCacheGUI.getLocalAddress(): ", t);
         return null;
      }
   }

   private void load(Fqn fqn)
   {
      try
      {
         // this will cause the cache to load the relevant node from a cache loader.
         cache.getRoot().getChild(fqn);
      }
      catch (Throwable t)
      {
         log.error("JBossCacheGUI.load(): " + t);
      }
   }

   private List<Address> getMembers()
   {
      try
      {
         return new ArrayList<Address>(cache.getMembers());
      }
      catch (Throwable t)
      {
         log.error("TreeCacheGui.getMembers(): ", t);
         return null;
      }
   }

   private String[] getRowPlaceHolderData()
   {
      Collection keys = extractKeys(tableModel.getDataVector());
      // try all place holders

      int count = 0;
      String placeHolderKey = "{ --- Add Key --- }";
      while (keys.contains(placeHolderKey))
      {
         count++;
         placeHolderKey = "{ --- Add Key " + count + " --- }";
      }

      String placeHolderValue = "{ --- Add Value " + (count == 0 ? "" : count) + " --- }";
      return new String[]{placeHolderKey, placeHolderValue};
   }

   private boolean isPlaceHolder(String s)
   {
      if (s.startsWith("{ --- Add Key ") && s.endsWith(" --- }")) return true;
      if (s.startsWith("{ --- Add Value ") && s.endsWith(" --- }")) return true;
      return false;
   }

   private Collection extractKeys(Vector<Vector> v)
   {
      // very odd data structure.  Entire table is represented as a Vector.  Each row (element in the Vector) is a Vector of 2 elements (key and value)
      List l = new LinkedList();
      for (Vector row : v)
      {
         // just add keys
         l.add(row.get(0));
      }
      return l;
   }

   private void printAndLogStacktrace(String message, Throwable t)
   {
      t.printStackTrace();
      log.error(message, t);
   }

   /* -------------------------- End of Private Methods ------------------------------ */

   /*----------------------- Actions ---------------------------*/

   class ExitAction extends AbstractAction
   {
      private static final long serialVersionUID = -5364163916172148038L;

      public void actionPerformed(ActionEvent e)
      {
         stopGui();
      }
   }


   void stopGui()
   {
      if (cache != null)
      {
         try
         {
            cache.stop();
            cache.destroy();
            cache = null;
         }
         catch (Throwable t)
         {
            printAndLogStacktrace("Stopping and destroying cache failed: ", t);
         }
      }
      dispose();
      System.exit(0);
   }


   class InsertRowAction extends AbstractAction
   {
      private static final long serialVersionUID = 7084928639244438800L;

      public void actionPerformed(ActionEvent e)
      {
         tableModel.addRow(getRowPlaceHolderData());
      }
   }

   class RemoveLastRowAction extends AbstractAction
   {
      private static final long serialVersionUID = 7084928639244438800L;

      public void actionPerformed(ActionEvent e)
      {
         int lastRow = tableModel.getRowCount() - 1;
         if (lastRow > -1)
         {
            String keyToRemove = (String) tableModel.getValueAt(lastRow, 0);
            tableModel.removeRow(lastRow);
            selected_node.remove(keyToRemove);
         }
      }
   }


   class AddNodeAction extends AbstractAction
   {
      private static final long serialVersionUID = 7084928639244438800L;

      public void actionPerformed(ActionEvent e)
      {
         JTextField fqnTextField = new JTextField();
         if (selected_node != null)
         {
            fqnTextField.setText(selected_node.getFqn().toString());
         }
         else
         {
            fqnTextField.setText(Fqn.SEPARATOR);
         }
         Object[] information = {"Enter fully qualified name",
                                 fqnTextField};
         final String btnString1 = "OK";
         final String btnString2 = "Cancel";
         Object[] options = {btnString1, btnString2};
         int userChoice = JOptionPane.showOptionDialog(null,
               information,
               "Add DataNode",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.PLAIN_MESSAGE,
               null,
               options,
               options[0]);
         if (userChoice == 0)
         {
            String userInput = fqnTextField.getText();
            cache.put(Fqn.fromString(userInput), null);
         }
      }
   }

   class LoadAction extends AbstractAction
   {
      private static final long serialVersionUID = -6998760732995584428L;

      public void actionPerformed(ActionEvent e)
      {
         JTextField fqnTextField = new JTextField();
         if (selected_node != null)
         {
            fqnTextField.setText(selected_node.getFqn().toString());
         }
         else
         {
            fqnTextField.setText(Fqn.SEPARATOR);
         }

         Object[] information = {"Enter fully qualified name",
                                 fqnTextField};
         final String btnString1 = "OK";
         final String btnString2 = "Cancel";
         Object[] options = {btnString1, btnString2};
         int userChoice = JOptionPane.showOptionDialog(null,
               information,
               "Load DataNode",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.PLAIN_MESSAGE,
               null,
               options,
               options[0]);
         if (userChoice == 0)
         {
            String userInput = fqnTextField.getText();
            load(Fqn.fromString(userInput));
         }
      }
   }

   class EvictAction extends AbstractAction
   {
      private static final long serialVersionUID = 6007500908549034215L;

      public void actionPerformed(ActionEvent e)
      {
         JTextField fqnTextField = new JTextField();
         if (selected_node != null)
         {
            fqnTextField.setText(selected_node.getFqn().toString());
         }
         else
         {
            fqnTextField.setText(Fqn.SEPARATOR);
         }
         Object[] information = {"Enter fully qualified name",
                                 fqnTextField};
         final String btnString1 = "OK";
         final String btnString2 = "Cancel";
         Object[] options = {btnString1, btnString2};
         int userChoice = JOptionPane.showOptionDialog(null,
               information,
               "Evict DataNode",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.PLAIN_MESSAGE,
               null,
               options,
               options[0]);
         if (userChoice == 0)
         {
            String userInput = fqnTextField.getText();
            cache.evict(Fqn.fromString(userInput), true);
         }
      }
   }


   class StartTransaction extends AbstractAction
   {
      private static final long serialVersionUID = 7059131008813144857L;

      public void actionPerformed(ActionEvent e)
      {
         if (tx_mgr == null)
         {
            log.error("no TransactionManager specified");
            return;
         }
         if (tx != null)
         {
            log.error("transaction is already running: " + tx);
            return;
         }
         try
         {
            tx_mgr.begin();
            tx = tx_mgr.getTransaction();
         }
         catch (Throwable t)
         {
            printAndLogStacktrace("Creating transaction failed: ", t);
         }
      }
   }

   class CommitTransaction extends AbstractAction
   {
      private static final long serialVersionUID = 5426108920883879873L;

      public void actionPerformed(ActionEvent e)
      {
         if (tx == null)
         {
            log.error("transaction is not running");
            return;
         }
         try
         {
            tx.commit();
         }
         catch (Throwable t)
         {
            printAndLogStacktrace("Commiting transaction failed: ", t);
         }
         finally
         {
            tx = null;
         }
      }
   }

   class RollbackTransaction extends AbstractAction
   {
      private static final long serialVersionUID = -4836748411400541430L;

      public void actionPerformed(ActionEvent e)
      {
         if (tx == null)
         {
            log.error("transaction is not running");
            return;
         }
         try
         {
            tx.rollback();
         }
         catch (Throwable t)
         {
            printAndLogStacktrace("Transaction rollback failed: ", t);
         }
         finally
         {
            tx = null;
         }
      }
   }

   class PrintLockInfoAction extends AbstractAction
   {
      private static final long serialVersionUID = -2171307516592250436L;

      public void actionPerformed(ActionEvent e)
      {
         if (bshConsole != null)
         {
            new Thread()
            {
               public void run()
               {
                  bshConsole.getOut().println("\n*** lock information ****\n" + CachePrinter.printCacheLockingInfo(cache));
               }
            }.start();

         }
         else
         {
            new Thread()
            {
               public void run()
               {
                  System.out.println("\n*** lock information ****\n" + CachePrinter.printCacheLockingInfo(cache));
               }
            }.start();
         }
      }
   }

   class RemoveNodeAction extends AbstractAction
   {
      private static final long serialVersionUID = 3746013603940497991L;

      public void actionPerformed(ActionEvent e)
      {
         try
         {
            cache.removeNode(selected_node.getFqn());
         }
         catch (Throwable t)
         {
            log.error("RemoveNodeAction.actionPerformed(): " + t);
         }
      }
   }

   class AddModifyDataForNodeAction extends AbstractAction
   {
      private static final long serialVersionUID = -7656592171312920825L;

      public void actionPerformed(ActionEvent e)
      {
         if (log.isTraceEnabled())
         {
            log.trace("node added/modified, updating GUI: " + e);
         }
         if (selected_node == null) return;

         clearTable();
         Map<String, String> data = selected_node.getData();
         if (data == null || data.isEmpty())
         {
            data = new HashMap<String, String>();
            String[] placeHolder = getRowPlaceHolderData();
            data.put(placeHolder[0], placeHolder[1]);
         }

         populateTable(data);

         mainPanel.add(tablePanel, BorderLayout.SOUTH);
         validate();
      }
   }


   class DisplayNode extends DefaultMutableTreeNode
   {
      private static final long serialVersionUID = 4882445905140460053L;
      String name = "<unnamed>";


      DisplayNode(String name)
      {
         this.name = name;
      }


      /**
       * Adds a new node to the view. Intermediary nodes will be created if they don't yet exist.
       * Returns the first node that was created or null if node already existed
       */
      public JBossCacheGUI.DisplayNode add(Fqn fqn)
      {
         JBossCacheGUI.DisplayNode curr, n, ret = null;

         if (fqn == null) return null;
         curr = this;

         for (Object childName : fqn.peekElements())
         {
            n = curr.findChild((String) childName);
            if (n == null)
            {
               n = new JBossCacheGUI.DisplayNode((String) childName);
               if (ret == null) ret = n;
               curr.add(n);
            }
            curr = n;
         }
         return ret;
      }


      /**
       * Removes a node from the view. Child nodes will be removed as well
       */
      public void remove()
      {
         removeFromParent();
      }


      private JBossCacheGUI.DisplayNode findNode(Fqn fqn)
      {
         JBossCacheGUI.DisplayNode curr, n;

         if (fqn == null) return null;
         curr = this;

         for (Object childName : fqn.peekElements())
         {
            n = curr.findChild((String) childName);
            if (n == null)
            {
               return null;
            }
            curr = n;
         }
         return curr;
      }


      private JBossCacheGUI.DisplayNode findChild(String relative_name)
      {
         JBossCacheGUI.DisplayNode child;

         if (relative_name == null || getChildCount() == 0)
         {
            return null;
         }
         for (int i = 0; i < getChildCount(); i++)
         {
            child = (JBossCacheGUI.DisplayNode) getChildAt(i);
            if (child.name == null)
            {
               continue;
            }

            if (child.name.equals(relative_name))
            {
               return child;
            }
         }
         return null;
      }

      public String toString()
      {
         return name;
      }

   }
}
