ch.vorburger.minecraft.command.AnnotatedCommandManager.java Source code

Java tutorial

Introduction

Here is the source code for ch.vorburger.minecraft.command.AnnotatedCommandManager.java

Source

/*
 * This file is part of ch.vorburger.minecraft, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2015 Michael Vorburger <http://www.vorburger.ch>
 *
 * 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 ch.vorburger.minecraft.command;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.spongepowered.api.Game;
import org.spongepowered.api.command.CommandMapping;
import org.spongepowered.api.command.CommandSource;
import org.spongepowered.api.command.args.CommandContext;
import org.spongepowered.api.command.args.CommandElement;
import org.spongepowered.api.command.args.GenericArguments;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.text.Text;

import com.google.common.collect.Iterables;
import com.google.inject.Inject;

/**
 * TODO Better doc.
 *
 * TODO Finish implementation (or re-simplify again?) - this currently only works for simple cases.
 *
 * Supports Optional<T> to indicate optional Command arguments (creates GenericArguments.optional).
 * Supports "varargs" String... to indicate "everything that follows" (creates GenericArguments.remainingJoinedStrings)
 *
 * @author Michael Vorburger
 */
public class AnnotatedCommandManager {

    protected @Inject Logger logger;
    protected @Inject Game game;
    protected @Inject CommandHelper commandHelper;
    protected @Inject CommandRegistry commandRegistry;

    // @see org.spongepowered.common.event.SpongeEventManager.register(PluginContainer, Object)
    public void register(final PluginContainer plugin, final Object instanceOfClassWithCommandAnnotatedMethods) {
        final Class<?> handle = instanceOfClassWithCommandAnnotatedMethods.getClass();
        for (final Method method : handle.getMethods()) {
            Command commandAnnotation = method.getAnnotation(Command.class);
            if (commandAnnotation != null) {
                if (isValidHandler(method)) {
                    final String commandDescription = commandAnnotation.value();
                    final List<String> commandNameAndAliases = getCommandNameAndAliases(method);
                    final List<CommandElement> args = getCommandElements(method);
                    Optional<CommandMapping> cmd = commandHelper.createCommand(plugin, commandNameAndAliases,
                            commandDescription, new CommandExecutorWithoutResultThrowsThrowable() {
                                @Override
                                public void execute(CommandSource src, CommandContext args) throws Throwable {
                                    Player player = (Player) src; // TODO if instanceof
                                    method.invoke(instanceOfClassWithCommandAnnotatedMethods, player);
                                }
                            }, args.toArray(new CommandElement[0]));
                    if (cmd.isPresent())
                        commandRegistry.register(cmd.get());
                    else
                        logger.warn("Failed to createCommand() for @Command {} ", method);
                }
            }
        }
    }

    protected List<String> getCommandNameAndAliases(Method method) {
        final Command commandAnnotation = method.getAnnotation(Command.class);
        final List<String> aliases = Arrays.asList(commandAnnotation.aliases());
        final List<String> commandNameAndAliases = new ArrayList<String>(aliases.size() + 1);
        commandNameAndAliases.add(method.getName());
        commandNameAndAliases.addAll(aliases);
        return commandNameAndAliases;
    }

    protected static class MethodArg {
        String name;
        Type type;
        boolean optional = false;
        boolean vararg = false;
    }

    protected List<MethodArg> getMethodArgs(Method method) {
        // TODO rewrite this in nice Java 8 functional "mapping" style
        Parameter[] parameters = method.getParameters();
        List<MethodArg> args = new ArrayList<>(parameters.length);
        for (Parameter parameter : parameters) {
            if (!parameter.isNamePresent())
                // https://docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html
                throw new IllegalStateException(
                        "Needs javac -parameters; or, in Eclipse: 'Store information about method parameters (usable via reflection)' in Window -> Preferences -> Java -> Compiler");
            MethodArg arg = new MethodArg();
            args.add(arg);

            String name = parameter.getName();
            arg.name = name;

            Type type = parameter.getParameterizedType();
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                Type[] actualTypes = parameterizedType.getActualTypeArguments();
                Type rawType = parameterizedType.getRawType();
                if (Optional.class.isAssignableFrom((Class<?>) rawType)) {
                    arg.optional = true;
                    arg.type = actualTypes[0];
                }
            }

            if (arg.type == null) {
                arg.type = type;
            }
        }
        if (method.isVarArgs()) {
            Iterables.getLast(args).vararg = true;
        }
        return args;
    }

    protected List<CommandElement> getCommandElements(Method method) {
        List<CommandElement> commandElements = new ArrayList<>(method.getParameterCount());
        for (MethodArg arg : getMethodArgs(method)) {
            Optional<CommandElement> optCommandElement = getCommandElement(arg.name, arg.type, arg.vararg);
            if (!optCommandElement.isPresent())
                throw new IllegalArgumentException(
                        "@Command method " + method.toString() + " parameter unsupported type: " + arg.type);
            CommandElement commandElement = optCommandElement.get();
            if (arg.optional)
                commandElement = GenericArguments.optional(commandElement);
            if (arg.vararg)
                if (String.class.equals(arg.type))
                    commandElement = GenericArguments.remainingJoinedStrings(Text.of(arg.name));
                else
                    throw new IllegalArgumentException("@Command method " + method.toString()
                            + " last vararg parameter unsupported type, must be String: " + arg.type);
            else
                commandElement = GenericArguments.onlyOne(commandElement);
            commandElements.add(commandElement);
        }
        return commandElements;
    }

    protected Optional<CommandElement> getCommandElement(String name, Type type, boolean isVarArgs) {
        if (type instanceof Class) {
            Class<?> typeClass = (Class<?>) type;
            if (Player.class.isAssignableFrom(typeClass)) {
                return Optional.of(GenericArguments.player(Text.of(name)));
            } else if (CommandSource.class.isAssignableFrom(typeClass)) {
                return Optional.of(GenericArguments.playerOrSource(Text.of(name)));
            } else if (String.class.isAssignableFrom(typeClass)) {
                return Optional.of(GenericArguments.string(Text.of(name)));
            } else if (typeClass.isArray() && isVarArgs && typeClass.getComponentType().equals(String.class)) {
                return Optional.of(GenericArguments.string(Text.of(name)));
            } else if (Integer.TYPE.isAssignableFrom(typeClass) || Integer.class.isAssignableFrom(typeClass)) {
                return Optional.of(GenericArguments.integer(Text.of(name)));
            }
        }
        return Optional.empty();
    }

    protected boolean isValidHandler(final Method method) {
        final String msg = "The @Command method {} ";
        final int modifiers = method.getModifiers();
        if (Modifier.isStatic(modifiers)) {
            logger.warn(msg + "cannot be static", method);
            return false;
        } else if (!Modifier.isPublic(modifiers)) {
            logger.warn(msg + "must be public", method);
            return false;
        } else if (Modifier.isAbstract(modifiers)) {
            logger.warn(msg + "cannot be abstract", method);
            return false;
        } else if (method.getDeclaringClass().isInterface()) {
            logger.warn(msg + "must be on class instead of interface", method);
            return false;
            // No need for this, it's an unncessary constraint, and harmful for Xtend def with inferred return type
            //      } else if (method.getReturnType() != void.class) {
            //         logger.warn(msg + "must have no (void) return type", method);
            //         return false;
        } else
            return true;
    }

    public void unregisterAll() {
        commandRegistry.unregisterAll();
    }

}