ModuleManager.java

        package com.niklasarndt.discordbutler;

import com.niklasarndt.discordbutler.modules.ButlerCommand;
import com.niklasarndt.discordbutler.modules.ButlerContext;
import com.niklasarndt.discordbutler.modules.ButlerModule;
import com.niklasarndt.discordbutler.modules.core.command.RedoCommand;
import com.niklasarndt.discordbutler.util.ResultBuilder;
import net.dv8tion.jda.api.entities.Message;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;


/**
 * Created by Niklas on 2020/07/25.
 */
public class ModuleManager {

    private static final Logger logger = LoggerFactory.getLogger(ModuleManager.class);

    private final Butler instance;
    private final List<ButlerModule> modules = new ArrayList<>();
    private String mostRecentMessage;

    public ModuleManager(Butler instance) {
        this.instance = instance;
    }

    public void loadAll() {
        unloadAll();
        final Set<Class<? extends ButlerModule>> classes =
                new Reflections("com.niklasarndt.discordbutler.modules")
                        .getSubTypesOf(ButlerModule.class);

        classes.forEach(item -> {
            try {
                final ButlerModule module = item.getDeclaredConstructor().newInstance();
                registerModule(module);
            } catch (Exception e) {
                logger.error("Unable to register module \"{}\"", item.getSimpleName(), e);
            }
        });
        logger.info("{} modules loaded.", modules.size());
        logger.info("{} commands registered.", getCommandCount());
    }

    public void unloadAll() {
        modules.forEach(mod -> unregisterModule(mod, false));
        modules.clear();
        logger.info("Unloaded all modules.");
    }

    public void registerModule(ButlerModule module) {
        if (hasModule(module.info().getName())) {
            throw new IllegalStateException(String.format("There's already a module " +
                    "claiming the name \"%s\".", module.info().getName()));
        }
        modules.add(module);
        module.onStartup();
    }

    public void unregisterModule(String name) {
        unregisterModule(modules.stream()
                .filter(item -> item.info().getName().equals(name)).findFirst().orElse(null));
    }

    public void unregisterModule(ButlerModule module) {
        unregisterModule(module, true);
    }

    public void unregisterModule(ButlerModule module, boolean removeFromList) {
        if (module == null || !hasModule(module)) return;

        module.onShutdown();
        if (removeFromList) modules.remove(module);
    }

    private boolean hasModule(String name) {
        return modules.stream().anyMatch(item -> item.info().getName().equals(name));
    }

    private boolean hasModule(ButlerModule module) {
        return modules.contains(module);
    }

    public ResultBuilder execute(Message message) {
        return execute(message.getContentRaw(), message);
    }

    public ResultBuilder execute(String content, Message origin) {
        String[] parts = content.split(" ");
        String name = parts[0].toLowerCase();
        String[] args = Arrays.copyOfRange(parts, 1, parts.length);


        Optional<ButlerCommand> command = findCommand(name);

        if (command.isPresent()) {
            ButlerCommand cmd = command.get();

            ButlerContext context = new ButlerContext(instance, origin, name, args,
                    new ResultBuilder(command.get().module().info()));

            if (args.length < cmd.info().getArgsMin() || args.length > cmd.info().getArgsMax()) {

                context.resultBuilder().invalidArgsLength(
                        cmd.info().getArgsMin(), cmd.info().getArgsMax(), args.length);
            } else {
                try {
                    cmd.execute(context);
                } catch (Exception e) {
                    logger.error("Could not execute command '{}' (module: {})",
                            name, cmd.module().info().getName(), e);
                    context.resultBuilder().error(e);
                }
            }

            if (!(cmd instanceof RedoCommand)) this.mostRecentMessage = content;
            return context.resultBuilder();
        } else return ResultBuilder.NOT_FOUND;
    }

    public List<ButlerModule> getModules() {
        return Collections.unmodifiableList(modules);
    }

    public Optional<ButlerCommand> findCommand(String name) {
        return modules.stream()
                .map(m -> m.getCommand(name)).flatMap(Optional::stream)
                .findFirst();
    }

    private int getCommandCount() {
        return modules.stream().mapToInt(ButlerModule::getCommandCount).sum();
    }

    public Optional<ButlerModule> getModule(String name) {
        return modules.stream().filter(m -> m.info().getName().equals(name)).findFirst();
    }

    public ResultBuilder redo() {
        return execute(mostRecentMessage, null);
    }

    public String getMostRecentMessage() {
        return mostRecentMessage;
    }
}