blob: 33d4575d6eec7748a950469823f03bdb21f56758 [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.help;
import static org.apache.geode.management.internal.cli.GfshParser.LINE_SEPARATOR;
import static org.apache.geode.management.internal.cli.GfshParser.LONG_OPTION_SPECIFIER;
import static org.apache.geode.management.internal.cli.GfshParser.OPTION_VALUE_SPECIFIER;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.shell.core.MethodTarget;
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.apache.geode.management.cli.CliMetaData;
import org.apache.geode.management.internal.i18n.CliStrings;
/**
*
*
* @since GemFire 7.0
*/
public class Helper {
static final String NAME_NAME = "NAME";
static final String SYNONYMS_NAME = "SYNONYMS";
static final String SYNOPSIS_NAME = "SYNOPSIS";
static final String SYNTAX_NAME = "SYNTAX";
static final String OPTIONS_NAME = "PARAMETERS";
static final String IS_AVAILABLE_NAME = "IS AVAILABLE";
private static final String REQUIRED_SUB_NAME = "Required: ";
private static final String SYNONYMS_SUB_NAME = "Synonyms: ";
private static final String SPECIFIEDDEFAULTVALUE_SUB_NAME =
"Default (if the parameter is specified without value): ";
private static final String UNSPECIFIEDDEFAULTVALUE_VALUE_SUB_NAME =
"Default (if the parameter is not specified): ";
private static final String VALUE_FIELD = "value";
private static final String TRUE_TOKEN = "true";
private static final String FALSE_TOKEN = "false";
private static final String AVAILABLE = "Available";
private static final String NOT_AVAILABLE = "Not Available";
private static final String NO_HELP_EXISTS_FOR_THIS_COMMAND = "No help exists for this command.";
private static final String HELP_INSTRUCTIONS = LINE_SEPARATOR + "Use " + CliStrings.HELP
+ " <command name> to display detailed usage information for a specific command."
+ LINE_SEPARATOR
+ "Help with command and parameter completion can also be obtained by entering all or a portion of either followed by the \"TAB\" key.";
private final Map<String, Topic> topics = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private final Map<String, Method> commands = new TreeMap<>();
private final Map<String, MethodTarget> availabilityIndicators = new HashMap<>();
public Helper() {
initTopic(CliStrings.DEFAULT_TOPIC_GEODE, CliStrings.DEFAULT_TOPIC_GEODE__DESC);
initTopic(CliStrings.TOPIC_GEODE_REGION, CliStrings.TOPIC_GEODE_REGION__DESC);
initTopic(CliStrings.TOPIC_GEODE_WAN, CliStrings.TOPIC_GEODE_WAN__DESC);
initTopic(CliStrings.TOPIC_GEODE_JMX, CliStrings.TOPIC_GEODE_JMX__DESC);
initTopic(CliStrings.TOPIC_GEODE_DISKSTORE, CliStrings.TOPIC_GEODE_DISKSTORE__DESC);
initTopic(CliStrings.TOPIC_GEODE_LOCATOR, CliStrings.TOPIC_GEODE_LOCATOR__DESC);
initTopic(CliStrings.TOPIC_GEODE_SERVER, CliStrings.TOPIC_GEODE_SERVER__DESC);
initTopic(CliStrings.TOPIC_GEODE_MANAGER, CliStrings.TOPIC_GEODE_MANAGER__DESC);
initTopic(CliStrings.TOPIC_GEODE_STATISTICS, CliStrings.TOPIC_GEODE_STATISTICS__DESC);
initTopic(CliStrings.TOPIC_GEODE_LIFECYCLE, CliStrings.TOPIC_GEODE_LIFECYCLE__DESC);
initTopic(CliStrings.TOPIC_GEODE_M_AND_M, CliStrings.TOPIC_GEODE_M_AND_M__DESC);
initTopic(CliStrings.TOPIC_GEODE_DATA, CliStrings.TOPIC_GEODE_DATA__DESC);
initTopic(CliStrings.TOPIC_GEODE_CONFIG, CliStrings.TOPIC_GEODE_CONFIG__DESC);
initTopic(CliStrings.TOPIC_GEODE_FUNCTION, CliStrings.TOPIC_GEODE_FUNCTION__DESC);
initTopic(CliStrings.TOPIC_GEODE_HELP, CliStrings.TOPIC_GEODE_HELP__DESC);
initTopic(CliStrings.TOPIC_GEODE_DEBUG_UTIL, CliStrings.TOPIC_GEODE_DEBUG_UTIL__DESC);
initTopic(CliStrings.TOPIC_GFSH, CliStrings.TOPIC_GFSH__DESC);
initTopic(CliStrings.TOPIC_LOGS, CliStrings.TOPIC_LOGS__DESC);
initTopic(CliStrings.TOPIC_CLIENT, CliStrings.TOPIC_CLIENT__DESC);
}
private void initTopic(String topic, String desc) {
topics.put(topic, new Topic(topic, desc));
}
public void addCommand(CliCommand command, Method commandMethod) {
// put all the command synonyms in the command map
Arrays.stream(command.value()).forEach(cmd -> commands.put(cmd, commandMethod));
// resolve the hint message for each method
CliMetaData cliMetaData = commandMethod.getDeclaredAnnotation(CliMetaData.class);
if (cliMetaData == null) {
return;
}
String[] related = cliMetaData.relatedTopic();
// for hint message, we only need to show the first synonym
String commandString = command.value()[0];
Arrays.stream(related).forEach(topic -> {
Topic foundTopic = topics.get(topic);
if (foundTopic == null) {
throw new IllegalArgumentException("No such topic found in the initial map: " + topic);
}
foundTopic.addRelatedCommand(commandString, command.help());
});
}
public void addAvailabilityIndicator(CliAvailabilityIndicator availability, MethodTarget target) {
Arrays.stream(availability.value())
.forEach(command -> availabilityIndicators.put(command, target));
}
/**
* get mini-help for commands entered without all required parameters
*
* @return null if unable to identify anything missing
*/
public String getMiniHelp(String userInput) {
if (StringUtils.isBlank(userInput)) {
return null;
}
List<Method> methodList = commands.keySet()
.stream()
.filter(key -> key.startsWith(getCommandPart(userInput)))
.map(commands::get).collect(Collectors.toList());
if (methodList.size() != 1) {
// can't validate arguments if buffer is not a single command
return null;
}
Method m = methodList.get(0);
CliCommand cliCommand = m.getDeclaredAnnotation(CliCommand.class);
Annotation[][] annotations = m.getParameterAnnotations();
if (annotations == null || annotations.length == 0) {
// can't validate arguments if command doesn't have any
return null;
}
// loop through the required options and check that they appear in the buffer
StringBuilder builder = new StringBuilder();
for (Annotation[] annotation : annotations) {
CliOption cliOption = getAnnotation(annotation, CliOption.class);
String option = getPrimaryKey(cliOption);
boolean required = cliOption.mandatory();
boolean requiredWithEquals = !isNonEmptyAnnotation(cliOption.specifiedDefaultValue());
if (isNonEmptyAnnotation(cliOption.unspecifiedDefaultValue())) {
required = false;
}
if (required) {
String lookFor = "--" + option + (requiredWithEquals ? "=" : "");
if (!userInput.contains(lookFor)) {
builder.append(" --").append(option).append(requiredWithEquals ? "=" : "")
.append(" is required").append(LINE_SEPARATOR);
}
}
}
if (builder.length() > 0) {
String commandName = cliCommand.value()[0];
builder.append("Use \"help ").append(commandName)
.append("\" (without the quotes) for detailed usage information.")
.append(LINE_SEPARATOR);
return builder.toString();
} else {
return null;
}
}
private String getCommandPart(String userInput) {
int parms = userInput.indexOf(" --");
return (parms < 0 ? userInput : userInput.substring(0, parms)).trim();
}
/**
* get help string for a specific command, or a brief description of all the commands if buffer is
* null or empty
*/
public String getHelp(String buffer, int terminalWidth) {
if (StringUtils.isBlank(buffer)) {
return getHelp().toString(terminalWidth);
}
List<Method> methodList = commands.keySet()
.stream()
.filter(key -> key.startsWith(buffer))
.map(commands::get).collect(Collectors.toList());
boolean summarize = methodList.size() > 1;
String helpString = methodList.stream()
.map(m -> getHelp(m.getDeclaredAnnotation(CliCommand.class),
summarize ? null : m.getParameterAnnotations(),
summarize ? null : m.getParameterTypes()))
.map(helpBlock -> helpBlock.toString(terminalWidth))
.reduce((s, s2) -> s + s2)
.orElse(NO_HELP_EXISTS_FOR_THIS_COMMAND);
if (summarize) {
helpString += HELP_INSTRUCTIONS;
}
return helpString;
}
public String getHint(String buffer) {
List<String> topicKeys = topics.keySet()
.stream()
.filter(t -> buffer == null || t.toLowerCase().startsWith(buffer.toLowerCase()))
.sorted()
.collect(Collectors.toList());
StringBuilder builder = new StringBuilder();
// if no topic is provided, return a list of topics
if (topicKeys.isEmpty()) {
builder.append(CliStrings.format(CliStrings.HINT__MSG__UNKNOWN_TOPIC, buffer))
.append(LINE_SEPARATOR).append(LINE_SEPARATOR);
} else if (topicKeys.size() == 1) {
Topic oneTopic = topics.get(topicKeys.get(0));
builder.append(oneTopic.desc).append(LINE_SEPARATOR)
.append(LINE_SEPARATOR);
oneTopic.relatedCommands.stream().sorted().forEach(command -> builder.append(command.command)
.append(": ").append(command.desc).append(LINE_SEPARATOR));
} else {
builder.append(CliStrings.HINT__MSG__TOPICS_AVAILABLE).append(LINE_SEPARATOR)
.append(LINE_SEPARATOR);
topicKeys.forEach(topic -> builder.append(topic).append(LINE_SEPARATOR));
}
return builder.toString();
}
public Set<String> getTopicNames() {
return topics.keySet();
}
private boolean isAvailable(String command) {
MethodTarget target = availabilityIndicators.get(command);
if (target == null) {
return true;
}
try {
return (Boolean) target.getMethod().invoke(target.getTarget());
} catch (Exception e) {
return false;
}
}
public boolean hasAvailabilityIndicator(String command) {
return availabilityIndicators.get(command) != null;
}
private HelpBlock getHelp() {
HelpBlock root = new HelpBlock();
commands.keySet().stream().sorted().map(commands::get).forEach(method -> root
.addChild(getHelp(method.getDeclaredAnnotation(CliCommand.class), null, null)));
return root;
}
/**
* returns a short description and help string of the command if annotations is null or returns a
* details description of the command with the syntax and parameter description
*/
HelpBlock getHelp(CliCommand cliCommand, Annotation[][] annotations, Class<?>[] parameterTypes) {
String commandName = cliCommand.value()[0];
boolean isAvailable = isAvailable(commandName);
if (annotations == null && parameterTypes == null) {
String available = isAvailable ? AVAILABLE : NOT_AVAILABLE;
HelpBlock help = new HelpBlock(commandName + " (" + available + ")");
help.addChild(new HelpBlock(cliCommand.help()));
return help;
}
HelpBlock root = new HelpBlock();
// First we will have the block for NAME of the command
HelpBlock name = new HelpBlock(NAME_NAME);
name.addChild(new HelpBlock(commandName));
root.addChild(name);
// add the availability flag
HelpBlock availability = new HelpBlock(IS_AVAILABLE_NAME);
availability.addChild(new HelpBlock(isAvailable + ""));
root.addChild(availability);
// Now add synonyms if any
String[] allNames = cliCommand.value();
if (allNames.length > 1) {
HelpBlock synonyms = new HelpBlock(SYNONYMS_NAME);
for (int i = 1; i < allNames.length; i++) {
synonyms.addChild(new HelpBlock(allNames[i]));
}
root.addChild(synonyms);
}
// Now comes the turn to display synopsis if any
if (StringUtils.isNotBlank(cliCommand.help())) {
HelpBlock synopsis = new HelpBlock(SYNOPSIS_NAME);
synopsis.addChild(new HelpBlock(cliCommand.help()));
root.addChild(synopsis);
}
// Now display the syntax for the command
HelpBlock syntaxBlock = new HelpBlock(SYNTAX_NAME);
String syntax = getSyntaxString(commandName, annotations, parameterTypes);
syntaxBlock.addChild(new HelpBlock(syntax));
root.addChild(syntaxBlock);
// Detailed description of all the Options
if (annotations.length > 0) {
HelpBlock options = new HelpBlock(OPTIONS_NAME);
for (Annotation[] annotation : annotations) {
CliOption cliOption = getAnnotation(annotation, CliOption.class);
HelpBlock optionNode = getOptionDetail(cliOption);
options.addChild(optionNode);
}
root.addChild(options);
}
return root;
}
HelpBlock getOptionDetail(CliOption cliOption) {
HelpBlock optionNode = new HelpBlock(getPrimaryKey(cliOption));
String help = cliOption.help();
optionNode.addChild(new HelpBlock((StringUtils.isNotBlank(help) ? help : "")));
if (getSynonyms(cliOption).size() > 0) {
StringBuilder builder = new StringBuilder();
for (String string : getSynonyms(cliOption)) {
if (builder.length() > 0) {
builder.append(",");
}
builder.append(string);
}
optionNode.addChild(new HelpBlock(SYNONYMS_SUB_NAME + builder));
}
optionNode.addChild(
new HelpBlock(REQUIRED_SUB_NAME + ((cliOption.mandatory()) ? TRUE_TOKEN : FALSE_TOKEN)));
if (isNonEmptyAnnotation(cliOption.specifiedDefaultValue())) {
optionNode.addChild(
new HelpBlock(SPECIFIEDDEFAULTVALUE_SUB_NAME + cliOption.specifiedDefaultValue()));
}
if (isNonEmptyAnnotation(cliOption.unspecifiedDefaultValue())) {
optionNode.addChild(new HelpBlock(
UNSPECIFIEDDEFAULTVALUE_VALUE_SUB_NAME + cliOption.unspecifiedDefaultValue()));
}
return optionNode;
}
@SuppressWarnings("unchecked")
private <T> T getAnnotation(Annotation[] annotations, Class<T> klass) {
for (Annotation annotation : annotations) {
if (klass.isAssignableFrom(annotation.getClass())) {
return (T) annotation;
}
}
return null;
}
String getSyntaxString(String commandName, Annotation[][] annotations, Class[] parameterTypes) {
StringBuilder buffer = new StringBuilder();
buffer.append(commandName);
for (int i = 0; i < annotations.length; i++) {
CliOption cliOption = getAnnotation(annotations[i], CliOption.class);
String optionString = getOptionString(cliOption, parameterTypes[i]);
if (cliOption.mandatory()) {
buffer.append(" ").append(optionString);
} else {
buffer.append(" [").append(optionString).append("]");
}
}
return buffer.toString();
}
/**
* this builds the following format of strings: key (as in sh and help) --key=value --key(=value)?
* (if has specifiedDefaultValue) --key=value(,value)* (if the value is a list)
*
* @return option string
*/
private static String getOptionString(CliOption cliOption, Class<?> optionType) {
String key0 = cliOption.key()[0];
if ("".equals(key0)) {
return (cliOption.key()[1]);
}
StringBuilder buffer = new StringBuilder();
buffer.append(LONG_OPTION_SPECIFIER).append(key0);
boolean hasSpecifiedDefault = isNonEmptyAnnotation(cliOption.specifiedDefaultValue());
if (hasSpecifiedDefault) {
buffer.append("(");
}
buffer.append(OPTION_VALUE_SPECIFIER).append(VALUE_FIELD);
if (hasSpecifiedDefault) {
buffer.append(")?");
}
if (isCollectionOrArrayType(optionType)) {
buffer.append("(").append(",").append(VALUE_FIELD).append(")*");
}
return buffer.toString();
}
private static boolean isCollectionOrArrayType(Class<?> typeToCheck) {
return typeToCheck != null
&& (typeToCheck.isArray() || Collection.class.isAssignableFrom(typeToCheck));
}
private static String getPrimaryKey(CliOption option) {
String[] keys = option.key();
if (keys.length == 0) {
throw new RuntimeException("Invalid option keys");
} else if ("".equals(keys[0])) {
return keys[1];
} else {
return keys[0];
}
}
private static List<String> getSynonyms(CliOption option) {
List<String> synonyms = new ArrayList<>();
String[] keys = option.key();
if (keys.length < 2) {
return synonyms;
}
// if the primary key is empty (like sh and help command), then there should be no synonyms.
if ("".equals(keys[0])) {
return synonyms;
}
synonyms.addAll(Arrays.asList(keys).subList(1, keys.length));
return synonyms;
}
private static boolean isNonEmptyAnnotation(String value) {
return !StringUtils.isBlank(value) && !CliMetaData.ANNOTATION_NULL_VALUE.equals(value);
}
public Set<String> getCommands() {
return commands.keySet();
}
public Method getCommandMethod(String command) {
return commands.get(command);
}
// methods added for future option comparison
public Set<String> getOptions(String command) {
Method method = getCommandMethod(command);
Set<String> optionList = new HashSet<>();
Annotation[][] annotations = method.getParameterAnnotations();
if (annotations == null || annotations.length == 0) {
// can't validate arguments if command doesn't have any
return optionList;
}
for (Annotation[] annotation : annotations) {
CliOption cliOption = getAnnotation(annotation, CliOption.class);
optionList.add(getPrimaryKey(cliOption));
}
return optionList;
}
}