/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.jmeter.gui;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JTree;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import org.apache.jmeter.gui.action.UndoCommand;
import org.apache.jmeter.gui.tree.JMeterTreeModel;
import org.apache.jmeter.gui.tree.JMeterTreeNode;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
/**
* This class serves storing Test Tree state and navigating through it
* to give the undo/redo ability for test plan changes
*
* @since 2.12
*/
public class UndoHistory implements TreeModelListener, Serializable {
/**
*
*/
private static final long serialVersionUID = -974269825492906010L;
/**
* Interface to be implemented by components interested in UndoHistory
*/
public interface HistoryListener {
void notifyChangeInHistory(UndoHistory history);
}
/**
* Avoid storing too many elements
*
* @param <T>
*/
private static class LimitedArrayList<T> extends ArrayList<T> {
/**
*
*/
private static final long serialVersionUID = -6574380490156356507L;
private int limit;
public LimitedArrayList(int limit) {
this.limit = limit;
}
@Override
public boolean add(T item) {
if (this.size() + 1 > limit) {
this.remove(0);
}
return super.add(item);
}
}
private static final Logger log = LoggingManager.getLoggerForClass();
/**
* temporary storage for GUI tree expansion state
*/
private ArrayList<Integer> savedExpanded = new ArrayList<Integer>();
/**
* temporary storage for GUI tree selected row
*/
private int savedSelected = 0;
private static final int INITIAL_POS = -1;
private int position = INITIAL_POS;
private static final int HISTORY_SIZE = JMeterUtils.getPropDefault("undo.history.size", 0);
private List<UndoHistoryItem> history = new LimitedArrayList<UndoHistoryItem>(HISTORY_SIZE);
/**
* flag to prevent recursive actions
*/
private boolean working = false;
/**
* History listeners
*/
private List<HistoryListener> listeners = new ArrayList<UndoHistory.HistoryListener>();
public UndoHistory() {
}
/**
* Clears the undo history
*/
public void clear() {
if (working) {
return;
}
log.debug("Clearing undo history");
history.clear();
position = INITIAL_POS;
notifyListeners();
}
/**
* Add tree model copy to the history
* <p/>
* This method relies on the rule that the record in history made AFTER
* change has been made to test plan
*
* @param treeModel JMeterTreeModel
* @param comment String
*/
public void add(JMeterTreeModel treeModel, String comment) {
if(!isEnabled()) {
log.debug("undo.history.size is set to 0, undo/redo feature is disabled");
return;
}
// don't add element if we are in the middle of undo/redo or a big loading
if (working) {
log.debug("Not adding history because of noop");
return;
}
JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot();
if (root.getChildCount() < 1) {
log.debug("Not adding history because of no children");
return;
}
String name = root.getName();
log.debug("Adding history element " + name + ": " + comment);
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);
}
// cloning is required because we need to immute stored data
HashTree copy = UndoCommand.convertAndCloneSubTree(tree);
history.add(new UndoHistoryItem(copy, comment));
log.debug("Added history element, position: " + position + ", size: " + history.size());
working = false;
notifyListeners();
}
/**
* Goes through undo history, changing GUI
*
* @param offset the direction to go to, usually -1 for undo or 1 for redo
* @param acceptorModel TreeModel to accept the changes
*/
public void moveInHistory(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;
}
if (offset > 0 && !canRedo()) {
log.warn("Can't redo, we're already on the first record");
return;
}
if (history.isEmpty()) {
log.warn("Can't proceed, the history is empty");
return;
}
position += offset;
final GuiPackage guiInstance = GuiPackage.getInstance();
// save tree expansion and selection state before changing the tree
saveTreeState(guiInstance);
// load the tree
loadHistoricalTree(acceptorModel, guiInstance);
// load tree UI state
restoreTreeState(guiInstance);
log.debug("Current position " + position + ", size is " + history.size());
// refresh the all ui
guiInstance.updateCurrentGui();
guiInstance.getMainFrame().repaint();
notifyListeners();
}
/**
* Load the undo item into acceptorModel tree
*
* @param acceptorModel tree to accept the data
* @param guiInstance
*/
private void loadHistoricalTree(JMeterTreeModel acceptorModel, GuiPackage guiInstance) {
HashTree newModel = history.get(position).getTree();
acceptorModel.removeTreeModelListener(this);
working = true;
try {
guiInstance.getTreeModel().clearTestPlan();
guiInstance.addSubTree(newModel);
} catch (Exception ex) {
log.error("Failed to load from history", ex);
}
acceptorModel.addTreeModelListener(this);
working = false;
}
/**
* @return true if remaing items
*/
public boolean canRedo() {
return position < history.size() - 1;
}
/**
* @return true if not at first element
*/
public boolean canUndo() {
return position > INITIAL_POS + 1;
}
/**
* Record the changes in the node as the undo step
*
* @param tme
*/
@Override
public void treeNodesChanged(TreeModelEvent tme) {
String name = ((JMeterTreeNode) tme.getTreePath().getLastPathComponent()).getName();
log.debug("Nodes changed " + name);
final JMeterTreeModel sender = (JMeterTreeModel) tme.getSource();
add(sender, "Node changed " + name);
}
/**
* Record adding nodes as the undo step
*
* @param tme
*/
@Override
public void treeNodesInserted(TreeModelEvent tme) {
String name = ((JMeterTreeNode) tme.getTreePath().getLastPathComponent()).getName();
log.debug("Nodes inserted " + name);
final JMeterTreeModel sender = (JMeterTreeModel) tme.getSource();
add(sender, "Add " + name);
}
/**
* Record deleting nodes as the undo step
*
* @param tme
*/
@Override
public void treeNodesRemoved(TreeModelEvent tme) {
String name = ((JMeterTreeNode) tme.getTreePath().getLastPathComponent()).getName();
log.debug("Nodes removed: " + name);
add((JMeterTreeModel) tme.getSource(), "Remove " + name);
}
/**
* Record some other change
*
* @param tme
*/
@Override
public void treeStructureChanged(TreeModelEvent tme) {
log.debug("Nodes struct changed");
add((JMeterTreeModel) tme.getSource(), "Complex Change");
}
/**
* Save tree expanded and selected state
*
* @param guiPackage
*/
private void saveTreeState(GuiPackage guiPackage) {
savedExpanded.clear();
MainFrame mainframe = guiPackage.getMainFrame();
if (mainframe != null) {
final JTree tree = mainframe.getTree();
savedSelected = tree.getMinSelectionRow();
for (int rowN = 0; rowN < tree.getRowCount(); rowN++) {
if (tree.isExpanded(rowN)) {
savedExpanded.add(rowN);
}
}
}
}
/**
* Restore tree expanded and selected state
*
* @param guiPackage
*/
private void restoreTreeState(GuiPackage guiInstance) {
final JTree tree = guiInstance.getMainFrame().getTree();
if (savedExpanded.size() > 0) {
for (int rowN : savedExpanded) {
tree.expandRow(rowN);
}
} else {
tree.expandRow(0);
}
tree.setSelectionRow(savedSelected);
}
/**
*
* @return true if history is enabled
*/
boolean isEnabled() {
return HISTORY_SIZE > 0;
}
/**
* Register HistoryListener
* @param listener
*/
public void registerHistoryListener(HistoryListener listener) {
listeners.add(listener);
}
/**
* Notify listener
*/
private void notifyListeners() {
for (HistoryListener listener : listeners) {
listener.notifyChangeInHistory(this);
}
}
}