Make command not found configurable
- New CommandNotFoundResultHandler which handles CommandNotFound to be able to customize error shown. - New CommandNotFoundMessageProvider which is a plain function given a "context" and returns a string. Context contains common info to provide better error messages. - Default provider gives same message but removes long stacktrace(which previously originated from a common ThrowableResultHandler. - Relates #778
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Copyright 2017-2023
|
||||
* 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
|
||||
@@ -18,17 +17,28 @@ package org.springframework.shell;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
/**
|
||||
* A result to be handled by the {@link ResultHandler} when no command could be mapped to user input
|
||||
*/
|
||||
public class CommandNotFound extends RuntimeException {
|
||||
|
||||
private final List<String> words;
|
||||
private final Map<String, CommandRegistration> registrations;
|
||||
private final String text;
|
||||
|
||||
public CommandNotFound(List<String> words) {
|
||||
this(words, null, null);
|
||||
}
|
||||
|
||||
public CommandNotFound(List<String> words, Map<String, CommandRegistration> registrations, String text) {
|
||||
this.words = words;
|
||||
this.registrations = registrations;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,4 +54,22 @@ public class CommandNotFound extends RuntimeException {
|
||||
public List<String> getWords(){
|
||||
return new ArrayList<>(words);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets command registrations known when this error was created.
|
||||
*
|
||||
* @return known command registrations
|
||||
*/
|
||||
public Map<String, CommandRegistration> getRegistrations() {
|
||||
return registrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a raw text input.
|
||||
*
|
||||
* @return raw text input
|
||||
*/
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.shell;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -201,13 +202,14 @@ public class Shell {
|
||||
String line = words.stream().collect(Collectors.joining(" ")).trim();
|
||||
String command = findLongestCommand(line, false);
|
||||
|
||||
Map<String, CommandRegistration> registrations = commandRegistry.getRegistrations();
|
||||
if (command == null) {
|
||||
return new CommandNotFound(words);
|
||||
return new CommandNotFound(words, new HashMap<>(registrations), input.rawText());
|
||||
}
|
||||
|
||||
log.debug("Evaluate input with line=[{}], command=[{}]", line, command);
|
||||
|
||||
Optional<CommandRegistration> commandRegistration = commandRegistry.getRegistrations().values().stream()
|
||||
Optional<CommandRegistration> commandRegistration = registrations.values().stream()
|
||||
.filter(r -> {
|
||||
if (r.getCommand().equals(command)) {
|
||||
return true;
|
||||
@@ -222,7 +224,7 @@ public class Shell {
|
||||
.findFirst();
|
||||
|
||||
if (commandRegistration.isEmpty()) {
|
||||
return new CommandNotFound(words);
|
||||
return new CommandNotFound(words, new HashMap<>(registrations), input.rawText());
|
||||
}
|
||||
|
||||
if (this.exitCodeMappings != null) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2023 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.result;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.result.CommandNotFoundMessageProvider.ProviderContext;
|
||||
|
||||
/**
|
||||
* Provider for a message used within {@link CommandNotFoundResultHandler}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandNotFoundMessageProvider extends Function<ProviderContext, String> {
|
||||
|
||||
static ProviderContext contextOf(Throwable error, List<String> commands, Map<String, CommandRegistration> registrations, String text) {
|
||||
return new ProviderContext() {
|
||||
|
||||
@Override
|
||||
public Throwable error() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> commands() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, CommandRegistration> registrations() {
|
||||
return registrations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String text() {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for {@link CommandNotFoundResultHandler}.
|
||||
*/
|
||||
interface ProviderContext {
|
||||
|
||||
/**
|
||||
* Gets an actual error.
|
||||
*
|
||||
* @return actual error
|
||||
*/
|
||||
Throwable error();
|
||||
|
||||
/**
|
||||
* Gets a list of commands parsed.
|
||||
*
|
||||
* @return list of commands parsed
|
||||
*/
|
||||
List<String> commands();
|
||||
|
||||
/**
|
||||
* Gets a command registrations.
|
||||
*
|
||||
* @return a command registrations
|
||||
*/
|
||||
Map<String, CommandRegistration> registrations();
|
||||
|
||||
/**
|
||||
* Gets a raw input text.
|
||||
*
|
||||
* @return a raw input text
|
||||
*/
|
||||
String text();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.shell.CommandNotFound;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.shell.result.CommandNotFoundMessageProvider.ProviderContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link ResultHandler} for {@link CommandNotFound} using
|
||||
* {@link CommandNotFoundMessageProvider} to provide an error message.
|
||||
* Default internal provider simply provides message from a {@link CommandNotFound}
|
||||
* with a red color. Provider can be defined by providing a custom
|
||||
* {@link CommandNotFoundMessageProvider} bean.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class CommandNotFoundResultHandler extends TerminalAwareResultHandler<CommandNotFound> {
|
||||
|
||||
private CommandNotFoundMessageProvider provider;
|
||||
|
||||
public CommandNotFoundResultHandler(Terminal terminal, ObjectProvider<CommandNotFoundMessageProvider> provider) {
|
||||
super(terminal);
|
||||
Assert.notNull(provider, "provider cannot be null");
|
||||
this.provider = provider.getIfAvailable(() -> new DefaultProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doHandleResult(CommandNotFound result) {
|
||||
ProviderContext context = CommandNotFoundMessageProvider.contextOf(result, result.getWords(),
|
||||
result.getRegistrations(), result.getText());
|
||||
String message = provider.apply(context);
|
||||
if (StringUtils.hasText(message)) {
|
||||
terminal.writer().println(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultProvider implements CommandNotFoundMessageProvider {
|
||||
|
||||
@Override
|
||||
public String apply(ProviderContext context) {
|
||||
String message = new AttributedString(context.error().getMessage(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi();
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,4 +70,10 @@ public class ResultHandlerConfig {
|
||||
ShellContext shellContext, ObjectProvider<InteractiveShellRunner> interactiveApplicationRunner) {
|
||||
return new ThrowableResultHandler(terminal, commandCatalog, shellContext, interactiveApplicationRunner);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandNotFoundResultHandler commandNotFoundResultHandler(Terminal terminal,
|
||||
ObjectProvider<CommandNotFoundMessageProvider> provider) {
|
||||
return new CommandNotFoundResultHandler(terminal, provider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2023 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.result;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.shell.CommandNotFound;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CommandNotFoundResultHandlerTests {
|
||||
|
||||
@Mock
|
||||
ObjectProvider<CommandNotFoundMessageProvider> provider;
|
||||
|
||||
@Mock
|
||||
Terminal terminal;
|
||||
|
||||
@Test
|
||||
void defaultProviderPrintsBasicMessage() {
|
||||
StringWriter out = new StringWriter();
|
||||
PrintWriter writer = new PrintWriter(out);
|
||||
given(provider.getIfAvailable()).willReturn(null);
|
||||
given(provider.getIfAvailable(any())).willCallRealMethod();
|
||||
given(terminal.writer()).willReturn(writer);
|
||||
CommandNotFoundResultHandler handler = new CommandNotFoundResultHandler(terminal, provider);
|
||||
CommandNotFound e = new CommandNotFound(Arrays.asList("one", "two"), Collections.emptyMap(), "one two --xxx");
|
||||
handler.handleResult(e);
|
||||
String string = out.toString();
|
||||
assertThat(string).contains("No command found for 'one two'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customProviderPrintsBasicMessage() {
|
||||
StringWriter out = new StringWriter();
|
||||
PrintWriter writer = new PrintWriter(out);
|
||||
given(provider.getIfAvailable()).willReturn(ctx -> "hi");
|
||||
given(provider.getIfAvailable(any())).willCallRealMethod();
|
||||
given(terminal.writer()).willReturn(writer);
|
||||
CommandNotFoundResultHandler handler = new CommandNotFoundResultHandler(terminal, provider);
|
||||
CommandNotFound e = new CommandNotFound(Arrays.asList("one", "two"), Collections.emptyMap(), "one two --xxx");
|
||||
handler.handleResult(e);
|
||||
String string = out.toString();
|
||||
assertThat(string).contains("hi");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customProviderGetsContext() {
|
||||
StringWriter out = new StringWriter();
|
||||
PrintWriter writer = new PrintWriter(out);
|
||||
List<String> commands = Arrays.asList("one", "two");
|
||||
Map<String, CommandRegistration> registrations = Collections.emptyMap();
|
||||
CommandNotFound e = new CommandNotFound(commands, registrations, "text");
|
||||
given(provider.getIfAvailable()).willReturn(ctx -> {
|
||||
return String.format("%s%s%s%s%s", "hi", ctx.error() == e ? "true" : "false",
|
||||
ctx.commands().stream().collect(Collectors.joining()),
|
||||
ctx.registrations() == registrations ? "true" : "false", ctx.text());
|
||||
});
|
||||
given(provider.getIfAvailable(any())).willCallRealMethod();
|
||||
given(terminal.writer()).willReturn(writer);
|
||||
CommandNotFoundResultHandler handler = new CommandNotFoundResultHandler(terminal, provider);
|
||||
handler.handleResult(e);
|
||||
String string = out.toString();
|
||||
assertThat(string).contains("hitrueonetwotruetext");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user