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

cloud.commandframework.brigadier.CloudBrigadierManager Maven / Gradle / Ivy

//
// MIT License
//
// Copyright (c) 2022 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.brigadier;

import cloud.commandframework.Command;
import cloud.commandframework.CommandManager;
import cloud.commandframework.CommandTree;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.StaticArgument;
import cloud.commandframework.arguments.compound.CompoundArgument;
import cloud.commandframework.arguments.compound.FlagArgument;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.arguments.parser.MappedArgumentParser;
import cloud.commandframework.arguments.standard.BooleanArgument;
import cloud.commandframework.arguments.standard.ByteArgument;
import cloud.commandframework.arguments.standard.DoubleArgument;
import cloud.commandframework.arguments.standard.FloatArgument;
import cloud.commandframework.arguments.standard.IntegerArgument;
import cloud.commandframework.arguments.standard.LongArgument;
import cloud.commandframework.arguments.standard.ShortArgument;
import cloud.commandframework.arguments.standard.StringArgument;
import cloud.commandframework.arguments.standard.StringArrayArgument;
import cloud.commandframework.brigadier.argument.WrappedBrigadierParser;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.permission.CommandPermission;
import cloud.commandframework.permission.Permission;
import cloud.commandframework.types.tuples.Pair;
import com.mojang.brigadier.LiteralMessage;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.DoubleArgumentType;
import com.mojang.brigadier.arguments.FloatArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.ParsedCommandNode;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.geantyref.TypeToken;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * Manager used to map cloud {@link Command}
 * 

