Add dynamic command availability

Introduce availability concept on MethodTarget (with reason if not available)
Add bridge to @CliAvailabilityIndicator to Legacy registrar

Fixes #138

Add help for unavailable commands

Add standard API for availability
This commit is contained in:
Eric Bottard
2017-08-22 18:22:51 +02:00
parent 6c231a072c
commit 1eea04ad2f
16 changed files with 658 additions and 45 deletions

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2017 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
*
* http://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;
import org.springframework.util.Assert;
/**
* Indicates whether or not a command is currently available. When not available, provides
* a reason.
*
* @author Eric Bottard
*/
public class Availability {
private final String reason;
private Availability(String reason) {
this.reason = reason;
}
public static Availability available() {
return new Availability(null);
}
public static Availability unavailable(String reason) {
Assert.notNull(reason, "Reason for not being available must be provided");
return new Availability(reason);
}
public boolean isAvailable() {
return reason == null;
}
public String getReason() {
return reason;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2017 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
*
* http://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;
/**
* Indicates that a command exists but is currently not invokable.
*
* @author Eric Bottard
*/
public class CommandNotCurrentlyAvailable extends RuntimeException {
private final String command;
private final Availability availability;
public CommandNotCurrentlyAvailable(String command, Availability availability) {
super(String.format("Command '%s' exists but is not currently available because %s", command, availability.getReason()));
this.command = command;
this.availability = availability;
}
public String getCommand() {
return command;
}
public Availability getAvailability() {
return availability;
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.shell;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
@@ -36,7 +37,16 @@ public class MethodTarget {
private final String help;
/**
* If not null, returns whether or not the command is currently available. Implementations must be idempotent.
*/
private final Supplier<Availability> availabilityIndicator;
public MethodTarget(Method method, Object bean, String help) {
this(method, bean, help, null);
}
public MethodTarget(Method method, Object bean, String help, Supplier<Availability> availabilityIndicator) {
Assert.notNull(method, "Method cannot be null");
Assert.notNull(bean, "Bean cannot be null");
Assert.hasText(help, String.format("Help cannot be blank when trying to define command based on '%s'", method));
@@ -44,6 +54,7 @@ public class MethodTarget {
this.method = method;
this.bean = bean;
this.help = help;
this.availabilityIndicator = availabilityIndicator != null ? availabilityIndicator : () -> Availability.available();
}
/**
@@ -51,13 +62,21 @@ public class MethodTarget {
* in case of overloaded method.
*/
public static MethodTarget of(String name, Object bean, String help) {
return of(name, bean, help, null);
}
/**
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
* in case of overloaded method.
*/
public static MethodTarget of(String name, Object bean, String help, Supplier<Availability> availabilityIndicator) {
Set<Method> found = new HashSet<>();
ReflectionUtils.doWithMethods(bean.getClass(), found::add, m -> m.getName().equals(name));
if (found.size() != 1) {
throw new IllegalArgumentException(String.format("Could not find unique method named '%s' on object of class %s. Found %s",
name, bean.getClass(), found));
}
return new MethodTarget(found.iterator().next(), bean, help);
return new MethodTarget(found.iterator().next(), bean, help, availabilityIndicator);
}
public Method getMethod() {
@@ -72,6 +91,10 @@ public class MethodTarget {
return help;
}
public Availability getAvailability() {
return availabilityIndicator.get();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -131,16 +131,21 @@ public class Shell implements CommandRegistry {
Object result;
if (command != null) {
MethodTarget methodTarget = methodTargets.get(command);
List<String> wordsForArgs = wordsForArguments(command, words);
Method method = methodTarget.getMethod();
Availability availability = methodTarget.getAvailability();
if (availability.isAvailable()) {
List<String> wordsForArgs = wordsForArguments(command, words);
Method method = methodTarget.getMethod();
try {
Object[] args = resolveArgs(method, wordsForArgs);
validateArgs(args, methodTarget);
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
}
catch (Exception e) {
result = e;
try {
Object[] args = resolveArgs(method, wordsForArgs);
validateArgs(args, methodTarget);
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
}
catch (Exception e) {
result = e;
}
} else {
result = new CommandNotCurrentlyAvailable(command, availability);
}
}
else {