All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cloud.commandframework.Command Maven / Gradle / Ivy

There is a newer version: 1.8.4
Show newest version
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package cloud.commandframework;

import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.StaticArgument;
import cloud.commandframework.arguments.compound.ArgumentPair;
import cloud.commandframework.arguments.compound.ArgumentTriplet;
import cloud.commandframework.arguments.compound.FlagArgument;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.execution.CommandExecutionHandler;
import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.meta.SimpleCommandMeta;
import cloud.commandframework.permission.CommandPermission;
import cloud.commandframework.permission.Permission;
import cloud.commandframework.types.tuples.Pair;
import cloud.commandframework.types.tuples.Triplet;
import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;

/**
 * A command consists out of a chain of {@link CommandArgument command arguments}.
 *
 * @param  Command sender type
 */
public class Command {

    private final Map<@NonNull CommandArgument, @NonNull Description> arguments;
    private final CommandExecutionHandler commandExecutionHandler;
    private final Class senderType;
    private final CommandPermission commandPermission;
    private final CommandMeta commandMeta;

    /**
     * Construct a new command
     *
     * @param commandArguments        Command argument and description pairs
     * @param commandExecutionHandler Execution handler
     * @param senderType              Required sender type. May be {@code null}
     * @param commandPermission       Command permission
     * @param commandMeta             Command meta instance
     */
    public Command(
            final @NonNull Map<@NonNull CommandArgument, @NonNull Description> commandArguments,
            final @NonNull CommandExecutionHandler<@NonNull C> commandExecutionHandler,
            final @Nullable Class senderType,
            final @NonNull CommandPermission commandPermission,
            final @NonNull CommandMeta commandMeta
    ) {
        this.arguments = Objects.requireNonNull(commandArguments, "Command arguments may not be null");
        if (this.arguments.size() == 0) {
            throw new IllegalArgumentException("At least one command argument is required");
        }
        // Enforce ordering of command arguments
        boolean foundOptional = false;
        for (final CommandArgument argument : this.arguments.keySet()) {
            if (argument.getName().isEmpty()) {
                throw new IllegalArgumentException("Argument names may not be empty");
            }
            if (foundOptional && argument.isRequired()) {
                throw new IllegalArgumentException(
                        String.format(
                                "Command argument '%s' cannot be placed after an optional argument",
                                argument.getName()
                        ));
            } else if (!argument.isRequired()) {
                foundOptional = true;
            }
        }
        this.commandExecutionHandler = commandExecutionHandler;
        this.senderType = senderType;
        this.commandPermission = commandPermission;
        this.commandMeta = commandMeta;
    }

    /**
     * Construct a new command
     *
     * @param commandArguments        Command arguments
     * @param commandExecutionHandler Execution handler
     * @param senderType              Required sender type. May be {@code null}
     * @param commandMeta             Command meta instance
     */
    public Command(
            final @NonNull Map<@NonNull CommandArgument, @NonNull Description> commandArguments,
            final @NonNull CommandExecutionHandler<@NonNull C> commandExecutionHandler,
            final @Nullable Class senderType,
            final @NonNull CommandMeta commandMeta
    ) {
        this(commandArguments, commandExecutionHandler, senderType, Permission.empty(), commandMeta);
    }

    /**
     * Construct a new command
     *
     * @param commandArguments        Command arguments
     * @param commandExecutionHandler Execution handler
     * @param commandPermission       Command permission
     * @param commandMeta             Command meta instance
     */
    public Command(
            final @NonNull Map<@NonNull CommandArgument, @NonNull Description> commandArguments,
            final @NonNull CommandExecutionHandler<@NonNull C> commandExecutionHandler,
            final @NonNull CommandPermission commandPermission,
            final @NonNull CommandMeta commandMeta
    ) {
        this(commandArguments, commandExecutionHandler, null, commandPermission, commandMeta);
    }

    /**
     * Create a new command builder. Is recommended to use the builder methods
     * in {@link CommandManager} rather than invoking this method directly.
     *
     * @param commandName Base command argument
     * @param commandMeta Command meta instance
     * @param description Command description
     * @param aliases     Command aliases
     * @param          Command sender type
     * @return Command builder
     */
    public static  @NonNull Builder newBuilder(
            final @NonNull String commandName,
            final @NonNull CommandMeta commandMeta,
            final @NonNull Description description,
            final @NonNull String... aliases
    ) {
        final Map<@NonNull CommandArgument, @NonNull Description> map = new LinkedHashMap<>();
        map.put(StaticArgument.of(commandName, aliases), description);
        return new Builder<>(
                null,
                commandMeta,
                null,
                map,
                new CommandExecutionHandler.NullCommandExecutionHandler<>(),
                Permission.empty(),
                Collections.emptyList()
        );
    }

