I am currenly contributing to a Chat-Bot to be used across the whole SE-Network's chat which is implemented in Java 8.
This bot is supposed to have commands. These commands again are supposed to be as flexible as possible, and based on chat messages. To allow easy and extensible implementation of arbitrary commands I have created the following two classes:
- CommandHandle: This class defines when a command is "triggered", and specifies the code executed. It's supposed to be as "generic" as possible while allowing clean and specialized code.
- CommandHandleBuilder: This class is responsible for building instances of CommandHandles and defines a chainable API that's supposed to be easy to use for anybody willing to create their own commands.
Currently the bot itself is a work in progress, but a working one can be found on github.
Anyways let's get into code:
/**
* Simple handle for a Command. Consists of a {@link Predicate} to match
* messages (aka. invocations) against, a helpText,
* an infoText and a {@link Consumer Consumer} for {@link ChatMessage ChatMessages}
*
* @author vogel612<<a href="mailto:[email protected]">[email protected]</a>>
*/
public class CommandHandle {
private final Predicate<String> matchesSyntax;
private final String helpText;
private final String infoText;
private final Consumer<ChatMessage> executor;
public CommandHandle(
Predicate<String> matchesSyntax, Consumer<ChatMessage> executor,
String helpText, String infoText) {
this.matchesSyntax = matchesSyntax;
this.helpText = helpText;
this.infoText = infoText;
this.executor = executor;
}
public void execute(ChatMessage message) {
executor.accept(message);
}
public boolean matchesSyntax(String commandCall) {
return matchesSyntax.test(commandCall);
}
public String getHelpText() {
return helpText;
}
public String getInfoText() {
return infoText;
}
}
/**
* Command Builder for assembling commands. The command builder is not intended
* to be threadsafe or reusable.
* After building a command it should be disposed of, or undefined results may
* occur
*
* @author vogel612<<a href="mailto:[email protected]">[email protected]</a>>
*/
public final class CommandHandleBuilder {
private static final Predicate<String> FALSE = s -> false;
private static final Logger LOGGER = Logger.getLogger(CommandHandleBuilder.class.getName());
private Predicate<String> matchesSyntax;
private String helpText = "";
private String infoText = "";
private Consumer<ChatMessage> executor;
public CommandHandleBuilder() {
matchesSyntax = FALSE;
}
/**
* Build the command. Can be seen as ending the Builder's life span.
*
* @return the resulting {@link CommandHandle} assembled from the actions
* performed on this instance.
* @throws IllegalStateException
* if no syntax or execution were added.
*/
public CommandHandle build() throws IllegalStateException {
LOGGER.info("Building Command");
if (FALSE == matchesSyntax || null == executor) { throw new IllegalStateException("Internal builder state does not allow building command yet, an execution and at least one syntax is required"); }
return new CommandHandle(matchesSyntax, executor, helpText, infoText);
}
/**
* Adds an additonal syntax to the command. Different Syntaxes are
* shortcircuit-ORed together, as described by
* {@link Predicate#or(Predicate) Predicate} This means if any of the given
* syntaxes match the String later passed to the command it will return
* true;
*
* @param matcher
* A {@link Predicate Predicate<String>} describing the syntax
* @return The Builder for chaining calls
*/
public CommandHandleBuilder addSyntax(Predicate<String> matcher) {
matchesSyntax = matchesSyntax.or(matcher);
return this;
}
/**
* Sets the command to be executed by the builder. Multiple calls to this
* method overwrite each other, meaning only the latest given executor will
* be in the built {@link CommandHandle}
*
* @param executor
* the final version of the Command "handler"
* @return The Builder for chaining calls
*/
public CommandHandleBuilder setExecution(Consumer<ChatMessage> executor) {
if (null != this.executor) {
LOGGER.info("Overwriting existing executor");
}
this.executor = executor;
return this;
}
/**
* Sets the command's help text. The previously set helpText is overwritten,
* only the latest given helpText will be added to the built {@link CommandHandle}
*
* @param help
* The help text for the command
* @return The Builder for chaining calls
*/
public CommandHandleBuilder setHelpText(String help) {
this.helpText = help;
return this;
}
/**
* Sets the command's info text. The previously set infoText is overwritten,
* only the latest given infoText will be added to the built {@link CommandHandle}
*
* @param info
* The info text for the command
* @return The Builder for chaining calls
*/
public CommandHandleBuilder setInfoText(String info) {
this.infoText = info;
return this;
}
}
For demonstration, here's a usage example:
CommandHandle shutdown = new CommandHandleBuilder().addSyntax(s -> {
return s.trim().equals(TRIGGER + "shutdown");
}).setExecution(message -> {
chatInterface.broadcast("*~going down*");
executor.shutdownNow();
System.exit(0);
}).setHelpText("Command shutdown: call using:'" + TRIGGER + "shutdown'. Terminates the bot")
.setInfoText("Shuts down the executing JVM. The bot will no more respond")
.build();
I am especially interested in possible improvements to the Builder and Naming.
The formatting is currently defined on a project-wide scale by the repository owner. While comments are appreciated, I doubt I will be able to change much there ;)
Objects.requireNonNull()
instead of testing for nulls all along? Also, you can consider using JSR 305 annotations in this case \$\endgroup\$Objects.requireNonNull()
will throw a InvalidArgumentException in places where there's no arguments. That's confusing and unintuitive. \$\endgroup\$java.util.Objects
? \$\endgroup\$