Support exception handling with annotated methods
- New annotations ExceptionResolver and ExitCode - New needed functionality is in classes ExceptionResolverMethodResolver and MethodCommandExceptionResolver. - Hook these annotations with StandardMethodTargetRegistrar and Shell classes - Fixes #597
This commit is contained in:
@@ -252,7 +252,12 @@ public class Shell {
|
||||
return ute.getCause();
|
||||
}
|
||||
catch (CommandExecutionException e1) {
|
||||
return e1.getCause();
|
||||
if (e1.getCause() instanceof Exception e11) {
|
||||
e = e11;
|
||||
}
|
||||
else {
|
||||
return e1.getCause();
|
||||
}
|
||||
}
|
||||
catch (Exception e2) {
|
||||
e = e2;
|
||||
@@ -265,9 +270,13 @@ public class Shell {
|
||||
CommandHandlingResult processException = processException(commandExceptionResolvers, e);
|
||||
processExceptionNonInt = processException;
|
||||
if (processException != null) {
|
||||
handlingResultNonInt = e;
|
||||
this.terminal.writer().append(processException.message());
|
||||
this.terminal.writer().flush();
|
||||
if (processException.isPresent()) {
|
||||
handlingResultNonInt = e;
|
||||
if (StringUtils.hasText(processException.message())) {
|
||||
this.terminal.writer().append(processException.message());
|
||||
this.terminal.writer().flush();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e1) {
|
||||
@@ -298,12 +307,7 @@ public class Shell {
|
||||
}
|
||||
}
|
||||
if (r != null) {
|
||||
if (r.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.springframework.lang.Nullable;
|
||||
public interface CommandHandlingResult {
|
||||
|
||||
/**
|
||||
* Gets a message for this {@code HandlingResult}.
|
||||
* Gets a message for this {@code CommandHandlingResult}.
|
||||
*
|
||||
* @return a message
|
||||
*/
|
||||
@@ -33,7 +33,7 @@ public interface CommandHandlingResult {
|
||||
String message();
|
||||
|
||||
/**
|
||||
* Gets an exit code for this {@code HandlingResult}. Exit code only has meaning
|
||||
* Gets an exit code for this {@code CommandHandlingResult}. Exit code only has meaning
|
||||
* if shell is in non-interactive mode.
|
||||
*
|
||||
* @return an exit code
|
||||
@@ -41,44 +41,44 @@ public interface CommandHandlingResult {
|
||||
Integer exitCode();
|
||||
|
||||
/**
|
||||
* Indicate whether this {@code HandlingResult} has a result.
|
||||
* Indicate whether this {@code CommandHandlingResult} has a result.
|
||||
*
|
||||
* @return true if result exist
|
||||
*/
|
||||
public boolean isPresent();
|
||||
|
||||
/**
|
||||
* Indicate whether this {@code HandlingResult} does not have a result.
|
||||
* Indicate whether this {@code CommandHandlingResult} does not have a result.
|
||||
*
|
||||
* @return true if result doesn't exist
|
||||
*/
|
||||
public boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Gets an empty instance of {@code HandlingResult}.
|
||||
* Gets an empty instance of {@code CommandHandlingResult}.
|
||||
*
|
||||
* @return empty instance of {@code HandlingResult}
|
||||
* @return empty instance of {@code CommandHandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult empty() {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of {@code HandlingResult}.
|
||||
* Gets an instance of {@code CommandHandlingResult}.
|
||||
*
|
||||
* @param message the message
|
||||
* @return instance of {@code HandlingResult}
|
||||
* @return instance of {@code CommandHandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult of(@Nullable String message) {
|
||||
return of(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of {@code HandlingResult}.
|
||||
* Gets an instance of {@code CommandHandlingResult}.
|
||||
*
|
||||
* @param message the message
|
||||
* @param exitCode the exit code
|
||||
* @return instance of {@code HandlingResult}
|
||||
* @return instance of {@code CommandHandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult of(@Nullable String message, Integer exitCode) {
|
||||
return new DefaultHandlingResult(message, exitCode);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
|
||||
/**
|
||||
* Annotation for handling exceptions in specific command classes and/or its methods.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
@Reflective
|
||||
public @interface ExceptionResolver {
|
||||
|
||||
/**
|
||||
* Exceptions handled by the annotated method. If empty, will default to any
|
||||
* exceptions listed in the method argument list.
|
||||
*
|
||||
* @return Exceptions handled by annotated method
|
||||
*/
|
||||
Class<? extends Throwable>[] value() default {};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ExceptionDepthComparator;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ExceptionResolverMethodResolver {
|
||||
|
||||
private static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
|
||||
AnnotatedElementUtils.hasAnnotation(method, ExceptionResolver.class);
|
||||
private static final Method NO_MATCHING_EXCEPTION_HANDLER_METHOD;
|
||||
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
|
||||
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
|
||||
|
||||
static {
|
||||
try {
|
||||
NO_MATCHING_EXCEPTION_HANDLER_METHOD =
|
||||
ExceptionResolverMethodResolver.class.getDeclaredMethod("noMatchingExceptionHandler");
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new IllegalStateException("Expected method not found: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor that finds {@link ExceptionResolver} methods in the given type.
|
||||
*
|
||||
* @param handlerType the type to introspect
|
||||
*/
|
||||
public ExceptionResolverMethodResolver(Class<?> handlerType) {
|
||||
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
|
||||
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
|
||||
addExceptionMapping(exceptionType, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract exception mappings from the {@code @ExceptionResolver} annotation first,
|
||||
* and then as a fallback from the method signature itself.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
|
||||
List<Class<? extends Throwable>> result = new ArrayList<>();
|
||||
detectAnnotationExceptionMappings(method, result);
|
||||
if (result.isEmpty()) {
|
||||
for (Class<?> paramType : method.getParameterTypes()) {
|
||||
if (Throwable.class.isAssignableFrom(paramType)) {
|
||||
result.add((Class<? extends Throwable>) paramType);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
throw new IllegalStateException("No exception types mapped to " + method);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
|
||||
ExceptionResolver ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionResolver.class);
|
||||
Assert.state(ann != null, "No ExceptionResolver annotation");
|
||||
result.addAll(Arrays.asList(ann.value()));
|
||||
}
|
||||
|
||||
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
|
||||
Method oldMethod = this.mappedMethods.put(exceptionType, method);
|
||||
if (oldMethod != null && !oldMethod.equals(method)) {
|
||||
throw new IllegalStateException("Ambiguous @ExceptionResolver method mapped for [" +
|
||||
exceptionType + "]: {" + oldMethod + ", " + method + "}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the contained type has any exception mappings.
|
||||
*/
|
||||
public boolean hasExceptionMappings() {
|
||||
return !this.mappedMethods.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given exception.
|
||||
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
|
||||
* @param exception the exception
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethod(Exception exception) {
|
||||
return resolveMethodByThrowable(exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given Throwable.
|
||||
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
|
||||
*
|
||||
* @param exception the exception
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethodByThrowable(Throwable exception) {
|
||||
Method method = resolveMethodByExceptionType(exception.getClass());
|
||||
if (method == null) {
|
||||
Throwable cause = exception.getCause();
|
||||
if (cause != null) {
|
||||
method = resolveMethodByThrowable(cause);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given exception type. This can be
|
||||
* useful if an {@link Exception} instance is not available (e.g. for tools).
|
||||
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
|
||||
*
|
||||
* @param exceptionType the exception type
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
|
||||
Method method = this.exceptionLookupCache.get(exceptionType);
|
||||
if (method == null) {
|
||||
method = getMappedMethod(exceptionType);
|
||||
this.exceptionLookupCache.put(exceptionType, method);
|
||||
}
|
||||
return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link Method} mapped to the given exception type, or
|
||||
* {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none.
|
||||
*/
|
||||
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
|
||||
List<Class<? extends Throwable>> matches = new ArrayList<>();
|
||||
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
|
||||
if (mappedException.isAssignableFrom(exceptionType)) {
|
||||
matches.add(mappedException);
|
||||
}
|
||||
}
|
||||
if (!matches.isEmpty()) {
|
||||
if (matches.size() > 1) {
|
||||
matches.sort(new ExceptionDepthComparator(exceptionType));
|
||||
}
|
||||
return this.mappedMethods.get(matches.get(0));
|
||||
}
|
||||
else {
|
||||
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For the {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} constant.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private void noMatchingExceptionHandler() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Defines exit code. Typically used with {@link ExceptionResolver}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
@Reflective
|
||||
public @interface ExitCode {
|
||||
|
||||
/**
|
||||
* Exit code value.
|
||||
*
|
||||
* @return exit code
|
||||
* @see #code()
|
||||
*/
|
||||
@AliasFor("code")
|
||||
int value() default 0;
|
||||
|
||||
/**
|
||||
* Exit code value.
|
||||
*
|
||||
* @return exit code
|
||||
* @see #value()
|
||||
*/
|
||||
@AliasFor("value")
|
||||
int code() default 0;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.command.CommandExceptionResolver;
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
import org.springframework.shell.command.invocation.InvocableShellMethod;
|
||||
import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class MethodCommandExceptionResolver implements CommandExceptionResolver {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(MethodCommandExceptionResolver.class);
|
||||
private final Object bean;
|
||||
private final Terminal terminal;
|
||||
|
||||
public MethodCommandExceptionResolver(Object bean) {
|
||||
this(bean, null);
|
||||
}
|
||||
|
||||
public MethodCommandExceptionResolver(Object bean, Terminal terminal) {
|
||||
Assert.notNull(bean, "Target bean must be set");
|
||||
this.bean = bean;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception ex) {
|
||||
try {
|
||||
ExceptionResolverMethodResolver resolver = new ExceptionResolverMethodResolver(bean.getClass());
|
||||
Method exceptionResolverMethod = resolver.resolveMethodByThrowable(ex);
|
||||
if (exceptionResolverMethod == null) {
|
||||
return null;
|
||||
}
|
||||
InvocableShellMethod invocable = new InvocableShellMethod(bean, exceptionResolverMethod);
|
||||
|
||||
ShellMethodArgumentResolverComposite argumentResolvers = new ShellMethodArgumentResolverComposite();
|
||||
argumentResolvers.addResolver(new TerminalResolver());
|
||||
invocable.setMessageMethodArgumentResolvers(argumentResolvers);
|
||||
|
||||
ArrayList<Throwable> exceptions = new ArrayList<>();
|
||||
Throwable exToExpose = ex;
|
||||
while (exToExpose != null) {
|
||||
exceptions.add(exToExpose);
|
||||
Throwable cause = exToExpose.getCause();
|
||||
exToExpose = (cause != exToExpose ? cause : null);
|
||||
}
|
||||
Object[] arguments = new Object[exceptions.size() + 1];
|
||||
exceptions.toArray(arguments);
|
||||
|
||||
MessageBuilder<Object[]> messageBuilder = MessageBuilder.withPayload(arguments);
|
||||
messageBuilder.setHeader("terminal", terminal);
|
||||
|
||||
MethodParameter returnType = invocable.getReturnType();
|
||||
Class<?> parameterType = returnType.getParameterType();
|
||||
boolean isVoid = void.class.isAssignableFrom(parameterType);
|
||||
Object invoke = invocable.invoke(messageBuilder.build(), arguments);
|
||||
|
||||
Integer ecFromAnn = null;
|
||||
ExitCode ecAnn = AnnotationUtils.findAnnotation(exceptionResolverMethod, ExitCode.class);
|
||||
if (ecAnn != null && ecAnn.code() > 0) {
|
||||
ecFromAnn = ecAnn.code();
|
||||
}
|
||||
|
||||
if (isVoid) {
|
||||
if (ecFromAnn != null) {
|
||||
return CommandHandlingResult.of(null, ecFromAnn);
|
||||
}
|
||||
return CommandHandlingResult.empty();
|
||||
}
|
||||
else if (invoke instanceof CommandHandlingResult result) {
|
||||
if (ecFromAnn != null) {
|
||||
return CommandHandlingResult.of(result.message(), ecFromAnn);
|
||||
}
|
||||
return (CommandHandlingResult)invoke;
|
||||
}
|
||||
else if (invoke instanceof String msg) {
|
||||
if (ecFromAnn != null) {
|
||||
return CommandHandlingResult.of(msg, ecFromAnn);
|
||||
}
|
||||
return CommandHandlingResult.of(msg, 1);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
// TODO: should think how to report this to user without logging
|
||||
log.warn("Failure in @ExceptionResolver", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class TerminalResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return Terminal.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
Terminal terminal = message.getHeaders().get("terminal", Terminal.class);
|
||||
return terminal;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ExceptionResolverMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void resolvesFromAnnotation() {
|
||||
ExceptionResolverMethodResolver resolver = new ExceptionResolverMethodResolver(InAnnotation.class);
|
||||
assertThat(resolver.hasExceptionMappings()).isTrue();
|
||||
assertThat(resolver.resolveMethod(new RuntimeException()))
|
||||
.isSameAs(ReflectionUtils.findMethod(InAnnotation.class, "errorHandler"));
|
||||
assertThat(resolver.resolveMethod(new IOException()))
|
||||
.isSameAs(ReflectionUtils.findMethod(InAnnotation.class, "errorHandler"));
|
||||
}
|
||||
|
||||
private static class InAnnotation {
|
||||
|
||||
@ExceptionResolver({ RuntimeException.class, IOException.class })
|
||||
void errorHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesFromMethodParameters() {
|
||||
ExceptionResolverMethodResolver resolver = new ExceptionResolverMethodResolver(InMethodParameter.class);
|
||||
assertThat(resolver.hasExceptionMappings()).isTrue();
|
||||
assertThat(resolver.resolveMethod(new RuntimeException())).isSameAs(ReflectionUtils
|
||||
.findMethod(InMethodParameter.class, "errorHandler", RuntimeException.class, IOException.class));
|
||||
assertThat(resolver.resolveMethod(new IOException())).isSameAs(ReflectionUtils
|
||||
.findMethod(InMethodParameter.class, "errorHandler", RuntimeException.class, IOException.class));
|
||||
}
|
||||
|
||||
private static class InMethodParameter {
|
||||
|
||||
@ExceptionResolver
|
||||
void errorHandler(RuntimeException e1, IOException e2) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MethodCommandExceptionResolverTests {
|
||||
|
||||
@Test
|
||||
void annoHaveMatchingParameter() {
|
||||
AnnoHaveMatchingParameter bean = new AnnoHaveMatchingParameter();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
CustomException1 e = new CustomException1();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(e).isSameAs(bean.called);
|
||||
}
|
||||
|
||||
private static class AnnoHaveMatchingParameter {
|
||||
|
||||
Exception called;
|
||||
|
||||
@ExceptionResolver({ CustomException1.class })
|
||||
CommandHandlingResult errorHandler(Exception e) {
|
||||
called = e;
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void annoHaveNotMatchingParameter() {
|
||||
AnnoHaveNotMatchingParameter bean = new AnnoHaveNotMatchingParameter();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
CustomException1 e = new CustomException1();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(bean.called).isTrue();
|
||||
}
|
||||
|
||||
private static class AnnoHaveNotMatchingParameter {
|
||||
|
||||
boolean called;
|
||||
|
||||
@ExceptionResolver({ CustomException1.class })
|
||||
CommandHandlingResult errorHandler() {
|
||||
called = true;
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvedFromParameter() {
|
||||
ResolvedFromParameter bean = new ResolvedFromParameter();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
CustomException1 e = new CustomException1();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(e).isSameAs(bean.called);
|
||||
}
|
||||
|
||||
private static class ResolvedFromParameter {
|
||||
|
||||
Exception called;
|
||||
|
||||
@ExceptionResolver
|
||||
CommandHandlingResult errorHandler(CustomException1 e) {
|
||||
called = e;
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMappedExceptions() {
|
||||
NoMappedExceptions bean = new NoMappedExceptions();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
assertThat(resolver).isNotNull();
|
||||
}
|
||||
|
||||
private static class NoMappedExceptions {
|
||||
|
||||
@ExceptionResolver
|
||||
CommandHandlingResult errorHandler() {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldErrorWhenResolving() {
|
||||
ShouldErrorWhenResolving bean = new ShouldErrorWhenResolving();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
RuntimeException e = new RuntimeException();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
// Internal error doesn't get through - IOException cannot be resolved
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
private static class ShouldErrorWhenResolving {
|
||||
|
||||
@ExceptionResolver
|
||||
CommandHandlingResult errorHandler(RuntimeException e1, IOException e2) {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandHandlingResult RESULT = CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
|
||||
private static class CustomException1 extends RuntimeException {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.invocation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InvocableShellMethodTests {
|
||||
|
||||
@Test
|
||||
public void resolveArg() throws Exception {
|
||||
Handler bean = new Handler();
|
||||
Method method = ReflectionUtils.findMethod(Handler.class, "handle", Integer.class, String.class);
|
||||
InvocableShellMethod invocable = new InvocableShellMethod(bean, method);
|
||||
Object value = invocable.invoke(null, 99, "value");
|
||||
assertThat(value).isEqualTo("99-value");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class Handler {
|
||||
|
||||
public String handle(Integer intArg, String stringArg) {
|
||||
return intArg + "-" + stringArg;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user