net.kautler.command.api.Command Maven / Gradle / Ivy
Show all versions of command-framework Show documentation
/*
* Copyright 2019 Björn Kautler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.kautler.command.api;
import net.kautler.command.InvalidAnnotationCombinationException;
import net.kautler.command.api.annotation.Alias;
import net.kautler.command.api.annotation.Asynchronous;
import net.kautler.command.api.annotation.Description;
import net.kautler.command.api.annotation.RestrictedTo;
import net.kautler.command.api.annotation.RestrictionPolicy;
import net.kautler.command.api.annotation.Usage;
import net.kautler.command.api.restriction.AllOf;
import net.kautler.command.api.restriction.AnyOf;
import net.kautler.command.api.restriction.Everyone;
import net.kautler.command.api.restriction.NoneOf;
import net.kautler.command.api.restriction.Restriction;
import net.kautler.command.api.restriction.RestrictionChainElement;
import net.kautler.command.api.restriction.javacord.ChannelJavacord;
import net.kautler.command.api.restriction.javacord.RoleJavacord;
import net.kautler.command.api.restriction.javacord.ServerJavacord;
import net.kautler.command.api.restriction.javacord.UserJavacord;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import static java.lang.Character.toLowerCase;
import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import static net.kautler.command.api.annotation.RestrictionPolicy.Policy.NONE_OF;
/**
* A command that can be triggered by messages of the given type.
*
* @param the class of the messages for which this command can be triggered
*/
public interface Command {
/**
* The pattern that is used to split parameters. It matches an arbitrary amount of whitespaces except newlines.
*/
Pattern PARAMETER_SEPARATOR_PATTERN = Pattern.compile("[\\s&&[^\\n]]++");
/**
* Returns the aliases for this command.
*
* The default implementation of this method returns the aliases configured using the {@link Alias @Alias}
* annotation. If no alias is configured by annotation, the class name, stripped by {@code Command} or {@code Cmd}
* suffix if present and the first letter lowercased is used as default.
*
*
If this method is overwritten and there are annotations, the method overwrite takes precedence.
*
* @return the aliases for this command
* @see Alias @Alias
*/
default List getAliases() {
List annotatedAliases = Arrays
.stream(getClass().getAnnotationsByType(Alias.class))
.map(Alias::value)
.collect(toList());
if (!annotatedAliases.isEmpty()) {
return annotatedAliases;
}
String className = getClass().getSimpleName();
String defaultAlias = className
.replaceFirst("(?i)(?:Command|Cmd)$", "")
.replaceFirst("^.", Character.toString(toLowerCase(className.charAt(0))));
return singletonList(defaultAlias);
}
/**
* Returns the description of this command.
* Currently this description is used nowhere, but can for example be displayed in an own help command.
*
* The default implementation of this method returns the description configured using the
* {@link Description @Description} annotation. If no description is configured by annotation,
* an empty {@code Optional} is used as default.
*
*
If this method is overwritten and the annotation is present, the method overwrite takes precedence.
*
* @return the description of this command
* @see Description @Description
*/
default Optional getDescription() {
return Optional
.ofNullable(getClass().getAnnotation(Description.class))
.map(Description::value);
}
/**
* Returns the usage of this command.
* This usage can for example be displayed in an own help command.
*
* When using the {@link ParameterParser}, the usage string has to follow a pre-defined format.
*
* - Placeholders for free text without whitespaces (in the value) look like {@code
}
* -
* One placeholder for free text with whitespaces (in the value) is allowed as effectively last parameter
* and looks like {@code
}
*
* - Literal parameters look like {@code 'literal'}
* - Optional parts are enclosed in square brackets like {@code [
]}
* -
* Alternatives are enclosed in parentheses and are separated by pipe characters
* like {@code ('all' | 'some' | 'none')}
*
* - Whitespace characters between the defined tokens are optional and ignored
*
* Examples:
*
* - {@code @Usage("
")}
* - {@code @Usage("['all'] ['exact']")}
* - {@code @Usage("[
]")}
* - {@code @Usage("(
'|' | ) ")}
*
* The default implementation of this method returns the usage configured using the {@link Usage @Usage}
* annotation. If no usage is configured by annotation, an empty {@code Optional} is used as default.
*
*
If this method is overwritten and the annotation is present, the method overwrite takes precedence.
*
* @return the usage of this command
* @see Usage @Usage
* @see ParameterParser
*/
default Optional getUsage() {
return Optional
.ofNullable(getClass().getAnnotation(Usage.class))
.map(Usage::value);
}
/**
* Returns the restriction rules chain for this command.
*
* Complex boolean logic can either be formulated using the methods of {@code RestrictionChainElement} or by
* providing an own {@link Restriction} implementation. For the latter also helpers like {@link ChannelJavacord},
* {@link RoleJavacord}, {@link ServerJavacord}, {@link UserJavacord}, {@link AllOf}, {@link AnyOf}, or
* {@link NoneOf} can be used as super classes.
*
*
The default implementation of this method returns the restrictions configured using the
* {@link RestrictedTo @RestrictedTo} annotation combined according to the
* {@link RestrictionPolicy @RestrictionPolicy} annotation. If no {@code @RestrictedTo} annotation is present,
* this method behaves as if the {@link Everyone} restriction was applied.
*
*
If this method is overwritten and the annotation is present, the method overwrite takes precedence.
*
* @return the restriction rules for this command
* @see RestrictedTo @RestrictedTo
* @see RestrictionPolicy @RestrictionPolicy
*/
default RestrictionChainElement getRestrictionChain() {
List>> restrictions = Arrays.stream(getClass().getAnnotationsByType(RestrictedTo.class))
.map(RestrictedTo::value)
.collect(toList());
int restrictionsAmount = restrictions.size();
// no restrictions, everyone can use it
if (restrictionsAmount == 0) {
return new RestrictionChainElement(Everyone.class);
}
RestrictionPolicy restrictionPolicy = getClass().getAnnotation(RestrictionPolicy.class);
// one restriction
if (restrictionsAmount == 1) {
return restrictions.stream()
.map(RestrictionChainElement::new)
.map(restrictionChainElement ->
(restrictionPolicy != null) && (restrictionPolicy.value() == NONE_OF)
? restrictionChainElement.negate()
: restrictionChainElement)
.findAny()
.orElseThrow(AbstractMethodError::new);
}
// multiple restrictions, but no policy
if (restrictionPolicy == null) {
throw new InvalidAnnotationCombinationException(format("@RestrictionPolicy is mandatory if multiple @RestrictedTo annotations are given (%s)", getClass()));
}
switch (restrictionPolicy.value()) {
case ALL_OF:
return restrictions.stream()
.map(RestrictionChainElement::new)
.reduce(RestrictionChainElement::and)
.orElseThrow(AssertionError::new);
case NONE_OF:
return restrictions.stream()
.map(RestrictionChainElement::new)
.reduce(RestrictionChainElement::or)
.orElseThrow(AssertionError::new)
.negate();
case ANY_OF:
return restrictions.stream()
.map(RestrictionChainElement::new)
.reduce(RestrictionChainElement::or)
.orElseThrow(AssertionError::new);
default:
throw new AssertionError(format("Unhandled switch case for policy '%s'", restrictionPolicy.value()));
}
}
/**
* Returns whether this command should be executed asynchronously.
*
* How exactly this is implemented is up to the command handler that evaluates this command. Usually the command
* will be execute in some thread pool. But it would also be valid for a command handler to execute each
* asynchronous command execution in a new thread, so using this can add significant overhead if overused. As long
* as a command is not doing long-running or blocking operations it might be a good idea to not execute the command
* asynchronously. But if long-running or blocking operations are done in the command code directly, depending on
* the underlying message framework it might be a good idea to execute the command asynchronously to not block
* message dispatching which could introduce serious lag to the command execution.
*
*
As the command executions are potentially done on different threads, special care must be taken
* if the command holds state, to make sure this state is accessed in a thread-safe manner. This can of course also
* happen without the command being configured asynchronously if the underlying message framework dispatches message
* events on different threads.
*
*
The default implementation of this method returns whether the {@link Asynchronous @Asynchronous} annotation is
* present on this command.
*
*
If this method is overwritten and the annotation is present, the method overwrite takes precedence.
*
* @return whether this command should be executed asynchronously
* @see Asynchronous @Asynchronous
*/
default boolean isAsynchronous() {
return getClass().getAnnotation(Asynchronous.class) != null;
}
/**
* Executes this command. The given parameter string can be split into single parameters using
* {@link #getParameters(String, int)} or semantically parsed into parameters using the {@link ParameterParser}.
*
* @param incomingMessage the incoming message that caused this command to be executed
* @param prefix the command prefix that was used to trigger this command
* @param usedAlias the alias that was used to trigger this command
* @param parameterString the remaining text after the command prefix and alias
* @see #getParameters(String, int)
* @see ParameterParser
*/
void execute(M incomingMessage, String prefix, String usedAlias, String parameterString);
/**
* Returns an array of parameters from the given parameter string. The parameter string is split at any sequence of
* non-newline whitespace characters. If you expect three parameters, you should set {@code maxParameters} to four,
* so you can easily test the length of the returned array whether too many parameters were given to the command.
*
*
For a syntactically and semantically parsing of the parameter string, you can have a look at the
* {@link ParameterParser} for which you can define the command syntax as pattern which is then parsed and returned
* accordingly.
*
* @param parameterString the parameter string to split into single parameters
* @param maxParameters the maximum amount of parameters to return, the last will hold all remaining text
* @return an array of parameters from the given parameter string
* @see ParameterParser
*/
static String[] getParameters(String parameterString, int maxParameters) {
if (parameterString.chars().allMatch(Character::isWhitespace)) {
return new String[0];
}
return PARAMETER_SEPARATOR_PATTERN.split(parameterString, maxParameters + 1);
}
}