    /**
     * Create a new command builder. Is recommended to use the builder methods
     * in {@link CommandManager} rather than invoking this method directly.
     *
     * @param commandName Base command argument
     * @param commandMeta Command meta instance
     * @param aliases     Command aliases
     * @param          Command sender type
     * @return Command builder
     */
    public static  @NonNull Builder newBuilder(
            final @NonNull String commandName,
            final @NonNull CommandMeta commandMeta,
            final @NonNull String... aliases
    ) {
        final Map, Description> map = new LinkedHashMap<>();
        map.put(StaticArgument.of(commandName, aliases), Description.empty());
        return new Builder<>(
                null,
                commandMeta,
                null,
                map,
                new CommandExecutionHandler.NullCommandExecutionHandler<>(),
                Permission.empty(),
                Collections.emptyList()
        );
    }

    /**
     * Return a copy of the command argument array
     *
     * @return Copy of the command argument array
     */
    public @NonNull List> getArguments() {
        return new ArrayList<>(this.arguments.keySet());
    }

    /**
     * Get the command execution handler
     *
     * @return Command execution handler
     */
    public CommandExecutionHandler<@NonNull C> getCommandExecutionHandler() {
        return this.commandExecutionHandler;
    }

    /**
     * Get the required sender type, if one has been specified
     *
     * @return Required sender type
     */
    public @NonNull Optional> getSenderType() {
        return Optional.ofNullable(this.senderType);
    }

    /**
     * Get the command permission
     *
     * @return Command permission
     */
    public @NonNull CommandPermission getCommandPermission() {
        return this.commandPermission;
    }

    /**
     * Get the command meta instance
     *
     * @return Command meta
     */
    public @NonNull CommandMeta getCommandMeta() {
        return this.commandMeta;
    }

    /**
     * Get the description for an argument
     *
     * @param argument Argument
     * @return Argument description
     */
    public @NonNull String getArgumentDescription(final @NonNull CommandArgument argument) {
        return this.arguments.get(argument).getDescription();
    }

    @Override
    public final String toString() {
        final StringBuilder stringBuilder = new StringBuilder();
        for (final CommandArgument argument : this.getArguments()) {
            stringBuilder.append(argument.getName()).append(' ');
        }
        final String build = stringBuilder.toString();
        return build.substring(0, build.length() - 1);
    }

    /**
     * Check whether or not the command is hidden
     *
     * @return {@code true} if the command is hidden, {@code false} if not
     */
    public boolean isHidden() {
        return this.getCommandMeta().getOrDefault("hidden", "true").equals("true");
    }


    /**
     * Builder for {@link Command} instances. The builder is immutable, and each
     * setter method will return a new builder instance.
     *
     * @param  Command sender type
     */
    public static final class Builder {

        private final CommandMeta commandMeta;
        private final Map, Description> commandArguments;
        private final CommandExecutionHandler commandExecutionHandler;
        private final Class senderType;
        private final CommandPermission commandPermission;
        private final CommandManager commandManager;
        private final Collection> flags;

        private Builder(
                final @Nullable CommandManager commandManager,
                final @NonNull CommandMeta commandMeta,
                final @Nullable Class senderType,
                final @NonNull Map<@NonNull CommandArgument, @NonNull Description> commandArguments,
                final @NonNull CommandExecutionHandler<@NonNull C> commandExecutionHandler,
                final @NonNull CommandPermission commandPermission,
                final @NonNull Collection> flags
        ) {
            this.commandManager = commandManager;
            this.senderType = senderType;
            this.commandArguments = Objects.requireNonNull(commandArguments, "Arguments may not be null");
            this.commandExecutionHandler = Objects.requireNonNull(commandExecutionHandler, "Execution handler may not be null");
            this.commandPermission = Objects.requireNonNull(commandPermission, "Permission may not be null");
            this.commandMeta = Objects.requireNonNull(commandMeta, "Meta may not be null");
            this.flags = Objects.requireNonNull(flags, "Flags may not be null");
        }

