Rework the cliche library, using Strategy to delegate command processing. Implement the Command pattern into a simplified version of Pacemaker. Extend the implementation to include undo/redo capability
Clone this repository:
Create a new Eclipse java project called 'cliche' and incorporate the source into this project. The simplest way of doing this is to create a new eclipse project and drag/drop the src/main/java/com
folder directly into the src
folder of the blank eclipse project.
Using the pacemaker-console project from lab-01, or this repo here:
Import into eclipse.
This project contains an jar file for cliche. Remove this from the project. This will generate errors.
Fix these errors by making the pacemaker-console project depend on the cliche project (Project Properties->Java Build Path->Projects)
You will need to change the imports in PacemakerShell, as the packages have been renamed in the github repo:
import com.budhash.cliche.Command;
import com.budhash.cliche.Param;
import com.budhash.cliche.Shell;
import com.budhash.cliche.ShellFactory;
Verify that pacemaker-console works as expected.
Make the following small change to pacemaker-console:
This will trigger a range of visibility errors, as PacemakerShell is now in a different package. All of the errors can be removed by making all methods in PacemakerShell public.
We would like to make a small adjustment to the cliche library, using the Strategy pattern to allow clients to 'hook into' the command processing engine.
In the com.budhash.cliche package, introduce the following interface:
package com.budhash.cliche;
public interface CommandProcessor
{
public void doCommand(ShellCommand command, Object[] parameters);
}
In the Shell class, introduce the following public member:
public CommandProcessor processor;
In the ShellFactory, adjust the following method to initialise this new member:
public static Shell createConsoleShell(String prompt, String appName, Object mainHandler, CommandProcessor processor) {
Shell shell = createConsoleShell(prompt, appName, mainHandler, new EmptyMultiMap<String, Object>());
shell.processor = processor;
return shell;
}
Finally, rework this Shell method to engage this strategy if it it initialized:
private void processCommand(String discriminator, List<Token> tokens) throws CLIException {
assert discriminator != null;
assert ! discriminator.equals("");
ShellCommand commandToInvoke = commandTable.lookupCommand(discriminator, tokens);
Class[] paramClasses = commandToInvoke.getMethod().getParameterTypes();
Object[] parameters = inputConverter.convertToParameters(tokens, paramClasses,
commandToInvoke.getMethod().isVarArgs());
outputHeader(commandToInvoke.getHeader(), parameters);
long timeBefore = Calendar.getInstance().getTimeInMillis();
Object invocationResult = null;
//Object invocationResult = commandToInvoke.invoke(parameters);
if (processor!= null)
{
processor.doCommand(commandToInvoke, parameters);
}
else
{
invocationResult = commandToInvoke.invoke(parameters);
}
long timeAfter = Calendar.getInstance().getTimeInMillis();
if (invocationResult != null) {
output.output(invocationResult, outputConverter);
}
if (displayTime) {
final long time = timeAfter - timeBefore;
if (time != 0L) {
output.output(String.format(TIME_MS_FORMAT_STRING, time), outputConverter);
}
}
}
In the above the relevant changes are just this:
Object invocationResult = null;
//Object invocationResult = commandToInvoke.invoke(parameters);
if (processor!= null)
{
processor.doCommand(commandToInvoke, parameters);
}
else
{
invocationResult = commandToInvoke.invoke(parameters);
}
The overall effect of these adjustments is that we can choose to short circuit the command processor, and have all commands rerouted to our processor.
We will make use of this shortly.
Introduce a new package called 'command' into the pacemaker-console project.
Create a new class called Command
:
package command;
import parsers.Parser;
import controllers.PacemakerAPI;
public abstract class Command
{
protected PacemakerAPI pacemaker;
protected Parser parser;
public Command()
{}
public Command(PacemakerAPI pacemaker, Parser parser)
{
this.pacemaker = pacemaker;
this.parser = parser;
}
public abstract void doCommand(Object[] parameters) throws Exception;
}
Include the following three commands:
package command;
import parsers.Parser;
import controllers.PacemakerAPI;
public class ListUsersCommand extends Command
{
public ListUsersCommand(PacemakerAPI pacemaker, Parser parser)
{
super(pacemaker, parser);
}
public void doCommand(Object[] parameters) throws Exception
{
System.out.println(parser.renderUsers(pacemaker.getUsers()));
}
}
package command;
import models.User;
import parsers.Parser;
import controllers.PacemakerAPI;
public class CreateUserCommand extends Command
{
User user;
public CreateUserCommand(PacemakerAPI pacemaker, Parser parser)
{
super(pacemaker, parser);
}
public void doCommand(Object[] parameters) throws Exception
{
Long id = pacemaker.createUser((String)parameters[0], (String)parameters[1], (String)parameters[2], (String)parameters[3]);
System.out.println(parser.renderUser(pacemaker.getUser(id)));
this.user = pacemaker.getUser(id);
}
}
package command;
import models.User;
import parsers.Parser;
import controllers.PacemakerAPI;
public class DeleteUserCommand extends Command
{
private User user;
public DeleteUserCommand(PacemakerAPI pacemaker, Parser parser)
{
super(pacemaker, parser);
}
public void doCommand(Object[] parameters) throws Exception
{
this.user = pacemaker.getUser((Long)parameters[0]);
pacemaker.deleteUser((Long)parameters[0]);
}
}
Also in the command package, incorporate this class:
package command;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
public class CommandDispatcher
{
private Map<String, Command> commands;
public CommandDispatcher()
{
commands = new HashMap<String, Command>();
}
public void addCommand(String commandName, Command command)
{
commands.put(commandName, command);
}
public boolean dispatchCommand(String commandName, Object [] parameters) throws Exception
{
boolean dispatched = false;
Command command = commands.get(commandName);
if (command != null)
{
dispatched = true;
command.doCommand(parameters);
}
return dispatched;
}
}
Finally, this class here will encapsulate the 'cliche' command specifications:
package command;
import com.budhash.cliche.Command;
import com.budhash.cliche.Param;
public class CommandSpecifications
{
@Command(description="List all users details")
public void listUsers () throws Exception
{}
@Command(description="Create a new User")
public void createUser (@Param(name="first name") String firstname, @Param(name="last name") String lastname,
@Param(name="email") String email, @Param(name="password") String password) throws Exception
{}
@Command(description="Delete a User")
public void deleteUser (@Param(name="id") Long id)
{}
}
Note, however, that we do not have any implementations here, as we will be using the command objects + command dispatcher to trigger the commands themselves.
We are going to significantly simplify the project in order to explore command without too much distraction. So, delete the 'PacemakerService' class from the 'controllers' package.
Now, replace the PacemakerShell class with the following simpler version:
package main;
import parsers.AsciiParser;
import parsers.Parser;
import command.CommandDispatcher;
import command.CommandSpecifications;
import command.CreateUserCommand;
import command.DeleteUserCommand;
import command.ListUsersCommand;
import controllers.PacemakerAPI;
import com.budhash.cliche.CommandProcessor;
import com.budhash.cliche.Shell;
import com.budhash.cliche.ShellCommand;
import com.budhash.cliche.ShellFactory;
public class PacemakerShell implements CommandProcessor
{
private CommandDispatcher dispatcher;
private PacemakerAPI paceApi;
public PacemakerShell()
{
Parser parser = new AsciiParser();
paceApi = new PacemakerAPI();
dispatcher = new CommandDispatcher();
dispatcher.addCommand("list-users", new ListUsersCommand(paceApi, parser));
dispatcher.addCommand("create-user", new CreateUserCommand(paceApi, parser));
dispatcher.addCommand("delete-user", new DeleteUserCommand(paceApi, parser));
}
@Override
public void doCommand(ShellCommand command, Object[] parameters)
{
try
{
dispatcher.dispatchCommand(command.getName(), parameters);
}
catch (Exception e)
{
System.out.println("Error executing command");
}
}
public static void main(String[] args) throws Exception
{
PacemakerShell main = new PacemakerShell();
CommandSpecifications commandSpecs = new CommandSpecifications();
Shell shell = ShellFactory.createConsoleShell("pm", "Welcome to pacemaker-console - ?help for instructions", commandSpecs, main);
shell.commandLoop();
}
}
This application should run now. We only support three commands:
Experiment with them and verify the behave as expected.
In order to support undo/redo, we can equip the dispatcher with and undo/redo stack:
private Stack<Command> undoBuffer;
private Stack<Command> redoBuffer;
... and in the constructor intialise this:
public CommandDispatcher()
{
undoBuffer = new Stack<Command>();
redoBuffer = new Stack<Command>();
commands = new HashMap<String, Command>();
commands.put("undo", new UndoCommand(undoBuffer, redoBuffer));
commands.put("redo", new RedoCommand(undoBuffer, redoBuffer));
}
These are the Undo and Redo command classes:
package command;
import java.util.Stack;
public class UndoCommand extends Command
{
private Stack<Command> undoBuffer;
private Stack<Command> redoBuffer;
public UndoCommand(Stack<Command> undoBuffer, Stack<Command> redoBuffer)
{
this.undoBuffer = undoBuffer;
this.redoBuffer = redoBuffer;
}
public void doCommand(Object[] parameters) throws Exception
{
if (undoBuffer.size() > 0)
{
Command command = undoBuffer.pop();
command.undoCommand();
redoBuffer.push(command);
}
}
}
package command;
import java.util.Stack;
public class RedoCommand extends Command
{
private Stack<Command> undoBuffer;
private Stack<Command> redoBuffer;
public RedoCommand(Stack<Command> undoBuffer, Stack<Command> redoBuffer)
{
this.undoBuffer = undoBuffer;
this.redoBuffer = redoBuffer;
}
public void doCommand(Object[] parameters) throws Exception
{
if (redoBuffer.size() > 0)
{
Command command = redoBuffer.pop();
command.redoCommand();
undoBuffer.push(command);
}
}
}
Note that they require 'undo' and 'redo methods in the command class:
public abstract class Command
{
//...
public void undoCommand() throws Exception
{}
public void redoCommand() throws Exception
{}
}
Which we can implement if we choose. They make sense for CreateUser and DeleteUser, but not for ListUsers:
public class CreateUserCommand extends Command
{
//...
public void undoCommand() throws Exception
{
pacemaker.deleteUser(user.id);
}
public void redoCommand() throws Exception
{
pacemaker.createUser(user.firstname, user.lastname, user.email, user.password);
}
}
public class DeleteUserCommand extends Command
{
//...
public void undoCommand() throws Exception
{
pacemaker.createUser(user.firstname, user.lastname, user.email, user.password);
}
public void redoCommand() throws Exception
{
pacemaker.deleteUser(user.id);
}
}
We also need to make these commands known to cliche by putting them in the CommandDescriptor class:
@Command(description="undo last command")
public void undo () throws Exception
{}
@Command(description="redo last command")
public void redo () throws Exception
{}
The dispatchCommand method will need to be reworked as follows:
public boolean dispatchCommand(String commandName, Object [] parameters) throws Exception
{
boolean dispatched = false;
Command command = commands.get(commandName);
if (command != null)
{
dispatched = true;
command.doCommand(parameters);
if ((command instanceof CreateUserCommand) || (command instanceof DeleteUserCommand))
{
undoBuffer.push(command);
}
}
return dispatched;
}
Note that only CreateUserCommand and DeleteUserCommand are 'undoable'.
We should be in a position to test this now. A session might go something like this:
Welcome to pacemaker-console - ?help for instructions
pm> cu a a a a
+----+-----------+----------+-------+----------+
| ID | FIRSTNAME | LASTNAME | EMAIL | PASSWORD |
+----+-----------+----------+-------+----------+
| 1 | a | a | a | a |
+----+-----------+----------+-------+----------+
pm> undo
pm> lu
pm>
Redo should also work:
Welcome to pacemaker-console - ?help for instructions
pm> cu a a a a
+----+-----------+----------+-------+----------+
| ID | FIRSTNAME | LASTNAME | EMAIL | PASSWORD |
+----+-----------+----------+-------+----------+
| 1 | a | a | a | a |
+----+-----------+----------+-------+----------+
pm> undo
pm> lu
pm> redo
pm> lu
+----+-----------+----------+-------+----------+
| ID | FIRSTNAME | LASTNAME | EMAIL | PASSWORD |
+----+-----------+----------+-------+----------+
| 2 | a | a | a | a |
+----+-----------+----------+-------+----------+
pm>
Introduce some more commands from the reference implementation. This will involve:
Try the following:
Welcome to pacemaker-console - ?help for instructions
pm> cu a a a a
+----+-----------+----------+-------+----------+
| ID | FIRSTNAME | LASTNAME | EMAIL | PASSWORD |
+----+-----------+----------+-------+----------+
| 1 | a | a | a | a |
+----+-----------+----------+-------+----------+
pm> cu b b b b
+----+-----------+----------+-------+----------+
| ID | FIRSTNAME | LASTNAME | EMAIL | PASSWORD |
+----+-----------+----------+-------+----------+
| 2 | b | b | b | b |
+----+-----------+----------+-------+----------+
pm> undo
pm> undo
Error executing command
pm>
Why are we getting the above error?
If you resolve this error, you will note it has a serious design flaw, requiring some significant rethink in order to make this work as expected.
HINT:
This pattern here may prove useful: