# This patch file was generated by NetBeans IDE # Following Index: paths are relative to: /home/undera/NetBeansProjects/JMeter/trunk # This patch can be applied using context Tools: Patch action on respective folder. # It uses platform neutral UTF-8 encoding and \n newlines. # Above lines and this line are ignored by the patching process. Index: src/core/org/apache/jmeter/gui/GuiPackage.java --- src/core/org/apache/jmeter/gui/GuiPackage.java Base (BASE) +++ src/core/org/apache/jmeter/gui/GuiPackage.java Locally Modified (Based On LOCAL) @@ -32,6 +32,7 @@ import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; +import javax.swing.tree.TreePath; import org.apache.jmeter.engine.util.ValueReplacer; import org.apache.jmeter.exceptions.IllegalUserActionException; @@ -43,6 +44,9 @@ import org.apache.jmeter.testbeans.gui.TestBeanGUI; import org.apache.jmeter.testelement.TestElement; import org.apache.jmeter.testelement.TestPlan; +import org.apache.jmeter.testelement.property.JMeterProperty; +import org.apache.jmeter.testelement.property.PropertyIterator; +import org.apache.jmeter.testelement.property.TestElementProperty; import org.apache.jmeter.util.JMeterUtils; import org.apache.jmeter.util.LocaleChangeEvent; import org.apache.jmeter.util.LocaleChangeListener; @@ -394,7 +398,12 @@ log.debug("Updating current node " + currentNode.getName()); JMeterGUIComponent comp = getGui(currentNode.getTestElement()); TestElement el = currentNode.getTestElement(); + int before=getTestElementCheckSum(el); comp.modifyTestElement(el); + int after=getTestElementCheckSum(el); + if (before!=after) { + treeModel.saveUndoPoint(new TreePath(currentNode.getPath()), "Properties Changed"); + } currentNode.nameChanged(); // Bug 50221 - ensure label is updated } // The current node is now updated @@ -684,4 +693,22 @@ list.addAll(stoppables); return list; } + + // better would override hashCode for TestElement, but I decided to not touch it + // the method calculates properties checksum to detect testelement modifications + private int getTestElementCheckSum(TestElement el) { + int ret = el.getClass().hashCode(); + PropertyIterator it = el.propertyIterator(); + while (it.hasNext()) { + JMeterProperty obj = it.next(); + if (obj instanceof TestElementProperty) { + ret ^= getTestElementCheckSum(((TestElementProperty) obj).getElement()); + } else { + ret ^= obj.getName().hashCode(); + ret ^= obj.getStringValue().hashCode(); } + } + + return ret; + } +} \ No newline at end of file Index: src/core/org/apache/jmeter/gui/action/ActionNames.java --- src/core/org/apache/jmeter/gui/action/ActionNames.java Base (BASE) +++ src/core/org/apache/jmeter/gui/action/ActionNames.java Locally Modified (Based On LOCAL) @@ -83,6 +83,8 @@ public static final String SUB_TREE_SAVED = "sub_tree_saved"; // $NON-NLS-1$ public static final String TOGGLE = "toggle"; // $NON-NLS-1$ enable/disable public static final String WHAT_CLASS = "what_class"; // $NON-NLS-1$ + public static final String UNDO = "undo"; // $NON-NLS-1$ + public static final String REDO = "redo"; // $NON-NLS-1$ // Prevent instantiation private ActionNames(){ Index: src/core/org/apache/jmeter/gui/action/KeyStrokes.java --- src/core/org/apache/jmeter/gui/action/KeyStrokes.java Base (BASE) +++ src/core/org/apache/jmeter/gui/action/KeyStrokes.java Locally Modified (Based On LOCAL) @@ -56,6 +56,9 @@ public static final KeyStroke PASTE = KeyStroke.getKeyStroke(KeyEvent.VK_V, CONTROL_MASK); public static final KeyStroke WHAT_CLASS = KeyStroke.getKeyStroke(KeyEvent.VK_W, CONTROL_MASK); public static final KeyStroke CUT = KeyStroke.getKeyStroke(KeyEvent.VK_X, CONTROL_MASK); + public static final KeyStroke UNDO = KeyStroke.getKeyStroke(KeyEvent.VK_Z, CONTROL_MASK); + // does Ctrl+Shift+Z right standard for redo? + public static final KeyStroke REDO = KeyStroke.getKeyStroke(KeyEvent.VK_Z, CONTROL_MASK | KeyEvent.SHIFT_DOWN_MASK); public static final KeyStroke REMOTE_STOP_ALL = KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.ALT_DOWN_MASK); public static final KeyStroke REMOTE_SHUT_ALL = KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.ALT_DOWN_MASK); Index: src/core/org/apache/jmeter/gui/action/UndoCommand.java --- src/core/org/apache/jmeter/gui/action/UndoCommand.java Locally New +++ src/core/org/apache/jmeter/gui/action/UndoCommand.java Locally New @@ -0,0 +1,61 @@ +package org.apache.jmeter.gui.action; + +import java.awt.event.ActionEvent; +import java.util.HashSet; +import java.util.Set; +import javax.swing.JTree; +import javax.swing.tree.TreePath; +import org.apache.jmeter.exceptions.IllegalUserActionException; +import org.apache.jmeter.gui.GuiPackage; +import org.apache.jorphan.collections.HashTree; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; + +/** + * + * @author apc@apc.kg + */ +public class UndoCommand implements Command { + + private static final Logger log = LoggingManager.getLoggerForClass(); + private static final Set commands = new HashSet(); + + static { + commands.add(ActionNames.UNDO); + commands.add(ActionNames.REDO); + } + + @Override + public void doAction(ActionEvent e) throws IllegalUserActionException { + GuiPackage guiPackage = GuiPackage.getInstance(); + final String command = e.getActionCommand(); + + TreePath path; + if (command.equals(ActionNames.UNDO)) { + path = guiPackage.getTreeModel().goInHistory(-1); + } else if (command.equals(ActionNames.REDO)) { + path = guiPackage.getTreeModel().goInHistory(1); + } else { + throw new IllegalArgumentException("Wrong action called: " + command); + } + + // we need to go to recorded tree path + // fixme: we have a problem with unselected tree item then + // also the GUI reflects old GUI properties + //final JTree tree = GuiPackage.getInstance().getMainFrame().getTree(); + //tree.setSelectionPath(path); + guiPackage.updateCurrentGui(); + guiPackage.getMainFrame().repaint(); + } + + @Override + public Set getActionNames() { + return commands; + } + + // wrapper to use package-visible method + public static void convertSubTree(HashTree tree) { + Save executor = new Save(); + executor.convertSubTree(tree); + } +} Index: src/core/org/apache/jmeter/gui/tree/JMeterTreeModel.java --- src/core/org/apache/jmeter/gui/tree/JMeterTreeModel.java Base (BASE) +++ src/core/org/apache/jmeter/gui/tree/JMeterTreeModel.java Locally Modified (Based On LOCAL) @@ -24,6 +24,8 @@ import java.util.List; import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; import org.apache.jmeter.config.gui.AbstractConfigGui; import org.apache.jmeter.control.gui.TestPlanGui; @@ -42,10 +44,12 @@ public class JMeterTreeModel extends DefaultTreeModel { private static final long serialVersionUID = 240L; + private UndoHistory undoHistory = new UndoHistory(); public JMeterTreeModel(TestElement tp, TestElement wb) { super(new JMeterTreeNode(wb, null)); - initTree(tp,wb); + addTreeModelListener(undoHistory); + initTree(tp, wb); } public JMeterTreeModel() { @@ -242,5 +246,27 @@ // This should not be necessary, but without it, nodes are not shown when the user // uses the Close menu item nodeStructureChanged((JMeterTreeNode)getRoot()); + undoHistory.clear(); + saveUndoPoint(new TreePath(((JMeterTreeNode)getRoot()).getPath()), "Initial Tree"); } + + public TreePath goInHistory(int offset) { + return undoHistory.getRelativeState(offset, this); } + + public void saveUndoPoint(TreePath path, String comment) { + undoHistory.add(this, path, comment); + } + + public void clearUndo() { + undoHistory.clear(); + } + + public boolean canRedo() { + return undoHistory.canRedo(); + } + + public boolean canUndo() { + return undoHistory.canUndo(); + } +} Index: src/core/org/apache/jmeter/gui/tree/UndoHistory.java --- src/core/org/apache/jmeter/gui/tree/UndoHistory.java Locally New +++ src/core/org/apache/jmeter/gui/tree/UndoHistory.java Locally New @@ -0,0 +1,192 @@ +package org.apache.jmeter.gui.tree; + +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Map.Entry; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreePath; +import org.apache.jmeter.engine.TreeCloner; +import org.apache.jmeter.gui.GuiPackage; +import org.apache.jmeter.gui.action.Load; +import org.apache.jmeter.gui.action.UndoCommand; +import org.apache.jorphan.collections.HashTree; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; + +/** + * Users expected record situations: initial empty tree; before node deletion; + * before node insertion; after each walk off edited node (modifyTestElement) + * + * @author apc@apc.kg + */ +public class UndoHistory + implements TreeModelListener { + + private static final int INITIAL_POS = -1; + private static final Logger log = LoggingManager.getLoggerForClass(); + + private static class HistoryItem { + + private final HashTree tree; + private final TreePath path; + // maybe the comment should be removed since it is not used yet + private final String comment; + + public HistoryItem(HashTree copy, TreePath apath, String acomment) { + tree = copy; + path = apath; + comment = acomment; + } + + public HashTree getKey() { + return tree; + } + + public TreePath getValue() { + return path; + } + + public String getComment() { + return comment; + } + } + private ArrayList history = new ArrayList(); + private int position = INITIAL_POS; + /** + * flag to prevent recursive actions + */ + private boolean working = false; + + public UndoHistory() { + } + + public void clear() { + if (working) { + return; + } + log.debug("Clearing history", new Throwable()); + history.clear(); + position = INITIAL_POS; + } + + /** + * this method relies on the rule that the record in history made AFTER + * change has been made to test plan + * + * @param treeModel + * @param path + * @param comment + */ + void add(JMeterTreeModel treeModel, TreePath path, String comment) { + // don't add element if we are in the middle of undo/redo + if (working) { + return; + } + + log.debug("Adding history element", new Throwable()); + JMeterTreeNode root = (JMeterTreeNode) ((JMeterTreeNode) treeModel.getRoot()); + if (root.getChildCount() < 1) { + return; + } + + working = true; + // get test plan tree + HashTree tree = treeModel.getCurrentSubTree((JMeterTreeNode) treeModel.getRoot()); + // first clone to not convert original tree + tree = (HashTree) tree.getTree(tree.getArray()[0]).clone(); + + position++; + while (history.size() > position) { + log.debug("Removing further record, position: " + position + ", size: " + history.size()); + history.remove(history.size() - 1); + } + + // convert before clone! + UndoCommand.convertSubTree(tree); + TreeCloner cloner = new TreeCloner(false); + tree.traverse(cloner); + HashTree copy = cloner.getClonedTree(); + + + history.add(new HistoryItem(copy, path, comment)); + + log.debug("Added history element, position: " + position + ", size: " + history.size()); + working = false; + } + + public TreePath getRelativeState(int offset, JMeterTreeModel acceptorModel) { + log.debug("Moving history from position " + position + " with step " + offset + ", size is " + history.size()); + if (offset < 0 && !canUndo()) { + log.warn("Can't undo, we're already on the last record"); + return null; + } + + if (offset > 0 && !canRedo()) { + log.warn("Can't redo, we're already on the first record"); + return null; + } + + position += offset; + + if (!history.isEmpty()) { + HashTree newModel = history.get(position).getKey(); + acceptorModel.removeTreeModelListener(this); + working = true; + try { + boolean res = Load.insertLoadedTree(ActionEvent.ACTION_PERFORMED, newModel); + if (!res) { + throw new RuntimeException("Loaded data is not TestPlan"); + } + + } catch (Exception ex) { + log.error("Failed to load from history", ex); + } + acceptorModel.addTreeModelListener(this); + working = false; + } + log.debug("Current position " + position + ", size is " + history.size()); + // select historical path + return history.get(position).getValue(); + } + + public boolean canRedo() { + return position < history.size() - 1; + } + + public boolean canUndo() { + return position > INITIAL_POS + 1; + } + + public void treeNodesChanged(TreeModelEvent tme) { + log.debug("Nodes changed"); + } + + // is there better way to record test plan load events? + // currently it records each node added separately + public void treeNodesInserted(TreeModelEvent tme) { + log.debug("Nodes inserted"); + final JMeterTreeModel sender = (JMeterTreeModel) tme.getSource(); + add(sender, getTreePathToRecord(tme), "Add"); + } + + public void treeNodesRemoved(TreeModelEvent tme) { + log.debug("Nodes removed"); + add((JMeterTreeModel) tme.getSource(), getTreePathToRecord(tme), "Remove"); + } + + public void treeStructureChanged(TreeModelEvent tme) { + log.debug("Nodes struct changed"); + add((JMeterTreeModel) tme.getSource(), getTreePathToRecord(tme), "Complex Change"); + } + + private TreePath getTreePathToRecord(TreeModelEvent tme) { + TreePath path; + if (GuiPackage.getInstance() != null) { + path = GuiPackage.getInstance().getMainFrame().getTree().getSelectionPath(); + } else { + path = tme.getTreePath(); + } + return path; + } +} Index: src/core/org/apache/jmeter/gui/util/MenuFactory.java --- src/core/org/apache/jmeter/gui/util/MenuFactory.java Base (BASE) +++ src/core/org/apache/jmeter/gui/util/MenuFactory.java Locally Modified (Based On LOCAL) @@ -44,6 +44,7 @@ import org.apache.jmeter.gui.action.ActionNames; import org.apache.jmeter.gui.action.ActionRouter; import org.apache.jmeter.gui.action.KeyStrokes; +import org.apache.jmeter.gui.action.UndoCommand; import org.apache.jmeter.gui.tree.JMeterTreeNode; import org.apache.jmeter.samplers.Sampler; import org.apache.jmeter.testbeans.TestBean; @@ -194,6 +195,10 @@ } public static void addFileMenu(JPopupMenu menu) { + // the undo/redo as a standard goes first in Edit menus + // maybe there's better place for them in JMeter? + addUndoItems(menu); + addSeparator(menu); menu.add(makeMenuItemRes("open", ActionNames.OPEN));// $NON-NLS-1$ menu.add(makeMenuItemRes("menu_merge", ActionNames.MERGE));// $NON-NLS-1$ @@ -679,4 +684,19 @@ Collections.sort(menuToSort, new MenuInfoComparator()); } } + + private static void addUndoItems(JPopupMenu menu) { + addSeparator(menu); + + JMenuItem redo = makeMenuItemRes("redo", ActionNames.REDO); //$NON-NLS-1$ + redo.setAccelerator(KeyStrokes.REDO); + // we could even show some hints on action being undone here + // if this will be required (by passing those hints into history records) + redo.setEnabled(GuiPackage.getInstance().getTreeModel().canRedo()); + menu.add(redo); + JMenuItem undo = makeMenuItemRes("undo", ActionNames.UNDO); //$NON-NLS-1$ + undo.setAccelerator(KeyStrokes.UNDO); + undo.setEnabled(GuiPackage.getInstance().getTreeModel().canUndo()); + menu.add(undo); } +} Index: src/core/org/apache/jmeter/resources/messages.properties --- src/core/org/apache/jmeter/resources/messages.properties Base (BASE) +++ src/core/org/apache/jmeter/resources/messages.properties Locally Modified (Based On LOCAL) @@ -1090,3 +1090,5 @@ you_must_enter_a_valid_number=You must enter a valid number zh_cn=Chinese (Simplified) zh_tw=Chinese (Traditional) +undo=Undo +redo=Redo \ No newline at end of file Index: test/src/org/apache/jmeter/gui/tree/UndoHistoryTest.java --- test/src/org/apache/jmeter/gui/tree/UndoHistoryTest.java Locally New +++ test/src/org/apache/jmeter/gui/tree/UndoHistoryTest.java Locally New @@ -0,0 +1,90 @@ +package org.apache.jmeter.gui.tree; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; +import javax.swing.tree.TreePath; +import org.apache.jmeter.util.JMeterUtils; + +/** + * + * @author apc@apc.kg + */ +public class UndoHistoryTest extends junit.framework.TestCase { + + public UndoHistoryTest() { + File propsFile = null; + try { + propsFile = File.createTempFile("jmeter-plugins", "testProps"); + propsFile.deleteOnExit(); + } catch (IOException ex) { + ex.printStackTrace(System.err); + } + + //propsFile=new File("/home/undera/NetBeansProjects/jmeter/trunk/bin/jmeter.properties"); + + JMeterUtils.loadJMeterProperties(propsFile.getAbsolutePath()); + JMeterUtils.setLocale(new Locale("ignoreResources")); + } + + /* + * public void testGetTestElementCheckSum() { + * System.out.println("getTestElementCheckSum"); TestElement el = new + * TestAction(); int result = UndoHistory.getTestElementCheckSum(el); + * assertTrue(result!=0); el.setProperty(new BooleanProperty()); + * assertTrue(result != UndoHistory.getTestElementCheckSum(el)); } + * + * public void testGetTestElementCheckSum_stable() { + * System.out.println("getTestElementCheckSum stable"); TestElement el = new + * ThreadGroup(); AbstractJMeterGuiComponent gui = new ThreadGroupGui(); + * + * gui.modifyTestElement(el); int result1 = + * UndoHistory.getTestElementCheckSum(el); gui.modifyTestElement(el); int + * result2 = UndoHistory.getTestElementCheckSum(el); assertEquals(result1, + * result2); el.setProperty(new BooleanProperty()); assertTrue(result1 != + * UndoHistory.getTestElementCheckSum(el)); } + */ + public void testClear() { + System.out.println("clear"); + UndoHistory instance = new UndoHistory(); + instance.clear(); + } + + public void testAdd() throws Exception { + System.out.println("add"); + JMeterTreeModel treeModel = new JMeterTreeModel(); + UndoHistory instance = new UndoHistory(); + instance.add(treeModel, new TreePath(this), ""); + } + + public void testGetRelativeState() throws Exception { + System.out.println("getRelativeState"); + JMeterTreeModel treeModelRecv = new JMeterTreeModel(); + UndoHistory instance = new UndoHistory(); + + // safety check + instance.getRelativeState(-1, treeModelRecv); + instance.getRelativeState(1, treeModelRecv); + + + JMeterTreeModel treeModel1 = new JMeterTreeModel(); + JMeterTreeModel treeModel2 = new JMeterTreeModel(); + JMeterTreeModel treeModel3 = new JMeterTreeModel(); + instance.add(treeModel1, new TreePath(this), ""); + instance.add(treeModel2, new TreePath(this), ""); + instance.add(treeModel3, new TreePath(this), ""); + + // regular work check + instance.getRelativeState(-1, treeModelRecv); + instance.getRelativeState(-1, treeModelRecv); + instance.getRelativeState(-1, treeModelRecv); // undo ignored + instance.getRelativeState(1, treeModelRecv); + instance.getRelativeState(1, treeModelRecv); + instance.getRelativeState(1, treeModelRecv); // redo ignored + + // overwrite check + instance.getRelativeState(-1, treeModelRecv); + instance.getRelativeState(-1, treeModelRecv); + instance.add(treeModel3, new TreePath(this), ""); + } +}