        /**
         * Add command meta to the internal command meta map
         *
         * @param key   Meta key
         * @param value Meta value
         * @return New builder instance using the inserted meta key-value pair
         */
        public @NonNull Builder meta(final @NonNull String key, final @NonNull String value) {
            final CommandMeta commandMeta = SimpleCommandMeta.builder().with(this.commandMeta).with(key, value).build();
            return new Builder<>(
                    this.commandManager,
                    commandMeta,
                    this.senderType,
                    this.commandArguments,
                    this.commandExecutionHandler,
                    this.commandPermission,
                    this.flags
            );
        }

        /**
         * Supply a command manager instance to the builder. This will be used when attempting to
         * retrieve command argument parsers, in the case that they're needed. This
         * is optional
         *
         * @param commandManager Command manager
         * @return New builder instance using the provided command manager
         */
        public @NonNull Builder manager(final @Nullable CommandManager commandManager) {
            return new Builder<>(
                    commandManager,
                    this.commandMeta,
                    this.senderType,
                    this.commandArguments,
                    this.commandExecutionHandler,
                    this.commandPermission,
                    this.flags
            );
        }

        /**
         * Inserts a required {@link StaticArgument} into the command chain
         *
         * @param main    Main argument name
         * @param aliases Argument aliases
         * @return New builder instance with the modified command chain
         */
        public @NonNull Builder literal(
                final @NonNull String main,
                final @NonNull String... aliases
        ) {
            return this.argument(StaticArgument.of(main, aliases));
        }

        /**
         * Inserts a required {@link StaticArgument} into the command chain
         *
         * @param main        Main argument name
         * @param description Literal description
         * @param aliases     Argument aliases
         * @return New builder instance with the modified command chain
         */
        public @NonNull Builder literal(
                final @NonNull String main,
                final @NonNull Description description,
                final @NonNull String... aliases
        ) {
            return this.argument(StaticArgument.of(main, aliases), description);
        }

        /**
         * Add a new command argument with an empty description to the command
         *
         * @param builder Argument to add. {@link CommandArgument.Builder#build()} will be invoked
         *                and the result will be registered in the command.
         * @param      Argument type
         * @return New builder instance with the command argument inserted into the argument list
         */
        public  @NonNull Builder argument(final CommandArgument.@NonNull Builder builder) {
            return this.argument(builder.build(), Description.empty());
        }

        /**
         * Add a new command argument with an empty description to the command
         *
         * @param argument Argument to add
         * @param       Argument type
         * @return New builder instance with the command argument inserted into the argument list
         */
        public  @NonNull Builder argument(final @NonNull CommandArgument argument) {
            return this.argument(argument, Description.empty());
        }

        /**
         * Add a new command argument to the command
         *
         * @param argument    Argument to add
         * @param description Argument description
         * @param          Argument type
         * @return New builder instance with the command argument inserted into the argument list
         */
        public  @NonNull Builder argument(
                final @NonNull CommandArgument argument,
                final @NonNull Description description
        ) {
            if (argument.isArgumentRegistered()) {
                throw new IllegalArgumentException("The provided argument has already been associated with a command."
                        + " Use CommandArgument#copy to create a copy of the argument.");
            }
            argument.setArgumentRegistered();
            final Map, Description> commandArgumentMap = new LinkedHashMap<>(this.commandArguments);
            commandArgumentMap.put(argument, description);
            return new Builder<>(
                    this.commandManager,
                    this.commandMeta,
                    this.senderType,
                    commandArgumentMap,
                    this.commandExecutionHandler,
                    this.commandPermission,
                    this.flags
            );
        }

        /**
         * Add a new command argument to the command
         *
         * @param builder     Argument to add. {@link CommandArgument.Builder#build()} will be invoked
         *                    and the result will be registered in the command.
         * @param description Argument description
         * @param          Argument type
         * @return New builder instance with the command argument inserted into the argument list
         */
        public  @NonNull Builder argument(
                final CommandArgument.@NonNull Builder builder,
                final @NonNull Description description
        ) {
            final Map, Description> commandArgumentMap = new LinkedHashMap<>(this.commandArguments);
            commandArgumentMap.put(builder.build(), description);
            return new Builder<>(
                    this.commandManager,
                    this.commandMeta,
                    this.senderType,
                    commandArgumentMap,
                    this.commandExecutionHandler,
                    this.commandPermission,
                    this.flags
            );
        }

