package com.kostbot.zoodirector.ui; import com.google.common.base.Strings; import com.kostbot.zoodirector.ui.helpers.UIUtils; import com.kostbot.zoodirector.ui.workers.LoadDataWorker; import com.kostbot.zoodirector.ui.workers.SaveDataWorker; import com.kostbot.zoodirector.zookeepersync.ZookeeperSync; import org.apache.zookeeper.data.Stat; import org.joda.time.DateTime; import javax.swing.*; import javax.swing.undo.*; import java.awt.*; import java.awt.event.*; /** * Panel used for viewing and editing zookeeper nodes. */ public class ZooDirectorNodeEditPanel extends JPanel { private static final String PATH = "Path"; private static final String PATH_EPHEMERAL = "Path (Ephemeral)"; private ZookeeperSync zookeeperSync; private SwingWorker<Void, Void> swingWorker; private volatile String path; private volatile String initData; // Used for detecting data edit changes private final JLabel pathLabel; private final JTextField pathTextField; private final JTextField cTimeTextField; private final JTextField mTimeTextField; private final JTextField versionTextField; private final JTextField ephemeralOwnerTextField; private final JTextArea dataTextArea; private final JLabel dataSizeLabel; private final UndoManager undoManager; private final JButton saveButton; private final JButton clearButton; private final JButton reloadButton; class GridBagPanelBuilder { JPanel panel; GridBagConstraints c; GridBagPanelBuilder(JPanel panel) { this.panel = panel; panel.setLayout(new GridBagLayout()); c = new GridBagConstraints(); c.anchor = GridBagConstraints.FIRST_LINE_START; c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 0.5; c.weighty = 0; c.gridx = 0; c.gridy = 0; c.insets = new Insets(2, 5, 0, 5); } void setWeightY(double weighty) { c.weighty = weighty; } void setFill(int fill) { c.fill = fill; } void addComponents(int width, JComponent... components) { c.gridwidth = width; for (JComponent component : components) { panel.add(component, c); c.gridx += 1; } c.gridx = 0; c.gridy += 1; } } JTextField createNoEditTextField(String tooltip) { JTextField textField = new JTextField(50); textField.setEditable(false); textField.setFont(ZooDirectorFrame.FONT_MONOSPACED); textField.setToolTipText(tooltip); return textField; } ZooDirectorNodeEditPanel() { super(); GridBagPanelBuilder gridBagPanelBuilder = new GridBagPanelBuilder(this); pathLabel = new JLabel(PATH); gridBagPanelBuilder.addComponents(2, pathLabel); pathTextField = createNoEditTextField("Zookeeper Path"); gridBagPanelBuilder.addComponents(2, pathTextField); gridBagPanelBuilder.addComponents(1, new JLabel("Created"), new JLabel("Modified")); cTimeTextField = createNoEditTextField("Path Creation Time"); mTimeTextField = createNoEditTextField("Path Modification Time"); gridBagPanelBuilder.addComponents(1, cTimeTextField, mTimeTextField); gridBagPanelBuilder.addComponents(1, new JLabel("Version"), new JLabel("Owner ID")); versionTextField = createNoEditTextField("modification count"); ephemeralOwnerTextField = createNoEditTextField("ephemeral owner id (0 if persistent)"); gridBagPanelBuilder.addComponents(1, versionTextField, ephemeralOwnerTextField); undoManager = new UndoManager(); dataTextArea = new JTextArea(10, 50); dataTextArea.getDocument().addUndoableEditListener(undoManager); dataTextArea.setFont(ZooDirectorFrame.FONT_MONOSPACED); dataTextArea.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { isDataUpdated(); switch (e.getKeyCode()) { case KeyEvent.VK_F5: reload(); break; case KeyEvent.VK_S: // Ctrl + S if (e.isControlDown()) { save(); } break; case KeyEvent.VK_Z: // Ctrl + Z if (e.isControlDown()) { if (undoManager.canUndo()) { undoManager.undo(); } } break; case KeyEvent.VK_R: // Ctrl + R if (e.isControlDown()) { if (undoManager.canRedo()) { undoManager.redo(); } } break; } } }); gridBagPanelBuilder.addComponents(2, new JLabel("Data")); gridBagPanelBuilder.setWeightY(1.0); gridBagPanelBuilder.setFill(GridBagConstraints.BOTH); gridBagPanelBuilder.addComponents(2, new JScrollPane(dataTextArea)); gridBagPanelBuilder.setWeightY(0.0); JPanel bottomPanel = new JPanel(new BorderLayout()); JPanel buttonPanel = new JPanel(new FlowLayout()); // Reload reloadButton = new JButton("Reload"); reloadButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { reload(); } }); buttonPanel.add(reloadButton); // Clear clearButton = new JButton("Clear"); clearButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { dataTextArea.setText(""); isDataUpdated(); } }); buttonPanel.add(clearButton); // Save saveButton = new JButton("Save"); saveButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { save(); } }); buttonPanel.add(saveButton); bottomPanel.add(buttonPanel, BorderLayout.WEST); dataSizeLabel = new JLabel(""); JPanel dataSizePanel = new JPanel(); dataSizePanel.add(dataSizeLabel); bottomPanel.add(dataSizePanel, BorderLayout.EAST); gridBagPanelBuilder.setFill(GridBagConstraints.HORIZONTAL); gridBagPanelBuilder.addComponents(2, bottomPanel); } /** * Check if the nodes data has been updated. Sets the dataStatusLabel text accordingly. * * @return true is data has been updated, false otherwise */ private boolean isDataUpdated() { String currentData = dataTextArea.getText(); dataSizeLabel.setText(UIUtils.humanReadableByteCount(currentData.getBytes().length)); clearButton.setEnabled(!Strings.isNullOrEmpty(currentData)); if (initData != null) { if (!initData.equals(currentData)) { saveButton.setEnabled(true); return true; } } saveButton.setEnabled(false); return false; } /** * If the data has been updated since last fetch data will be set in zookeeper. */ private void save() { if (isDataUpdated()) { executeSwingWorker(new SaveDataWorker(zookeeperSync, path, Integer.parseInt(versionTextField.getText()), dataTextArea.getText().getBytes(), new SaveDataWorker.Callback() { @Override public void onComplete(String path) { if (ZooDirectorNodeEditPanel.this.path != null && ZooDirectorNodeEditPanel.this.path.equals(path)) { reload(); } } @Override public void onFailure(String path) { // TODO popup error? with force save option } })); } } /** * Helper method for cancelling current swingWorker if it exists and execute provided swingWorker if it exists. * * @param swingWorker */ synchronized private void executeSwingWorker(SwingWorker<Void, Void> swingWorker) { if (this.swingWorker != null) { this.swingWorker.cancel(false); } this.swingWorker = swingWorker; if (swingWorker != null) { swingWorker.execute(); } } /** * Reload the edit panel content from zookeeper. */ private void reload() { setZookeeperPath(this.path, false); } /** * If path is different than current path updates the edit panel with values for the given zookeeper path and clears * the undo history. * * @param path node path to edit * @see #setZookeeperPath(String, boolean) */ public void setZookeeperPath(String path) { if (this.path == null || !this.path.equals(path)) { setZookeeperPath(path, true); } } /** * Update the edit panel with values for the given zookeeper path. * * @param path node path to edit * @param clearUndoManager clear undo events if setting new path */ private void setZookeeperPath(String path, final boolean clearUndoManager) { this.path = path; executeSwingWorker(new LoadDataWorker(zookeeperSync, path, new LoadDataWorker.Callback() { @Override public void onComplete(String path, Stat stat, byte[] data) { setData(path, stat, data); if (clearUndoManager) { ZooDirectorNodeEditPanel.this.undoManager.discardAllEdits(); } } })); } /** * Helper for setting panel text fields and enabling/disabling buttons accordingly. * <p/> * EDT thread safe * * @param path * @param stat * @param data */ private void setData(String path, Stat stat, byte[] data) { if (stat == null) { versionTextField.setText(""); ephemeralOwnerTextField.setText(""); cTimeTextField.setText(""); mTimeTextField.setText(""); pathTextField.setText(""); dataTextArea.setText(""); dataSizeLabel.setText(""); initData = null; pathLabel.setText(PATH); pathTextField.setText(path == null ? "" : path); dataTextArea.setEnabled(false); reloadButton.setEnabled(false); clearButton.setEnabled(false); saveButton.setEnabled(false); } else { dataTextArea.setEnabled(true); dataTextArea.setEditable(true); reloadButton.setEnabled(true); pathLabel.setText(stat.getEphemeralOwner() == 0 ? PATH : PATH_EPHEMERAL); pathTextField.setText(path); versionTextField.setText(Integer.toString(stat.getVersion())); ephemeralOwnerTextField.setText(Long.toString(stat.getEphemeralOwner())); cTimeTextField.setText(new DateTime(stat.getCtime()).toString(ZooDirectorFrame.DATE_FORMAT)); mTimeTextField.setText(new DateTime(stat.getMtime()).toString(ZooDirectorFrame.DATE_FORMAT)); initData = data == null ? "" : new String(data); dataTextArea.setText(initData); isDataUpdated(); } } /** * Disable editing of zookeeper node. */ public void setOffline() { executeSwingWorker(null); dataTextArea.setEditable(false); dataTextArea.setEnabled(false); clearButton.setEnabled(false); saveButton.setEnabled(false); reloadButton.setEnabled(false); } public void setZookeeperSync(ZookeeperSync zookeeperSync) { this.zookeeperSync = zookeeperSync; } }