Line 0
Link Here
|
|
|
1 |
/* |
2 |
* Licensed to the Apache Software Foundation (ASF) under one or more |
3 |
* contributor license agreements. See the NOTICE file distributed with |
4 |
* this work for additional information regarding copyright ownership. |
5 |
* The ASF licenses this file to You under the Apache License, Version 2.0 |
6 |
* (the "License"); you may not use this file except in compliance with |
7 |
* the License. You may obtain a copy of the License at |
8 |
* |
9 |
* http://www.apache.org/licenses/LICENSE-2.0 |
10 |
* |
11 |
* Unless required by applicable law or agreed to in writing, software |
12 |
* distributed under the License is distributed on an "AS IS" BASIS, |
13 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 |
* See the License for the specific language governing permissions and |
15 |
* limitations under the License. |
16 |
* |
17 |
*/ |
18 |
|
19 |
package org.apache.jmeter.gui; |
20 |
|
21 |
import java.io.Serializable; |
22 |
import java.util.ArrayList; |
23 |
import java.util.List; |
24 |
|
25 |
import javax.swing.*; |
26 |
import javax.swing.event.TreeModelEvent; |
27 |
import javax.swing.event.TreeModelListener; |
28 |
|
29 |
import org.apache.jmeter.gui.action.UndoCommand; |
30 |
import org.apache.jmeter.gui.tree.JMeterTreeModel; |
31 |
import org.apache.jmeter.gui.tree.JMeterTreeNode; |
32 |
import org.apache.jorphan.collections.HashTree; |
33 |
import org.apache.jorphan.logging.LoggingManager; |
34 |
import org.apache.log.Logger; |
35 |
|
36 |
/** |
37 |
* Users expected record situations: initial empty tree; before node deletion; |
38 |
* before node insertion; after each walk off edited node (modifyTestElement) |
39 |
*/ |
40 |
public class UndoHistory implements TreeModelListener, Serializable { |
41 |
private ArrayList<Integer> savedExpanded = new ArrayList<Integer>(); |
42 |
private int savedSelected = 0; |
43 |
|
44 |
/** |
45 |
* Avoid storing too many elements |
46 |
* |
47 |
* @param <T> |
48 |
*/ |
49 |
private static class LimitedArrayList<T> extends ArrayList<T> { |
50 |
/** |
51 |
* |
52 |
*/ |
53 |
private static final long serialVersionUID = -6574380490156356507L; |
54 |
private int limit; |
55 |
|
56 |
public LimitedArrayList(int limit) { |
57 |
this.limit = limit; |
58 |
} |
59 |
|
60 |
@Override |
61 |
public boolean add(T item) { |
62 |
if (this.size() + 1 > limit) { |
63 |
this.remove(0); |
64 |
} |
65 |
return super.add(item); |
66 |
} |
67 |
} |
68 |
|
69 |
private static final int INITIAL_POS = -1; |
70 |
private static final Logger log = LoggingManager.getLoggerForClass(); |
71 |
|
72 |
private List<UndoHistoryItem> history = new LimitedArrayList<UndoHistoryItem>(25); // TODO Make this configurable or too many properties ? |
73 |
private int position = INITIAL_POS; |
74 |
|
75 |
/** |
76 |
* flag to prevent recursive actions |
77 |
*/ |
78 |
private boolean working = false; |
79 |
|
80 |
public UndoHistory() { |
81 |
} |
82 |
|
83 |
/** |
84 |
* @return true if must not put in history |
85 |
*/ |
86 |
private boolean noop() { |
87 |
return working; |
88 |
} |
89 |
|
90 |
/** |
91 |
* |
92 |
*/ |
93 |
public void clear() { |
94 |
if (noop()) { |
95 |
return; |
96 |
} |
97 |
log.debug("Clearing undo history", new Throwable()); |
98 |
history.clear(); |
99 |
position = INITIAL_POS; |
100 |
} |
101 |
|
102 |
/** |
103 |
* this method relies on the rule that the record in history made AFTER |
104 |
* change has been made to test plan |
105 |
* |
106 |
* @param treeModel JMeterTreeModel |
107 |
* @param comment String |
108 |
*/ |
109 |
public void add(JMeterTreeModel treeModel, String comment) { |
110 |
// don't add element if we are in the middle of undo/redo or a big loading |
111 |
if (noop()) { |
112 |
log.debug("Not adding history because of noop"); |
113 |
return; |
114 |
} |
115 |
JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot(); |
116 |
if (root.getChildCount() < 1) { |
117 |
log.debug("Not adding history because of no children", new Throwable()); |
118 |
return; |
119 |
} |
120 |
|
121 |
String name = ((JMeterTreeNode) treeModel.getRoot()).getName(); |
122 |
|
123 |
if (log.isDebugEnabled()) { |
124 |
log.debug("Adding history element " + name + ": " + comment, new Throwable()); |
125 |
} |
126 |
|
127 |
working = true; |
128 |
// get test plan tree |
129 |
HashTree tree = treeModel.getCurrentSubTree((JMeterTreeNode) treeModel.getRoot()); |
130 |
// first clone to not convert original tree |
131 |
tree = (HashTree) tree.getTree(tree.getArray()[0]).clone(); |
132 |
|
133 |
position++; |
134 |
while (history.size() > position) { |
135 |
log.debug("Removing further record, position: " + position + ", size: " + history.size()); |
136 |
history.remove(history.size() - 1); |
137 |
} |
138 |
|
139 |
// cloning is required because we need to immute stored data |
140 |
HashTree copy = UndoCommand.convertSubTree(tree); |
141 |
|
142 |
history.add(new UndoHistoryItem(copy, comment)); |
143 |
|
144 |
log.debug("Added history element, position: " + position + ", size: " + history.size()); |
145 |
working = false; |
146 |
} |
147 |
|
148 |
public void getRelativeState(int offset, JMeterTreeModel acceptorModel) { |
149 |
log.debug("Moving history from position " + position + " with step " + offset + ", size is " + history.size()); |
150 |
if (offset < 0 && !canUndo()) { |
151 |
log.warn("Can't undo, we're already on the last record"); |
152 |
return; |
153 |
} |
154 |
|
155 |
if (offset > 0 && !canRedo()) { |
156 |
log.warn("Can't redo, we're already on the first record"); |
157 |
return; |
158 |
} |
159 |
|
160 |
if (history.isEmpty()) { |
161 |
log.warn("Can't proceed, the history is empty"); |
162 |
return; |
163 |
} |
164 |
|
165 |
position += offset; |
166 |
|
167 |
final GuiPackage guiInstance = GuiPackage.getInstance(); |
168 |
|
169 |
saveTreeState(guiInstance); |
170 |
|
171 |
loadHistoricalTree(acceptorModel, guiInstance); |
172 |
|
173 |
log.debug("Current position " + position + ", size is " + history.size()); |
174 |
|
175 |
restoreTreeState(guiInstance); |
176 |
|
177 |
guiInstance.updateCurrentGui(); |
178 |
guiInstance.getMainFrame().repaint(); |
179 |
} |
180 |
|
181 |
private void loadHistoricalTree(JMeterTreeModel acceptorModel, GuiPackage guiInstance) { |
182 |
HashTree newModel = history.get(position).getTree(); |
183 |
acceptorModel.removeTreeModelListener(this); |
184 |
working = true; |
185 |
try { |
186 |
guiInstance.getTreeModel().clearTestPlan(); |
187 |
guiInstance.addSubTree(newModel); |
188 |
} catch (Exception ex) { |
189 |
log.error("Failed to load from history", ex); |
190 |
} |
191 |
acceptorModel.addTreeModelListener(this); |
192 |
working = false; |
193 |
} |
194 |
|
195 |
/** |
196 |
* @return true if remaing items |
197 |
*/ |
198 |
public boolean canRedo() { |
199 |
return position < history.size() - 1; |
200 |
} |
201 |
|
202 |
/** |
203 |
* @return true if not at first element |
204 |
*/ |
205 |
public boolean canUndo() { |
206 |
return position > INITIAL_POS + 1; |
207 |
} |
208 |
|
209 |
public void treeNodesChanged(TreeModelEvent tme) { |
210 |
String name = ((JMeterTreeNode) tme.getTreePath().getLastPathComponent()).getName(); |
211 |
log.debug("Nodes changed " + name); |
212 |
final JMeterTreeModel sender = (JMeterTreeModel) tme.getSource(); |
213 |
add(sender, "Node changed " + name); |
214 |
} |
215 |
|
216 |
/** |
217 |
* |
218 |
*/ |
219 |
// FIXME: is there better way to record test plan load events? currently it records each node added separately |
220 |
public void treeNodesInserted(TreeModelEvent tme) { |
221 |
String name = ((JMeterTreeNode) tme.getTreePath().getLastPathComponent()).getName(); |
222 |
log.debug("Nodes inserted " + name); |
223 |
final JMeterTreeModel sender = (JMeterTreeModel) tme.getSource(); |
224 |
add(sender, "Add " + name); |
225 |
} |
226 |
|
227 |
/** |
228 |
* |
229 |
*/ |
230 |
public void treeNodesRemoved(TreeModelEvent tme) { |
231 |
String name = ((JMeterTreeNode) tme.getTreePath().getLastPathComponent()).getName(); |
232 |
log.debug("Nodes removed: " + name); |
233 |
add((JMeterTreeModel) tme.getSource(), "Remove " + name); |
234 |
} |
235 |
|
236 |
/** |
237 |
* |
238 |
*/ |
239 |
public void treeStructureChanged(TreeModelEvent tme) { |
240 |
log.debug("Nodes struct changed"); |
241 |
add((JMeterTreeModel) tme.getSource(), "Complex Change"); |
242 |
} |
243 |
|
244 |
/** |
245 |
* @param guiPackage |
246 |
* @return int[] |
247 |
*/ |
248 |
private void saveTreeState(GuiPackage guiPackage) { |
249 |
savedExpanded.clear(); |
250 |
|
251 |
MainFrame mainframe = guiPackage.getMainFrame(); |
252 |
if (mainframe != null) { |
253 |
final JTree tree = mainframe.getTree(); |
254 |
savedSelected = tree.getMinSelectionRow(); |
255 |
|
256 |
for (int rowN = 0; rowN < tree.getRowCount(); rowN++) { |
257 |
if (tree.isExpanded(rowN)) { |
258 |
savedExpanded.add(rowN); |
259 |
} |
260 |
} |
261 |
} |
262 |
} |
263 |
|
264 |
private void restoreTreeState(GuiPackage guiInstance) { |
265 |
final JTree tree = guiInstance.getMainFrame().getTree(); |
266 |
|
267 |
if (savedExpanded.size() > 0) { |
268 |
for (int rowN : savedExpanded) { |
269 |
tree.expandRow(rowN); |
270 |
} |
271 |
} else { |
272 |
tree.expandRow(0); |
273 |
} |
274 |
tree.setSelectionRow(savedSelected); |
275 |
} |
276 |
|
277 |
} |