blob: eed2695734ba8d09cb41f04af050a7cdb732ec20 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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.apache.geode.management.internal.cli;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.shell.converters.ArrayConverter;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.Completion;
import org.springframework.shell.core.Converter;
import org.springframework.shell.core.Parser;
import org.springframework.shell.core.SimpleParser;
import org.springframework.shell.event.ParseResult;
import org.apache.geode.management.cli.ConverterHint;
import org.apache.geode.management.internal.i18n.CliStrings;
/**
* Implementation of the {@link Parser} interface for GemFire SHell (gfsh) requirements.
*
* @since GemFire 7.0
*/
public class GfshParser extends SimpleParser {
public static final String LINE_SEPARATOR = System.lineSeparator();
public static final String OPTION_VALUE_SPECIFIER = "=";
public static final String OPTION_SEPARATOR = " ";
public static final String SHORT_OPTION_SPECIFIER = "-";
public static final String LONG_OPTION_SPECIFIER = "--";
public static final String COMMAND_DELIMITER = ";";
public static final String CONTINUATION_CHARACTER = "\\";
private static final char ASCII_UNIT_SEPARATOR = '\u001F';
public static final String J_ARGUMENT_DELIMITER = "" + ASCII_UNIT_SEPARATOR;
public static final String J_OPTION_CONTEXT = "splittingRegex=" + J_ARGUMENT_DELIMITER;
private final CommandManager commandManager;
public GfshParser(CommandManager commandManager) {
this.commandManager = commandManager;
for (CommandMarker command : commandManager.getCommandMarkers()) {
add(command);
}
for (Converter<?> converter : commandManager.getConverters()) {
if (converter.getClass().isAssignableFrom(ArrayConverter.class)) {
ArrayConverter arrayConverter = (ArrayConverter) converter;
arrayConverter.setConverters(new HashSet<>(commandManager.getConverters()));
}
add(converter);
}
}
static String convertToSimpleParserInput(String userInput) {
List<String> inputTokens = splitUserInput(userInput);
return getSimpleParserInputFromTokens(inputTokens);
}
/**
* it's assumed that the quoted string should not have escaped quotes inside it.
*/
private static List<String> splitWithWhiteSpace(String input) {
List<String> tokensList = new ArrayList<>();
StringBuilder token = new StringBuilder();
char insideQuoteOf = Character.MIN_VALUE;
for (char c : input.toCharArray()) {
if (Character.isWhitespace(c)) {
// if we are in the quotes
if (insideQuoteOf != Character.MIN_VALUE) {
token.append(c);
}
// if we are not in the quotes, terminate this token and add it to the list
else {
if (token.length() > 0) {
tokensList.add(token.toString());
}
token = new StringBuilder();
}
}
// not a white space
else {
token.append(c);
// if encountering a quote
if (c == '\'' || c == '\"') {
// if this is the beginning of quote
if (insideQuoteOf == Character.MIN_VALUE) {
insideQuoteOf = c;
}
// this is the ending of quote
else if (insideQuoteOf == c) {
insideQuoteOf = Character.MIN_VALUE;
}
}
}
}
if (token.length() > 0) {
tokensList.add(token.toString());
}
return tokensList;
}
static List<String> splitUserInput(String userInput) {
// first split with whitespaces except in quotes
List<String> splitWithWhiteSpaces = splitWithWhiteSpace(userInput);
List<String> furtherSplitWithEquals = new ArrayList<>();
for (String token : splitWithWhiteSpaces) {
// do not split with "=" if this part starts with quotes or is part of -D
if (token.startsWith("'") || token.startsWith("\"") || token.startsWith("-D")) {
furtherSplitWithEquals.add(token);
continue;
}
// if this token has equal sign, split around the first occurrence of it
int indexOfFirstEqual = token.indexOf('=');
if (indexOfFirstEqual < 0) {
furtherSplitWithEquals.add(token);
continue;
}
String left = token.substring(0, indexOfFirstEqual);
String right = token.substring(indexOfFirstEqual + 1);
if (left.length() > 0) {
furtherSplitWithEquals.add(left);
}
if (right.length() > 0) {
furtherSplitWithEquals.add(right);
}
}
return furtherSplitWithEquals;
}
static String getSimpleParserInputFromTokens(List<String> tokens) {
// make a copy of the input since we need to do add/remove
List<String> inputTokens = new ArrayList<>();
// get the --J arguments from the list of tokens
int firstJIndex = -1;
List<String> jArguments = new ArrayList<>();
for (int i = 0; i < tokens.size(); i++) {
String token = tokens.get(i);
if ("--J".equals(token)) {
if (firstJIndex < 1) {
firstJIndex = i;
}
i++;
if (i < tokens.size()) {
String jArg = tokens.get(i);
// remove the quotes around each --J arugments
if (jArg.charAt(0) == '"' || jArg.charAt(0) == '\'') {
jArg = jArg.substring(1, jArg.length() - 1);
}
if (jArg.length() > 0) {
jArguments.add(jArg);
}
}
} else {
inputTokens.add(token);
}
}
// concatenate the remaining tokens with space
StringBuilder rawInput = new StringBuilder();
// firstJIndex must be less than or equal to the length of the inputToken
for (int i = 0; i <= inputTokens.size(); i++) {
// stick the --J arguments in the orginal first --J position
if (i == firstJIndex) {
rawInput.append("--J ");
if (jArguments.size() > 0) {
// quote the entire J argument with double quotes, and delimited with a special delimiter,
// and we
// need to tell the gfsh parser to use this delimiter when splitting the --J argument in
// each command
rawInput.append("\"").append(StringUtils.join(jArguments, J_ARGUMENT_DELIMITER))
.append("\" ");
}
}
// then add the next inputToken
if (i < inputTokens.size()) {
rawInput.append(inputTokens.get(i)).append(" ");
}
}
return rawInput.toString().trim();
}
@Override
public GfshParseResult parse(String userInput) {
String rawInput = convertToSimpleParserInput(userInput);
// this tells the simpleParser not to interpret backslash as escaping character
rawInput = rawInput.replace("\\", "\\\\");
// User SimpleParser to parse the input
ParseResult result = super.parse(rawInput);
if (result == null) {
// do a quick check for required arguments, since SimpleParser unhelpfully suggests everything
String missingHelp = commandManager.getHelper().getMiniHelp(userInput);
if (missingHelp != null) {
System.out.println(missingHelp);
}
return null;
}
return new GfshParseResult(result.getMethod(), result.getInstance(), result.getArguments(),
userInput);
}
/**
*
* The super class's completeAdvanced has the following limitations: 1) for option name
* completion, you need to end your buffer with --. 2) For command name completion, you need to
* end your buffer with a space. 3) the above 2 completions, the returned value is always 0, and
* the completion is the entire command 4) for value completion, you also need to end your buffer
* with space, the returned value is the length of the original string, and the completion strings
* are the possible values.
*
* With these limitations, we will need to overwrite this command with some customization
*
* @param cursor this input is ignored, we always move the cursor to the end of the userInput
* @return the cursor point at which the candidate string will begin, this is important if you
* have only one candidate, cause tabbing will use it to complete the string for you.
*/
@Override
public int completeAdvanced(String userInput, int cursor, final List<Completion> candidates) {
// move the cursor to the end of the input
List<String> inputTokens = splitUserInput(userInput);
// check if the input is before any option is specified, e.g. (start, describe)
boolean inputIsBeforeOption = true;
for (String token : inputTokens) {
if (token.startsWith("--")) {
inputIsBeforeOption = false;
break;
}
}
// in the case of we are still trying to complete the command name
if (inputIsBeforeOption) {
// workaround for SimpleParser bugs with "" option key, and spaces in option values
int curs =
completeSpecial(candidates, userInput, inputTokens, CliStrings.HELP, ConverterHint.HELP);
if (curs > 0) {
return curs;
}
curs =
completeSpecial(candidates, userInput, inputTokens, CliStrings.HINT, ConverterHint.HINT);
if (curs > 0) {
return curs;
}
List<Completion> potentials = getCandidates(userInput);
if (potentials.size() == 1 && potentials.get(0).getValue().equals(userInput)) {
potentials = getCandidates(userInput.trim() + " ");
}
if (potentials.size() > 0) {
candidates.addAll(potentials);
return 0;
}
// otherwise, falling down to the potentials.size==0 case below
}
// now we are either trying to complete the option or a value
// trying to get candidates using the converted input
String buffer = getSimpleParserInputFromTokens(inputTokens);
String lastToken = inputTokens.get(inputTokens.size() - 1);
boolean lastTokenIsOption = lastToken.startsWith("--");
// In the original user input, where to begin the candidate string for completion
int candidateBeginAt;
// initially assume we are trying to complete the last token
List<Completion> potentials = getCandidates(buffer);
// if the last token is already complete (or user deliberately ends with a space denoting the
// last token is complete, then add either space or " --" and try again
if (potentials.size() == 0 || userInput.endsWith(" ")) {
candidateBeginAt = buffer.length();
// last token is an option
if (lastTokenIsOption) {
// add a space to the buffer to get the option value candidates
potentials = getCandidates(buffer + " ");
lastTokenIsOption = false;
}
// last token is a value, we need to add " --" to it and retry to get the next list of options
else {
potentials = getCandidates(buffer + " --");
lastTokenIsOption = true;
}
} else {
if (lastTokenIsOption) {
candidateBeginAt = buffer.length() - lastToken.length();
} else {
// need to return the index before the "=" sign, since later on we are going to add the
// "=" sign to the completion candidates
candidateBeginAt = buffer.length() - lastToken.length() - 1;
}
}
// manipulate the candidate strings
if (lastTokenIsOption) {
// strip off the beginning part of the candidates from the cursor point
potentials.replaceAll(
completion -> new Completion(completion.getValue().substring(candidateBeginAt)));
}
// if the completed values are option, and the userInput doesn't ends with an "=" sign,
else if (!userInput.endsWith("=")) {
// these potentials do not have "=" in front of them, manually add them
potentials.replaceAll(completion -> new Completion("=" + completion.getValue()));
}
candidates.addAll(potentials);
// usually we want to return the cursor at candidateBeginAt, but since we consolidated
// --J options into one, and added quotes around we need to consider the length difference
// between userInput and the converted input
cursor = candidateBeginAt + (userInput.trim().length() - buffer.length());
return cursor;
}
/**
* gets a specific String converter from the list of registered converters
*/
private Converter<?> converterFor(String converterHint) {
for (Converter<?> candidate : getConverters()) {
if (candidate.supports(String.class, converterHint)) {
return candidate;
}
}
return null;
}
/**
* uses a specific converter directly, bypassing the need to find it by the command's options
*/
private int completeSpecial(List<Completion> candidates, String userInput,
List<String> inputTokens, String cmd,
String converterHint) {
if (inputTokens.get(0).equals(cmd)) {
String prefix = userInput.equals(cmd) ? " " : "";
String existing = String.join(" ", inputTokens.subList(1, inputTokens.size())).toLowerCase();
List<Completion> all = new ArrayList<>();
Converter<?> converter = converterFor(converterHint);
if (converter != null) {
converter.getAllPossibleValues(all, null, null, null, null);
candidates.addAll(all.stream().filter(c -> c.getValue().toLowerCase().startsWith(existing))
.map(c -> new Completion(prefix + c.getValue()))
.collect(Collectors.toList()));
return Math.min(userInput.length(), cmd.length() + 1);
}
}
return 0;
}
/**
* @param buffer use the buffer to find the completion candidates
*
* Note the cursor may not be the size the buffer
*/
private List<Completion> getCandidates(String buffer) {
List<Completion> candidates = new ArrayList<>();
// always pass the buffer length as the cursor position for simplicity purpose
super.completeAdvanced(buffer, buffer.length(), candidates);
// trimming the candidates
candidates.replaceAll(completion -> new Completion(completion.getValue().trim()));
return candidates;
}
}