/*
* Copyright 2017 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.logic.console;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.context.Context;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.logic.console.commandSystem.ConsoleCommand;
import org.terasology.logic.console.commandSystem.exceptions.CommandExecutionException;
import org.terasology.logic.permission.PermissionManager;
import org.terasology.naming.Name;
import org.terasology.network.ClientComponent;
import org.terasology.network.NetworkSystem;
import org.terasology.rendering.FontColor;
import org.terasology.rendering.FontUnderline;
import org.terasology.utilities.collection.CircularBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The console handles commands and messages.
*
*/
public class ConsoleImpl implements Console {
private static final String PARAM_SPLIT_REGEX = " (?=([^\"]*\"[^\"]*\")*[^\"]*$)";
private static final int MAX_MESSAGE_HISTORY = 255;
private static final int MAX_COMMAND_HISTORY = 30;
private static final Logger logger = LoggerFactory.getLogger(ConsoleImpl.class);
private final CircularBuffer<Message> messageHistory = CircularBuffer.create(MAX_MESSAGE_HISTORY);
private final CircularBuffer<String> localCommandHistory = CircularBuffer.create(MAX_COMMAND_HISTORY);
private final Map<Name, ConsoleCommand> commandRegistry = Maps.newHashMap();
private final Set<ConsoleSubscriber> messageSubscribers = Sets.newSetFromMap(new MapMaker().weakKeys().<ConsoleSubscriber, Boolean>makeMap());
private NetworkSystem networkSystem;
private Context context;
public ConsoleImpl(Context context) {
this.networkSystem = context.get(NetworkSystem.class);
this.context = context;
}
/**
* Registers a {@link org.terasology.logic.console.commandSystem.ConsoleCommand}.
*
* @param command The command to be registered
*/
@Override
public void registerCommand(ConsoleCommand command) {
Name commandName = command.getName();
if (commandRegistry.containsKey(commandName)) {
logger.warn("Command with name '{}' already registered by class '{}', skipping '{}'",
commandName, commandRegistry.get(commandName).getSource().getClass().getCanonicalName(),
command.getSource().getClass().getCanonicalName());
} else {
commandRegistry.put(commandName, command);
logger.debug("Command '{}' successfully registered for class '{}'.", commandName,
command.getSource().getClass().getCanonicalName());
}
}
@Override
public void dispose() {
commandRegistry.clear();
messageHistory.clear();
}
/**
* Adds a message to the console (as a CoreMessageType.CONSOLE message)
*
* @param message The message content
*/
@Override
public void addMessage(String message) {
addMessage(new Message(message));
}
/**
* Adds a message to the console
*
* @param message The content of the message
* @param type The type of the message
*/
@Override
public void addMessage(String message, MessageType type) {
addMessage(new Message(message, type));
}
private void addErrorMessage(String message) {
addMessage(new Message(message, CoreMessageType.ERROR));
}
/**
* Adds a message to the console
*
* @param message The message to be added
*/
@Override
public void addMessage(Message message) {
String uncoloredText = FontUnderline.strip(FontColor.stripColor(message.getMessage()));
logger.info("[{}] {}", message.getType(), uncoloredText);
messageHistory.add(message);
for (ConsoleSubscriber subscriber : messageSubscribers) {
subscriber.onNewConsoleMessage(message);
}
}
@Override
public void removeMessage(Message message) {
messageHistory.remove(message);
}
/**
* Clears the console of all previous messages.
*/
@Override
public void clear() {
messageHistory.clear();
}
@Override
public void replaceMessage(Message oldMsg, Message newMsg) {
int idx = messageHistory.indexOf(oldMsg);
if (idx >= 0) {
messageHistory.set(idx, newMsg);
}
}
/**
* @return An iterator over all messages in the console
*/
@Override
public Iterable<Message> getMessages() {
return messageHistory;
}
@Override
public Iterable<Message> getMessages(MessageType... types) {
final List<MessageType> allowedTypes = Arrays.asList(types);
return Collections2.filter(messageHistory, input -> allowedTypes.contains(input.getType()));
}
@Override
public List<String> getPreviousCommands() {
return ImmutableList.copyOf(localCommandHistory);
}
/**
* Subscribe for notification of all messages added to the console
*/
@Override
public void subscribe(ConsoleSubscriber subscriber) {
this.messageSubscribers.add(subscriber);
}
/**
* Unsubscribe from receiving notification of messages being added to the console
*/
@Override
public void unsubscribe(ConsoleSubscriber subscriber) {
this.messageSubscribers.remove(subscriber);
}
@Override
public boolean execute(String rawCommand, EntityRef callingClient) {
String commandName = processCommandName(rawCommand);
List<String> processedParameters = processParameters(rawCommand);
ClientComponent cc = callingClient.getComponent(ClientComponent.class);
if (cc.local) {
if (!rawCommand.isEmpty() && (localCommandHistory.isEmpty() || !localCommandHistory.getLast().equals(rawCommand))) {
localCommandHistory.add(rawCommand);
}
}
return execute(new Name(commandName), processedParameters, callingClient);
}
@Override
public boolean execute(Name commandName, List<String> params, EntityRef callingClient) {
if (commandName.isEmpty()) {
return false;
}
//get the command
ConsoleCommand cmd = getCommand(commandName);
//check if the command is loaded
if (cmd == null) {
addErrorMessage("Unknown command '" + commandName + "'");
return false;
}
String requiredPermission = cmd.getRequiredPermission();
if (!clientHasPermission(callingClient, requiredPermission)) {
callingClient.send(new ErrorMessageEvent("You do not have enough permissions to execute this command (" + requiredPermission + ")."));
return false;
}
if (params.size() < cmd.getRequiredParameterCount()) {
callingClient.send(new ErrorMessageEvent("Please, provide required arguments marked by <>."));
callingClient.send(new ConsoleMessageEvent(cmd.getUsage()));
return false;
}
if (cmd.isRunOnServer() && !networkSystem.getMode().isAuthority()) {
callingClient.send(new CommandEvent(commandName, params));
return true;
} else {
try {
String result = cmd.execute(params, callingClient);
if (!Strings.isNullOrEmpty(result)) {
callingClient.send(new ConsoleMessageEvent(result));
}
return true;
} catch (CommandExecutionException e) {
Throwable cause = e.getCause();
String causeMessage;
if (cause != null) {
causeMessage = cause.getLocalizedMessage();
if (Strings.isNullOrEmpty(causeMessage)) {
causeMessage = cause.toString();
}
} else {
causeMessage = e.getLocalizedMessage();
}
logger.error("An error occurred while executing a command", e);
if (!Strings.isNullOrEmpty(causeMessage)) {
callingClient.send(new ErrorMessageEvent("An error occurred while executing command '"
+ cmd.getName() + "': " + causeMessage));
}
return false;
}
}
}
private boolean clientHasPermission(EntityRef callingClient, String requiredPermission) {
Preconditions.checkNotNull(callingClient, "The calling client must not be null!");
PermissionManager permissionManager = context.get(PermissionManager.class);
boolean hasPermission = true;
if (permissionManager != null && requiredPermission != null
&& !requiredPermission.equals(PermissionManager.NO_PERMISSION)) {
hasPermission = false;
ClientComponent clientComponent = callingClient.getComponent(ClientComponent.class);
if (permissionManager.hasPermission(clientComponent.clientInfo, requiredPermission)) {
hasPermission = true;
}
}
return hasPermission;
}
private static String cleanCommand(String rawCommand) {
// trim and remove double spaces
return rawCommand.trim().replaceAll("\\s\\s+", " ");
}
@Override
public String processCommandName(String rawCommand) {
String cleanedCommand = cleanCommand(rawCommand);
int commandEndIndex = cleanedCommand.indexOf(" ");
if (commandEndIndex >= 0) {
return cleanedCommand.substring(0, commandEndIndex);
} else {
return cleanedCommand;
}
}
@Override
public List<String> processParameters(String rawCommand) {
String cleanedCommand = cleanCommand(rawCommand);
//get the command name
int commandEndIndex = cleanedCommand.indexOf(" ");
if (commandEndIndex < 0) {
commandEndIndex = cleanedCommand.length();
}
//remove command name from string
String parameterPart = cleanedCommand.substring(commandEndIndex).trim();
//get the parameters
List<String> params = splitParameters(parameterPart);
return params;
}
private static List<String> splitParameters(String paramStr) {
String[] rawParams = paramStr.split(PARAM_SPLIT_REGEX);
List<String> params = Lists.newArrayList();
for (String s : rawParams) {
String param = s;
if (param.trim().isEmpty()) {
continue;
}
if (param.length() > 1 && param.startsWith("\"") && param.endsWith("\"")) {
param = param.substring(1, param.length() - 1);
}
params.add(param);
}
return params;
}
/**
* Get a group of commands by their name. These will vary by the number of parameters they accept
*
* @param name The name of the command.
* @return An array of commands with given name
*/
@Override
public ConsoleCommand getCommand(Name name) {
return commandRegistry.get(name);
}
/**
* Get the list of all loaded commands.
*
* @return Returns the command list.
*/
@Override
public Collection<ConsoleCommand> getCommands() {
return commandRegistry.values();
}
}