currently refactoring a lot.
This commit is contained in:
parent
fe0b04c7d1
commit
9aa138058c
32 changed files with 484 additions and 254 deletions
|
|
@ -6,6 +6,14 @@ plugins {
|
|||
id("de.kentoj.scrow.java-library")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("net.kyori:adventure-api:5.2.0")
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("KenCommandAPI") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package de.kentoj.kencommandapi.api;
|
||||
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
import de.kentoj.kencommandapi.api.node.CommandNode;
|
||||
import de.kentoj.kencommandapi.api.node.RootCommandNode;
|
||||
import de.kentoj.kencommandapi.api.parser.CommandParser;
|
||||
import de.kentoj.kencommandapi.api.parser.ParseResult;
|
||||
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
|
||||
import de.kentoj.kencommandapi.api.platform.SendMessageMethod;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class CommandHandler<T> {
|
||||
|
||||
private final CommandParser<T> commandParser;
|
||||
private final SendMessageMethod<T> sendMessageMethod;
|
||||
private final HasPermissionMethod<T> hasPermissionMethod;
|
||||
|
||||
public void invoke(RootCommandNode<T> rootNode, T sender, String[] args) {
|
||||
var parseResult = commandParser.parseLiteral(rootNode, sender, args, true);
|
||||
if (!(parseResult instanceof ParseResult.Success<T>)) {
|
||||
sendMessageMethod.send(sender, rootNode.getMessageStyle().err(parseResult.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
var ctx = new CommandContext<>(sender, ((ParseResult.Success<T>) parseResult).getParsedArguments());
|
||||
assert parseResult.getLiteral().getExecutor() != null;
|
||||
parseResult.getLiteral().getExecutor().execute(ctx).whenComplete((result, ex) -> {
|
||||
if (ex != null) {
|
||||
sendMessageMethod.send(sender, rootNode.getMessageStyle().exception(ex));
|
||||
}
|
||||
result.ifFailure(failure ->
|
||||
sendMessageMethod.send(sender, rootNode.getMessageStyle().err(failure))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<List<String>> getSuggestions(RootCommandNode<T> rootNode, T sender, String[] args) {
|
||||
var parseResult = commandParser.parseLiteral(rootNode, sender, args, false);
|
||||
|
||||
if (parseResult instanceof ParseResult.ArgumentExpected<T>) {
|
||||
var arg = ((ParseResult.ArgumentExpected<T>) parseResult).getArgument();
|
||||
return arg.getSuggestionProvider().suggest(sender);
|
||||
}
|
||||
|
||||
String filter = null;
|
||||
if (parseResult instanceof ParseResult.UnknownLiteral<T>)
|
||||
filter = ((ParseResult.UnknownLiteral<T>) parseResult).getName();
|
||||
else if (parseResult instanceof ParseResult.NoPermission<T>)
|
||||
filter = ((ParseResult.NoPermission<T>) parseResult).getName();
|
||||
return getSuggestions(sender, parseResult.getLiteral(), filter);
|
||||
}
|
||||
|
||||
private CompletableFuture<List<String>> getSuggestions(T sender, CommandNode<T> literal, @Nullable String filter) {
|
||||
var stream = literal.getLiterals().stream()
|
||||
.filter(lit -> lit.getPermission() != null && hasPermissionMethod.hasPermission(sender, lit.getPermission()))
|
||||
.map(CommandNode::getNames)
|
||||
.flatMap(Arrays::stream);
|
||||
if (filter != null)
|
||||
stream = stream.filter(f -> f.toLowerCase().contains(filter.toLowerCase()));
|
||||
|
||||
return CompletableFuture.completedFuture(stream.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package de.kentoj.kencommandapi.api.invocation;
|
||||
|
||||
import com.leakyabstractions.result.api.Result;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface CommandExecutor<T> {
|
||||
|
||||
Result execute(CommandContext<T> ctx);
|
||||
CompletableFuture<Result<Void, String>> execute(CommandContext<T> ctx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package de.kentoj.kencommandapi.api.invocation;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public class Result {
|
||||
|
||||
public static Result success() {
|
||||
return new Result(null);
|
||||
}
|
||||
|
||||
public static Result error(String error) {
|
||||
return new Result(error);
|
||||
}
|
||||
|
||||
private final @Nullable String error;
|
||||
|
||||
public boolean isError() {
|
||||
return error != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package de.kentoj.kencommandapi.api;
|
||||
package de.kentoj.kencommandapi.api.node;
|
||||
|
||||
import de.kentoj.kencommandapi.api.platform.MessageStyle;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
import de.kentoj.kencommandapi.internal.CommandNodeImpl;
|
||||
import de.kentoj.kencommandapi.internal.RootCommandNodeImpl;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
|
@ -14,6 +16,10 @@ public interface CommandNode<T> {
|
|||
return new CommandNodeImpl<>(name);
|
||||
}
|
||||
|
||||
static <T> RootCommandNode<T> rootNode(MessageStyle messageStyle, String...name) {
|
||||
return new RootCommandNodeImpl<>(messageStyle, name);
|
||||
}
|
||||
|
||||
@NotNull String[] getNames();
|
||||
|
||||
void setDescription(@Nullable String description);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package de.kentoj.kencommandapi.api.node;
|
||||
|
||||
import de.kentoj.kencommandapi.api.platform.MessageStyle;
|
||||
|
||||
public interface RootCommandNode<T> extends CommandNode<T> {
|
||||
|
||||
MessageStyle getMessageStyle();
|
||||
|
||||
void setMessageStyle(MessageStyle messageStyle);
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
package de.kentoj.kencommandapi.internal.parser;
|
||||
package de.kentoj.kencommandapi.api.parser;
|
||||
|
||||
import de.kentoj.kencommandapi.api.CommandNode;
|
||||
import com.leakyabstractions.result.api.Result;
|
||||
import com.leakyabstractions.result.core.Results;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.invocation.ParsedArgument;
|
||||
import de.kentoj.kencommandapi.api.platform.HasPermissionMethod;
|
||||
import de.kentoj.kencommandapi.api.node.CommandNode;
|
||||
import de.kentoj.kencommandapi.api.node.RootCommandNode;
|
||||
import de.kentoj.kencommandapi.internal.ParsedArgumentImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
|
|
@ -14,28 +18,25 @@ import java.util.Map;
|
|||
@RequiredArgsConstructor
|
||||
public class CommandParser<T> {
|
||||
|
||||
private final PermissionChecker<T> hasPermissionMethod;
|
||||
private final HasPermissionMethod<T> hasPermissionMethod;
|
||||
|
||||
public ParseResult<T> parseLiteral(CommandNode<T> rootNode, T sender, String[] args) {
|
||||
var cur = rootNode;
|
||||
public ParseResult<T> parseLiteral(RootCommandNode<T> rootNode, T sender, String[] args, boolean parseArgs) {
|
||||
CommandNode<T> cur = rootNode;
|
||||
final Iterator<String> argStrIter = Arrays.stream(args).iterator();
|
||||
final Map<String, ParsedArgument<T, ?>> parsedArguments = new HashMap<>();
|
||||
|
||||
while (argStrIter.hasNext()) {
|
||||
for (var nodeArg : cur.getArguments()) {
|
||||
ParsedArgument<T, Object> parsed;
|
||||
try {
|
||||
parsed = parseArgument(argStrIter, sender, nodeArg);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return new ParseResult.IllegalArgument<>(cur, ex.getMessage());
|
||||
}
|
||||
if (parsed == null) return new ParseResult.IncompleteArgument<>(cur, nodeArg);
|
||||
parsedArguments.put(nodeArg.getId(), parsed);
|
||||
var parsed = parseArgument(rootNode, nodeArg, argStrIter, sender, !parseArgs);
|
||||
if (parsed.hasFailure())
|
||||
return parsed.getFailure().orElseThrow();
|
||||
if (parseArgs)
|
||||
parsedArguments.put(nodeArg.getId(), parsed.getSuccess().orElseThrow());
|
||||
}
|
||||
|
||||
if (!argStrIter.hasNext()) {
|
||||
if (cur.getExecutor() == null)
|
||||
return new ParseResult.IncompleteCommand<>(cur);
|
||||
return new ParseResult.LiteralExpected<>(cur);
|
||||
return new ParseResult.Success<>(cur, parsedArguments);
|
||||
}
|
||||
|
||||
|
|
@ -48,23 +49,40 @@ public class CommandParser<T> {
|
|||
cur = nextNode;
|
||||
}
|
||||
|
||||
throw new RuntimeException("code reached unexpected line");
|
||||
if (cur.getExecutor() == null)
|
||||
return new ParseResult.LiteralExpected<>(cur);
|
||||
return new ParseResult.Success<>(cur, parsedArguments);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ParsedArgument<T, Object> parseArgument(Iterator<String> args, T sender, CommandArgument<T, ?> nodeArg) {
|
||||
private Result<ParsedArgument<T, Object>, ParseResult<T>> parseArgument(
|
||||
CommandNode<T> node,
|
||||
CommandArgument<T, ?> nodeArg,
|
||||
Iterator<String> args,
|
||||
T sender,
|
||||
boolean fakeParse
|
||||
) {
|
||||
Object value;
|
||||
|
||||
if (args.hasNext()) {
|
||||
var argStr = getRawArgument(args, nodeArg);
|
||||
value = nodeArg.getType().parseInput(argStr);
|
||||
if (fakeParse) {
|
||||
value = null;
|
||||
} else {
|
||||
try {
|
||||
value = nodeArg.getType().parseInput(argStr);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return Results.failure(new ParseResult.IllegalArgument<>(node, nodeArg, ex.getMessage(), argStr));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (nodeArg.getDefaultValueProvider() == null)
|
||||
return null;
|
||||
return Results.failure(new ParseResult.ArgumentExpected<>(node, nodeArg));
|
||||
value = nodeArg.getDefaultValueProvider().apply(sender);
|
||||
}
|
||||
|
||||
return new ParsedArgumentImpl<>(value, (CommandArgument<T, Object>) nodeArg);
|
||||
var parsedArg = new ParsedArgumentImpl<>(value, (CommandArgument<T, Object>) nodeArg);
|
||||
return Results.success(parsedArg);
|
||||
}
|
||||
|
||||
private String getRawArgument(Iterator<String> args, CommandArgument<T, ?> arg) {
|
||||
|
|
@ -80,4 +98,4 @@ public class CommandParser<T> {
|
|||
|
||||
return rawArgument.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
package de.kentoj.kencommandapi.internal.parser;
|
||||
package de.kentoj.kencommandapi.api.parser;
|
||||
|
||||
import de.kentoj.kencommandapi.api.CommandNode;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.invocation.ParsedArgument;
|
||||
import de.kentoj.kencommandapi.api.node.CommandNode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument, ParseResult.IncompleteArgument,
|
||||
ParseResult.IncompleteCommand, ParseResult.NoPermission, ParseResult.Success, ParseResult.UnknownLiteral {
|
||||
public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument, ParseResult.ArgumentExpected,
|
||||
ParseResult.LiteralExpected, ParseResult.NoPermission, ParseResult.Success, ParseResult.UnknownLiteral {
|
||||
|
||||
/**
|
||||
/**
|
||||
* @return last successfully literal
|
||||
*/
|
||||
public abstract CommandNode<T> getLiteral();
|
||||
|
|
@ -19,25 +19,27 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
|
|||
public abstract String getMessage();
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public static final class IncompleteArgument<T> extends ParseResult<T> {
|
||||
public static final class ArgumentExpected<T> extends ParseResult<T> {
|
||||
@Getter
|
||||
private final CommandNode<T> literal;
|
||||
@Getter
|
||||
private final CommandArgument<T, ?> argument;
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Missing value for argument: " + argument.getId();
|
||||
return "Argument expected: " + argument.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public static final class UnknownLiteral<T> extends ParseResult<T> {
|
||||
private final CommandNode<T> previousLiteral;
|
||||
@Getter
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Unknown literal: " + name;
|
||||
return "Unknown subcommand: " + name;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -47,13 +49,13 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
|
|||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public static final class IncompleteCommand<T> extends ParseResult<T> {
|
||||
public static final class LiteralExpected<T> extends ParseResult<T> {
|
||||
@Getter
|
||||
private final CommandNode<T> literal;
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Incomplete command";
|
||||
return "Subcommand expected";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,11 +76,12 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
|
|||
public static final class NoPermission<T> extends ParseResult<T> {
|
||||
@Getter
|
||||
private final CommandNode<T> literal;
|
||||
@Getter
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Unknown literal: " + name;
|
||||
return "Unknown subcommand: " + name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +89,26 @@ public abstract sealed class ParseResult<T> permits ParseResult.IllegalArgument,
|
|||
public static final class IllegalArgument<T> extends ParseResult<T> {
|
||||
@Getter
|
||||
private final CommandNode<T> literal;
|
||||
@Getter
|
||||
private final CommandArgument<T, ?> argument;
|
||||
private final String message;
|
||||
/**
|
||||
* Input passed to the parser
|
||||
*/
|
||||
@Getter
|
||||
private final String argStr;
|
||||
|
||||
public IllegalArgument(CommandNode<T> literal, CommandArgument<T, ?> argument, Throwable throwable, String argStr) {
|
||||
this.literal = literal;
|
||||
this.argument = argument;
|
||||
this.argStr = argStr;
|
||||
while (throwable.getMessage() == null && throwable.getCause() != null)
|
||||
throwable = throwable.getCause();
|
||||
message = throwable.getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Illegal argument '" + argStr + "' for " + argument.getId() + ":" + message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HasPermissionMethod<T> {
|
||||
boolean hasPermission(T sender, String permission);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
public interface MessageStyle {
|
||||
|
||||
MessageStyle PLAIN = new PlainMessageStyle();
|
||||
|
||||
Component ok(Component msg);
|
||||
|
||||
Component err(Component msg);
|
||||
|
||||
Component exception(Component msg);
|
||||
|
||||
default Component ok(String msg) {
|
||||
return ok(Component.text(msg));
|
||||
}
|
||||
|
||||
default Component err(String msg) {
|
||||
return err(Component.text(msg));
|
||||
}
|
||||
|
||||
default Component exception(String msg) {
|
||||
return exception(Component.text(msg));
|
||||
}
|
||||
|
||||
default Component exception(Throwable ex) {
|
||||
while (ex.getMessage() == null && ex.getCause() != null)
|
||||
ex = ex.getCause();
|
||||
return exception(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
public class PlainMessageStyle implements MessageStyle {
|
||||
@Override
|
||||
public Component ok(Component msg) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component err(Component msg) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component exception(Component msg) {
|
||||
return Component.text("ERROR: ").append(msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package de.kentoj.kencommandapi.api.platform;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SendMessageMethod<T> {
|
||||
void send(T to, Component msg);
|
||||
}
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
package de.kentoj.kencommandapi.api.suggestion;
|
||||
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class NoSuggestionProvider<T> implements SuggestionProvider<T> {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Set<String>> suggest(CommandContext<T> __) {
|
||||
return CompletableFuture.completedFuture(Collections.emptySet());
|
||||
public CompletableFuture<List<String>> suggest(T __) {
|
||||
return CompletableFuture.completedFuture(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package de.kentoj.kencommandapi.api.suggestion;
|
||||
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandContext;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface SuggestionProvider<T> {
|
||||
|
||||
CompletableFuture<Set<String>> suggest(CommandContext<T> context);
|
||||
/**
|
||||
* @return an unfiltered(no permission checks or filtering by input) list of strings that may be used for the argument
|
||||
*/
|
||||
CompletableFuture<List<String>> suggest(T sender);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
|
|||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
|
@ -22,7 +23,13 @@ public class CommandArgumentImpl<T, V> implements CommandArgument<T, V> {
|
|||
@Setter
|
||||
private @Nullable Function<T, V> defaultValueProvider;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private @Nullable SuggestionProvider<T> suggestionProvider;
|
||||
|
||||
@Override
|
||||
public @NotNull SuggestionProvider<T> getSuggestionProvider() {
|
||||
if (suggestionProvider == null)
|
||||
return getType().defaultSuggestionProvider();
|
||||
return this.suggestionProvider;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package de.kentoj.kencommandapi.internal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.CommandNode;
|
||||
import de.kentoj.kencommandapi.api.node.CommandNode;
|
||||
import de.kentoj.kencommandapi.api.argument.CommandArgument;
|
||||
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
|
||||
import lombok.Getter;
|
||||
|
|
@ -65,6 +65,7 @@ public class CommandNodeImpl<T> implements CommandNode<T> {
|
|||
}
|
||||
|
||||
private boolean isGreedy() {
|
||||
if (arguments.isEmpty()) return false;
|
||||
var arg = arguments.getLast();
|
||||
return arg != null && arg.getType().isGreedy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package de.kentoj.kencommandapi.internal;
|
||||
|
||||
import de.kentoj.kencommandapi.api.platform.MessageStyle;
|
||||
import de.kentoj.kencommandapi.api.node.RootCommandNode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class RootCommandNodeImpl<T> extends CommandNodeImpl<T> implements RootCommandNode<T> {
|
||||
@Setter
|
||||
@Getter
|
||||
private MessageStyle messageStyle;
|
||||
|
||||
public RootCommandNodeImpl(MessageStyle messageStyle, String[] names) {
|
||||
super(names);
|
||||
this.messageStyle = messageStyle;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package de.kentoj.kencommandapi.internal.parser;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PermissionChecker<T> {
|
||||
boolean hasPermission(T sender, String permission);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue