Skip to content

Commit 76557c7

Browse files
committed
Add support for exception mapping
1 parent 23d42b4 commit 76557c7

File tree

8 files changed

+139
-4
lines changed

8 files changed

+139
-4
lines changed

spring-shell-core/src/main/java/org/springframework/shell/core/command/AbstractCommand.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
import java.util.List;
2121
import java.util.Objects;
2222

23+
import org.jspecify.annotations.Nullable;
24+
2325
import org.springframework.shell.core.command.availability.Availability;
2426
import org.springframework.shell.core.command.availability.AvailabilityProvider;
27+
import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper;
2528

2629
/**
2730
* Base class helping to build shell commands.
@@ -44,6 +47,8 @@ public abstract class AbstractCommand implements Command {
4447

4548
private AvailabilityProvider availabilityProvider = AvailabilityProvider.alwaysAvailable();
4649

50+
@Nullable private ExitStatusExceptionMapper exitStatusExceptionMapper;
51+
4752
private List<String> aliases = new ArrayList<>();
4853

4954
public AbstractCommand(String name, String description) {
@@ -105,6 +110,14 @@ public void setAvailabilityProvider(AvailabilityProvider availabilityProvider) {
105110
this.availabilityProvider = availabilityProvider;
106111
}
107112

113+
@Nullable public ExitStatusExceptionMapper getExitStatusExceptionMapper() {
114+
return exitStatusExceptionMapper;
115+
}
116+
117+
public void setExitStatusExceptionMapper(ExitStatusExceptionMapper exitStatusExceptionMapper) {
118+
this.exitStatusExceptionMapper = exitStatusExceptionMapper;
119+
}
120+
108121
@Override
109122
public ExitStatus execute(CommandContext commandContext) throws Exception {
110123
Availability availability = getAvailabilityProvider().get();
@@ -118,7 +131,17 @@ public ExitStatus execute(CommandContext commandContext) throws Exception {
118131
println(getHelp(), commandContext);
119132
return ExitStatus.OK;
120133
}
121-
return doExecute(commandContext);
134+
try {
135+
return doExecute(commandContext);
136+
}
137+
catch (Exception e) {
138+
if (getExitStatusExceptionMapper() != null) {
139+
return getExitStatusExceptionMapper().apply(e);
140+
}
141+
else {
142+
throw e;
143+
}
144+
}
122145
}
123146

124147
protected void println(String message, CommandContext commandContext) {

spring-shell-core/src/main/java/org/springframework/shell/core/command/Command.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
import java.util.function.Consumer;
2323
import java.util.function.Function;
2424

25+
import org.jspecify.annotations.Nullable;
26+
2527
import org.springframework.shell.core.command.adapter.ConsumerCommandAdapter;
2628
import org.springframework.shell.core.command.adapter.FunctionCommandAdapter;
2729
import org.springframework.shell.core.command.availability.AvailabilityProvider;
30+
import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper;
2831
import org.springframework.util.Assert;
2932

3033
/**
@@ -127,6 +130,8 @@ final class Builder {
127130

128131
private AvailabilityProvider availabilityProvider = AvailabilityProvider.alwaysAvailable();
129132

133+
@Nullable ExitStatusExceptionMapper exitStatusExceptionMapper;
134+
130135
private List<String> aliases = new ArrayList<>();
131136

132137
public Builder name(String name) {
@@ -159,6 +164,11 @@ public Builder availabilityProvider(AvailabilityProvider availabilityProvider) {
159164
return this;
160165
}
161166

167+
public Builder exitStatusExceptionMapper(ExitStatusExceptionMapper exitStatusExceptionMapper) {
168+
this.exitStatusExceptionMapper = exitStatusExceptionMapper;
169+
return this;
170+
}
171+
162172
public Builder aliases(String... aliases) {
163173
this.aliases = Arrays.asList(aliases);
164174
return this;
@@ -171,6 +181,9 @@ public AbstractCommand execute(Consumer<CommandContext> commandExecutor) {
171181
commandExecutor);
172182
command.setAliases(aliases);
173183
command.setAvailabilityProvider(availabilityProvider);
184+
if (exitStatusExceptionMapper != null) {
185+
command.setExitStatusExceptionMapper(exitStatusExceptionMapper);
186+
}
174187

175188
return command;
176189
}
@@ -182,6 +195,9 @@ public AbstractCommand execute(Function<CommandContext, String> commandExecutor)
182195
commandExecutor);
183196
command.setAliases(aliases);
184197
command.setAvailabilityProvider(availabilityProvider);
198+
if (exitStatusExceptionMapper != null) {
199+
command.setExitStatusExceptionMapper(exitStatusExceptionMapper);
200+
}
185201

186202
return command;
187203
}

spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/Command.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,10 @@
9393
*/
9494
String availabilityProvider() default "";
9595

