Implement more flexible error handling
- Add exception handling around new interface CommandExceptionResolver which allows to define a chain of resolvers to process errors before exception is bubbled up to result handlers. - Will be foundation to add more sophisticated error handling features compared to what spring itself have for rest layer. - Resolver returns CommandHandlingResult holder which further can be used to make a choice what to print into console and if spesific exit code should be used in non-interactive mode. - Exception handling can be defined globally and per command giving a change for user to customise i.e. error thrown by parser. - CommandParserExceptionResolver replaces CommandParserExceptionsExceptionResultHandler and provides more meaninful message for missing options. - Fixes #503
This commit is contained in:
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
@@ -32,6 +32,38 @@
|
||||
"projectName": "spring-shell-samples",
|
||||
"args": "fail --elementType TYPE"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "e2e reg error-handling",
|
||||
"request": "launch",
|
||||
"mainClass": "org.springframework.shell.samples.SpringShellSample",
|
||||
"projectName": "spring-shell-samples",
|
||||
"args": "e2e reg error-handling"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "e2e reg error-handling arg1 throw1",
|
||||
"request": "launch",
|
||||
"mainClass": "org.springframework.shell.samples.SpringShellSample",
|
||||
"projectName": "spring-shell-samples",
|
||||
"args": "e2e reg error-handling --arg1 throw1"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "e2e reg error-handling arg1 throw2",
|
||||
"request": "launch",
|
||||
"mainClass": "org.springframework.shell.samples.SpringShellSample",
|
||||
"projectName": "spring-shell-samples",
|
||||
"args": "e2e reg error-handling --arg1 throw2"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "e2e reg error-handling arg1 throw3",
|
||||
"request": "launch",
|
||||
"mainClass": "org.springframework.shell.samples.SpringShellSample",
|
||||
"projectName": "spring-shell-samples",
|
||||
"args": "e2e reg error-handling --arg1 throw3"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "e2e exit-code noarg",
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('e2e commands exit-code', () => {
|
||||
cli.run();
|
||||
await waitForExpect(async () => {
|
||||
const screen = cli.screen();
|
||||
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Missing option')]));
|
||||
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Missing mandatory option')]));
|
||||
});
|
||||
await expect(cli.exitCode()).resolves.toBe(2);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('e2e commands required-value', () => {
|
||||
cli.run();
|
||||
await waitForExpect(async () => {
|
||||
const screen = cli.screen();
|
||||
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Missing option')]));
|
||||
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Missing mandatory option')]));
|
||||
});
|
||||
await expect(cli.exitCode()).resolves.toBe(2);
|
||||
};
|
||||
@@ -34,7 +34,7 @@ describe('e2e commands required-value', () => {
|
||||
cli.run();
|
||||
await waitForExpect(async () => {
|
||||
const screen = cli.screen();
|
||||
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Missing option')]));
|
||||
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Missing mandatory option')]));
|
||||
});
|
||||
await expect(cli.exitCode()).resolves.toBe(2);
|
||||
};
|
||||
|
||||
@@ -20,11 +20,13 @@ import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.boot.ExitCodeExceptionMapper;
|
||||
import org.springframework.boot.ExitCodeGenerator;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.command.CommandExecution;
|
||||
import org.springframework.shell.exit.ExitCodeExceptionProvider;
|
||||
import org.springframework.shell.exit.ExitCodeMappings;
|
||||
|
||||
/**
|
||||
@@ -47,6 +49,12 @@ public class ExitCodeAutoConfiguration {
|
||||
return new ShellExitCodeMappingsExceptionMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ExitCodeExceptionProvider exitCodeExceptionProvider() {
|
||||
return (exception, code) -> new ShellExitCodeException(exception, code);
|
||||
}
|
||||
|
||||
static class ShellExitCodeExceptionMapper implements ExitCodeExceptionMapper {
|
||||
|
||||
@Override
|
||||
@@ -85,4 +93,19 @@ public class ExitCodeAutoConfiguration {
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
static class ShellExitCodeException extends RuntimeException implements ExitCodeGenerator {
|
||||
|
||||
private int code;
|
||||
|
||||
ShellExitCodeException(Throwable throwable, int code) {
|
||||
super(throwable);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExitCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import java.util.stream.Stream;
|
||||
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.Signals;
|
||||
import org.slf4j.Logger;
|
||||
@@ -39,13 +38,16 @@ import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.shell.command.CommandAlias;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandExecution;
|
||||
import org.springframework.shell.command.CommandOption;
|
||||
import org.springframework.shell.command.CommandExecution.CommandExecutionException;
|
||||
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
|
||||
import org.springframework.shell.command.CommandOption;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandExceptionResolver;
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.context.ShellContext;
|
||||
import org.springframework.shell.exit.ExitCodeExceptionProvider;
|
||||
import org.springframework.shell.exit.ExitCodeMappings;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -73,6 +75,8 @@ public class Shell {
|
||||
private ConversionService conversionService = new DefaultConversionService();
|
||||
private final ShellContext shellContext;
|
||||
private final ExitCodeMappings exitCodeMappings;
|
||||
private Exception handlingResultNonInt = null;
|
||||
private CommandHandlingResult processExceptionNonInt = null;
|
||||
|
||||
/**
|
||||
* Marker object to distinguish unresolved arguments from {@code null}, which is a valid
|
||||
@@ -81,6 +85,7 @@ public class Shell {
|
||||
protected static final Object UNRESOLVED = new Object();
|
||||
|
||||
private Validator validator = Utils.defaultValidator();
|
||||
private List<CommandExceptionResolver> exceptionResolvers = new ArrayList<>();
|
||||
|
||||
public Shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal,
|
||||
ShellContext shellContext, ExitCodeMappings exitCodeMappings) {
|
||||
@@ -111,6 +116,18 @@ public class Shell {
|
||||
this.validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setExceptionResolvers(List<CommandExceptionResolver> exceptionResolvers) {
|
||||
this.exceptionResolvers = exceptionResolvers;
|
||||
}
|
||||
|
||||
private ExitCodeExceptionProvider exitCodeExceptionProvider;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setExitCodeExceptionProvider(ExitCodeExceptionProvider exitCodeExceptionProvider) {
|
||||
this.exitCodeExceptionProvider = exitCodeExceptionProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main program loop: acquire input, try to match it to a command and evaluate. Repeat
|
||||
* until a {@link ResultHandler} causes the process to exit or there is no input.
|
||||
@@ -152,6 +169,13 @@ public class Shell {
|
||||
else if (result instanceof Exception) {
|
||||
throw (Exception) result;
|
||||
}
|
||||
if (handlingResultNonInt instanceof CommandExecution.CommandParserExceptionsException) {
|
||||
throw (CommandExecution.CommandParserExceptionsException) handlingResultNonInt;
|
||||
}
|
||||
else if (processExceptionNonInt != null && processExceptionNonInt.exitCode() != null
|
||||
&& exitCodeExceptionProvider != null) {
|
||||
throw exitCodeExceptionProvider.apply(null, processExceptionNonInt.exitCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,73 +189,123 @@ public class Shell {
|
||||
* result
|
||||
* </p>
|
||||
*/
|
||||
public Object evaluate(Input input) {
|
||||
private Object evaluate(Input input) {
|
||||
if (noInput(input)) {
|
||||
return NO_INPUT;
|
||||
}
|
||||
|
||||
String line = input.words().stream().collect(Collectors.joining(" ")).trim();
|
||||
List<String> words = input.words();
|
||||
String line = words.stream().collect(Collectors.joining(" ")).trim();
|
||||
String command = findLongestCommand(line);
|
||||
|
||||
List<String> words = input.words();
|
||||
log.debug("Evaluate input with line=[{}], command=[{}]", line, command);
|
||||
if (command != null) {
|
||||
|
||||
Optional<CommandRegistration> commandRegistration = commandRegistry.getRegistrations().values().stream()
|
||||
.filter(r -> {
|
||||
if (r.getCommand().equals(command)) {
|
||||
return true;
|
||||
}
|
||||
for (CommandAlias a : r.getAliases()) {
|
||||
if (a.getCommand().equals(command)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst();
|
||||
|
||||
if (commandRegistration.isPresent()) {
|
||||
if (this.exitCodeMappings != null) {
|
||||
List<Function<Throwable, Integer>> mappingFunctions = commandRegistration.get().getExitCode()
|
||||
.getMappingFunctions();
|
||||
this.exitCodeMappings.reset(mappingFunctions);
|
||||
}
|
||||
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
|
||||
Thread commandThread = Thread.currentThread();
|
||||
Object sh = Signals.register("INT", () -> commandThread.interrupt());
|
||||
try {
|
||||
CommandExecution execution = CommandExecution
|
||||
.of(argumentResolvers != null ? argumentResolvers.getResolvers() : null, validator, terminal, conversionService);
|
||||
return execution.evaluate(commandRegistration.get(), wordsForArgs.toArray(new String[0]));
|
||||
}
|
||||
catch (UndeclaredThrowableException e) {
|
||||
if (e.getCause() instanceof InterruptedException || e.getCause() instanceof ClosedByInterruptException) {
|
||||
Thread.interrupted(); // to reset interrupted flag
|
||||
}
|
||||
return e.getCause();
|
||||
}
|
||||
catch (CommandExecutionException e) {
|
||||
return e.getCause();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
finally {
|
||||
Signals.unregister("INT", sh);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return new CommandNotFound(words);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (command == null) {
|
||||
return new CommandNotFound(words);
|
||||
}
|
||||
|
||||
log.debug("Evaluate input with line=[{}], command=[{}]", line, command);
|
||||
|
||||
Optional<CommandRegistration> commandRegistration = commandRegistry.getRegistrations().values().stream()
|
||||
.filter(r -> {
|
||||
if (r.getCommand().equals(command)) {
|
||||
return true;
|
||||
}
|
||||
for (CommandAlias a : r.getAliases()) {
|
||||
if (a.getCommand().equals(command)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst();
|
||||
|
||||
if (commandRegistration.isEmpty()) {
|
||||
return new CommandNotFound(words);
|
||||
}
|
||||
|
||||
if (this.exitCodeMappings != null) {
|
||||
List<Function<Throwable, Integer>> mappingFunctions = commandRegistration.get().getExitCode()
|
||||
.getMappingFunctions();
|
||||
this.exitCodeMappings.reset(mappingFunctions);
|
||||
}
|
||||
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
|
||||
Thread commandThread = Thread.currentThread();
|
||||
Object sh = Signals.register("INT", () -> commandThread.interrupt());
|
||||
|
||||
CommandExecution execution = CommandExecution.of(
|
||||
argumentResolvers != null ? argumentResolvers.getResolvers() : null, validator, terminal,
|
||||
conversionService);
|
||||
|
||||
List<CommandExceptionResolver> commandExceptionResolvers = commandRegistration.get().getExceptionResolvers();
|
||||
|
||||
Object evaluate = null;
|
||||
Exception e = null;
|
||||
try {
|
||||
evaluate = execution.evaluate(commandRegistration.get(), wordsForArgs.toArray(new String[0]));
|
||||
}
|
||||
catch (UndeclaredThrowableException ute) {
|
||||
if (ute.getCause() instanceof InterruptedException || ute.getCause() instanceof ClosedByInterruptException) {
|
||||
Thread.interrupted(); // to reset interrupted flag
|
||||
}
|
||||
return ute.getCause();
|
||||
}
|
||||
catch (CommandExecutionException e1) {
|
||||
return e1.getCause();
|
||||
}
|
||||
catch (Exception e2) {
|
||||
e = e2;
|
||||
}
|
||||
finally {
|
||||
Signals.unregister("INT", sh);
|
||||
}
|
||||
if (e != null) {
|
||||
try {
|
||||
CommandHandlingResult processException = processException(commandExceptionResolvers, e);
|
||||
processExceptionNonInt = processException;
|
||||
if (processException != null) {
|
||||
handlingResultNonInt = e;
|
||||
this.terminal.writer().append(processException.message());
|
||||
this.terminal.writer().flush();
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e1) {
|
||||
e = e1;
|
||||
}
|
||||
}
|
||||
if (e != null) {
|
||||
evaluate = e;
|
||||
}
|
||||
return evaluate;
|
||||
}
|
||||
|
||||
private CommandHandlingResult processException(List<CommandExceptionResolver> commandExceptionResolvers, Exception e)
|
||||
throws Exception {
|
||||
CommandHandlingResult r = null;
|
||||
for (CommandExceptionResolver resolver : commandExceptionResolvers) {
|
||||
r = resolver.resolve(e);
|
||||
if (r != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r == null) {
|
||||
for (CommandExceptionResolver resolver : exceptionResolvers) {
|
||||
r = resolver.resolve(e);
|
||||
if (r != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (r != null) {
|
||||
if (r.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the parsed input ends up being empty (<em>e.g.</em> hitting ENTER on an
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.command;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by objects that can resolve exceptions thrown
|
||||
* during command processing, in the typical case error response. Implementors
|
||||
* are typically registered as beans in the application context or directly
|
||||
* with command.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandExceptionResolver {
|
||||
|
||||
/**
|
||||
* Try to resolve the given exception that got thrown during command processing.
|
||||
*
|
||||
* @param ex the exception
|
||||
* @return a corresponding {@code HandlingResult} framework to handle, or
|
||||
* {@code null} for default processing in the resolution chain
|
||||
*/
|
||||
CommandHandlingResult resolve(Exception ex);
|
||||
}
|
||||
@@ -201,7 +201,7 @@ public interface CommandExecution {
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandParserExceptionsException extends RuntimeException {
|
||||
public static class CommandParserExceptionsException extends RuntimeException {
|
||||
|
||||
private final List<CommandParserException> parserExceptions;
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.command;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Holder for handling some processing, typically with {@link CommandExceptionResolver}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandHandlingResult {
|
||||
|
||||
/**
|
||||
* Gets a message for this {@code HandlingResult}.
|
||||
*
|
||||
* @return a message
|
||||
*/
|
||||
@Nullable
|
||||
String message();
|
||||
|
||||
/**
|
||||
* Gets an exit code for this {@code HandlingResult}. Exit code only has meaning
|
||||
* if shell is in non-interactive mode.
|
||||
*
|
||||
* @return an exit code
|
||||
*/
|
||||
Integer exitCode();
|
||||
|
||||
/**
|
||||
* Indicate whether this {@code HandlingResult} has a result.
|
||||
*
|
||||
* @return true if result exist
|
||||
*/
|
||||
public boolean isPresent();
|
||||
|
||||
/**
|
||||
* Indicate whether this {@code HandlingResult} does not have a result.
|
||||
*
|
||||
* @return true if result doesn't exist
|
||||
*/
|
||||
public boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Gets an empty instance of {@code HandlingResult}.
|
||||
*
|
||||
* @return empty instance of {@code HandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult empty() {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of {@code HandlingResult}.
|
||||
*
|
||||
* @param message the message
|
||||
* @return instance of {@code HandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult of(@Nullable String message) {
|
||||
return of(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of {@code HandlingResult}.
|
||||
*
|
||||
* @param message the message
|
||||
* @param exitCode the exit code
|
||||
* @return instance of {@code HandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult of(@Nullable String message, Integer exitCode) {
|
||||
return new DefaultHandlingResult(message, exitCode);
|
||||
}
|
||||
|
||||
static class DefaultHandlingResult implements CommandHandlingResult {
|
||||
|
||||
private final String message;
|
||||
private final Integer exitCode;
|
||||
|
||||
DefaultHandlingResult(String message) {
|
||||
this(message, null);
|
||||
}
|
||||
|
||||
DefaultHandlingResult(String message, Integer exitCode) {
|
||||
this.message = message;
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String message() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer exitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPresent() {
|
||||
return message != null || exitCode != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return !isPresent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,7 +484,7 @@ public interface CommandParser {
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandParserException extends RuntimeException {
|
||||
public static class CommandParserException extends RuntimeException {
|
||||
|
||||
public CommandParserException(String message) {
|
||||
super(message);
|
||||
@@ -499,7 +499,7 @@ public interface CommandParser {
|
||||
}
|
||||
}
|
||||
|
||||
static class MissingOptionException extends CommandParserException {
|
||||
public static class MissingOptionException extends CommandParserException {
|
||||
|
||||
private CommandOption option;
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.command;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
|
||||
import org.springframework.shell.command.CommandParser.MissingOptionException;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Handles {@link CommandParserExceptionsException}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandParserExceptionResolver implements CommandExceptionResolver {
|
||||
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception ex) {
|
||||
if (ex instanceof CommandParserExceptionsException cpee) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
cpee.getParserExceptions().stream().forEach(e -> {
|
||||
if (e instanceof MissingOptionException moe) {
|
||||
CommandOption option = moe.getOption();
|
||||
if (option.getLongNames().length > 0) {
|
||||
handleLong(builder, option);
|
||||
}
|
||||
else if (option.getShortNames().length > 0) {
|
||||
handleShort(builder, option);
|
||||
}
|
||||
}
|
||||
else {
|
||||
builder.append(new AttributedString(e.getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
builder.append("\n");
|
||||
});
|
||||
String as = builder.toAttributedString().toAnsi();
|
||||
return CommandHandlingResult.of(as);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void handleLong(AttributedStringBuilder builder, CommandOption option) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("Missing mandatory option --");
|
||||
buf.append(option.getLongNames()[0]);
|
||||
if (StringUtils.hasText(option.getDescription())) {
|
||||
buf.append(", ");
|
||||
buf.append(option.getDescription());
|
||||
}
|
||||
buf.append(".");
|
||||
builder.append(new AttributedString(buf.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
|
||||
private static void handleShort(AttributedStringBuilder builder, CommandOption option) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("Missing mandatory option -");
|
||||
buf.append(option.getShortNames()[0]);
|
||||
if (StringUtils.hasText(option.getDescription())) {
|
||||
buf.append(", ");
|
||||
buf.append(option.getDescription());
|
||||
}
|
||||
buf.append(".");
|
||||
builder.append(new AttributedString(buf.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
@@ -106,6 +107,13 @@ public interface CommandRegistration {
|
||||
*/
|
||||
CommandExitCode getExitCode();
|
||||
|
||||
/**
|
||||
* Gets an exception resolvers.
|
||||
*
|
||||
* @return the exception resolvers
|
||||
*/
|
||||
List<CommandExceptionResolver> getExceptionResolvers();
|
||||
|
||||
/**
|
||||
* Gets a new instance of a {@link Builder}.
|
||||
*
|
||||
@@ -439,6 +447,27 @@ public interface CommandRegistration {
|
||||
Builder and();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec defining an error handling.
|
||||
*/
|
||||
public interface ErrorHandlingSpec {
|
||||
|
||||
/**
|
||||
* Add {@link CommandExceptionResolver}.
|
||||
*
|
||||
* @param resolver the resolver
|
||||
* @return a error handling for chaining
|
||||
*/
|
||||
ErrorHandlingSpec resolver(CommandExceptionResolver resolver);
|
||||
|
||||
/**
|
||||
* Return a builder for chaining.
|
||||
*
|
||||
* @return a builder for chaining
|
||||
*/
|
||||
Builder and();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder interface for {@link CommandRegistration}.
|
||||
*/
|
||||
@@ -516,6 +545,13 @@ public interface CommandRegistration {
|
||||
*/
|
||||
ExitCodeSpec withExitCode();
|
||||
|
||||
/**
|
||||
* Define an error handling what this command should use
|
||||
*
|
||||
* @return error handling spec for chaining
|
||||
*/
|
||||
ErrorHandlingSpec withErrorHandling();
|
||||
|
||||
/**
|
||||
* Builds a {@link CommandRegistration}.
|
||||
*
|
||||
@@ -804,6 +840,27 @@ public interface CommandRegistration {
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultErrorHandlingSpec implements ErrorHandlingSpec {
|
||||
|
||||
private BaseBuilder builder;
|
||||
private final List<CommandExceptionResolver> resolvers = new ArrayList<>();
|
||||
|
||||
DefaultErrorHandlingSpec(BaseBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorHandlingSpec resolver(CommandExceptionResolver resolver) {
|
||||
this.resolvers.add(resolver);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder and() {
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultCommandRegistration implements CommandRegistration {
|
||||
|
||||
private String command;
|
||||
@@ -815,10 +872,12 @@ public interface CommandRegistration {
|
||||
private DefaultTargetSpec targetSpec;
|
||||
private List<DefaultAliasSpec> aliasSpecs;
|
||||
private DefaultExitCodeSpec exitCodeSpec;
|
||||
private DefaultErrorHandlingSpec errorHandlingSpec;
|
||||
|
||||
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group,
|
||||
String description, Supplier<Availability> availability, List<DefaultOptionSpec> optionSpecs,
|
||||
DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs, DefaultExitCodeSpec exitCodeSpec) {
|
||||
DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs, DefaultExitCodeSpec exitCodeSpec,
|
||||
DefaultErrorHandlingSpec errorHandlingSpec) {
|
||||
this.command = commandArrayToName(commands);
|
||||
this.interactionMode = interactionMode;
|
||||
this.group = group;
|
||||
@@ -828,6 +887,7 @@ public interface CommandRegistration {
|
||||
this.targetSpec = targetSpec;
|
||||
this.aliasSpecs = aliasSpecs;
|
||||
this.exitCodeSpec = exitCodeSpec;
|
||||
this.errorHandlingSpec = errorHandlingSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -897,6 +957,16 @@ public interface CommandRegistration {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandExceptionResolver> getExceptionResolvers() {
|
||||
if (this.errorHandlingSpec == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
else {
|
||||
return this.errorHandlingSpec.resolvers;
|
||||
}
|
||||
}
|
||||
|
||||
private static String commandArrayToName(String[] commands) {
|
||||
return Arrays.asList(commands).stream()
|
||||
.flatMap(c -> Stream.of(c.split(" ")))
|
||||
@@ -921,6 +991,7 @@ public interface CommandRegistration {
|
||||
private List<DefaultAliasSpec> aliasSpecs = new ArrayList<>();
|
||||
private DefaultTargetSpec targetSpec;
|
||||
private DefaultExitCodeSpec exitCodeSpec;
|
||||
private DefaultErrorHandlingSpec errorHandlingSpec;
|
||||
|
||||
@Override
|
||||
public Builder command(String... commands) {
|
||||
@@ -986,13 +1057,20 @@ public interface CommandRegistration {
|
||||
return spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorHandlingSpec withErrorHandling() {
|
||||
DefaultErrorHandlingSpec spec = new DefaultErrorHandlingSpec(this);
|
||||
this.errorHandlingSpec = spec;
|
||||
return spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandRegistration build() {
|
||||
Assert.notNull(commands, "command cannot be empty");
|
||||
Assert.notNull(targetSpec, "target cannot be empty");
|
||||
Assert.state(!(targetSpec.bean != null && targetSpec.function != null), "only one target can exist");
|
||||
return new DefaultCommandRegistration(commands, interactionMode, group, description, availability,
|
||||
optionSpecs, targetSpec, aliasSpecs, exitCodeSpec);
|
||||
optionSpecs, targetSpec, aliasSpecs, exitCodeSpec, errorHandlingSpec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.exit;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Interface to provide exception for an exit code. Typically providing
|
||||
* exception implementing boot's {@code ExitCodeGenerator}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface ExitCodeExceptionProvider extends BiFunction<Exception, Integer, Exception> {
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;;
|
||||
|
||||
/**
|
||||
* Displays command parsing errors on the terminal.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandParserExceptionsExceptionResultHandler extends TerminalAwareResultHandler<CommandParserExceptionsException> {
|
||||
|
||||
public CommandParserExceptionsExceptionResultHandler(Terminal terminal) {
|
||||
super(terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doHandleResult(CommandParserExceptionsException result) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
result.getParserExceptions().stream().forEach(e -> {
|
||||
builder.append(new AttributedString(e.getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
builder.append("\n");
|
||||
});
|
||||
terminal.writer().append(builder.toAnsi());
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.TerminalSizeAware;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandParserExceptionResolver;
|
||||
import org.springframework.shell.context.ShellContext;
|
||||
import org.springframework.shell.jline.InteractiveShellRunner;
|
||||
|
||||
@@ -57,8 +58,8 @@ public class ResultHandlerConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandParserExceptionsExceptionResultHandler commandParserExceptionsExceptionResultHandler(Terminal terminal) {
|
||||
return new CommandParserExceptionsExceptionResultHandler(terminal);
|
||||
public CommandParserExceptionResolver commandParserExceptionResolver() {
|
||||
return new CommandParserExceptionResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.command;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class CommandHandlingResultTests {
|
||||
|
||||
@Test
|
||||
void hasMessageFromOf() {
|
||||
CommandHandlingResult result = CommandHandlingResult.of("fake");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.message()).isEqualTo("fake");
|
||||
assertThat(result.isPresent()).isTrue();
|
||||
assertThat(result.isEmpty()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasMessageAndCodeFromOf() {
|
||||
CommandHandlingResult result = CommandHandlingResult.of("fake", 1);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.message()).isEqualTo("fake");
|
||||
assertThat(result.exitCode()).isEqualTo(1);
|
||||
assertThat(result.isPresent()).isTrue();
|
||||
assertThat(result.isEmpty()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasNotMessageFromOf() {
|
||||
CommandHandlingResult result = CommandHandlingResult.of(null);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.message()).isNull();
|
||||
assertThat(result.isPresent()).isFalse();
|
||||
assertThat(result.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasNotMessageAndCodeFromOf() {
|
||||
CommandHandlingResult result = CommandHandlingResult.of(null);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.message()).isNull();
|
||||
assertThat(result.exitCode()).isNull();
|
||||
assertThat(result.isPresent()).isFalse();
|
||||
assertThat(result.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasNotMessageFromEmpty() {
|
||||
CommandHandlingResult result = CommandHandlingResult.empty();
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.message()).isNull();
|
||||
assertThat(result.exitCode()).isNull();
|
||||
assertThat(result.isPresent()).isFalse();
|
||||
assertThat(result.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isPresentJustWithCode() {
|
||||
CommandHandlingResult result = CommandHandlingResult.of(null, 1);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.isPresent()).isTrue();
|
||||
assertThat(result.isEmpty()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserException;
|
||||
import org.springframework.shell.command.CommandParser.MissingOptionException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CommandParserExceptionResolverTests {
|
||||
|
||||
private final CommandParserExceptionResolver resolver = new CommandParserExceptionResolver();
|
||||
|
||||
@Test
|
||||
void resolvesMissingLongOption() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("Desc arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(of(registration.getOptions().get(0)));
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("--arg1", "Desc arg1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesMissingLongOptionWhenAlsoShort() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.shortNames('x')
|
||||
.description("Desc arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(of(registration.getOptions().get(0)));
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("--arg1", "Desc arg1");
|
||||
assertThat(resolve.message()).doesNotContain("-x", "Desc x");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesMissingShortOption() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.shortNames('x')
|
||||
.description("Desc x")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(of(registration.getOptions().get(0)));
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("-x", "Desc x");
|
||||
}
|
||||
|
||||
static CommandParserExceptionsException of(CommandOption option) {
|
||||
MissingOptionException moe = new MissingOptionException("msg", option);
|
||||
List<CommandParserException> parserExceptions = Arrays.asList(moe);
|
||||
return new CommandParserExceptionsException("msg", parserExceptions);
|
||||
}
|
||||
}
|
||||
@@ -455,4 +455,34 @@ public class CommandRegistrationTests extends AbstractCommandTests {
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getCompletion()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorHandling() {
|
||||
CommandExceptionResolver er1 = new CommandExceptionResolver() {
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception e) {
|
||||
return CommandHandlingResult.empty();
|
||||
}
|
||||
};
|
||||
CommandRegistration registration;
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withErrorHandling()
|
||||
.resolver(er1)
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getExceptionResolvers()).hasSize(1);
|
||||
assertThat(registration.getExceptionResolvers().get(0)).isSameAs(er1);
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getExceptionResolvers()).hasSize(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[[dynamic-command-exitcode]]
|
||||
=== Exit Code
|
||||
=== Exception Handling
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Many command line applications when applicable return an _exit code_ which running environment
|
||||
@@ -7,6 +7,44 @@ can use to differentiate if command has been executed successfully or not. In a
|
||||
this mostly relates when a command is run on a non-interactive mode meaning one command
|
||||
is always executed once with an instance of a `spring-shell`.
|
||||
|
||||
==== Exception Resolving
|
||||
|
||||
Unhandled exceptions will bubble up into shell's `ResultHandlerService` and then eventually
|
||||
handled by some instance of `ResultHandler`. Chain of `ExceptionResolver` implementations
|
||||
can be used to resolve exceptions and gives you flexibility to return message to get written
|
||||
into console together with exit code which are wrapped within `CommandHandlingResult`.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/ErrorHandlingSnippets.java[tag=my-exception-resolver-class]
|
||||
----
|
||||
====
|
||||
|
||||
`CommandExceptionResolver` implementations can be defined globally as beans or defined
|
||||
per `CommandRegistration` if it's applicable only for a particular command itself.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/ErrorHandlingSnippets.java[tag=example1]
|
||||
----
|
||||
====
|
||||
|
||||
Use you own exception types which can also be an instance of boot's `ExitCodeGenerator` if
|
||||
you want to define exit code there.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/ErrorHandlingSnippets.java[tag=my-exception-class]
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: With annotation based configuration exception resolving can only be customised globally
|
||||
|
||||
==== Exit Code Mappings
|
||||
|
||||
Default behaviour of an exit codes is as:
|
||||
|
||||
- Errors from a command option parsing will result code of `2`
|
||||
@@ -37,3 +75,4 @@ include::{snippets}/ExitCodeSnippets.java[tag=example1]
|
||||
====
|
||||
|
||||
NOTE: Exit codes cannot be customized with annotation based configuration
|
||||
|
||||
@@ -19,4 +19,4 @@ include::using-shell-commands-organize.adoc[]
|
||||
|
||||
include::using-shell-commands-availability.adoc[]
|
||||
|
||||
include::using-shell-commands-exitcode.adoc[]
|
||||
include::using-shell-commands-exceptionhandling.adoc[]
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.docs;
|
||||
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandExceptionResolver;
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
|
||||
class ErrorHandlingSnippets {
|
||||
|
||||
// tag::my-exception-class[]
|
||||
static class CustomException extends RuntimeException {}
|
||||
// end::my-exception-class[]
|
||||
|
||||
// tag::my-exception-resolver-class[]
|
||||
static class CustomExceptionResolver implements CommandExceptionResolver {
|
||||
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception e) {
|
||||
if (e instanceof CustomException) {
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// end::my-exception-resolver-class[]
|
||||
|
||||
void dump1() {
|
||||
// tag::example1[]
|
||||
CommandRegistration.builder()
|
||||
.withErrorHandling()
|
||||
.resolver(new CustomExceptionResolver())
|
||||
.and()
|
||||
.build();
|
||||
// end::example1[]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.samples.e2e;
|
||||
|
||||
import org.springframework.boot.ExitCodeGenerator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandExceptionResolver;
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
|
||||
/**
|
||||
* Commands used for e2e test.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@ShellComponent
|
||||
public class ErrorHandlingCommands extends BaseE2ECommands {
|
||||
|
||||
@Bean
|
||||
public CommandRegistration testErrorHandlingRegistration() {
|
||||
return CommandRegistration.builder()
|
||||
.command(REG, "error-handling")
|
||||
.group(GROUP)
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withErrorHandling()
|
||||
.resolver(new CustomExceptionResolver())
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(ctx -> {
|
||||
String arg1 = ctx.getOptionValue("arg1");
|
||||
if ("throw1".equals(arg1)) {
|
||||
throw new CustomException1();
|
||||
}
|
||||
if ("throw2".equals(arg1)) {
|
||||
throw new CustomException2(11);
|
||||
}
|
||||
if ("throw3".equals(arg1)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return "Hello " + arg1;
|
||||
})
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class CustomException1 extends RuntimeException {
|
||||
}
|
||||
|
||||
private static class CustomException2 extends RuntimeException implements ExitCodeGenerator {
|
||||
|
||||
private int code;
|
||||
|
||||
CustomException2(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExitCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class CustomExceptionResolver implements CommandExceptionResolver {
|
||||
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception e) {
|
||||
if (e instanceof CustomException1) {
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class RequiredValueCommands extends BaseE2ECommands {
|
||||
|
||||
@ShellMethod(key = LEGACY_ANNO + "required-value", group = GROUP)
|
||||
public String testRequiredValueAnnotation(
|
||||
@ShellOption String arg1
|
||||
@ShellOption(help = "Desc arg1") String arg1
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public class RequiredValueCommands extends BaseE2ECommands {
|
||||
.group(GROUP)
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("Desc arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.samples.e2e;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
|
||||
/**
|
||||
* Commands used for e2e test.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@ShellComponent
|
||||
public class ValidatedValueCommands extends BaseE2ECommands {
|
||||
|
||||
@ShellMethod(key = LEGACY_ANNO + "validated-value", group = GROUP)
|
||||
public String testValidatedValueAnnotation(
|
||||
@ShellOption @Min(value = 1) Integer arg1,
|
||||
@ShellOption @Min(value = 1) Integer arg2
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandRegistration testValidatedValueRegistration() {
|
||||
return CommandRegistration.builder()
|
||||
.command(REG, "validated-value")
|
||||
.group(GROUP)
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.type(Integer.class)
|
||||
.required()
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.type(Integer.class)
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(ctx -> {
|
||||
Integer arg1 = ctx.getOptionValue("arg1");
|
||||
return "Hello " + arg1;
|
||||
})
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user