/* * Copyright 2014 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.commandSystem; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Queues; import com.google.common.collect.Sets; import com.google.common.primitives.Primitives; import org.terasology.context.Context; import org.terasology.entitySystem.entity.EntityRef; import org.terasology.logic.console.commandSystem.exceptions.CommandExecutionException; import org.terasology.logic.console.commandSystem.exceptions.CommandInitializationException; import org.terasology.logic.console.commandSystem.exceptions.CommandParameterParseException; import org.terasology.logic.console.commandSystem.exceptions.CommandSuggestionException; import org.terasology.logic.permission.PermissionManager; import org.terasology.naming.Name; import org.terasology.utilities.reflection.SpecificAccessibleObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.stream.Collectors; /** * The core ICommand implementation and command information * */ public abstract class AbstractCommand implements ConsoleCommand { private final Name name; private final String requiredPermission; private final boolean runOnServer; private final String description; private final String helpText; private final SpecificAccessibleObject<Method> executionMethod; private ImmutableList<CommandParameter> commandParameters; private ImmutableList<Parameter> executionMethodParameters; private int requiredParameterCount; private String usage; public AbstractCommand(Name name, String requiredPermission, boolean runOnServer, String description, String helpText, SpecificAccessibleObject<Method> executionMethod, Context context) { Preconditions.checkNotNull(executionMethod); Preconditions.checkNotNull(description); Preconditions.checkNotNull(helpText); this.name = name; this.requiredPermission = requiredPermission != null ? requiredPermission : PermissionManager.DEBUG_PERMISSION; this.runOnServer = runOnServer; this.description = description; this.helpText = helpText; this.executionMethod = executionMethod; constructParametersNotNull(context); registerParameters(); validateExecutionMethod(); initUsage(); } /** * @return A list of parameter types provided to the execution method. */ protected abstract List<Parameter> constructParameters(Context context); private void constructParametersNotNull(Context context) { List<Parameter> constructedParameters = constructParameters(context); if (constructedParameters == null || constructedParameters.size() <= 0) { commandParameters = ImmutableList.of(); executionMethodParameters = ImmutableList.of(); return; } ImmutableList.Builder<CommandParameter> commandParameterBuilder = ImmutableList.builder(); for (int i = 0; i < constructedParameters.size(); i++) { Parameter type = constructedParameters.get(i); if (type == null) { throw new CommandInitializationException("Invalid parameter definition #" + i + "; must not be null"); } else if (type instanceof CommandParameter) { commandParameterBuilder.add((CommandParameter) type); } } commandParameters = commandParameterBuilder.build(); executionMethodParameters = ImmutableList.copyOf(constructedParameters); } private void registerParameters() throws CommandInitializationException { requiredParameterCount = 0; boolean optionalFound = false; for (int i = 0; i < commandParameters.size(); i++) { CommandParameter parameter = commandParameters.get(i); if (parameter == null) { throw new CommandInitializationException("A command parameter must not be null! Index: " + i); } if (parameter.isArray() && i < commandParameters.size() - 1) { throw new CommandInitializationException("A varargs parameter must be at the end. Invalid: " + i + "; " + parameter.getName()); } if (parameter.isRequired()) { if (!optionalFound) { requiredParameterCount++; } else { throw new CommandInitializationException("A command definition must not contain a required" + " parameter (" + i + "; " + parameter.getName() + ") after an optional parameter."); } } else if (!optionalFound) { optionalFound = true; } } } private void checkArgumentCompatibility(Method method) throws CommandInitializationException { Class<?>[] methodParameters = method.getParameterTypes(); int executionMethodParametersSize = executionMethodParameters.size(); int methodParameterCount = methodParameters.length; for (int i = 0; i < methodParameterCount || i < executionMethodParametersSize; i++) { if (i >= methodParameterCount) { throw new CommandInitializationException("Missing " + (executionMethodParametersSize - methodParameterCount) + " parameters in method " + method.getName() + ", follow the parameter definitions from the" + " 'constructParameters' method."); } else if (i >= executionMethodParametersSize) { throw new CommandInitializationException("Too many (" + (methodParameterCount - executionMethodParametersSize) + ") parameters in method " + method.getName() + ", follow the parameter definitions from the" + " 'constructParameters' method."); } Parameter expectedParameterType = executionMethodParameters.get(i); Optional<? extends Class<?>> expectedType = expectedParameterType.getProvidedType(); Class<?> providedType = methodParameters[i]; if (providedType.isPrimitive()) { providedType = Primitives.wrap(providedType); } if (expectedType.isPresent() && !expectedType.get().isAssignableFrom(providedType)) { throw new CommandInitializationException("Cannot assign command argument from " + providedType.getSimpleName() + " to " + expectedType.get().getSimpleName() + "; " + "command method parameter index: " + i); } } } private void validateExecutionMethod() { checkArgumentCompatibility(executionMethod.getAccessibleObject()); } private void initUsage() { StringBuilder builder = new StringBuilder(name.toString()); for (CommandParameter param : commandParameters) { builder.append(' ').append(param.getUsage()); } usage = builder.toString(); } private Object[] processParametersMethod(List<String> rawParameters, EntityRef sender) throws CommandParameterParseException { Object[] processedParameters = new Object[executionMethodParameters.size()]; Queue<String> parameterStrings = Queues.newArrayDeque(rawParameters); for (int i = 0; i < executionMethodParameters.size(); i++) { Parameter parameterType = executionMethodParameters.get(i); if (parameterType instanceof CommandParameter) { CommandParameter parameter = (CommandParameter) parameterType; if (parameterStrings.isEmpty()) { if (parameter.isArray()) { processedParameters[i] = parameter.getArrayValue(Collections.<String>emptyList()); } else { processedParameters[i] = null; } } else if (parameter.isArray()) { processedParameters[i] = parameter.getArrayValue(Lists.newArrayList(parameterStrings)); parameterStrings.clear(); } else { processedParameters[i] = parameter.getValue(parameterStrings.poll()); } } else if (parameterType == MarkerParameters.SENDER) { processedParameters[i] = sender; } } return processedParameters; } @Override public final String execute(List<String> rawParameters, EntityRef sender) throws CommandExecutionException { Object[] processedParameters; try { processedParameters = processParametersMethod(rawParameters, sender); } catch (CommandParameterParseException e) { String warning = "Invalid parameter '" + e.getParameter() + "'"; String message = e.getMessage(); if (message != null) { warning += ": " + message; } return warning; } try { Object result = executionMethod.getAccessibleObject().invoke(executionMethod.getTarget(), processedParameters); return result != null ? String.valueOf(result) : null; } catch (InvocationTargetException t) { if (t.getCause() != null) { throw new CommandExecutionException(t.getCause()); //Skip InvocationTargetException } else { throw new CommandExecutionException(t); } } catch (IllegalAccessException | RuntimeException t) { throw new CommandExecutionException(t); } } @Override public final Set<String> suggest(final String currentValue, List<String> rawParameters, EntityRef sender) throws CommandSuggestionException { //Generate an array to be used as a parameter in the 'suggest' method Object[] processedParameters; try { processedParameters = processParametersMethod(rawParameters, sender); } catch (CommandParameterParseException e) { String warning = "Invalid parameter '" + e.getParameter() + "'"; String message = e.getMessage(); if (message != null) { warning += ": " + message; } throw new CommandSuggestionException(warning); } //Get the suggested parameter to compare the result with CommandParameter suggestedParameter = null; Iterator<CommandParameter> paramIter = commandParameters.iterator(); for (Object processedParameter : processedParameters) { if (sender.equals(processedParameter)) { continue; } if (processedParameter == null) { suggestedParameter = paramIter.next(); break; } paramIter.next(); } if (suggestedParameter == null) { return Sets.newHashSet(); } Set<Object> result = null; result = suggestedParameter.suggest(sender, processedParameters); if (result == null) { return Sets.newHashSet(); } Class<?> requiredClass = suggestedParameter.getType(); for (Object resultComponent : result) { if (resultComponent == null && requiredClass.isPrimitive()) { throw new CommandSuggestionException("The 'suggest' method of command class " + getClass().getCanonicalName() + " returns a collection containing an invalid type. Required: " + requiredClass.getCanonicalName() + "; provided: null"); } else if (resultComponent != null && !requiredClass.isAssignableFrom(resultComponent.getClass())) { throw new CommandSuggestionException("The 'suggest' method of command class " + getClass().getCanonicalName() + " returns a collection containing an invalid type. Required: " + requiredClass.getCanonicalName() + "; provided: " + resultComponent.getClass().getCanonicalName()); } } Set<String> stringSuggestions = convertToString(result, suggestedParameter); //Only return results starting with currentValue return Sets.filter(stringSuggestions, input -> input != null && (currentValue == null || input.startsWith(currentValue))); } private static Set<String> convertToString(Set<Object> collection, CommandParameter parameter) { return collection.stream().map(parameter::convertToString).collect(Collectors.toCollection(HashSet::new)); } @Override public ImmutableList<CommandParameter> getCommandParameters() { return commandParameters; } @Override public boolean isRunOnServer() { return runOnServer; } @Override public String getDescription() { return description; } @Override public String getHelpText() { return helpText; } @Override public String getUsage() { return usage; } @Override public Name getName() { return name; } @Override public int getRequiredParameterCount() { return requiredParameterCount; } @Override public boolean endsWithVarargs() { return commandParameters.size() > 0 && commandParameters.get(commandParameters.size() - 1).isArray(); } @Override public Object getSource() { return executionMethod.getTarget(); } @Override public int compareTo(ConsoleCommand o) { return ConsoleCommand.COMPARATOR.compare(this, o); } @Override public String getRequiredPermission() { return requiredPermission; } public SpecificAccessibleObject<Method> getExecutionMethod() { return executionMethod; } }