Undo/redo command stack in Java
18. November 2009 um 14:48 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