Skip to content

Commit 0213287

Browse files
committed
Add support for command availability
- Simplify the way to configure the availability provider by using a new attribute in the Command annotation instead of a separate annotation CommandAvailability - Make Availability a record - Add utility methods to create common availability providers - Add new exit status for unavailable commands - Remove unused CommandNotCurrentlyAvailableException
1 parent 555d83a commit 0213287

File tree

13 files changed

+278
-102
lines changed

13 files changed

+278
-102
lines changed

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import java.util.List;
2121
import java.util.Objects;
2222

23+
import org.springframework.shell.core.command.availability.Availability;
24+
import org.springframework.shell.core.command.availability.AvailabilityProvider;
25+
2326
/**
2427
* Base class helping to build shell commands.
2528
*
@@ -39,6 +42,8 @@ public abstract class AbstractCommand implements Command {
3942

4043
private final boolean hidden;
4144

45+
private AvailabilityProvider availabilityProvider = AvailabilityProvider.alwaysAvailable();
46+
4247
private List<String> aliases = new ArrayList<>();
4348

4449
public AbstractCommand(String name, String description) {
@@ -91,8 +96,23 @@ public void setAliases(List<String> aliases) {
9196
this.aliases = aliases;
9297
}
9398

99+
@Override
100+
public AvailabilityProvider getAvailabilityProvider() {
101+
return availabilityProvider;
102+
}
103+
104+
public void setAvailabilityProvider(AvailabilityProvider availabilityProvider) {
105+
this.availabilityProvider = availabilityProvider;
106+
}
107+
94108
@Override
95109
public ExitStatus execute(CommandContext commandContext) throws Exception {
110+
Availability availability = getAvailabilityProvider().get();
111+
if (!availability.isAvailable()) {
112+
println("Command '" + getName() + "' exists but is not currently available because "
113+
+ availability.reason(), commandContext);
114+
return ExitStatus.AVAILABILITY_ERROR;
115+
}
96116
List<CommandOption> options = commandContext.parsedInput().options();
97117
if (options.size() == 1 && isHelp(options.get(0))) {
98118
println(getHelp(), commandContext);

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.shell.core.command.adapter.ConsumerCommandAdapter;
2626
import org.springframework.shell.core.command.adapter.FunctionCommandAdapter;
27+
import org.springframework.shell.core.command.availability.AvailabilityProvider;
2728
import org.springframework.util.Assert;
2829

2930
/**
@@ -82,6 +83,14 @@ default List<String> getAliases() {
8283
return Collections.emptyList();
8384
}
8485

86+
/**
87+
* Get the availability provider of the command. Defaults to always available.
88+
* @return the availability provider of the command
89+
*/
90+
default AvailabilityProvider getAvailabilityProvider() {
91+
return AvailabilityProvider.alwaysAvailable();
92+
}
93+
8594
/**
8695
* Execute the command within the given context.
8796
* @param commandContext the context of the command
@@ -116,6 +125,8 @@ final class Builder {
116125

117126
private boolean hidden = false;
118127

128+
private AvailabilityProvider availabilityProvider = AvailabilityProvider.alwaysAvailable();
129+
119130
private List<String> aliases = new ArrayList<>();
120131

121132
public Builder name(String name) {
@@ -143,6 +154,11 @@ public Builder hidden(boolean hidden) {
143154
return this;
144155
}
145156

157+
public Builder availabilityProvider(AvailabilityProvider availabilityProvider) {
158+
this.availabilityProvider = availabilityProvider;
159+
return this;
160+
}
161+
146162
public Builder aliases(String... aliases) {
147163
this.aliases = Arrays.asList(aliases);
148164
return this;
@@ -154,6 +170,7 @@ public AbstractCommand execute(Consumer<CommandContext> commandExecutor) {
154170
ConsumerCommandAdapter command = new ConsumerCommandAdapter(name, description, group, help, hidden,
155171
commandExecutor);
156172
command.setAliases(aliases);
173+
command.setAvailabilityProvider(availabilityProvider);
157174

158175
return command;
159176
}
@@ -164,6 +181,7 @@ public AbstractCommand execute(Function<CommandContext, String> commandExecutor)
164181
FunctionCommandAdapter command = new FunctionCommandAdapter(name, description, group, help, hidden,
165182
commandExecutor);
166183
command.setAliases(aliases);
184+
command.setAvailabilityProvider(availabilityProvider);
167185

168186
return command;
169187
}

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

Lines changed: 0 additions & 46 deletions
This file was deleted.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ public record ExitStatus(int code, String description) {
3030

3131
public static ExitStatus USAGE_ERROR = new ExitStatus(-2, "USAGE_ERROR");
3232

33+
public static ExitStatus AVAILABILITY_ERROR = new ExitStatus(-3, "AVAILABILITY_ERROR");
34+
3335
}

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
@@ -87,4 +87,10 @@
8787
*/
8888
String help() default "";
8989

90+
/**
91+
* Define availability provider bean name.
92+
* @return the availability provider bean name
93+
*/
94+
String availabilityProvider() default "";
95+
9096
}

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

