|
|
|
|
@@ -27,6 +27,7 @@ import java.util.Set;
|
|
|
|
|
import java.util.TreeSet;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
import org.hibernate.validator.internal.engine.MessageInterpolatorContext;
|
|
|
|
|
import org.jline.utils.AttributedStringBuilder;
|
|
|
|
|
import org.jline.utils.AttributedStyle;
|
|
|
|
|
|
|
|
|
|
@@ -37,6 +38,10 @@ import org.springframework.shell.standard.ShellComponent;
|
|
|
|
|
import org.springframework.shell.standard.ShellMethod;
|
|
|
|
|
import org.springframework.shell.standard.ShellOption;
|
|
|
|
|
|
|
|
|
|
import javax.validation.MessageInterpolator;
|
|
|
|
|
import javax.validation.Validation;
|
|
|
|
|
import javax.validation.metadata.ConstraintDescriptor;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A command to display help about all available commands.
|
|
|
|
|
*
|
|
|
|
|
@@ -48,20 +53,29 @@ public class Help {
|
|
|
|
|
/**
|
|
|
|
|
* Marker interface for beans providing {@literal help} functionality to the shell.
|
|
|
|
|
*
|
|
|
|
|
* <p>To override the help command, simply register your own bean implementing that interface
|
|
|
|
|
* and the standard implementation will back off.</p>
|
|
|
|
|
* <p>
|
|
|
|
|
* To override the help command, simply register your own bean implementing that interface
|
|
|
|
|
* and the standard implementation will back off.
|
|
|
|
|
* </p>
|
|
|
|
|
*
|
|
|
|
|
* <p>To disable the {@literal help} command entirely, set the {@literal spring.shell.command.help.enabled=false}
|
|
|
|
|
* property in the environment.</p>
|
|
|
|
|
* <p>
|
|
|
|
|
* To disable the {@literal help} command entirely, set the
|
|
|
|
|
* {@literal spring.shell.command.help.enabled=false} property in the environment.
|
|
|
|
|
* </p>
|
|
|
|
|
*
|
|
|
|
|
* @author Eric Bottard
|
|
|
|
|
*/
|
|
|
|
|
public interface Command {}
|
|
|
|
|
public interface Command {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private final List<ParameterResolver> parameterResolvers;
|
|
|
|
|
|
|
|
|
|
private CommandRegistry commandRegistry;
|
|
|
|
|
|
|
|
|
|
@Autowired(required = false)
|
|
|
|
|
private MessageInterpolator messageInterpolator = Validation.buildDefaultValidatorFactory()
|
|
|
|
|
.getMessageInterpolator();
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
public Help(List<ParameterResolver> parameterResolvers) {
|
|
|
|
|
this.parameterResolvers = parameterResolvers;
|
|
|
|
|
@@ -74,10 +88,9 @@ public class Help {
|
|
|
|
|
|
|
|
|
|
@ShellMethod(value = "Display help about available commands.", prefix = "-")
|
|
|
|
|
public CharSequence help(
|
|
|
|
|
@ShellOption(defaultValue = ShellOption.NULL,
|
|
|
|
|
valueProvider = CommandValueProvider.class,
|
|
|
|
|
value = {"-C", "--command"},
|
|
|
|
|
help = "The command to obtain help for.") String command) throws IOException {
|
|
|
|
|
@ShellOption(defaultValue = ShellOption.NULL, valueProvider = CommandValueProvider.class, value = { "-C",
|
|
|
|
|
"--command" }, help = "The command to obtain help for.") String command)
|
|
|
|
|
throws IOException {
|
|
|
|
|
if (command == null) {
|
|
|
|
|
return listCommands();
|
|
|
|
|
}
|
|
|
|
|
@@ -96,25 +109,46 @@ public class Help {
|
|
|
|
|
throw new IllegalArgumentException("Unknown command '" + command + "'");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NAME
|
|
|
|
|
AttributedStringBuilder result = new AttributedStringBuilder().append("\n\n");
|
|
|
|
|
result.append("NAME", AttributedStyle.BOLD).append("\n\t");
|
|
|
|
|
result.append(command).append(" - ").append(methodTarget.getHelp()).append("\n\n");
|
|
|
|
|
List<ParameterDescription> parameterDescriptions = getParameterDescriptions(methodTarget);
|
|
|
|
|
|
|
|
|
|
// NAME
|
|
|
|
|
documentCommandName(result, command, methodTarget.getHelp());
|
|
|
|
|
|
|
|
|
|
// SYNOPSYS
|
|
|
|
|
documentSynopsys(result, command, parameterDescriptions);
|
|
|
|
|
|
|
|
|
|
// OPTIONS
|
|
|
|
|
documentOptions(result, parameterDescriptions);
|
|
|
|
|
|
|
|
|
|
// ALSO KNOWN AS
|
|
|
|
|
documentAliases(result, command, methodTarget);
|
|
|
|
|
|
|
|
|
|
// AVAILABILITY
|
|
|
|
|
documentAvailability(result, methodTarget);
|
|
|
|
|
|
|
|
|
|
result.append("\n");
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void documentCommandName(AttributedStringBuilder result, String command, String help) {
|
|
|
|
|
result.append("NAME", AttributedStyle.BOLD).append("\n\t");
|
|
|
|
|
result.append(command).append(" - ").append(help).append("\n\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void documentSynopsys(AttributedStringBuilder result, String command,
|
|
|
|
|
List<ParameterDescription> parameterDescriptions) {
|
|
|
|
|
result.append("SYNOPSYS", AttributedStyle.BOLD).append("\n\t");
|
|
|
|
|
result.append(command, AttributedStyle.BOLD);
|
|
|
|
|
result.append(" ");
|
|
|
|
|
|
|
|
|
|
List<ParameterDescription> parameterDescriptions = getParameterDescriptions(methodTarget);
|
|
|
|
|
|
|
|
|
|
for (ParameterDescription description : parameterDescriptions) {
|
|
|
|
|
|
|
|
|
|
if (description.defaultValue().isPresent() && description.formal().length()>0) {
|
|
|
|
|
if (description.defaultValue().isPresent() && description.formal().length() > 0) {
|
|
|
|
|
result.append("["); // Whole parameter is optional, as there is a default value (1)
|
|
|
|
|
}
|
|
|
|
|
List<String> keys = description.keys();
|
|
|
|
|
if(!keys.isEmpty()) {
|
|
|
|
|
if (!keys.isEmpty()) {
|
|
|
|
|
if (!description.mandatoryKey()) {
|
|
|
|
|
result.append("["); // Specifying a key is optional (ie positional params). (2)
|
|
|
|
|
}
|
|
|
|
|
@@ -133,19 +167,21 @@ public class Help {
|
|
|
|
|
if (description.defaultValueWhenFlag().isPresent()) {
|
|
|
|
|
result.append("]"); // (close 3)
|
|
|
|
|
}
|
|
|
|
|
if (description.defaultValue().isPresent() && description.formal().length()>0) {
|
|
|
|
|
if (description.defaultValue().isPresent() && description.formal().length() > 0) {
|
|
|
|
|
result.append("]"); // (close 1)
|
|
|
|
|
}
|
|
|
|
|
result.append(" "); // two spaces between each param for better legibility
|
|
|
|
|
}
|
|
|
|
|
result.append("\n\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OPTIONS
|
|
|
|
|
private void documentOptions(AttributedStringBuilder result, List<ParameterDescription> parameterDescriptions) {
|
|
|
|
|
if (!parameterDescriptions.isEmpty()) {
|
|
|
|
|
result.append("OPTIONS", AttributedStyle.BOLD).append("\n");
|
|
|
|
|
}
|
|
|
|
|
for (ParameterDescription description : parameterDescriptions) {
|
|
|
|
|
result.append("\t").append(description.keys().stream().collect(Collectors.joining(" or ")), AttributedStyle.BOLD);
|
|
|
|
|
result.append("\t").append(description.keys().stream().collect(Collectors.joining(" or ")),
|
|
|
|
|
AttributedStyle.BOLD);
|
|
|
|
|
if (description.formal().length() > 0) {
|
|
|
|
|
if (!description.keys().isEmpty()) {
|
|
|
|
|
result.append(" ");
|
|
|
|
|
@@ -159,34 +195,42 @@ public class Help {
|
|
|
|
|
result.append("\n\t");
|
|
|
|
|
}
|
|
|
|
|
result.append("\t");
|
|
|
|
|
result.append(description.help());
|
|
|
|
|
result.append(description.help()).append('\n');
|
|
|
|
|
// Optional parameter
|
|
|
|
|
if (description.defaultValue().isPresent()) {
|
|
|
|
|
result
|
|
|
|
|
.append(" [Optional, default = ", AttributedStyle.BOLD)
|
|
|
|
|
.append("\t\t[Optional, default = ", AttributedStyle.BOLD)
|
|
|
|
|
.append(description.defaultValue().get(), AttributedStyle.BOLD.italic());
|
|
|
|
|
description.defaultValueWhenFlag().ifPresent(
|
|
|
|
|
s -> result.append(", or ", AttributedStyle.BOLD)
|
|
|
|
|
.append(s, AttributedStyle.BOLD.italic())
|
|
|
|
|
.append(" if used as a flag", AttributedStyle.BOLD)
|
|
|
|
|
);
|
|
|
|
|
s -> result.append(", or ", AttributedStyle.BOLD)
|
|
|
|
|
.append(s, AttributedStyle.BOLD.italic())
|
|
|
|
|
.append(" if used as a flag", AttributedStyle.BOLD));
|
|
|
|
|
|
|
|
|
|
result.append("]", AttributedStyle.BOLD);
|
|
|
|
|
} // Mandatory parameter, but with a default when used as a flag
|
|
|
|
|
else if (description.defaultValueWhenFlag().isPresent()) {
|
|
|
|
|
result
|
|
|
|
|
.append(" [Mandatory, default = ", AttributedStyle.BOLD)
|
|
|
|
|
.append(description.defaultValueWhenFlag().get(), AttributedStyle.BOLD.italic())
|
|
|
|
|
.append(" when used as a flag]", AttributedStyle.BOLD)
|
|
|
|
|
;
|
|
|
|
|
.append("\t\t[Mandatory, default = ", AttributedStyle.BOLD)
|
|
|
|
|
.append(description.defaultValueWhenFlag().get(), AttributedStyle.BOLD.italic())
|
|
|
|
|
.append(" when used as a flag]", AttributedStyle.BOLD);
|
|
|
|
|
} // true mandatory parameter
|
|
|
|
|
else {
|
|
|
|
|
result.append(" [Mandatory]", AttributedStyle.BOLD);
|
|
|
|
|
result.append("\t\t[Mandatory]", AttributedStyle.BOLD);
|
|
|
|
|
}
|
|
|
|
|
result.append("\n\n");
|
|
|
|
|
result.append('\n');
|
|
|
|
|
if (description.elementDescriptor() != null) {
|
|
|
|
|
for (ConstraintDescriptor<?> constraintDescriptor : description.elementDescriptor()
|
|
|
|
|
.getConstraintDescriptors()) {
|
|
|
|
|
String friendlyConstraint = messageInterpolator.interpolate(
|
|
|
|
|
constraintDescriptor.getMessageTemplate(), new DummyContext(constraintDescriptor));
|
|
|
|
|
result.append("\t\t[" + friendlyConstraint + "]\n", AttributedStyle.BOLD);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result.append('\n');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ALSO KNOWN AS
|
|
|
|
|
private void documentAliases(AttributedStringBuilder result, String command, MethodTarget methodTarget) {
|
|
|
|
|
Set<String> aliases = commandRegistry.listCommands().entrySet().stream()
|
|
|
|
|
.filter(e -> e.getValue().equals(methodTarget))
|
|
|
|
|
.map(Map.Entry::getKey)
|
|
|
|
|
@@ -199,7 +243,9 @@ public class Help {
|
|
|
|
|
result.append('\t').append(alias).append('\n');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void documentAvailability(AttributedStringBuilder result, MethodTarget methodTarget) {
|
|
|
|
|
Availability availability = methodTarget.getAvailability();
|
|
|
|
|
if (!availability.isAvailable()) {
|
|
|
|
|
result.append("CURRENTLY UNAVAILABLE", AttributedStyle.BOLD).append("\n");
|
|
|
|
|
@@ -207,9 +253,6 @@ public class Help {
|
|
|
|
|
.append(availability.getReason())
|
|
|
|
|
.append(".\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.append("\n");
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String first(List<String> keys) {
|
|
|
|
|
@@ -219,7 +262,8 @@ public class Help {
|
|
|
|
|
private CharSequence listCommands() {
|
|
|
|
|
Map<String, Set<String>> groupedByMethodTarget = commandRegistry.listCommands().entrySet().stream()
|
|
|
|
|
.collect(Collectors.groupingBy(e -> e.getValue().getHelp(), // Use help() as the grouping key
|
|
|
|
|
mapping(Map.Entry::getKey, toCollection(TreeSet::new)))); // accumulate the command 'names' into a sorted set
|
|
|
|
|
mapping(Map.Entry::getKey, toCollection(TreeSet::new)))); // accumulate the command 'names' into
|
|
|
|
|
// a sorted set
|
|
|
|
|
|
|
|
|
|
// Then display commands, sorted alphabetically by their first alias
|
|
|
|
|
AttributedStringBuilder result = new AttributedStringBuilder();
|
|
|
|
|
@@ -228,16 +272,16 @@ public class Help {
|
|
|
|
|
groupedByMethodTarget.entrySet().stream()
|
|
|
|
|
.sorted(sortByFirstElement())
|
|
|
|
|
.forEach(e -> result.append(isAvailable(e) ? " " : " * ")
|
|
|
|
|
.append(e.getValue().stream().collect(Collectors.joining(", ")), AttributedStyle.BOLD)
|
|
|
|
|
.append(": ")
|
|
|
|
|
.append(e.getKey())
|
|
|
|
|
.append('\n')
|
|
|
|
|
);
|
|
|
|
|
.append(e.getValue().stream().collect(Collectors.joining(", ")), AttributedStyle.BOLD)
|
|
|
|
|
.append(": ")
|
|
|
|
|
.append(e.getKey())
|
|
|
|
|
.append('\n'));
|
|
|
|
|
|
|
|
|
|
groupedByMethodTarget.entrySet().stream()
|
|
|
|
|
.filter(e -> !isAvailable(e))
|
|
|
|
|
.findAny()
|
|
|
|
|
.ifPresent(e -> result.append("\nCommands marked with (*) are currently unavailable.\nType `help <command>` to learn more.\n"));
|
|
|
|
|
.ifPresent(e -> result.append(
|
|
|
|
|
"\nCommands marked with (*) are currently unavailable.\nType `help <command>` to learn more.\n"));
|
|
|
|
|
|
|
|
|
|
return result.append("\n");
|
|
|
|
|
}
|
|
|
|
|
@@ -264,9 +308,34 @@ public class Help {
|
|
|
|
|
|
|
|
|
|
private List<ParameterDescription> getParameterDescriptions(MethodTarget methodTarget) {
|
|
|
|
|
return Utils.createMethodParameters(methodTarget.getMethod())
|
|
|
|
|
.flatMap(mp -> parameterResolvers.stream().filter(pr -> pr.supports(mp)).limit(1L).flatMap(pr -> pr.describe(mp)))
|
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
.flatMap(mp -> parameterResolvers.stream().filter(pr -> pr.supports(mp)).limit(1L)
|
|
|
|
|
.flatMap(pr -> pr.describe(mp)))
|
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class DummyContext implements MessageInterpolator.Context {
|
|
|
|
|
|
|
|
|
|
private final ConstraintDescriptor<?> descriptor;
|
|
|
|
|
|
|
|
|
|
private DummyContext(ConstraintDescriptor<?> descriptor) {
|
|
|
|
|
this.descriptor = descriptor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public ConstraintDescriptor<?> getConstraintDescriptor() {
|
|
|
|
|
return descriptor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public Object getValidatedValue() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public <T> T unwrap(Class<T> type) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|