package de.skuzzle.polly.sdk;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import de.skuzzle.polly.sdk.exceptions.CommandException;
import de.skuzzle.polly.sdk.exceptions.ConversationException;
import de.skuzzle.polly.sdk.exceptions.DisposingException;
import de.skuzzle.polly.sdk.exceptions.DuplicatedSignatureException;
import de.skuzzle.polly.sdk.exceptions.InsufficientRightsException;
import de.skuzzle.polly.sdk.roles.RoleManager;
import de.skuzzle.polly.sdk.roles.SecurityContainer;
import de.skuzzle.polly.sdk.roles.SecurityObject;
/**
* <p>This is the base class for all Commands that can be executed by polly. A command
* needs to have a name and can have an additional info text which describes how it
* is used.</p>
*
* <p>A command has a reference to all of its formal signatures. Add new formal signatures
* for a command via {@link #createSignature(String, Parameter...)} or
* {@link #createSignature(String, String, Parameter...)}. Upon execution of a command,
* the actual signature is passed. Please see {@link Signature} to find out how signatures
* are actually being matched.</p>
*
* This is an example of creating an own command:
* <pre>
* public class MyCommand extends Command {
* public MyCommand(MyPolly polly) throws DuplicatedSignatureException {
* super(polly, "mycmd"); // create command with name 'mycmd'
* // Create signature with a short help text, a String and a Number Parameter
* this.createSignature("Do something",
* new Parameter("ParamName", Types.STRING),
* new Parameter("ParamName2", Types.NUMBER));
* }
*
* <code>@Override</code>
* public void executeOnChannel(User executer, String channel, Signature signature) {
* if (signature.getId() == 0) {
* String string = signature.getStringValue(0);
* double number = signature.getNumberValue(1);
*
* // work with those values
* }
* }
*
* <code>@Override</code>
* public void executeOnQuery(User executer, Signature signature) {
* // do nothing. our command can only be executed in a channel
* }
* </pre>
*
* Now that you have this command, you need to register it with polly:
*
* <pre>
* Command myCmd = new MyCommand(myPolly);
* myPolly.commands().registerCommand(myCmd);
* </pre>
*
* <p>Or you may use the {@link PollyPlugin#addCommand(Command)} method to register
* commands.</p>
*
* <p>If you want your Command to have the same behavior whether its executed on a channel
* or on a query, you may override {@link #executeOnBoth(User, String, Signature)} and
* make it return <code>false</code>. Note that it returns <code>false</code> by default.
* That means if you want your command to be only executable on a query, you need to
* override {@link #executeOnBoth(User, String, Signature)} and make it return
* <code>true</code>. Now, both {@link #executeOnChannel(User, String, Signature)} and
* {@link #executeOnQuery(User, Signature)} are executed, depending on where the
* command has been called.</p>
*
* <p>It is essential for the usability of polly, that you set a proper help text using
* {@link #setHelpText(String)}.</p>
*
* <p>Commands are subject to pollys role based security system. Thus a command reports
* its required permissions using {@link #getRequiredPermission()}. The default
* implementation returns a Set containing only one element. If this command is set to be
* executable by only registered users, the set will contain the permission
* {@link RoleManager#REGISTERED_PERMISSION}, otherwise it will contain
* {@link RoleManager#NONE_PERMISSION} and will thus be executable by everyone.</p>
*
* <p>Additionally, each formal signature can as well have a set of permissions that are
* required to execute it. So a user must not only have the required permissions for this
* command but also the required permissions for the signature he wants to execute.</p>
*
* @author Simon
* @since zero day
* @version RC 1.0
*/
public abstract class Command extends AbstractDisposable implements Comparable<Command>,
SecurityContainer, SecurityObject {
/**
* This commands name.
*/
protected String commandName;
/**
* The {@link MyPolly} instance.
*/
protected MyPolly polly;
/**
* All formal signatures for this command.
*/
private List<FormalSignature> signatures;
/**
* Determines whether unregistered users may execute this command.
*/
protected boolean registeredOnly;
/**
* The userlevel for this command.
*/
protected int userLevel;
/**
* <code>true</code> if ths command can only be executed in qry.
* @since 0.7
*/
protected boolean qryCommand;
/**
* This commands help message.
*/
protected String helpText;
/**
* Constants that will be used for the next execution of this command.
*/
protected Map<String, Types> constants;
/**
* Formal signature to output help text.
*/
private FormalSignature helpSignature0;
/**
* Formal signature to output help text of certain real signature
*/
private FormalSignature helpSignature1;
/**
* A set that contains the permissions of all signatures for this command.
*/
private Set<String> containedPermissions;
/**
* Creates a new Command with the given MyPolly instance and command name.
* @param polly The MyPolly instance.
* @param commandName The command name.
*/
public Command(MyPolly polly, String commandName) {
this.commandName = commandName;
this.polly = polly;
this.signatures = new ArrayList<FormalSignature>();
this.helpText = Messages.bind(Messages.commandNoDescription, commandName);
this.constants = new HashMap<String, Types>();
this.helpSignature0 = new FormalSignature(commandName, 0, "", //$NON-NLS-1$
new Parameter("", Types.HELP)); //$NON-NLS-1$
this.helpSignature1 = new FormalSignature(commandName, 0, "", //$NON-NLS-1$
new Parameter("", Types.HELP), //$NON-NLS-1$
new Parameter("", Types.NUMBER)); //$NON-NLS-1$
this.containedPermissions = new HashSet<String>();
}
/**
* Returns the MyPolly instance this command was initialized with.
* @return The MyPolly instance.
*/
public final MyPolly getMyPolly() {
return this.polly;
}
/**
* Returns this commands name.
* @return the command name.
*/
public final String getCommandName() {
return this.commandName;
}
/**
* <p>Returns the help text for this command. The default implementation returns a
* suitable default string. Override it to provide your own help message.</p>
*
* <p>Your help messages should contain the numbers of all possible formal parameter
* ids, so that the user can retrieve infos for each signature of your command.</p>
*
* @return A help text for this command.
*/
public String getHelpText() {
String help = this.helpText.endsWith(".") //$NON-NLS-1$
? this.helpText + " " : this.helpText + ". "; //$NON-NLS-1$ //$NON-NLS-2$
help += Messages.bind(Messages.commandSignatures, this.signatures.size(),
this.commandName);
return help;
}
/**
* Sets the help text which can be displayed using the help command.
*
* @param helpText The help text for this command.
*/
public void setHelpText(String helpText) {
this.helpText = helpText;
}
/**
* Determines if this command can only be executed in query.
*
* @return <code>true</code> if this command can only be executed in query.
* @since 0.7
*/
public boolean isQryCommand() {
return this.qryCommand;
}
/**
* Sets whether this command can only be executed in a query.
*
* @param qryCommand <code>true</code> if this command can only be executed in query.
* @since 0.7
*/
public void setQryCommand(boolean qryCommand) {
this.qryCommand = qryCommand;
}
/**
* This method returns a help message for a formal signature with given id. If the
* id is not valid, it returns an error string.
*
* @param signatureId The id of the signature which help text shall be returned.
* @return A help text for this command and the given signature id.
*/
public String getHelpText(int signatureId) {
if (signatureId >= 0 && signatureId < this.signatures.size()) {
FormalSignature fs = this.signatures.get(signatureId);
return fs.getHelp();
}
return Messages.bind(Messages.commandNoSignature, this.commandName, signatureId);
}
/**
* Gets a readomly list of all signatures for this command.
* @return A list of all formal signatures of this command.
*/
public List<FormalSignature> getSignatures() {
return Collections.unmodifiableList(this.signatures);
}
/**
* Returns the required user level for executing this Command. The default value is
* {@link UserManager#UNKNOWN}, which means everyone may execute it.
*
* @return The required user level for this command which is 0 by default.
*/
public int getUserLevel() {
return this.userLevel;
}
/**
* Sets the required user level to execute this command. Use one of the default
* userlevel constants in {@link UserManager} or own values.
*
* @param userLevel The new userlevel for this command.
* @deprecated Userlevel has been replace by roles
*/
@Deprecated
public void setUserLevel(int userLevel) {
this.userLevel = userLevel;
}
/**
* The hashcode of a command equals the hashcode of its name.
* @return The commands hashcode.
*/
@Override
public final int hashCode() {
return this.commandName.hashCode();
}
/**
* Convenience method for replying directly to a user.
* @param user The user to send a private message to.
* @param message The message to send.
*/
protected void reply(User user, String message) {
this.getMyPolly().irc().sendMessage(user.getCurrentNickName(), message);
}
/**
* Convenience method for replying to a channel or a user.
* @param channel The channel (if preceded by "#") or the nickname to send a
* message to.
* @param message The message to send.
*/
protected void reply(String channel, String message) {
this.getMyPolly().irc().sendMessage(channel, message, this);
}
/**
* <p>This method is called by polly to execute this command. You should not
* call it yourself.</p>
*
* <p>It checks the users permission and if he has sufficient rights, it delivers
* the execution to your implementations of the executeOn methods.</p>
*
* <p>It also checks if the signature matches the two special signatures for
* displaying this commands help text. If so, the help is sent to the channel and
* the method returns.
* </p>
*
* <p>If {@link #executeOnBoth(User, String, Signature)} returns <code>true</code>,
* it will call {@link #executeOnChannel(User, String, Signature)} or
* {@link #executeOnQuery(User, Signature)} according to given parameters.
* If it returns <code>false</code>, none of the both methods will be called
* afterwards. That is the default implementation.</p>
*
* @param executer The user who executed this command.
* @param channel The channel this command was executed on.
* @param query Whether this command was executed on query.
* @param signature The actual signature that this command was executed with.
* @throws InsufficientRightsException If the executing user has not all permissions
* required by either this command or the signature he tries to execute.
* @throws CommandException Implementors can throw this to indicate an error during
* execution.
*/
public void doExecute(User executer, String channel, boolean query,
Signature signature) throws InsufficientRightsException, CommandException {
// check if help is requested
if (signature.equals(this.helpSignature0)) {
this.reply(channel, this.getHelpText());
return;
} else if (signature.equals(this.helpSignature1)) {
int num = (int) signature.getNumberValue(1);
if (num < 0 || num >= this.signatures.size()) {
this.reply(channel, Messages.bind(Messages.commandNoSignatureId, num));
return;
}
this.reply(channel, this.getHelpText(num));
this.reply(channel, Messages.bind(Messages.commandSample,
this.commandName, this.signatures.get(num).getSample()));
return;
}
FormalSignature formal = this.signatures.get(signature.getId());
this.checkPermissions(executer, formal);
// execute the command
try {
boolean runOthers = this.executeOnBoth(executer, channel, signature);
if (runOthers && query) {
this.executeOnQuery(executer, signature);
} else if (runOthers) {
this.executeOnChannel(executer, channel, signature);
}
} catch (InsufficientRightsException e) {
throw e;
} catch (Exception e) {
throw new CommandException(e.getMessage(), e);
}
}
/**
* Checks whether a user has the required permissions to execute this command
* and the given signature.
*
* @param executer The user to check.
* @param signature The signature to check.
* @throws InsufficientRightsException If the user is not allowed to execute this
* command.
*/
private void checkPermissions(User executer, FormalSignature signature)
throws InsufficientRightsException {
// get matching formal signature to the actual signature and check the
// permissions.
if (!this.getMyPolly().roles().canAccess(executer, this)) {
throw new InsufficientRightsException(this);
}
if (!this.getMyPolly().roles().hasPermission(executer,
signature.getRequiredPermission())) {
throw new InsufficientRightsException(signature);
}
}
/**
* <p>This method is called either if this command has been executed on a channel or
* on a query with a valid signature. That means the the passed signature matches
* one that this command was registered for.</p>
*
* <p>If it returns <code>true</code>, the methods
* {@link #executeOnChannel(User, String, Signature)} and
* {@link #executeOnQuery(User, Signature)} are run afterwards. The default
* implementation returns <code>false</code>.</p>
*
* <p>If you need to reply, reply to the <tt>channel</tt>. It will point to the user
* if this was executed on a query.</p>
*
* @param executer The user who executed this command.
* @param channel The channel this command was executed on.
* @param signature The actual signature that this command was executed with.
* @return Whether the 2 other executeX methods should be run afterwards.
* @throws CommandException Implementors can throw this to indicate an error during
* execution.
* @throws InsufficientRightsException If, for any reasons, the executor can not
* execute this command.
*/
protected boolean executeOnBoth(User executer, String channel, Signature signature)
throws CommandException, InsufficientRightsException {
return false;
}
/**
* <p>This method is called if this command has been executed on a channel with
* a valid signature. That means the the passed signature matches one that this
* command was registered for.
* The default implementation simply does nothing upon calling. Decide yourself
* if you want to override it.</p>
*
* <p>This method is not called if {@link #executeOnBoth(User, String, Signature)}
* returns true.</p>
*
* @param executer The user who executed this command.
* @param channel The channel this command was executed on.
* @param signature The actual signature that this command was executed with.
* @throws CommandException Implementors can throw this to indicate an error during
* execution.
* @throws InsufficientRightsException If, for any reasons, the executor can not
* execute this command.
*/
protected void executeOnChannel(User executer, String channel,
Signature signature) throws CommandException, InsufficientRightsException {}
/**
* <p>This method is called if this command has been executed on a query with
* a valid signature. That means the the passed signature matches one that this
* command was registered for.
* The default implementation simply does nothing upon calling. Decide yourself
* if you want to override it.</p>
*
* <p>This method is not called if {@link #executeOnBoth(User, String, Signature)}
* returns true.</p>
*
* @param executer The user who executed this command in a query.
* @param signature The actual signature that this command was executed with.
* @throws CommandException Implementors can throw this to indicate an error during
* execution.
* @throws InsufficientRightsException If, for any reasons, the executor can not
* execute this command.
*/
protected void executeOnQuery(User executer, Signature signature)
throws CommandException, InsufficientRightsException {}
/**
* Use this method in {@link #executeOnChannel(User, String, Signature)} and
* {@link #executeOnQuery(User, Signature)} to determine which formal signature
* matches the actual passed signature.
*
* @param actual The actual signature that has been passed by polly.
* @param formalId The id of the formal signature to check the actual against.
* @return <code>true</code> if the actual signature matches the formal id.
* <code>false</code> otherwise.
*/
protected boolean match(Signature actual, int formalId) {
return actual.getId() == formalId;
}
/**
* This is the formal signature for displaying a brief description of this command.
* It looks like 'commandName ?'
*
* @return the formal signature for this commands help text.
* @since 0.9
* @see #setHelpText(String)
*/
public FormalSignature getHelpSignature0() {
return this.helpSignature0;
}
/**
* This is the formal signature for displaying the help text of a signature of this
* command. It looks like 'commandName ? Number'
*
* @return the formal signature for this commands signatures help texts.
* @since 0.9
* @see FormalSignature#FormalSignature(String, int, String, Parameter...)
* @see FormalSignature#FormalSignature(String, int, String, String, Parameter...)
*/
public FormalSignature getHelpSignature1() {
return this.helpSignature1;
}
/**
* Factory method for creating a new signature for this command. The new signatures
* formal id gets incremented each call and this signature will have no required
* permission.
*
* @param help A description text for this signature.
* @param parameters The formal parameters for the new signature.
* @return A new signature for this command.
* @throws DuplicatedSignatureException If the same signature already exists.
*/
public Signature createSignature(String help, Parameter... parameters)
throws DuplicatedSignatureException {
return this.createSignature(help, RoleManager.NONE_PERMISSION, parameters);
}
/**
* Factory method for creating a new signature for this command. Its equivalent
* of creating a new Signature with this commands name. The new signatures
* formal id gets incremented each call.
*
* @param help A description text for this signature.
* @param permissionName The name of the permission that is required to execute this
* signature.
* @param parameters The formal parameters for the new signature.
* @return A new signature for this command.
* @throws DuplicatedSignatureException If the same signature already exists.
*/
public Signature createSignature(String help, String permissionName,
Parameter... parameters) throws DuplicatedSignatureException {
int id = this.signatures.size();
FormalSignature fs = new FormalSignature(
this.getCommandName(), id, help, permissionName, parameters);
if (this.signatures.contains(fs)) {
throw new DuplicatedSignatureException(fs.toString());
}
this.signatures.add(fs);
this.containedPermissions.addAll(fs.getRequiredPermission());
return fs;
}
@Override
public Set<String> getContainedPermissions() {
return this.containedPermissions;
}
@Override
public final Set<String> getRequiredPermission() {
if (this.registeredOnly) {
return Collections.singleton(RoleManager.REGISTERED_PERMISSION);
}
return Collections.singleton(RoleManager.NONE_PERMISSION);
}
/**
* Sets whether this command can be executed only by registered(signed on) users.
* @param registeredOnly Whether registered users only can execute this command.
*/
public void setRegisteredOnly(boolean registeredOnly) {
this.registeredOnly = registeredOnly;
}
/**
* Sets this command to be executable by registered users only.
*/
public void setRegisteredOnly() {
this.registeredOnly = true;
}
/**
* Determines whether this command can be executed by anyone or only registered users.
* @return <code>true</code> if only registered users can execute this command.
* @see #setRegisteredOnly()
* @see #setRegisteredOnly(boolean)
*/
public boolean isRegisteredOnly() {
return this.registeredOnly;
}
/**
* Determines whether this command will be stored in the {@link CommandManager}s
* command history. This method always returns <code>true</code> and may be
* overriden in order to achieve different behavior.
*
* @return If the command should be tracked in the command history.
* @since 0.8
*/
public boolean trackInHistory() {
return true;
}
/**
* Convenience wrapper method for creating a {@link Conversation}.
*
* @param user The user this conversation is for.
* @param channel The channel this conversation is for.
* @return The new {@link Conversation} instance.
* @throws ConversationException If there is already a conversation active with the
* same user on the same channel.
* @see ConversationManager
* @since 0.6.0
*/
protected Conversation createConversation(User user, String channel)
throws ConversationException {
return this.getMyPolly().conversations().create(this.getMyPolly().irc(), user,
channel);
}
/**
* Compares two commands and considers them equals if they have the same commandname.
* @param obj The object to compare this command with.
* @return <code>true</code> iff the other obj is an instanceof command and its
* commandname equals this commands name.
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Command)) {
return false;
}
return ((Command) obj).getCommandName().equals(this.getCommandName());
}
/**
* This method does nothing in its default implementation. It can be overridden to
* provide some command specific constants before actually evaluating a polly
* expression.
*
* @param map Map that can be filled with custom variables.
* @since 0.7
*/
public void renewConstants(Map<String, Types> map) { }
/**
* Returns a String representation of this command.
*
* @return The name of this command.
*/
@Override
public String toString() {
return this.getNameString();
}
/**
* Constructs the result string for the {@link #toString()} Method. It appends
* "R" and/or "qry" to the command name according to whether this command is
* only executable for registered user or in query only.
*
* @return The command name with additional infos.
*/
protected String getNameString() {
StringBuilder result = new StringBuilder(this.getCommandName().length() + 10);
result.append(this.commandName);
if (this.isRegisteredOnly() || this.isQryCommand()) {
result.append("("); //$NON-NLS-1$
if (this.registeredOnly) {
result.append("R"); //$NON-NLS-1$
}
if (this.isQryCommand()) {
if (this.isRegisteredOnly()) {
result.append(","); //$NON-NLS-1$
}
result.append("qry"); //$NON-NLS-1$
}
result.append(")"); //$NON-NLS-1$
}
return result.toString();
}
/**
* Implementation of the {@link Disposable} interface. The default implementation
* in the {@link Command} class is empty.
*/
@Override
protected void actualDispose() throws DisposingException {}
/**
* Compares 2 commands using their required user level. Commands will be sorted from
* low user level to high user level.
*
* @return {@inheritDoc}
*/
@Override
public int compareTo(Command o) {
int first = ((Integer)this.userLevel).compareTo(o.userLevel);
if (first == 0) {
return this.getCommandName().compareTo(o.getCommandName());
} else {
return first;
}
}
}