package com.kostbot.zoodirector.ui;
import com.kostbot.zoodirector.ui.helpers.UIUtils;
import com.kostbot.zoodirector.zookeepersync.ZookeeperSync;
import org.apache.zookeeper.CreateMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.awt.event.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
public class ZooDirectorNavPanel extends JPanel {
private static final Logger logger = LoggerFactory.getLogger(ZooDirectorNavPanel.class);
private final ZooDirectorPanel zooDirectorPanel;
protected final DefaultTreeModel treeModel;
protected final JTree tree;
protected final DefaultMutableTreeNode rootNode;
private final JMenuItem createNodeMenuItem;
private final JMenuItem deleteNodeMenuItem;
private final JMenuItem trimNodeMenuItem;
private final JMenuItem pruneNodeMenuItem;
private final JMenuItem addWatchMenuItem;
private final JMenuItem removeWatchMenuItem;
private final Set<String> createdPaths;
public ZooDirectorNavPanel(ZooDirectorPanel zooDirectorPanel) {
super(new BorderLayout());
this.zooDirectorPanel = zooDirectorPanel;
rootNode = new DefaultMutableTreeNode(ZookeeperNode.root);
treeModel = new DefaultTreeModel(rootNode);
tree = new JTree(treeModel);
tree.setShowsRootHandles(true);
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setBorder(BorderFactory.createEmptyBorder(2, 0, 4, 0));
JScrollPane scrollPane = new JScrollPane(tree);
this.add(scrollPane, BorderLayout.CENTER);
final JPopupMenu popupMenu = new JPopupMenu();
createNodeMenuItem = new JMenuItem("create");
createNodeMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
createNode(getSelectedNode());
}
});
popupMenu.add(createNodeMenuItem);
deleteNodeMenuItem = new JMenuItem("delete");
deleteNodeMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
deleteNode(getSelectedNode(), false);
}
});
popupMenu.add(deleteNodeMenuItem);
pruneNodeMenuItem = new JMenuItem("prune");
pruneNodeMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
pruneNode(getSelectedNode());
}
});
popupMenu.add(pruneNodeMenuItem);
trimNodeMenuItem = new JMenuItem("trim");
trimNodeMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
trimNode(getSelectedNode());
}
});
popupMenu.add(trimNodeMenuItem);
popupMenu.addSeparator();
JMenuItem expandPathMenuItem = new JMenuItem("expand all");
expandPathMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
expandAll(getSelectedNode());
}
});
popupMenu.add(expandPathMenuItem);
JMenuItem collapsePathMenuItem = new JMenuItem("collapse all");
collapsePathMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
collapseAll(getSelectedNode());
}
});
popupMenu.add(collapsePathMenuItem);
popupMenu.addSeparator();
addWatchMenuItem = new JMenuItem("add watch");
addWatchMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ZooDirectorNavPanel.this.zooDirectorPanel.addWatch(getZookeeperNodePath(getSelectedNode()));
}
});
popupMenu.add(addWatchMenuItem);
removeWatchMenuItem = new JMenuItem("remove watch");
removeWatchMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ZooDirectorNavPanel.this.zooDirectorPanel.removeWatch(getZookeeperNodePath(getSelectedNode()));
}
});
popupMenu.add(removeWatchMenuItem);
// Context menu for selected tree node
MouseListener ml = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// Select if row is clicked not just text
int row = tree.getClosestRowForLocation(e.getX(), e.getY());
tree.setSelectionRow(row);
if (SwingUtilities.isRightMouseButton(e)) {
DefaultMutableTreeNode selectedNode = getSelectedNode();
boolean isOnline = ZooDirectorNavPanel.this.zooDirectorPanel.isOnline();
createNodeMenuItem.setEnabled(isOnline);
deleteNodeMenuItem.setEnabled(isOnline && !selectedNode.isRoot());
pruneNodeMenuItem.setEnabled(isOnline && !selectedNode.isRoot());
trimNodeMenuItem.setEnabled(isOnline && selectedNode.getChildCount() > 0);
boolean hasWatch = ZooDirectorNavPanel.this.zooDirectorPanel.hasWatch(getZookeeperNodePath(selectedNode));
addWatchMenuItem.setEnabled(isOnline && !hasWatch);
removeWatchMenuItem.setEnabled(isOnline && hasWatch);
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
};
tree.addMouseListener(ml);
tree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent event) {
if (ZooDirectorNavPanel.this.zooDirectorPanel.isOnline()) {
String path = getZookeeperNodePath(getSelectedNode());
ZooDirectorNavPanel.this.zooDirectorPanel.viewEditTreeNode(path);
}
}
});
tree.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
DefaultMutableTreeNode node = getSelectedNode();
if (node == null) {
return;
}
switch (e.getKeyCode()) {
case KeyEvent.VK_DELETE:
deleteNode(node, e.isControlDown());
break;
case KeyEvent.VK_INSERT:
createNode(node);
break;
case KeyEvent.VK_MULTIPLY:
expandAll(node);
break;
case KeyEvent.VK_DIVIDE:
collapseAll(node);
break;
case KeyEvent.VK_W:
// Ctrl + (Shift) + W
if (e.isControlDown()) {
addWatch(node, e.isShiftDown());
}
break;
case KeyEvent.VK_R:
// Ctrl + (Shift) + R
if (e.isControlDown()) {
removeWatch(node, e.isShiftDown());
}
break;
}
}
});
createdPaths = new HashSet<String>();
}
public boolean wasCreated(String path) {
synchronized (createdPaths) {
return createdPaths.remove(path);
}
}
/**
* Get the selected tree node.
*
* @return selected tree node containing helpful ZookeeperNode object
*/
private DefaultMutableTreeNode getSelectedNode() {
TreePath currentSelection = tree.getSelectionPath();
if (currentSelection == null)
return null;
return (DefaultMutableTreeNode) currentSelection.getLastPathComponent();
}
protected DefaultMutableTreeNode getNodeFromPath(String path) {
DefaultMutableTreeNode parent = rootNode;
if (path == null || path.equals("/")) {
return parent;
}
if (!path.startsWith("/")) {
return null;
}
String[] segments = path.substring(1).split("/");
boolean foundParent = true;
for (int i = 0; foundParent && i < segments.length; ++i) {
String segment = segments[i];
foundParent = false;
for (int j = 0; j < parent.getChildCount(); ++j) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) parent.getChildAt(j);
if (segment.equals(child.toString())) {
// If we have found the path, remove it.
if (i == segments.length - 1) {
return child;
}
// Keep searching down path.
foundParent = true;
parent = child;
break;
}
}
}
return null;
}
private void selectTreeNode(DefaultMutableTreeNode node) {
TreePath treePath = getTreePath(node);
tree.setSelectionPath(treePath);
tree.scrollPathToVisible(treePath);
}
public DefaultMutableTreeNode selectTreeNode(String path) {
DefaultMutableTreeNode target = getNodeFromPath(path);
if (target != null) {
selectTreeNode(target);
}
return target;
}
/**
* Add the given path as a node on the tree in sorted order.
*
* @param path
*/
public void addNodeToTree(String path, boolean select) {
// Ignore root
if ("/".equals(path)) {
return;
}
DefaultMutableTreeNode parent = rootNode;
String[] segments = path.substring(1).split("/");
// Ensure a node exists for all segments of the path.
for (int i = 0; i < segments.length; ++i) {
String segment = segments[i];
DefaultMutableTreeNode node = null;
int insertAt = 0;
// Add to tree in sorted order.
for (int j = 0; j < parent.getChildCount(); ++j) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) parent.getChildAt(j);
if (segment.compareTo(child.toString()) > 0) {
insertAt = j + 1;
} else if (segment.equals(child.toString())) {
node = (DefaultMutableTreeNode) parent.getChildAt(j);
break;
}
}
if (node == null) {
node = new DefaultMutableTreeNode(ZookeeperNode.create(getZookeeperNodePath(parent), segment));
treeModel.insertNodeInto(node, parent, insertAt);
}
parent = node;
}
if (select) {
selectTreeNode(parent);
}
}
/**
* Remove the given path from the tree if it exists.
*
* @param path
*/
public void removeNodeFromTree(String path) {
DefaultMutableTreeNode target = getNodeFromPath(path);
if (target != null && target != rootNode)
treeModel.removeNodeFromParent(target);
}
/**
* Prune the branch the node is on. This call will delete the node plus all ancestors with only nodes on this path.
*
* @param node
* @throws Exception
*/
private void pruneNode(DefaultMutableTreeNode node) {
String path = getZookeeperNodePath(node);
int option = showYesNoDialog(
"Prune Node: " + path,
"Are you sure you want to prune this nodes and all its lonely ancestors?");
if (option != JOptionPane.YES_OPTION) {
return;
}
try {
// TODO run on SwingWorker or use ZK Background
selectTreeNode(zooDirectorPanel.getZookeeperSync().prune(path));
} catch (Exception e) {
logger.error("prune {} failed [{}]", path, e);
}
}
/**
* Delete input nodes children from zookeeper and tree with optional user confirmation.
*
* @param node node to have children delete for
*/
private void trimNode(DefaultMutableTreeNode node) {
String path = getZookeeperNodePath(node);
int option = showYesNoDialog(
"Delete Children: " + path,
"Are you sure you want to delete this nodes children and all its lovely descendants?");
if (option != JOptionPane.YES_OPTION) {
return;
}
try {
// TODO run on SwingWorker or use ZK Background
zooDirectorPanel.getZookeeperSync().trim(path);
} catch (Exception e) {
logger.error("trim {} failed [{}]", path, e);
}
}
/**
* Delete the input node (and all children) from zookeeper and the tree with optional user confirmation.
*
* @param node node to be deleted
* @param skipConfirmation if true user confirmation is bypassed
*/
private void deleteNode(DefaultMutableTreeNode node, boolean skipConfirmation) {
if (node.isRoot())
return;
String path = getZookeeperNodePath(node);
if (!skipConfirmation) {
int option = showYesNoDialog(
"Delete: " + node,
"Are you sure you want to delete this node" + (node.getChildCount() > 0 ? " and all of its lovely children?" : "?"));
if (option != JOptionPane.YES_OPTION) {
return;
}
}
try {
// TODO run on SwingWorker or use ZK Background
zooDirectorPanel.getZookeeperSync().delete(path);
selectTreeNode((DefaultMutableTreeNode) node.getParent());
} catch (Exception e) {
logger.error("delete {} failed [{}]", path, e);
}
}
private static final CreateMode[] CREATE_MODES = new CreateMode[]{CreateMode.PERSISTENT, CreateMode.EPHEMERAL};
/**
* Create a child node in zookeeper and add it to the tree based on user node name input.
*
* @param parent parent node to add child to
*/
private void createNode(DefaultMutableTreeNode parent) {
JPanel inputPanel = new JPanel(new BorderLayout());
JLabel messageLabel = new JLabel("Enter name or full path for new node");
inputPanel.add(messageLabel, BorderLayout.NORTH);
final JTextField pathTextField = new JTextField();
UIUtils.highlightIfConditionMetOnUpdate(pathTextField, new UIUtils.Condition() {
@Override
public boolean isMet() {
return ZookeeperSync.isValidPath(pathTextField.getText(), true);
}
});
inputPanel.add(pathTextField, BorderLayout.CENTER);
JComboBox<CreateMode> createModeComboBox = new JComboBox<CreateMode>(CREATE_MODES);
inputPanel.add(createModeComboBox, BorderLayout.SOUTH);
boolean isValid = false;
String path = null;
while (!isValid) {
int result = JOptionPane.showConfirmDialog(
SwingUtilities.getRoot(this),
inputPanel,
"Create",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
if (result != JOptionPane.OK_OPTION) {
return;
}
path = pathTextField.getText();
isValid = ZookeeperSync.isValidSubPath(path);
}
// Either add as child or as absolute path
if (!path.startsWith("/")) {
String parentPath = getZookeeperNodePath(parent);
path = ("/".equals(parentPath) ? "/" : (parentPath + "/")) + path;
}
synchronized (createdPaths) {
createdPaths.add(path);
}
try {
// TODO run on SwingWorker or use ZK Background
zooDirectorPanel.getZookeeperSync().create(path, (CreateMode) createModeComboBox.getSelectedItem());
} catch (Exception e) {
synchronized (createdPaths) {
createdPaths.remove(path);
}
logger.error("create {} failed [{}]", path, e);
}
}
/**
* Add watch for the given node's zookeeper path.
*
* @param node node to add watch to
* @param recursive if set watches for all descendant nodes will be created (if they do not already exist)
*/
private void addWatch(DefaultMutableTreeNode node, boolean recursive) {
zooDirectorPanel.addWatch(getZookeeperNodePath(node));
if (recursive) {
for (int i = 0; i < node.getChildCount(); ++i) {
addWatch((DefaultMutableTreeNode) node.getChildAt(i), true);
}
}
}
/**
* Remove watch for the given node's zookeeper path.
*
* @param node node to remove watch from
* @param recursive if true watches for all descendant nodes will be removed (if they exist)
*/
private void removeWatch(DefaultMutableTreeNode node, boolean recursive) {
zooDirectorPanel.removeWatch(getZookeeperNodePath(node));
if (recursive) {
for (int i = 0; i < node.getChildCount(); ++i) {
removeWatch((DefaultMutableTreeNode) node.getChildAt(i), true);
}
}
}
/**
* Expand all nodes under node
*
* @param node
*/
private void expandAll(DefaultMutableTreeNode node) {
Enumeration<DefaultMutableTreeNode> childNodes = node.breadthFirstEnumeration();
while (childNodes.hasMoreElements()) {
tree.expandPath(getTreePath(childNodes.nextElement()));
}
}
/**
* Collapse all nodes under node
*
* @param node
*/
private void collapseAll(DefaultMutableTreeNode node) {
Enumeration<DefaultMutableTreeNode> childNodes = node.depthFirstEnumeration();
while (childNodes.hasMoreElements()) {
tree.collapsePath(getTreePath(childNodes.nextElement()));
}
}
@Override
public void grabFocus() {
tree.grabFocus();
}
public void removeAll() {
rootNode.removeAllChildren();
treeModel.reload();
}
/**
* Helper method for getting tree path of node
*
* @param node node to get TreePath for
* @return TreePath of given node
*/
private static TreePath getTreePath(DefaultMutableTreeNode node) {
return new TreePath(node.getPath());
}
/**
* Helper method for extracting the Zookeeper path from tree node's user object.
*
* @param node tree node to extract ZookeeperNode instance from
* @return zookeeper path of node
*/
protected static String getZookeeperNodePath(DefaultMutableTreeNode node) {
if (node == null)
return null;
return ((ZookeeperNode) node.getUserObject()).path;
}
/**
* Simple class to represent zookeeper node path/name
*/
public static class ZookeeperNode {
public final static ZookeeperNode root = new ZookeeperNode("/");
private final String name;
public final String path;
private ZookeeperNode(String path) {
super();
this.path = path;
String[] subPaths = path.split("/");
if (subPaths.length > 1) {
name = subPaths[subPaths.length - 1];
} else {
name = "";
}
}
public static ZookeeperNode create(String parent, String name) {
if ("/".equals(parent)) {
return new ZookeeperNode('/' + name);
}
return new ZookeeperNode(parent + '/' + name);
}
@Override
public String toString() {
return name;
}
}
private static final String YES = "Yes";
private static final String NO = "No";
private static final Object[] YES_NO = {YES, NO};
int showYesNoDialog(String title, String message) {
return JOptionPane.showOptionDialog(
SwingUtilities.getRoot(this),
message,
title,
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
YES_NO,
NO);
}
}