Refactor packages and artifactIds to prepare for official migration to spring-projects repo.

Remove usage of component scan in favor of auto-conf

Fixes #61
This commit is contained in:
Eric Bottard
2017-08-03 18:06:28 +02:00
parent 5fd1f716d9
commit 6497df181d
91 changed files with 488 additions and 264 deletions

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.shell.standard.commands;
import org.jline.terminal.Terminal;
import org.jline.utils.InfoCmp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
/**
* ANSI console related commands.
*
* @author Eric Bottard
*/
@ShellComponent
public class Console {
@Autowired @Lazy
private Terminal terminal;
@ShellMethod(help = "Clear the shell screen.")
public void clear() {
terminal.puts(InfoCmp.Capability.clear_screen);
}
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.shell.standard.commands;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toCollection;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.MethodTarget;
import org.springframework.shell.ParameterDescription;
import org.springframework.shell.ParameterResolver;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.standard.CommandValueProvider;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.shell.Utils;
/**
* A command to display help about all available commands.
*
* @author Eric Bottard
*/
@ShellComponent
public class Help {
private final List<ParameterResolver> parameterResolvers;
private CommandRegistry commandRegistry;
@Autowired
public Help(List<ParameterResolver> parameterResolvers) {
this.parameterResolvers = parameterResolvers;
}
@Autowired // ctor injection impossible b/c of circular dependency
public void setCommandRegistry(CommandRegistry commandRegistry) {
this.commandRegistry = commandRegistry;
}
@ShellMethod(help = "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 {
if (command == null) {
return listCommands();
}
else {
return documentCommand(command);
}
}
/**
* Return a description of a specific command. Uses a layout inspired by *nix man pages.
*/
private CharSequence documentCommand(String command) {
MethodTarget methodTarget = commandRegistry.listCommands().get(command);
if (methodTarget == null) {
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");
// SYNOPSYS
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()) {
result.append("["); // Whole parameter is optional, as there is a default value (1)
}
List<String> keys = description.keys();
if(!keys.isEmpty()) {
if (!description.mandatoryKey()) {
result.append("["); // Specifying a key is optional (ie positional params). (2)
}
result.append(first(keys), AttributedStyle.BOLD);
if (!description.mandatoryKey()) {
result.append("]"); // (close 2)
}
if (!description.formal().isEmpty()) {
result.append(" ");
}
}
if (description.defaultValueWhenFlag().isPresent()) {
result.append("["); // Parameter can be used as a toggle flag (3)
}
appendUnderlinedFormal(result, description);
if (description.defaultValueWhenFlag().isPresent()) {
result.append("]"); // (close 3)
}
if (description.defaultValue().isPresent()) {
result.append("]"); // (close 1)
}
result.append(" "); // two spaces between each param for better legibility
}
result.append("\n\n");
// OPTIONS
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);
if (description.formal().length() > 0) {
if (!description.keys().isEmpty()) {
result.append(" ");
}
description.defaultValueWhenFlag().ifPresent(f -> result.append('['));
appendUnderlinedFormal(result, description);
description.defaultValueWhenFlag().ifPresent(f -> result.append(']'));
result.append("\n\t");
}
else if (description.keys().size() > 1) {
result.append("\n\t");
}
result.append("\t");
result.append(description.help());
// Optional parameter
if (description.defaultValue().isPresent()) {
result
.append(" [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)
);
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)
;
} // true mandatory parameter
else {
result.append(" [Mandatory]", AttributedStyle.BOLD);
}
result.append("\n\n");
}
// ALSO KNOWN AS
Set<String> aliases = commandRegistry.listCommands().entrySet().stream()
.filter(e -> e.getValue().equals(methodTarget))
.map(Map.Entry::getKey)
.filter(c -> !command.equals(c))
.collect(toCollection(TreeSet::new));
if (!aliases.isEmpty()) {
result.append("ALSO KNOWN AS", AttributedStyle.BOLD).append("\n");
for (String alias : aliases) {
result.append('\t').append(alias).append('\n');
}
}
result.append("\n");
return result;
}
private String first(List<String> keys) {
return keys.iterator().next();
}
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
// Then display commands, sorted alphabetically by their first alias
AttributedStringBuilder result = new AttributedStringBuilder();
result.append("AVAILABLE COMMANDS\n\n", AttributedStyle.BOLD);
groupedByMethodTarget.entrySet().stream()
.sorted(sortByFirstElement())
.forEach(e -> result.append("\t")
.append(e.getValue().stream().collect(Collectors.joining(", ")), AttributedStyle.BOLD)
.append(": ")
.append(e.getKey())
.append('\n')
);
return result.append("\n");
}
private Comparator<Map.Entry<String, Set<String>>> sortByFirstElement() {
return Comparator.comparing(e -> e.getValue().iterator().next());
}
private void appendUnderlinedFormal(AttributedStringBuilder result, ParameterDescription description) {
for (char c : description.formal().toCharArray()) {
if (c != ' ') {
result.append("" + c, AttributedStyle.DEFAULT.underline());
}
else {
result.append(c);
}
}
}
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());
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.shell.standard.commands;
import org.springframework.shell.ExitRequest;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
/**
* A command that terminates the running shell.
*
* @author Eric Bottard
*/
@ShellComponent
public class Quit {
@ShellMethod(help = "Exit the shell.", value = {"quit", "exit"})
public void quit() {
throw new ExitRequest();
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.shell.standard.commands;
import org.jline.terminal.Terminal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.shell.result.ThrowableResultHandler;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
/**
* A command to display the full stacktrace when an error occurs.
*/
@ShellComponent
public class Stacktrace {
@Autowired @Lazy
private Terminal terminal;
@Autowired
private ThrowableResultHandler throwableResultHandler;
@ShellMethod(value = ThrowableResultHandler.DETAILS_COMMAND_NAME, help = "Display the full stacktrace of the last error")
public void stacktrace() {
if (throwableResultHandler.getLastError() != null) {
throwableResultHandler.getLastError().printStackTrace(terminal.writer());
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.shell.standard.commands;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.ParameterResolver;
/**
* Creates beans for standard commands.
*
* @author Eric Bottard
*/
@Configuration
public class StandardCommandsAutoConfiguration {
@Bean
public Help help(List<ParameterResolver> parameterResolvers) {
return new Help(parameterResolvers);
}
@Bean
public Console console() {
return new Console();
}
@Bean
public Quit quit() {
return new Quit();
}
@Bean
public Stacktrace stacktrace() {
return new Stacktrace();
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.
*/
/**
* Contains default commands that ought to apply to each shell app.
*
* @author Eric Bottard
*/
package org.springframework.shell.standard.commands;

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.shell.standard.commands.StandardCommandsAutoConfiguration

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.shell.standard.commands;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.io.ClassPathResource;
import org.springframework.shell.standard.StandardParameterResolver;
import org.springframework.shell.MethodTarget;
import org.springframework.shell.ParameterResolver;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ReflectionUtils;
/**
* Tests for the {@link Help} command.
*
* @author Eric Bottard
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HelpTest.Config.class)
public class HelpTest {
@Autowired
private Help help;
@Rule
public TestName testName = new TestName();
@Test
public void testCommandHelp() throws Exception {
CharSequence help = this.help.help("first-command").toString();
Assertions.assertThat(help).isEqualTo(sample());
}
@Test
public void testCommandList() throws Exception {
String list = this.help.help(null).toString();
Assertions.assertThat(list).isEqualTo(sample());
}
@Test(expected = IllegalArgumentException.class)
public void testUnknownCommand() throws Exception {
this.help.help("some unknown command");
}
private String sample() throws IOException {
InputStream is = new ClassPathResource(HelpTest.class.getSimpleName() + "-" + testName.getMethodName() + ".txt", HelpTest.class).getInputStream();
return FileCopyUtils.copyToString(new InputStreamReader(is, "UTF-8")).replace("&", "");
}
@Configuration
static class Config {
@Bean
public Help help() {
return new Help(Collections.singletonList(parameterResolver()));
}
@Bean
public CommandRegistry shell() {
return () -> {
Map<String, MethodTarget> result = new HashMap<>();
Method method = ReflectionUtils.findMethod(Commands.class, "firstCommand", boolean.class, boolean.class, int.class, float[].class);
MethodTarget methodTarget = new MethodTarget(method, commands(), "A rather extensive description of some command.");
result.put("first-command", methodTarget);
result.put("1st-command", methodTarget);
method = ReflectionUtils.findMethod(Commands.class, "secondCommand");
methodTarget = new MethodTarget(method, commands(), "The second command. This one is known under several aliases as well.");
result.put("second-command", methodTarget);
result.put("yet-another-command", methodTarget);
method = ReflectionUtils.findMethod(Commands.class, "thirdCommand");
methodTarget = new MethodTarget(method, commands(), "The last command.");
result.put("third-command", methodTarget);
return result;
};
}
@Bean
public ParameterResolver parameterResolver() {
return new StandardParameterResolver(new DefaultConversionService());
}
@Bean
public Object commands() {
return new Commands();
}
}
@ShellComponent
static class Commands {
@ShellMethod(prefix = "--")
public void firstCommand(
// Single key and arity = 0. Help displayed on same line
@ShellOption(help = "Whether to delete recursively", arity = 0, value = "-r") boolean r,
// Multiple keys and arity 0. Help displayed on next line
@ShellOption(help = "Do not ask for confirmation. YOLO", arity = 0, value = {"-f", "--force"}) boolean force,
// Single key, arity >= 1. Help displayed on next line. Optional
@ShellOption(help = "The answer to everything", defaultValue = "42", value = "-n") int n,
// Single key, arity > 1.
@ShellOption(help = "Some other parameters", arity = 3, value = "-o") float[] o
) {
}
@ShellMethod
public void secondCommand() {
}
@ShellMethod
public void thirdCommand() {
}
}
}

View File

@@ -0,0 +1,23 @@
&
&
NAME&
first-command - A rather extensive description of some command.&
&
SYNOPSYS&
first-command [-r] [-f] [[-n] int] [-o] float float float &
&
OPTIONS&
-r Whether to delete recursively [Mandatory]&
&
-f or --force&
Do not ask for confirmation. YOLO [Mandatory]&
&
-n int&
The answer to everything [Optional, default = 42]&
&
-o float float float&
Some other parameters [Mandatory]&
&
ALSO KNOWN AS&
1st-command&
&

View File

@@ -0,0 +1,6 @@
AVAILABLE COMMANDS&
&
1st-command, first-command: A rather extensive description of some command.&
second-command, yet-another-command: The second command. This one is known under several aliases as well.&
third-command: The last command.&
&