Lines changed: 0 additions & 42 deletions
This file was deleted.

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.shell.core.command.Command;
3333
import org.springframework.shell.core.command.CommandCreationException;
3434
import org.springframework.shell.core.command.adapter.MethodInvokerCommandAdapter;
35+
import org.springframework.shell.core.command.availability.AvailabilityProvider;
3536
import org.springframework.util.Assert;
3637

3738
/**
@@ -66,6 +67,7 @@ public Command getObject() {
6667
String group = command.group();
6768
boolean hidden = command.hidden();
6869
String[] aliases = command.alias();
70+
String availabilityProvider = command.availabilityProvider();
6971
log.debug("Creating command bean for method '" + this.method + "' with name '" + name + "'");
7072
Class<?> declaringClass = this.method.getDeclaringClass();
7173
Object targetObject;
@@ -88,9 +90,21 @@ Ensure that the declaring class is annotated with a Spring stereotype annotation
8890
catch (BeansException e) {
8991
log.debug("No ConfigurableConversionService bean found, using a default conversion service.");
9092
}
93+
AvailabilityProvider availabilityProviderBean = AvailabilityProvider.alwaysAvailable();
94+
if (!availabilityProvider.isEmpty()) {
95+
try {
96+
availabilityProviderBean = this.applicationContext.getBean(availabilityProvider,
97+
AvailabilityProvider.class);
98+
}
99+
catch (BeansException e) {
100+
log.debug("No AvailabilityProvider bean found with name '" + availabilityProvider
101+
+ "', using always available provider.");
102+
}
103+
}
91104
MethodInvokerCommandAdapter methodInvokerCommandAdapter = new MethodInvokerCommandAdapter(name, description,
92105
group, help, hidden, this.method, targetObject, configurableConversionService);
93106
methodInvokerCommandAdapter.setAliases(Arrays.stream(aliases).toList());
107+
methodInvokerCommandAdapter.setAvailabilityProvider(availabilityProviderBean);
94108
return methodInvokerCommandAdapter;
95109
}
96110

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 the original author or authors.
2+
* Copyright 2017-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.
@@ -20,19 +20,14 @@
2020
import org.springframework.util.Assert;
2121

2222
/**
23-
* Indicates whether or not a command is currently available. When not available, provides
23+
* Indicates whether a command is currently available or not. When not available, provides
2424
* a reason.
2525
*
2626
* @author Eric Bottard
2727
* @author Piotr Olaszewski
28+
* @author Mahmoud Ben Hassine
2829
*/
29-
public class Availability {
30-
31-
private final @Nullable String reason;
32-
33-
private Availability(@Nullable String reason) {
34-
this.reason = reason;
35-
}
30+
public record Availability(@Nullable String reason) {
3631

3732
public static Availability available() {
3833
return new Availability(null);
@@ -47,8 +42,4 @@ public boolean isAvailable() {
4742
return reason == null;
4843
}
4944

50-
public @Nullable String getReason() {
51-
return reason;
52-
}
53-
5445
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 the original author or authors.
2+
* Copyright 2023-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.
@@ -21,8 +21,17 @@
2121
* Interface resolving {@link Availability}.
2222
*
2323
* @author Janne Valkealahti
24+
* @author Mahmoud Ben Hassine
2425
*/
2526
@FunctionalInterface
2627
public interface AvailabilityProvider extends Supplier<Availability> {
2728

29+
static AvailabilityProvider alwaysAvailable() {
30+
return of(Availability.available());
31+
}
32+
33+
static AvailabilityProvider of(Availability availability) {
34+
return () -> availability;
35+
}
36+
2837
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.core.command;
17+
18+
import java.io.PrintWriter;
19+
import java.io.StringWriter;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.shell.core.command.availability.Availability;
24+
import org.springframework.shell.core.command.availability.AvailabilityProvider;
25+
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.mockito.Mockito.mock;
28+
29+
/**
30+
* Tests for command availability.
31+
*
32+
* @author Mahmoud Ben Hassine
33+
*/
34+
public class CommandAvailabilityTests {
35+
36+
@Test
37+
public void testCommandAvailability() throws Exception {
38+
// given
39+
Command command = new AbstractCommand("test", "A test command") {
40+
41+
@Override
42+
public AvailabilityProvider getAvailabilityProvider() {
43+
return AvailabilityProvider.of(new Availability("you are not allowed to run this command"));
44+
}
45+
46+
@Override
47+
public ExitStatus doExecute(CommandContext commandContext) {
48+
return ExitStatus.OK;
49+
}
50+
};
51+
StringWriter stringWriter = new StringWriter();
52+
CommandContext commandContext = new CommandContext(mock(ParsedInput.class), mock(CommandRegistry.class),
53+
new PrintWriter(stringWriter));
54+
55+
// when
56+
ExitStatus exitStatus = command.execute(commandContext);
57+
58+
// then
59+
assertEquals(ExitStatus.AVAILABILITY_ERROR, exitStatus);
60+
assertEquals(
61+
"Command 'test' exists but is not currently available because you are not allowed to run this command",
62+
stringWriter.toString().trim());
63+
}
64+
65+
}

0 commit comments

Comments
 (0)