96+
/**
97+
* Define exit status exception mapper bean name.
98+
* @return the exit status exception mapper bean name
99+
*/
100+
String exitStatusExceptionMapper() default "";
101+
96102
}

spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/support/CommandFactoryBean.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.shell.core.command.CommandCreationException;
3434
import org.springframework.shell.core.command.adapter.MethodInvokerCommandAdapter;
3535
import org.springframework.shell.core.command.availability.AvailabilityProvider;
36+
import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper;
3637
import org.springframework.util.Assert;
3738

3839
/**
@@ -68,6 +69,7 @@ public Command getObject() {
6869
boolean hidden = command.hidden();
6970
String[] aliases = command.alias();
7071
String availabilityProvider = command.availabilityProvider();
72+
String exitStatusExceptionMapper = command.exitStatusExceptionMapper();
7173
log.debug("Creating command bean for method '" + this.method + "' with name '" + name + "'");
7274
Class<?> declaringClass = this.method.getDeclaringClass();
7375
Object targetObject;
@@ -101,10 +103,24 @@ Ensure that the declaring class is annotated with a Spring stereotype annotation
101103
+ "', using always available provider.");
102104
}
103105
}
106+
ExitStatusExceptionMapper exitStatusExceptionMapperBean = null;
107+
if (!exitStatusExceptionMapper.isEmpty()) {
108+
try {
109+
exitStatusExceptionMapperBean = this.applicationContext.getBean(exitStatusExceptionMapper,
110+
ExitStatusExceptionMapper.class);
111+
}
112+
catch (BeansException e) {
113+
log.debug("No ExitStatusExceptionMapper bean found with name '" + exitStatusExceptionMapper
114+
+ "', using default exception mapping strategy.");
115+
}
116+
}
104117
MethodInvokerCommandAdapter methodInvokerCommandAdapter = new MethodInvokerCommandAdapter(name, description,
105118
group, help, hidden, this.method, targetObject, configurableConversionService);
106119
methodInvokerCommandAdapter.setAliases(Arrays.stream(aliases).toList());
107120
methodInvokerCommandAdapter.setAvailabilityProvider(availabilityProviderBean);
121+
if (exitStatusExceptionMapperBean != null) {
122+
methodInvokerCommandAdapter.setExitStatusExceptionMapper(exitStatusExceptionMapperBean);
123+
}
108124
return methodInvokerCommandAdapter;
109125
}
110126

spring-shell-core/src/main/java/org/springframework/shell/core/exit/ExitStatusExceptionMapper.java renamed to spring-shell-core/src/main/java/org/springframework/shell/core/command/exit/ExitStatusExceptionMapper.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-present the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.shell.core.exit;
16+
package org.springframework.shell.core.command.exit;
1717

1818
import java.util.function.Function;
1919

@@ -24,6 +24,7 @@
2424
* implementing boot's {@code ExitCodeGenerator}.
2525
*
2626
* @author Janne Valkealahti
27+
* @author Mahmoud Ben Hassine
2728
*/
2829
public interface ExitStatusExceptionMapper extends Function<Exception, ExitStatus> {
2930

spring-shell-core/src/main/java/org/springframework/shell/core/exit/package-info.java renamed to spring-shell-core/src/main/java/org/springframework/shell/core/command/exit/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
@NullMarked
2-
package org.springframework.shell.core.exit;
2+
package org.springframework.shell.core.command.exit;
33

44
import org.jspecify.annotations.NullMarked;

spring-shell-docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
** xref:commands/context.adoc[]
1313
** xref:commands/registry.adoc[]
1414
** xref:commands/availability.adoc[]
15+
** xref:commands/exception-handling.adoc[]
1516
** xref:commands/help.adoc[]
1617
** xref:commands/builtin/index.adoc[]
1718
*** xref:commands/builtin/help.adoc[]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
[[exception-handling]]
2+
= Exception Handling
3+
4+
When writing commands, it is important to handle exceptions properly to ensure that your application behaves predictably in the face of errors.
5+
Spring Shell allows you to manage exceptions effectively and map them to specific exit codes and user-friendly messages thanks to the `ExitStatusExceptionMapper` interface.
6+
7+
The `ExitStatusExceptionMapper` interface provides a way to map exceptions thrown during command execution to specific exit codes and messages.
8+
By implementing this interface, you can define custom behavior for different types of exceptions, allowing you to provide meaningful feedback to users and control the exit status of your application.
9+
10+
== Implementing ExitStatusExceptionMapper
11+
12+
The `ExitStatusExceptionMapper` is a functional interface that extends `java.util.Function`, and which takes an `Exception` as input and returns an `ExitStatus`.
13+
You can implement this method to handle specific exceptions and return appropriate exit codes and messages. For example:
14+
15+
[source, java, indent=0]
16+
----
17+
@Bean
18+
public ExitStatusExceptionMapper myCustomExceptionMapper() {
19+
return exception -> new ExitStatus(42, "42! The answer to life, the universe and everything!");
20+
}
21+
----
22+
23+
== Registering the Mapper
24+
25+
=== Programmatic Registration
26+
27+
When registering commands programmatically, you can set the `ExitStatusExceptionMapper` using the `exitStatusExceptionMapper` method on the `Command.Builder`. For example:
28+
29+
[source, java, indent=0]
30+
----
31+
@Bean
32+
public Command sayHello() {
33+
return Command.builder()
34+
.name("hello")
35+
.description("Say hello to a given name")
36+
.group("Greetings")
37+
.help("A command that greets the user with 'Hello ${name}!'. Usage: hello [-n | --name]=<name>")
38+
.exitStatusExceptionMapper(exception -> new ExitStatus(42, "42! The answer to life, the universe and everything!"))
39+
.execute(context -> {
40+
String name = context.getOptionByName("name").value();
41+
if (name.equals("42")) {
42+
throw new RuntimeException("Error!");
43+
}
44+
System.out.println("Hello " + name + "!");
45+
});
46+
}
47+
----
48+
49+
=== Annotation-Based Registration
50+
51+
When using annotation-based command registration, you can specify the custom `ExitStatusExceptionMapper` by using the `exitStatusExceptionMapper` attribute of the `@Command` annotation. For example:
52+
53+
[source, java, indent=0]
54+
----
55+
@Command(name = "hello", description = "Say hello to a given name", group = "Greetings",
56+
help = "A command that greets the user with 'Hello ${name}!'. Usage: hello [-n | --name]=<name>",
57+
exitStatusExceptionMapper = "myCustomExceptionMapper")
58+
public void sayHello(@Option(shortName = 'n', longName = "name", description = "the name of the person to greet",
59+
defaultValue = "World") String name) {
60+
if (name.equals("42")) {
61+
throw new RuntimeException("Error!");
62+
}
63+
System.out.println("Hello " + name + "!");
64+
}
65+
66+
@Bean
67+
public ExitStatusExceptionMapper myCustomExceptionMapper() {
68+
return exception -> new ExitStatus(42, "42! The answer to life, the universe and everything!");
69+
}
70+
----
71+
72+
The custom mapper should be defined as a Spring bean so that it can be referenced by name in the `@Command` annotation.

0 commit comments

Comments
 (0)