* The structure of this class is largely inspired by * * ACFBrigadiermanager in the ACF project, which was originally written by MiniDigger and licensed under the MIT license. * * @param Command sender type * @param Brigadier sender type */ @SuppressWarnings({"unchecked", "rawtypes"}) public final class CloudBrigadierManager { private static final SuggestionProvider DELEGATE_TO_CLOUD = (c, b) -> b.buildFuture(); private final Map, BrigadierMapping> mappers; private final Map<@NonNull Class, @NonNull Supplier<@Nullable ArgumentType>> defaultArgumentTypeSuppliers; private final Supplier> dummyContextProvider; private final CommandManager commandManager; private Function brigadierCommandSenderMapper; private Function backwardsBrigadierCommandSenderMapper; /** * A sentinel value for declaring that suggestions should be delegated to cloud. * * @param the sender type * @return a singleton sentinel suggestion provider */ static SuggestionProvider delegateSuggestions() { return (SuggestionProvider) DELEGATE_TO_CLOUD; } /** * Create a new cloud brigadier manager * * @param commandManager Command manager * @param dummyContextProvider Provider of dummy context for completions */ public CloudBrigadierManager( final @NonNull CommandManager commandManager, final @NonNull Supplier<@NonNull CommandContext> dummyContextProvider ) { this.mappers = new HashMap<>(); this.defaultArgumentTypeSuppliers = new HashMap<>(); this.commandManager = commandManager; this.dummyContextProvider = dummyContextProvider; this.registerInternalMappings(); commandManager.registerCommandPreProcessor(ctx -> { if (this.backwardsBrigadierCommandSenderMapper != null) { ctx.getCommandContext().store( WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER, this.backwardsBrigadierCommandSenderMapper.apply(ctx.getCommandContext().getSender()) ); } }); } private void registerInternalMappings() { /* Map byte, short and int to IntegerArgumentType */ this.registerMapping(new TypeToken>() { }, builder -> builder.to(argument -> { return IntegerArgumentType.integer(argument.getMin(), argument.getMax()); })); this.registerMapping(new TypeToken>() { }, builder -> builder.to(argument -> { return IntegerArgumentType.integer(argument.getMin(), argument.getMax()); })); this.registerMapping(new TypeToken>() { }, builder -> builder.to(argument -> { if (!argument.hasMin() && !argument.hasMax()) { return IntegerArgumentType.integer(); } if (argument.hasMin() && !argument.hasMax()) { return IntegerArgumentType.integer(argument.getMin()); } else if (!argument.hasMin()) { // Brig uses Integer.MIN_VALUE and Integer.MAX_VALUE for default min/max return IntegerArgumentType.integer(Integer.MIN_VALUE, argument.getMax()); } return IntegerArgumentType.integer(argument.getMin(), argument.getMax()); })); /* Map float to FloatArgumentType */ this.registerMapping(new TypeToken>() { }, builder -> builder.to(argument -> { if (!argument.hasMin() && !argument.hasMax()) { return FloatArgumentType.floatArg(); } if (argument.hasMin() && !argument.hasMax()) { return FloatArgumentType.floatArg(argument.getMin()); } else if (!argument.hasMin()) { // Brig uses -Float.MAX_VALUE and Float.MAX_VALUE for default min/max return FloatArgumentType.floatArg(-Float.MAX_VALUE, argument.getMax()); } return FloatArgumentType.floatArg(argument.getMin(), argument.getMax()); })); /* Map double to DoubleArgumentType */ this.registerMapping(new TypeToken>() { }, builder -> builder.to(argument -> { if (!argument.hasMin() && !argument.hasMax()) { return DoubleArgumentType.doubleArg(); } if (argument.hasMin() && !argument.hasMax()) { return DoubleArgumentType.doubleArg(argument.getMin()); } else if (!argument.hasMin()) { // Brig uses -Double.MAX_VALUE and Double.MAX_VALUE for default min/max return DoubleArgumentType.doubleArg(-Double.MAX_VALUE, argument.getMax()); } return DoubleArgumentType.doubleArg(argument.getMin(), argument.getMax()); })); /* Map long parser to LongArgumentType */ this.registerMapping(new TypeToken>() { }, builder -> builder.to(longParser -> { if (!longParser.hasMin() && !longParser.hasMax()) { return LongArgumentType.longArg(); } if (longParser.hasMin() && !longParser.hasMax()) { return LongArgumentType.longArg(longParser.getMin()); } else if (!longParser.hasMin()) { // Brig uses Long.MIN_VALUE and Long.MAX_VALUE for default min/max return LongArgumentType.longArg(Long.MIN_VALUE, longParser.getMax()); } return LongArgumentType.longArg(longParser.getMin(), longParser.getMax()); })); /* Map boolean to BoolArgumentType */ this.registerMapping(new TypeToken>() { }, builder -> builder.toConstant(BoolArgumentType.bool())); /* Map String properly to StringArgumentType */ this.registerMapping(new TypeToken>() { }, builder -> builder.cloudSuggestions().to(argument -> { switch (argument.getStringMode()) { case QUOTED: return StringArgumentType.string(); case GREEDY: case GREEDY_FLAG_YIELDING: return StringArgumentType.greedyString(); default: return StringArgumentType.word(); } })); /* Map flags to a greedy string */ this.registerMapping(new TypeToken>() { }, builder -> builder.cloudSuggestions().toConstant(StringArgumentType.greedyString())); /* Map String[] to a greedy string */ this.registerMapping(new TypeToken>() { }, builder -> builder.cloudSuggestions().toConstant(StringArgumentType.greedyString())); /* Map wrapped parsers to their native types */ this.registerMapping(new TypeToken>() { }, builder -> builder.to(WrappedBrigadierParser::getNativeArgument)); } /** * Set the mapper between the Brigadier command sender type and the Cloud command sender type * * @param mapper Mapper * @since 1.2.0 */ public void brigadierSenderMapper( final @NonNull Function<@NonNull S, @Nullable C> mapper ) { this.brigadierCommandSenderMapper = mapper; } /** * Get the mapper between Brigadier and Cloud command senders, if one exists * * @return Mapper * @since 1.2.0 */ public @Nullable Function<@NonNull S, @Nullable C> brigadierSenderMapper() { return this.brigadierCommandSenderMapper; } /** * Set the backwards mapper from Cloud to Brigadier command senders. * *

This is passed to completion requests for mapped argument types.

* * @param mapper the reverse brigadier sender mapper * @since 1.5.0 */ public void backwardsBrigadierSenderMapper(final @NonNull Function<@NonNull C, @Nullable S> mapper) { this.backwardsBrigadierCommandSenderMapper = mapper; } /** * Set whether to use Brigadier's native suggestions for number argument types. *

* If Brigadier's suggestions are not used, cloud's default number suggestion provider will be used. * * @param nativeNumberSuggestions Whether or not Brigadier suggestions should be used for numbers * @since 1.2.0 */ public void setNativeNumberSuggestions(final boolean nativeNumberSuggestions) { this.setNativeSuggestions(new TypeToken>() { }, nativeNumberSuggestions); this.setNativeSuggestions(new TypeToken>() { }, nativeNumberSuggestions); this.setNativeSuggestions(new TypeToken>() { }, nativeNumberSuggestions); this.setNativeSuggestions(new TypeToken>() { }, nativeNumberSuggestions); this.setNativeSuggestions(new TypeToken>() { }, nativeNumberSuggestions); this.setNativeSuggestions(new TypeToken>() { }, nativeNumberSuggestions); } /** * Set whether to use Brigadier's native suggestions for an argument type with an already registered mapper. *

* If Brigadier's suggestions are not used, suggestions will fall back to the cloud suggestion provider. * * @param argumentType cloud argument parser type * @param nativeSuggestions Whether or not Brigadier suggestions should be used * @param argument type * @param cloud argument parser type * @throws IllegalArgumentException when there is no mapper registered for the provided argument type * @since 1.2.0 */ public > void setNativeSuggestions( final @NonNull TypeToken argumentType, final boolean nativeSuggestions ) throws IllegalArgumentException { final BrigadierMapping pair = this.mappers.get( GenericTypeReflector.erase(argumentType.getType()) ); if (pair == null) { throw new IllegalArgumentException( "No mapper registered for type: " + GenericTypeReflector .erase(argumentType.getType()) .toGenericString() ); } this.mappers.put( GenericTypeReflector.erase(argumentType.getType()), pair.withNativeSuggestions(nativeSuggestions) ); } /** * Register a cloud-Brigadier mapping * * @param argumentType cloud argument parser type * @param nativeSuggestions Whether or not Brigadier suggestions should be used * @param mapper mapper function * @param cloud argument value type * @param cloud argument type * @param Brigadier argument type value * @deprecated for removal since 1.5.0, use {@link #registerMapping(TypeToken, Consumer)} instead. */ @Deprecated @SuppressWarnings("unchecked") public , O> void registerMapping( final @NonNull TypeToken argumentType, final boolean nativeSuggestions, final @NonNull Function<@NonNull ? extends K, @NonNull ? extends ArgumentType> mapper ) { this.registerMapping(argumentType, builder -> { builder.to((Function>) mapper); if (!nativeSuggestions) { builder.cloudSuggestions(); } }); } /** * Register a cloud-Brigadier mapping. * * @param parserType The cloud argument parser type * @param configurer a callback that will configure the mapping attributes * @param cloud argument parser type * @since 1.5.0 */ public > void registerMapping( final @NonNull TypeToken parserType, final Consumer> configurer ) { final BrigadierMapping.BuilderImpl builder = new BrigadierMapping.BuilderImpl<>(); configurer.accept(builder); this.mappers.put(GenericTypeReflector.erase(parserType.getType()), builder.build()); } /** * Register a default mapping to between a class and a Brigadier argument type * * @param clazz Type to map * @param supplier Supplier that supplies the argument type */ public void registerDefaultArgumentTypeSupplier( final @NonNull Class clazz, final @NonNull Supplier<@Nullable ArgumentType> supplier ) { this.defaultArgumentTypeSuppliers.put(clazz, supplier); } @SuppressWarnings("all") private > @Nullable Pair<@NonNull ArgumentType, @Nullable SuggestionProvider> getArgument( final @NonNull TypeToken valueType, final @NonNull K argumentParser ) { /* Unwrap mapped arguments */ ArgumentParser commandArgument = (ArgumentParser) argumentParser; while (commandArgument instanceof MappedArgumentParser) { commandArgument = ((MappedArgumentParser) commandArgument).getBaseParser(); } final BrigadierMapping mapping = (BrigadierMapping) this.mappers .get(commandArgument.getClass()); if (mapping == null || mapping.getMapper() == null) { return this.createDefaultMapper(valueType); } return Pair.of( (ArgumentType) ((Function) mapping.getMapper()).apply(commandArgument), mapping.makeSuggestionProvider(argumentParser) ); } private @NonNull Pair<@NonNull ArgumentType, @Nullable SuggestionProvider> createDefaultMapper( final @NonNull TypeToken clazz ) { final Supplier> argumentTypeSupplier = this.defaultArgumentTypeSuppliers .get(GenericTypeReflector.erase(clazz.getType())); final @Nullable ArgumentType defaultType; if (argumentTypeSupplier != null) { defaultType = argumentTypeSupplier.get(); } else { defaultType = null; } if (defaultType != null) { return Pair.of(argumentTypeSupplier.get(), null); } return Pair.of(StringArgumentType.word(), delegateSuggestions()); } /** * Create a new literal command node * * @param label Command label * @param cloudCommand Cloud command instance * @param permissionChecker Permission checker * @param forceRegister Whether or not to force register an executor at every node * @param executor Command executor * @return Literal command node */ public @NonNull LiteralCommandNode createLiteralCommandNode( final @NonNull String label, final @NonNull Command cloudCommand, final @NonNull BiPredicate<@NonNull S, @NonNull CommandPermission> permissionChecker, final boolean forceRegister, final com.mojang.brigadier.@NonNull Command executor ) { final CommandTree.Node> node = this.commandManager .commandTree().getNamedNode(cloudCommand.getArguments().get(0).getName()); final SuggestionProvider provider = (context, builder) -> this.buildSuggestions( context, null, /* parent node, null for the literal command node root */ node.getValue(), builder ); final LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder .literal(label) .requires(sender -> permissionChecker.test(sender, (CommandPermission) node.getNodeMeta() .getOrDefault( "permission", Permission.empty() ))); if (forceRegister || (node.getValue() != null && node.getValue().getOwningCommand() != null)) { literalArgumentBuilder.executes(executor); } literalArgumentBuilder.executes(executor); final LiteralCommandNode constructedRoot = literalArgumentBuilder.build(); for (final CommandTree.Node> child : node.getChildren()) { constructedRoot.addChild(this.constructCommandNode(forceRegister, child, permissionChecker, executor, provider ).build()); } return constructedRoot; } /** * Create a literal command from Brigadier command info, and a cloud command instance * * @param cloudCommand Cloud root command * @param root Brigadier root command * @param suggestionProvider Brigadier suggestions provider * @param executor Brigadier command executor * @param permissionChecker Permission checker * @return Constructed literal command node */ public @NonNull LiteralCommandNode createLiteralCommandNode( final CommandTree.@NonNull Node<@NonNull CommandArgument> cloudCommand, final @NonNull LiteralCommandNode root, final @NonNull SuggestionProvider suggestionProvider, final com.mojang.brigadier.@NonNull Command executor, final @NonNull BiPredicate<@NonNull S, @NonNull CommandPermission> permissionChecker ) { final LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder.literal(root.getLiteral()) .requires(sender -> permissionChecker.test( sender, (CommandPermission) cloudCommand.getNodeMeta() .getOrDefault( "permission", Permission.empty() ) )); if (cloudCommand.getValue() != null && cloudCommand.getValue().getOwningCommand() != null) { literalArgumentBuilder.executes(executor); } final LiteralCommandNode constructedRoot = literalArgumentBuilder.build(); for (final CommandTree.Node> child : cloudCommand.getChildren()) { constructedRoot.addChild(this.constructCommandNode(true, child, permissionChecker, executor, suggestionProvider ).build()); } return constructedRoot; } private @NonNull ArgumentBuilder constructCommandNode( final boolean forceExecutor, final CommandTree.@NonNull Node> root, final @NonNull BiPredicate<@NonNull S, @NonNull CommandPermission> permissionChecker, final com.mojang.brigadier.@NonNull Command executor, final SuggestionProvider suggestionProvider ) { if (root.getValue() instanceof CompoundArgument) { @SuppressWarnings("unchecked") final CompoundArgument compoundArgument = (CompoundArgument) root.getValue(); final Object[] parsers = compoundArgument.getParserTuple().toArray(); final Object[] types = compoundArgument.getTypes().toArray(); final Object[] names = compoundArgument.getNames().toArray(); /* Build nodes backwards */ final ArgumentBuilder[] argumentBuilders = new ArgumentBuilder[parsers.length]; for (int i = parsers.length - 1; i >= 0; i--) { @SuppressWarnings("unchecked") final ArgumentParser parser = (ArgumentParser) parsers[i]; final Pair, SuggestionProvider> pair = this.getArgument( TypeToken.get((Class) types[i]), parser ); final SuggestionProvider provider = pair.getSecond() == delegateSuggestions() ? suggestionProvider : pair.getSecond(); final ArgumentBuilder fragmentBuilder = RequiredArgumentBuilder .argument((String) names[i], (ArgumentType) pair.getFirst()) .suggests(provider) .requires(sender -> permissionChecker.test( sender, (CommandPermission) root.getNodeMeta() .getOrDefault( "permission", Permission.empty() ) )); argumentBuilders[i] = fragmentBuilder; if (forceExecutor || ((i == parsers.length - 1) && (root.isLeaf() || !root.getValue().isRequired()))) { fragmentBuilder.executes(executor); } /* Link all previous builder to this one */ if ((i + 1) < parsers.length) { fragmentBuilder.then(argumentBuilders[i + 1]); } } for (final CommandTree.Node> node : root.getChildren()) { argumentBuilders[parsers.length - 1] .then(this.constructCommandNode(forceExecutor, node, permissionChecker, executor, suggestionProvider)); } return argumentBuilders[0]; } final ArgumentBuilder argumentBuilder; if (root.getValue() instanceof StaticArgument) { argumentBuilder = LiteralArgumentBuilder.literal(root.getValue().getName()) .requires(sender -> permissionChecker.test(sender, (CommandPermission) root.getNodeMeta() .getOrDefault( "permission", Permission.empty() ))) .executes(executor); } else { // Register argument final Pair, SuggestionProvider> pair = this.getArgument( root.getValue().getValueType(), root.getValue().getParser() ); final SuggestionProvider provider = pair.getSecond() == delegateSuggestions() ? (context, builder) -> this.buildSuggestions( context, root.getParent(), root.getValue(), builder ) : pair.getSecond(); argumentBuilder = RequiredArgumentBuilder .argument(root.getValue().getName(), (ArgumentType) pair.getFirst()) .suggests(provider) .requires(sender -> permissionChecker.test( sender, (CommandPermission) root.getNodeMeta() .getOrDefault( "permission", Permission.empty() ) )); } if (forceExecutor || root.isLeaf() || !root.getValue().isRequired()) { argumentBuilder.executes(executor); } if (root.getChildren().stream().noneMatch(node -> node.getValue().isRequired())) { argumentBuilder.executes(executor); } for (final CommandTree.Node> node : root.getChildren()) { argumentBuilder.then(this.constructCommandNode(forceExecutor, node, permissionChecker, executor, suggestionProvider)); } return argumentBuilder; } private @NonNull CompletableFuture buildSuggestions( final com.mojang.brigadier.context.@Nullable CommandContext senderContext, final CommandTree.@Nullable Node> parentNode, final @NonNull CommandArgument argument, final @NonNull SuggestionsBuilder builder ) { final CommandContext commandContext; String command = builder.getInput(); if (this.brigadierCommandSenderMapper == null || senderContext == null) { commandContext = this.dummyContextProvider.get(); if (command.startsWith("/") /* Minecraft specific */) { command = command.substring(1); } } else { final C cloudSender = this.brigadierCommandSenderMapper.apply(senderContext.getSource()); commandContext = new CommandContext<>( true, cloudSender, this.commandManager ); command = command.substring(getNodes(senderContext.getLastChild()).get(0).getSecond().getStart()); } /* Remove namespace */ final String leading = command.split(" ")[0]; if (leading.contains(":")) { command = command.substring(leading.split(":")[0].length() + 1); } final List suggestionsUnfiltered = this.commandManager.suggest( commandContext.getSender(), command ); /* Filter suggestions that are literal arguments to avoid duplicates, except for root arguments */ final List suggestions = new ArrayList<>(suggestionsUnfiltered); if (parentNode != null) { final Set siblingLiterals = parentNode.getChildren().stream() .map(CommandTree.Node::getValue) .flatMap(arg -> (arg instanceof StaticArgument) ? ((StaticArgument) arg).getAliases().stream() : Stream.empty()) .collect(Collectors.toSet()); suggestions.removeIf(siblingLiterals::contains); } SuggestionsBuilder suggestionsBuilder = builder; final int lastIndexOfSpaceInRemainingString = builder.getRemaining().lastIndexOf(' '); if (lastIndexOfSpaceInRemainingString != -1) { suggestionsBuilder = builder.createOffset(builder.getStart() + lastIndexOfSpaceInRemainingString + 1); } for (final String suggestion : suggestions) { String tooltip = argument.getName(); if (!(argument instanceof StaticArgument)) { if (argument.isRequired()) { tooltip = '<' + tooltip + '>'; } else { tooltip = '[' + tooltip + ']'; } } suggestionsBuilder = suggestionsBuilder.suggest(suggestion, new LiteralMessage(tooltip)); } return suggestionsBuilder.buildFuture(); } /** * Return type changed at some point, but information is essentially the same. This code works for both versions of the * method. * * @param commandContext command context * @param source type * @return parsed nodes */ private static List, StringRange>> getNodes( final com.mojang.brigadier.context.CommandContext commandContext ) { try { final Method getNodesMethod = commandContext.getClass().getDeclaredMethod("getNodes"); final Object nodes = getNodesMethod.invoke(commandContext); if (nodes instanceof List) { return ParsedCommandNodeHandler.toPairList((List) nodes); } else if (nodes instanceof Map) { return ((Map, StringRange>) nodes).entrySet().stream() .map(entry -> Pair.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } else { throw new IllegalStateException(); } } catch (final ReflectiveOperationException ex) { throw new RuntimeException(ex); } } // Inner class to prevent attempting to load ParsedCommandNode when it doesn't exist private static final class ParsedCommandNodeHandler { private ParsedCommandNodeHandler() { } private static List, StringRange>> toPairList(final List nodes) { return ((List>) nodes).stream() .map(n -> Pair.of(n.getNode(), n.getRange())) .collect(Collectors.toList()); } } }