        /**
         * Add a new command argument by interacting with a constructed command argument builder
         *
         * @param clazz           Argument class
         * @param name            Argument name
         * @param builderConsumer Builder consumer
         * @param              Argument type
         * @return New builder instance with the command argument inserted into the argument list
         */
        public  @NonNull Builder argument(
                final @NonNull Class clazz,
                final @NonNull String name,
                final @NonNull Consumer> builderConsumer
        ) {
            final CommandArgument.Builder builder = CommandArgument.ofType(clazz, name);
            if (this.commandManager != null) {
                builder.manager(this.commandManager);
            }
            builderConsumer.accept(builder);
            return this.argument(builder.build());
        }

        // Compound helper methods

        /**
         * Create a new argument pair that maps to {@link Pair}
         * 

* For this to work, there must be a {@link CommandManager} * attached to the command builder. To guarantee this, it is recommended to get the command builder instance * using {@link CommandManager#commandBuilder(String, String...)} * * @param name Name of the argument * @param names Pair containing the names of the sub-arguments * @param parserPair Pair containing the types of the sub-arguments. There must be parsers for these types registered * in the {@link cloud.commandframework.arguments.parser.ParserRegistry} used by the * {@link CommandManager} attached to this command * @param description Description of the argument * @param First type * @param Second type * @return Builder instance with the argument inserted */ public @NonNull Builder argumentPair( final @NonNull String name, final @NonNull Pair<@NonNull String, @NonNull String> names, final @NonNull Pair<@NonNull Class, @NonNull Class> parserPair, final @NonNull Description description ) { if (this.commandManager == null) { throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); } return this.argument(ArgumentPair.of(this.commandManager, name, names, parserPair).simple(), description); } /** * Create a new argument pair that maps to a custom type. *

* For this to work, there must be a {@link CommandManager} * attached to the command builder. To guarantee this, it is recommended to get the command builder instance * using {@link CommandManager#commandBuilder(String, String...)} * * @param name Name of the argument * @param outputType The output type * @param names Pair containing the names of the sub-arguments * @param parserPair Pair containing the types of the sub-arguments. There must be parsers for these types registered * in the {@link cloud.commandframework.arguments.parser.ParserRegistry} used by the * {@link CommandManager} attached to this command * @param mapper Mapper that maps from {@link Pair} to the custom type * @param description Description of the argument * @param First type * @param Second type * @param Output type * @return Builder instance with the argument inserted */ public @NonNull Builder argumentPair( final @NonNull String name, final @NonNull TypeToken outputType, final @NonNull Pair names, final @NonNull Pair, Class> parserPair, final @NonNull BiFunction, O> mapper, final @NonNull Description description ) { if (this.commandManager == null) { throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); } return this.argument( ArgumentPair.of(this.commandManager, name, names, parserPair).withMapper(outputType, mapper), description ); } /** * Create a new argument pair that maps to {@link cloud.commandframework.types.tuples.Triplet} *

* For this to work, there must be a {@link CommandManager} * attached to the command builder. To guarantee this, it is recommended to get the command builder instance * using {@link CommandManager#commandBuilder(String, String...)} * * @param name Name of the argument * @param names Triplet containing the names of the sub-arguments * @param parserTriplet Triplet containing the types of the sub-arguments. There must be parsers for these types * registered in the {@link cloud.commandframework.arguments.parser.ParserRegistry} * used by the {@link CommandManager} attached to this command * @param description Description of the argument * @param First type * @param Second type * @param Third type * @return Builder instance with the argument inserted */ public @NonNull Builder argumentTriplet( final @NonNull String name, final @NonNull Triplet names, final @NonNull Triplet, Class, Class> parserTriplet, final @NonNull Description description ) { if (this.commandManager == null) { throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); } return this.argument(ArgumentTriplet.of(this.commandManager, name, names, parserTriplet).simple(), description); } /** * Create a new argument triplet that maps to a custom type. *

* For this to work, there must be a {@link CommandManager} * attached to the command builder. To guarantee this, it is recommended to get the command builder instance * using {@link CommandManager#commandBuilder(String, String...)} * * @param name Name of the argument * @param outputType The output type * @param names Triplet containing the names of the sub-arguments * @param parserTriplet Triplet containing the types of the sub-arguments. There must be parsers for these types * registered in the {@link cloud.commandframework.arguments.parser.ParserRegistry} used by * the {@link CommandManager} attached to this command * @param mapper Mapper that maps from {@link Triplet} to the custom type * @param description Description of the argument * @param First type * @param Second type * @param Third type * @param Output type * @return Builder instance with the argument inserted */ public @NonNull Builder argumentTriplet( final @NonNull String name, final @NonNull TypeToken outputType, final @NonNull Triplet names, final @NonNull Triplet, Class, Class> parserTriplet, final @NonNull BiFunction, O> mapper, final @NonNull Description description ) { if (this.commandManager == null) { throw new IllegalStateException("This cannot be called from a command that has no command manager attached"); } return this.argument( ArgumentTriplet.of(this.commandManager, name, names, parserTriplet).withMapper(outputType, mapper), description ); } // End of compound helper methods /** * Specify the command execution handler * * @param commandExecutionHandler New execution handler * @return New builder instance using the command execution handler */ public @NonNull Builder handler(final @NonNull CommandExecutionHandler commandExecutionHandler) { return new Builder<>( this.commandManager, this.commandMeta, this.senderType, this.commandArguments, commandExecutionHandler, this.commandPermission, this.flags ); } /** * Specify a required sender type * * @param senderType Required sender type * @return New builder instance using the command execution handler */ public @NonNull Builder senderType(final @NonNull Class senderType) { return new Builder<>( this.commandManager, this.commandMeta, senderType, this.commandArguments, this.commandExecutionHandler, this.commandPermission, this.flags ); } /** * Specify a command permission * * @param permission Command permission * @return New builder instance using the command permission */ public @NonNull Builder permission(final @NonNull CommandPermission permission) { return new Builder<>( this.commandManager, this.commandMeta, this.senderType, this.commandArguments, this.commandExecutionHandler, permission, this.flags ); } /** * Specify a command permission * * @param permission Command permission * @return New builder instance using the command permission */ public @NonNull Builder permission(final @NonNull String permission) { return new Builder<>( this.commandManager, this.commandMeta, this.senderType, this.commandArguments, this.commandExecutionHandler, Permission.of(permission), this.flags ); } /** * Make the current command be a proxy of the supplied command. This means that * all of the proxied commands variable command arguments will be inserted into this * builder instance, in the order they are declared in the proxied command. Furthermore, * the proxied commands command handler will be showed by the command that is currently * being built. If the current command builder does not have a permission node set, this * too will be copied. * * @param command Command to proxy * @return New builder that proxies the given command */ public @NonNull Builder proxies(final @NonNull Command command) { Builder builder = this; for (final CommandArgument argument : command.getArguments()) { if (argument instanceof StaticArgument) { continue; } final CommandArgument builtArgument = argument.copy(); builder = builder.argument(builtArgument, Description.of(command.getArgumentDescription(argument))); } if (this.commandPermission.toString().isEmpty()) { builder = builder.permission(command.getCommandPermission()); } return builder.handler(command.commandExecutionHandler); } /** * Indicate that the command should be hidden from help menus * and other places where commands are exposed to users * * @return New builder instance that indicates that the constructed command should be hidden */ public @NonNull Builder hidden() { return this.meta("hidden", "true"); } /** * Register a new command flag * * @param flag Flag * @param Flag value type * @return New builder instance that uses the provided flag */ public @NonNull Builder flag(final @NonNull CommandFlag flag) { final List> flags = new ArrayList<>(this.flags); flags.add(flag); return new Builder<>( this.commandManager, this.commandMeta, this.senderType, this.commandArguments, this.commandExecutionHandler, this.commandPermission, Collections.unmodifiableList(flags) ); } /** * Register a new command flag * * @param builder Flag builder. {@link CommandFlag.Builder#build()} will be invoked. * @param Flag value type * @return New builder instance that uses the provided flag */ public @NonNull Builder flag(final CommandFlag.@NonNull Builder builder) { return this.flag(builder.build()); } /** * Build a command using the builder instance * * @return Built command */ public @NonNull Command build() { final LinkedHashMap, Description> commandArguments = new LinkedHashMap<>(this.commandArguments); /* Construct flag node */ if (!flags.isEmpty()) { final FlagArgument flagArgument = new FlagArgument<>(this.flags); commandArguments.put(flagArgument, Description.of("Command flags")); } return new Command<>( Collections.unmodifiableMap(commandArguments), this.commandExecutionHandler, this.senderType, this.commandPermission, this.commandMeta ); } } }