Undo/redo command stack in Java

Another fragment that turned out to be useless for the project, but may come handy in the future. A port to JavaScript should be easy enough, too:

import java.util.ArrayList;
import java.util.List;

public class CommandStack {

	/**
	 * Implement for each individual change that can occur in an editor.
	 * 

* Both {@link #execute()} and {@link #undo()} should fire property change * events for notifying the editor of an update. */ public static abstract class Command { private String name; public Command(String name) { this.name = name; } @Override public String toString() { return name; } /** * Implements the actual change. *

* Any state-saving for {@link #undo()} * has to occur elsewhere, eg. in the constructor. */ public abstract void execute(); /** * Reverts any changes applied by {@link #execute()}. */ public abstract void undo(); } private final List commands = new ArrayList(); private int currentLocation = -1; private int saveLocation = currentLocation; public void add(Command command) { clearInFrontOfCurrent(); command.execute(); commands.add(command); currentLocation++; } public void undo() { commands.get(currentLocation).undo(); currentLocation--; } public boolean undoEnabled() { return currentLocation >= 0; } public void redo() { currentLocation++; commands.get(currentLocation).execute(); } public boolean redoEnabled() { return currentLocation < commands.size() - 1; } public boolean dirty() { return currentLocation != saveLocation; } private void clearInFrontOfCurrent() { while (currentLocation < commands.size() - 1) { commands.remove(currentLocation + 1); } } public void markSaveLocation() { saveLocation = currentLocation; } @Override public String toString() { return commands.toString(); } }

And to illustrate the usage, the JUnit test:

import junit.framework.TestCase;

public class CommandStackTest extends TestCase {

	CommandStack stack = new CommandStack();

	private String name;
	private int age;

	class EditName extends Command {

		private String oldName = name;
		private String newName;

		public EditName(String newName) {
			super("Update name to " + newName);
			this.newName = newName;
		}

		public void execute() {
			name = newName;
		}

		public void undo() {
			name = oldName;
		}
	};
	
	class EditAge extends Command {

		private int oldAge = age;
		private int newAge;

		public EditAge(int newAge) {
			super("Update age to " + newAge);
			this.newAge = newAge;
		}

		public void execute() {
			age = newAge;
		}

		public void undo() {
			age = oldAge;
		}
	};

	public void test_basics() {
		assertNull(name);
		assertEquals(0, age);
		assertFalse(stack.dirty());
		assertFalse(stack.undoEnabled());
		assertFalse(stack.redoEnabled());
		
		stack.add(new EditName("Peter"));
		
		assertTrue(stack.dirty());
		assertEquals("Peter", name);
		assertTrue(stack.undoEnabled());
		assertFalse(stack.redoEnabled());
		
		stack.undo();
		
		assertNull(name);
		assertFalse(stack.undoEnabled());
		assertTrue(stack.redoEnabled());
		assertFalse(stack.dirty());
		
		stack.redo();
		
		assertTrue(stack.dirty());
		assertEquals("Peter", name);
		assertTrue(stack.undoEnabled());
		assertFalse(stack.redoEnabled());
		
		stack.markSaveLocation();
		
		assertFalse(stack.dirty());
		assertEquals("Peter", name);
		assertTrue(stack.undoEnabled());
		assertFalse(stack.redoEnabled());
		
		stack.add(new EditAge(10));
		
		assertTrue(stack.dirty());
		assertEquals(10, age);
		assertTrue(stack.undoEnabled());
		assertFalse(stack.redoEnabled());
		
		stack.undo();
		
		assertFalse(stack.dirty());
		assertEquals(0, age);
		assertTrue(stack.undoEnabled());
		assertTrue(stack.redoEnabled());
		
		stack.add(new EditName("Pan"));
		
		assertTrue(stack.dirty());
		assertEquals("Pan", name);
		assertEquals(0, age);
		assertTrue(stack.undoEnabled());
		assertFalse(stack.redoEnabled());
		
		stack.undo();
		stack.undo();
		
		assertTrue(stack.dirty());
		assertNull(name);
		assertEquals(0, age);
		assertFalse(stack.undoEnabled());
		assertTrue(stack.redoEnabled());
		
		stack.redo();
		stack.redo();
		
		assertTrue(stack.dirty());
		assertEquals("Pan", name);
		assertEquals(0, age);
		assertTrue(stack.undoEnabled());
		assertFalse(stack.redoEnabled());
	}

}
-Jörn