/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) 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 org.lanternpowered.server.command;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.spongepowered.api.command.CommandMessageFormatting.error;
import static org.spongepowered.api.util.SpongeApiTranslationHelper.t;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import org.lanternpowered.server.text.LanternTexts;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.command.CommandCallable;
import org.spongepowered.api.command.CommandException;
import org.spongepowered.api.command.CommandManager;
import org.spongepowered.api.command.CommandMapping;
import org.spongepowered.api.command.CommandPermissionException;
import org.spongepowered.api.command.CommandResult;
import org.spongepowered.api.command.CommandSource;
import org.spongepowered.api.command.InvocationCommandException;
import org.spongepowered.api.command.dispatcher.Disambiguator;
import org.spongepowered.api.command.dispatcher.SimpleDispatcher;
import org.spongepowered.api.event.SpongeEventFactory;
import org.spongepowered.api.event.cause.Cause;
import org.spongepowered.api.event.cause.NamedCause;
import org.spongepowered.api.event.command.SendCommandEvent;
import org.spongepowered.api.event.command.TabCompleteEvent;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.action.TextActions;
import org.spongepowered.api.util.TextMessageException;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.annotation.Nullable;
/**
* A simple implementation of {@link CommandManager}.
* This service calls the appropriate events for a command.
*/
public class LanternCommandManager implements CommandManager {
private final Logger log;
private final SimpleDispatcher dispatcher;
private final Multimap<PluginContainer, CommandMapping> owners = HashMultimap.create();
private final Map<CommandMapping, PluginContainer> reverseOwners = new ConcurrentHashMap<>();
private final Object lock = new Object();
/**
* Construct a simple {@link CommandManager}.
*
* @param logger The logger to log error messages to
* @param disambiguator The function to resolve a single command when multiple options are available
*/
public LanternCommandManager(Logger logger, Disambiguator disambiguator) {
this.log = logger;
this.dispatcher = new SimpleDispatcher(disambiguator);
}
@Override
public Optional<CommandMapping> register(Object plugin, CommandCallable callable, String... alias) {
return register(plugin, callable, Arrays.asList(alias));
}
@Override
public Optional<CommandMapping> register(Object plugin, CommandCallable callable, List<String> aliases) {
return register(plugin, callable, aliases, Function.identity());
}
@Override
public Optional<CommandMapping> register(Object plugin, CommandCallable callable, List<String> aliases,
Function<List<String>, List<String>> callback) {
checkNotNull(plugin, "plugin");
Optional<PluginContainer> containerOptional = Sponge.getGame().getPluginManager().fromInstance(plugin);
if (!containerOptional.isPresent()) {
throw new IllegalArgumentException(
"The provided plugin object does not have an associated plugin container "
+ "(in other words, is 'plugin' actually your plugin object?");
}
PluginContainer container = containerOptional.get();
synchronized (this.lock) {
// <namespace>:<alias> for all commands
final List<String> aliasesWithPrefix = new ArrayList<>(aliases.size() * 2);
for (String alias : aliases) {
final Collection<CommandMapping> ownedCommands = this.owners.get(container);
for (CommandMapping mapping : this.dispatcher.getAll(alias)) {
if (ownedCommands.contains(mapping)) {
throw new IllegalArgumentException("A plugin may not register multiple commands for the same alias ('" + alias + "')!");
}
}
aliasesWithPrefix.add(alias);
aliasesWithPrefix.add(container.getId() + ':' + alias);
}
final Optional<CommandMapping> mapping = this.dispatcher.register(callable, aliasesWithPrefix, callback);
if (mapping.isPresent()) {
this.owners.put(container, mapping.get());
this.reverseOwners.put(mapping.get(), container);
}
return mapping;
}
}
@Override
public Optional<CommandMapping> removeMapping(CommandMapping mapping) {
synchronized (this.lock) {
Optional<CommandMapping> removed = this.dispatcher.removeMapping(mapping);
if (removed.isPresent()) {
forgetMapping(removed.get());
}
return removed;
}
}
private void forgetMapping(CommandMapping mapping) {
Iterator<CommandMapping> it = this.owners.values().iterator();
while (it.hasNext()) {
if (it.next().equals(mapping)) {
it.remove();
break;
}
}
}
@Override
public Set<PluginContainer> getPluginContainers() {
synchronized (this.lock) {
return ImmutableSet.copyOf(this.owners.keySet());
}
}
@Override
public Set<CommandMapping> getCommands() {
return this.dispatcher.getCommands();
}
@Override
public Set<CommandMapping> getOwnedBy(Object instance) {
final Optional<PluginContainer> container = Sponge.getGame().getPluginManager().fromInstance(instance);
if (!container.isPresent()) {
throw new IllegalArgumentException("The provided plugin object does not have an associated plugin container "
+ "(in other words, is 'plugin' actually your plugin object?)");
}
synchronized (this.lock) {
return ImmutableSet.copyOf(this.owners.get(container.get()));
}
}
@Override
public Optional<PluginContainer> getOwner(CommandMapping mapping) {
return Optional.ofNullable(this.reverseOwners.get(checkNotNull(mapping, "mapping")));
}
@Override
public Set<String> getPrimaryAliases() {
return this.dispatcher.getPrimaryAliases();
}
@Override
public Set<String> getAliases() {
return this.dispatcher.getAliases();
}
@Override
public Optional<CommandMapping> get(String alias) {
return this.dispatcher.get(alias);
}
@Override
public Optional<? extends CommandMapping> get(String alias, @Nullable CommandSource source) {
return this.dispatcher.get(alias, source);
}
@Override
public Set<? extends CommandMapping> getAll(String alias) {
return this.dispatcher.getAll(alias);
}
@Override
public Multimap<String, CommandMapping> getAll() {
return this.dispatcher.getAll();
}
@Override
public boolean containsAlias(String alias) {
return this.dispatcher.containsAlias(alias);
}
@Override
public boolean containsMapping(CommandMapping mapping) {
return this.dispatcher.containsMapping(mapping);
}
@Override
public CommandResult process(CommandSource source, String commandLine) {
final String[] argSplit = commandLine.split(" ", 2);
final SendCommandEvent event = SpongeEventFactory.createSendCommandEvent(Cause.of(NamedCause.source(source)),
argSplit.length > 1 ? argSplit[1] : "", argSplit[0], CommandResult.empty());
Sponge.getGame().getEventManager().post(event);
if (event.isCancelled()) {
return event.getResult();
}
// Only the first part of argSplit is used at the moment, do the other in the future if needed.
argSplit[0] = event.getCommand();
commandLine = event.getCommand();
if (!event.getArguments().isEmpty()) {
commandLine = commandLine + ' ' + event.getArguments();
}
try {
try {
return this.dispatcher.process(source, commandLine);
} catch (InvocationCommandException ex) {
if (ex.getCause() != null) {
throw ex.getCause();
}
} catch (CommandPermissionException ex) {
Text text = ex.getText();
if (text != null) {
source.sendMessage(error(text));
}
} catch (CommandException ex) {
Text text = ex.getText();
if (text != null) {
source.sendMessage(error(text));
}
if (ex.shouldIncludeUsage()) {
final Optional<CommandMapping> mapping = this.dispatcher.get(argSplit[0], source);
if (mapping.isPresent()) {
source.sendMessage(error(t("commands.generic.usage",
t("/%s %s", argSplit[0], mapping.get().getCallable().getUsage(source)))));
}
}
}
} catch (Throwable thr) {
final Text.Builder excBuilder;
if (thr instanceof TextMessageException) {
final Text text = ((TextMessageException) thr).getText();
excBuilder = text == null ? Text.builder("null") : Text.builder().append(text);
} else {
excBuilder = Text.builder(String.valueOf(thr.getMessage()));
}
if (source.hasPermission("sponge.debug.hover-stacktrace")) {
final StringWriter writer = new StringWriter();
thr.printStackTrace(new PrintWriter(writer));
excBuilder.onHover(TextActions.showText(Text.of(writer.toString()
.replace("\t", " ")
.replace("\r\n", "\n")
.replace("\r", "\n")))); // I mean I guess somebody could be running this on like OS 9?
}
source.sendMessage(error(t("Error occurred while executing command: %s", excBuilder.build())));
this.log.error(LanternTexts.toLegacy(t("Error occurred while executing command '%s' for source %s: %s",
commandLine, source.toString(), String.valueOf(thr.getMessage()))), thr);
}
return CommandResult.empty();
}
@Override
public List<String> getSuggestions(CommandSource source, String arguments, @Nullable Location<World> targetPosition) {
return getSuggestions(source, arguments, targetPosition, false);
}
public List<String> getSuggestions(CommandSource source, String arguments, @Nullable Location<World> targetPosition, boolean usingBlock) {
try {
final List<String> suggestions;
final String[] argSplit = arguments.split(" ", 2);
// TODO: Fix this in the SimpleDispatcher -> in 'getSuggestions' add after
// 'argSplit.length == 1' the check '&& !arguments.endsWith(" ")'
if (argSplit.length == 1 && !arguments.endsWith(" ")) {
suggestions = this.dispatcher.getSuggestions(source, arguments, targetPosition);
} else {
Optional<? extends CommandMapping> cmdOptional = this.dispatcher.get(argSplit[0], source);
if (!cmdOptional.isPresent()) {
suggestions = ImmutableList.of();
} else {
suggestions = cmdOptional.get().getCallable().getSuggestions(source, argSplit[1], targetPosition);
}
}
final List<String> rawSuggestions = new ArrayList<>(suggestions);
final TabCompleteEvent.Command event = SpongeEventFactory.createTabCompleteEventCommand(Cause.source(source).build(),
ImmutableList.copyOf(suggestions), rawSuggestions, argSplit.length > 1 ? argSplit[1] : "", argSplit[0],
arguments, Optional.ofNullable(targetPosition), usingBlock);
Sponge.getGame().getEventManager().post(event);
if (event.isCancelled()) {
return ImmutableList.of();
} else {
return ImmutableList.copyOf(event.getTabCompletions());
}
} catch (CommandException e) {
source.sendMessage(error(t("Error getting suggestions: %s", e.getText())));
return Collections.emptyList();
}
}
@Override
public boolean testPermission(CommandSource source) {
return this.dispatcher.testPermission(source);
}
@Override
public Optional<Text> getShortDescription(CommandSource source) {
return this.dispatcher.getShortDescription(source);
}
@Override
public Optional<Text> getHelp(CommandSource source) {
return this.dispatcher.getHelp(source);
}
@Override
public Text getUsage(CommandSource source) {
return this.dispatcher.getUsage(source);
}
@Override
public int size() {
return this.dispatcher.size();
}
}