New annotation model
- This is a first commit to add new annotation model which eventually will replace old legacy annotations like ShellComponent, ShellMethod, @ShellOption, etc. - Adds subset of features needed for parity with manual use of CommandRegistration. - Relates #637 - Relates #638 - Relates #639 - Relates #640 - Relates #641
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.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.shell.context.InteractionMode;
|
||||
|
||||
/**
|
||||
* Annotation marking a method to be a candicate for a shell command target.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Documented
|
||||
public @interface Command {
|
||||
|
||||
/**
|
||||
* Define command as an array. Given that command should be
|
||||
* {@code command1 sub1} it can be defined as:
|
||||
*
|
||||
* <pre class="code">
|
||||
* command = { "command1", "sub1" }
|
||||
* command = "command1 sub1"
|
||||
* </pre>
|
||||
*
|
||||
* Values are split and trimmed meaning spaces doesn't matter.
|
||||
*
|
||||
* <p>
|
||||
* <b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level mappings inherit this primary
|
||||
* command to use it as a prefix.
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Command(command = "command1")
|
||||
* class MyCommands {
|
||||
*
|
||||
* @Command(command = "sub1")
|
||||
* void sub1(){}
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @return the command as an array
|
||||
*/
|
||||
String[] command() default {};
|
||||
|
||||
/**
|
||||
* Define alias as an array. Given that alias should be
|
||||
* {@code alias1 sub1} it can be defined as:
|
||||
*
|
||||
* <pre class="code">
|
||||
* command = { "alias1", "sub1" }
|
||||
* command = "alias1 sub1"
|
||||
* </pre>
|
||||
*
|
||||
* Values are split and trimmed meaning spaces doesn't matter.
|
||||
*
|
||||
* <p>
|
||||
* <b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level mappings inherit this primary
|
||||
* alias to use it as a prefix.
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Command(alias = "alias1")
|
||||
* class MyCommands {
|
||||
*
|
||||
* @Command(alias = "sub1")
|
||||
* void sub1(){}
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @return the aliases as an array
|
||||
*/
|
||||
String[] alias() default {};
|
||||
|
||||
/**
|
||||
* Define a command group.
|
||||
*
|
||||
* <p>
|
||||
* <b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level group inherit this primary
|
||||
* group. Can be overridden on method-level.
|
||||
*
|
||||
* @return the command group
|
||||
*/
|
||||
String group() default "";
|
||||
|
||||
/**
|
||||
* Define a command description.
|
||||
*
|
||||
* <p>
|
||||
* <b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level descriptions inherit this primary
|
||||
* field. Can be overridden on method-level.
|
||||
*
|
||||
* @return the command description
|
||||
*/
|
||||
String description() default "";
|
||||
|
||||
/**
|
||||
* Define command to be hidden.
|
||||
*
|
||||
* <p>
|
||||
* <b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level mappings inherit this primary
|
||||
* hidden field.
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Command(hidden = true)
|
||||
* class MyCommands {
|
||||
*
|
||||
* @Command
|
||||
* void sub1(){
|
||||
* // sub1 command is hidden
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @return true if command should be hidden
|
||||
*/
|
||||
boolean hidden() default false;
|
||||
|
||||
/**
|
||||
* Define interaction mode for a command as a hint when command should be
|
||||
* available. For example presense of some commands doesn't make sense if shell
|
||||
* is running as non-interactive mode and vice versa.
|
||||
*
|
||||
* <p>
|
||||
* <b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level mappings inherit this primary
|
||||
* field.
|
||||
*
|
||||
* Type is an array to be able to indicate that default don't have anyting defined.
|
||||
*
|
||||
* @return interaction modes
|
||||
*/
|
||||
InteractionMode[] interactionMode() default {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.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.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.shell.command.annotation.support.CommandScanRegistrar;
|
||||
|
||||
/**
|
||||
* Configures the base packages used when scanning for {@link Command @Comamnd}
|
||||
* classes. One of {@link #basePackageClasses()}, {@link #basePackages()} or its
|
||||
* alias {@link #value()} may be specified to define specific packages to scan.
|
||||
* If specific packages are not defined scanning will occur from the package of
|
||||
* the class with this annotation.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import(CommandScanRegistrar.class)
|
||||
@EnableCommand
|
||||
public @interface CommandScan {
|
||||
|
||||
/**
|
||||
* Alias for the {@link #basePackages()} attribute. Allows for more concise
|
||||
* annotation declarations e.g.: {@code @CommandScan("org.my.pkg")} instead of
|
||||
* {@code @CommandScan(basePackages="org.my.pkg")}.
|
||||
*
|
||||
* @return the base packages to scan
|
||||
*/
|
||||
@AliasFor("basePackages")
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* Base packages to scan for commands. {@link #value()} is an alias for (and
|
||||
* mutually exclusive with) this attribute.
|
||||
* <p>
|
||||
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based
|
||||
* package names.
|
||||
*
|
||||
* @return the base packages to scan
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String[] basePackages() default {};
|
||||
|
||||
/**
|
||||
* Type-safe alternative to {@link #basePackages()} for specifying the packages
|
||||
* to scan for commands. The package of each class specified will be scanned.
|
||||
* <p>
|
||||
* Consider creating a special no-op marker class or interface in each package
|
||||
* that serves no purpose other than being referenced by this attribute.
|
||||
*
|
||||
* @return classes from the base packages to scan
|
||||
*/
|
||||
Class<?>[] basePackageClasses() default {};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.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.context.annotation.Import;
|
||||
import org.springframework.shell.command.annotation.support.EnableCommandRegistrar;
|
||||
|
||||
/**
|
||||
* Enable support for {@link Command @Command} annotated classes.
|
||||
* {@code @Command} classes can be registered directly on this annotation.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import(EnableCommandRegistrar.class)
|
||||
public @interface EnableCommand {
|
||||
|
||||
/**
|
||||
* Defines candicate classes for shell commands.
|
||||
*
|
||||
* @return candidate classes for shell commands
|
||||
*/
|
||||
Class<?>[] value() default {};
|
||||
}
|
||||
@@ -21,8 +21,6 @@ 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.
|
||||
*
|
||||
@@ -31,7 +29,6 @@ import org.springframework.aot.hint.annotation.Reflective;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
@Reflective
|
||||
public @interface ExceptionResolver {
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.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.shell.command.CommandRegistration.OptionArity;
|
||||
|
||||
/**
|
||||
* Annotation marking a method parameter to be a candicate for an option.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Documented
|
||||
public @interface Option {
|
||||
|
||||
/**
|
||||
* Long names of an option. There can be multiple names where first is primary
|
||||
* one and other are aliases.
|
||||
*
|
||||
* @return Option long names, defaults to empty.
|
||||
*/
|
||||
String[] longNames() default {};
|
||||
|
||||
/**
|
||||
* Short names of an option. There can be multiple names where first is primary
|
||||
* one and other are aliases.
|
||||
*
|
||||
* @return Option short names, defaults to empty.
|
||||
*/
|
||||
char[] shortNames() default {};
|
||||
|
||||
/**
|
||||
* Mark option required.
|
||||
*
|
||||
* @return true if option is required, defaults to false.
|
||||
*/
|
||||
boolean required() default false;
|
||||
|
||||
/**
|
||||
* Define option default value.
|
||||
*
|
||||
* @return default value
|
||||
*/
|
||||
String defaultValue() default "";
|
||||
|
||||
/**
|
||||
* Return a short description of the option.
|
||||
*
|
||||
* @return description of the option
|
||||
*/
|
||||
String description() default "";
|
||||
|
||||
/**
|
||||
* Define option arity.
|
||||
*
|
||||
* @return option arity
|
||||
*/
|
||||
OptionArity arity() default OptionArity.NONE;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.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;
|
||||
|
||||
/**
|
||||
* Annotation marking a method parameter which completion proposals should be
|
||||
* used.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Documented
|
||||
public @interface OptionValues {
|
||||
|
||||
/**
|
||||
* Reference to a bean name
|
||||
* @return a bean name
|
||||
*/
|
||||
String ref() default "";
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Utilities to merge {@link Command} annotations using opinionated logic. In
|
||||
* this class {@code left} is meant for annotation on a class level and
|
||||
* {@code right} annotation on a method level. Class level is meant to provide
|
||||
* defaults and every field may have its own logic.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
class CommandAnnotationUtils {
|
||||
|
||||
private final static String COMMAND = "command";
|
||||
private final static String ALIAS = "alias";
|
||||
private final static String HIDDEN = "hidden";
|
||||
private final static String GROUP = "group";
|
||||
private final static String DESCRIPTION = "description";
|
||||
private final static String INTERACTION_MODE = "interactionMode";
|
||||
|
||||
/**
|
||||
* Deduce {@link Command#hidden()} from annotations.
|
||||
*
|
||||
* @param left the left side annotation
|
||||
* @param right the right side annotation
|
||||
* @return deduced boolean for hidden field
|
||||
*/
|
||||
static boolean deduceHidden(MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
Boolean def = right.getDefaultValue(HIDDEN, Boolean.class).orElse(null);
|
||||
|
||||
boolean l = left.getBoolean(HIDDEN);
|
||||
boolean r = right.getBoolean(HIDDEN);
|
||||
|
||||
if (def != null) {
|
||||
if (def != r) {
|
||||
l = r;
|
||||
}
|
||||
}
|
||||
else {
|
||||
l = r;
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduce {@link Command#command()} from annotations. Command array is supposed
|
||||
* to contain commands without leading or trailing white spaces, so strip, split
|
||||
* and assume that class level defines prefix for array.
|
||||
*
|
||||
* @param left the left side annotation
|
||||
* @param right the right side annotation
|
||||
* @return deduced boolean for command field
|
||||
*/
|
||||
static String[] deduceCommand(MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
return deduceStringArray(COMMAND, left, right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduce {@link Command#alias()} from annotations. Alias array is supposed
|
||||
* to contain commands without leading or trailing white spaces, so strip, split
|
||||
* and assume that class level defines prefix for array.
|
||||
*
|
||||
* @param left the left side annotation
|
||||
* @param right the right side annotation
|
||||
* @return deduced boolean for alias field
|
||||
*/
|
||||
static String[] deduceAlias(MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
return deduceStringArray(ALIAS, left, right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduce {@link Command#group()} from annotations. Right side overrides if it
|
||||
* has value.
|
||||
*
|
||||
* @param left the left side annotation
|
||||
* @param right the right side annotation
|
||||
* @return deduced String for group field
|
||||
*/
|
||||
static String deduceGroup(MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
return deduceStringRightOverrides(GROUP, left, right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduce {@link Command#description()} from annotations. Right side overrides if it
|
||||
* has value.
|
||||
*
|
||||
* @param left the left side annotation
|
||||
* @param right the right side annotation
|
||||
* @return deduced String for description field
|
||||
*/
|
||||
static String deduceDescription(MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
return deduceStringRightOverrides(DESCRIPTION, left, right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduce {@link Command#interactionMode()} from annotations. Right side overrides if.
|
||||
* Returns {@code null} if nothing defined.
|
||||
*
|
||||
* @param left the left side annotation
|
||||
* @param right the right side annotation
|
||||
* @return deduced InteractionMode for interaction mode field
|
||||
*/
|
||||
static @Nullable InteractionMode deduceInteractionMode(MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
InteractionMode mode = null;
|
||||
InteractionMode[] l = left.getEnumArray(INTERACTION_MODE, InteractionMode.class);
|
||||
for (InteractionMode m : l) {
|
||||
if (InteractionMode.ALL == m) {
|
||||
mode = m;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
mode = m;
|
||||
}
|
||||
}
|
||||
InteractionMode[] r = right.getEnumArray(INTERACTION_MODE, InteractionMode.class);
|
||||
for (InteractionMode m : r) {
|
||||
if (InteractionMode.ALL == m) {
|
||||
mode = m;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
mode = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
|
||||
private static String[] deduceStringArray(String field, MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
return Stream.of(left.getStringArray(field), right.getStringArray(field))
|
||||
.flatMap(commands -> Stream.of(commands))
|
||||
.flatMap(command -> Stream.of(command.split(" ")))
|
||||
.filter(command -> StringUtils.hasText(command))
|
||||
.map(command -> command.strip())
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private static String deduceStringRightOverrides(String field, MergedAnnotation<?> left, MergedAnnotation<?> right) {
|
||||
String r = right.getString(field);
|
||||
if (StringUtils.hasText(r)) {
|
||||
return r;
|
||||
}
|
||||
return left.getString(field);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.HierarchicalBeanFactory;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
|
||||
/**
|
||||
* Delegate used by {@link EnableCommandRegistrar} and
|
||||
* {@link CommandScanRegistrar} to register a bean definition(s) for a
|
||||
* {@link Command @Command} class.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class CommandRegistrationBeanRegistrar {
|
||||
|
||||
private final BeanDefinitionRegistry registry;
|
||||
private final BeanFactory beanFactory;
|
||||
private static final MethodFilter COMMAND_METHODS = method ->
|
||||
AnnotatedElementUtils.hasAnnotation(method, Command.class);
|
||||
|
||||
public CommandRegistrationBeanRegistrar(BeanDefinitionRegistry registry) {
|
||||
this.registry = registry;
|
||||
this.beanFactory = (BeanFactory) this.registry;
|
||||
}
|
||||
|
||||
public void register(Class<?> type) {
|
||||
MergedAnnotation<Command> annotation = MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY)
|
||||
.get(Command.class);
|
||||
register(type, annotation);
|
||||
}
|
||||
|
||||
void register(Class<?> type, MergedAnnotation<Command> annotation) {
|
||||
String name = type.getName();
|
||||
if (!containsBeanDefinition(name)) {
|
||||
registerCommandClassBeanDefinition(name, type, annotation);
|
||||
}
|
||||
scanMethods(type, name, annotation);
|
||||
}
|
||||
|
||||
void scanMethods(Class<?> type, String containerBean, MergedAnnotation<Command> classAnnotation) {
|
||||
Set<Method> methods = MethodIntrospector.selectMethods(type, COMMAND_METHODS);
|
||||
methods.forEach(m -> {
|
||||
String name = type.getName();
|
||||
String methodName = m.getName();
|
||||
Class<?>[] methodParameterTypes = m.getParameterTypes();
|
||||
String postfix = Stream.of(methodParameterTypes).map(clazz -> ClassUtils.getShortName(clazz))
|
||||
.collect(Collectors.joining());
|
||||
name = name + "/" + methodName + postfix;
|
||||
|
||||
if (!containsBeanDefinition(name)) {
|
||||
registerCommandMethodBeanDefinition(type, name, containerBean, methodName, methodParameterTypes);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void registerCommandClassBeanDefinition(String beanName, Class<?> type,
|
||||
MergedAnnotation<Command> annotation) {
|
||||
Assert.state(annotation.isPresent(), () -> "No " + Command.class.getSimpleName()
|
||||
+ " annotation found on '" + type.getName() + "'.");
|
||||
this.registry.registerBeanDefinition(beanName, createCommandClassBeanDefinition(type));
|
||||
}
|
||||
|
||||
private void registerCommandMethodBeanDefinition(Class<?> commandBeanType, String commandBeanName, String containerBean, String methodName,
|
||||
Class<?>[] methodParameterTypes) {
|
||||
this.registry.registerBeanDefinition(commandBeanName,
|
||||
createCommandMethodBeanDefinition(commandBeanType, containerBean, methodName, methodParameterTypes));
|
||||
}
|
||||
|
||||
private BeanDefinition createCommandClassBeanDefinition(Class<?> type) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition(type);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private BeanDefinition createCommandMethodBeanDefinition(Class<?> commandBeanType, String commandBeanName,
|
||||
String commandMethodName, Class<?>[] commandMethodParameters) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition(CommandRegistrationFactoryBean.class);
|
||||
definition.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_BEAN_TYPE, commandBeanType);
|
||||
definition.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_BEAN_NAME, commandBeanName);
|
||||
definition.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_METHOD_NAME, commandMethodName);
|
||||
definition.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_METHOD_PARAMETERS, commandMethodParameters);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private boolean containsBeanDefinition(String name) {
|
||||
return containsBeanDefinition(this.beanFactory, name);
|
||||
}
|
||||
|
||||
private boolean containsBeanDefinition(BeanFactory beanFactory, String name) {
|
||||
if (beanFactory instanceof ListableBeanFactory listableBeanFactory
|
||||
&& listableBeanFactory.containsBeanDefinition(name)) {
|
||||
return true;
|
||||
}
|
||||
if (beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory) {
|
||||
return containsBeanDefinition(hierarchicalBeanFactory.getParentBeanFactory(), name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandExceptionResolver;
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandRegistration.Builder;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionArity;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionSpec;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.ExceptionResolverMethodResolver;
|
||||
import org.springframework.shell.command.annotation.Option;
|
||||
import org.springframework.shell.command.annotation.OptionValues;
|
||||
import org.springframework.shell.command.invocation.InvocableShellMethod;
|
||||
import org.springframework.shell.completion.CompletionProvider;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Factory bean used in {@link CommandRegistrationBeanRegistrar} to build
|
||||
* instance of {@link CommandRegistration}. Main logic of constructing
|
||||
* {@link CommandRegistration} out from annotated command target is
|
||||
* in this factory.
|
||||
*
|
||||
* This factory needs a name of a {@code commandBeanName} which is a name of bean
|
||||
* hosting command methods, {@code commandBeanType} which is type of a bean,
|
||||
* {@code commandMethodName} which is a name of {@link Method} in a bean,
|
||||
* {@code commandMethodParameters} for method parameter types and optionally
|
||||
* {@code BuilderSupplier} if context provides pre-configured builder.
|
||||
*
|
||||
* This is internal class and not meant for generic use.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
class CommandRegistrationFactoryBean implements FactoryBean<CommandRegistration>, ApplicationContextAware, InitializingBean {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(CommandRegistrationFactoryBean.class);
|
||||
public static final String COMMAND_BEAN_TYPE = "commandBeanType";
|
||||
public static final String COMMAND_BEAN_NAME = "commandBeanName";
|
||||
public static final String COMMAND_METHOD_NAME = "commandMethodName";
|
||||
public static final String COMMAND_METHOD_PARAMETERS = "commandMethodParameters";
|
||||
|
||||
private ObjectProvider<CommandRegistration.BuilderSupplier> supplier;
|
||||
private ApplicationContext applicationContext;
|
||||
private Object commandBean;
|
||||
private Class<?> commandBeanType;
|
||||
private String commandBeanName;
|
||||
private String commandMethodName;
|
||||
private Class<?>[] commandMethodParameters;
|
||||
|
||||
@Override
|
||||
public CommandRegistration getObject() throws Exception {
|
||||
CommandRegistration registration = buildRegistration();
|
||||
return registration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return CommandRegistration.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.commandBean = applicationContext.getBean(commandBeanName);
|
||||
this.supplier = applicationContext.getBeanProvider(CommandRegistration.BuilderSupplier.class);
|
||||
}
|
||||
|
||||
public void setCommandBeanType(Class<?> commandBeanType) {
|
||||
this.commandBeanType = commandBeanType;
|
||||
}
|
||||
|
||||
public void setCommandBeanName(String commandBeanName) {
|
||||
this.commandBeanName = commandBeanName;
|
||||
}
|
||||
|
||||
public void setCommandMethodName(String commandMethodName) {
|
||||
this.commandMethodName = commandMethodName;
|
||||
}
|
||||
|
||||
public void setCommandMethodParameters(Class<?>[] commandMethodParameters) {
|
||||
this.commandMethodParameters = commandMethodParameters;
|
||||
}
|
||||
|
||||
private CommandRegistration.Builder getBuilder() {
|
||||
return supplier.getIfAvailable(() -> () -> CommandRegistration.builder()).get();
|
||||
}
|
||||
|
||||
private CommandRegistration buildRegistration() {
|
||||
Method method = ReflectionUtils.findMethod(commandBeanType, commandMethodName, commandMethodParameters);
|
||||
MergedAnnotation<Command> classAnn = MergedAnnotations.from(commandBeanType, SearchStrategy.TYPE_HIERARCHY)
|
||||
.get(Command.class);
|
||||
MergedAnnotation<Command> methodAnn = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY)
|
||||
.get(Command.class);
|
||||
|
||||
Builder builder = getBuilder();
|
||||
|
||||
// command
|
||||
String[] deduceCommand = CommandAnnotationUtils.deduceCommand(classAnn, methodAnn);
|
||||
if (deduceCommand.length == 0) {
|
||||
deduceCommand = new String[] { Utils.unCamelify(method.getName()) };
|
||||
}
|
||||
builder.command(deduceCommand);
|
||||
|
||||
// group
|
||||
String deduceGroup = CommandAnnotationUtils.deduceGroup(classAnn, methodAnn);
|
||||
builder.group(deduceGroup);
|
||||
|
||||
// hidden
|
||||
boolean deduceHidden = CommandAnnotationUtils.deduceHidden(classAnn, methodAnn);
|
||||
builder.hidden(deduceHidden);
|
||||
|
||||
// description
|
||||
String deduceDescription = CommandAnnotationUtils.deduceDescription(classAnn, methodAnn);
|
||||
builder.description(deduceDescription);
|
||||
|
||||
// interaction mode
|
||||
InteractionMode deduceInteractionMode = CommandAnnotationUtils.deduceInteractionMode(classAnn, methodAnn);
|
||||
builder.interactionMode(deduceInteractionMode);
|
||||
|
||||
// alias
|
||||
String[] deduceAlias = CommandAnnotationUtils.deduceAlias(classAnn, methodAnn);
|
||||
if (deduceAlias.length > 0) {
|
||||
builder.withAlias().command(deduceAlias);
|
||||
}
|
||||
|
||||
// target
|
||||
builder.withTarget().method(commandBean, method);
|
||||
|
||||
// options
|
||||
InvocableHandlerMethod ihm = new InvocableHandlerMethod(commandBean, method);
|
||||
for (MethodParameter mp : ihm.getMethodParameters()) {
|
||||
onCommandParameter(mp, builder);
|
||||
}
|
||||
|
||||
// error handling
|
||||
ExceptionResolverMethodResolver exceptionResolverMethodResolver = new ExceptionResolverMethodResolver(commandBean.getClass());
|
||||
MethodCommandExceptionResolver methodCommandExceptionResolver = new MethodCommandExceptionResolver();
|
||||
methodCommandExceptionResolver.bean = commandBean;
|
||||
methodCommandExceptionResolver.exceptionResolverMethodResolver = exceptionResolverMethodResolver;
|
||||
builder.withErrorHandling().resolver(methodCommandExceptionResolver);
|
||||
|
||||
CommandRegistration registration = builder.build();
|
||||
return registration;
|
||||
}
|
||||
|
||||
private void onCommandParameter(MethodParameter mp, Builder builder) {
|
||||
|
||||
MergedAnnotation<Option> optionAnn = MergedAnnotations.from(mp.getParameter(), SearchStrategy.TYPE_HIERARCHY)
|
||||
.get(Option.class);
|
||||
|
||||
Option so = mp.getParameterAnnotation(Option.class);
|
||||
log.debug("Registering with mp='{}' so='{}'", mp, so);
|
||||
if (so != null) {
|
||||
List<String> longNames = new ArrayList<>();
|
||||
List<Character> shortNames = new ArrayList<>();
|
||||
for(int i = 0; i < so.shortNames().length; i++) {
|
||||
shortNames.add(so.shortNames()[i]);
|
||||
}
|
||||
if (!ObjectUtils.isEmpty(so.longNames())) {
|
||||
for(int i = 0; i < so.longNames().length; i++) {
|
||||
longNames.add(so.longNames()[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// ShellOption value not defined
|
||||
mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
String longName = mp.getParameterName();
|
||||
Class<?> parameterType = mp.getParameterType();
|
||||
if (longName != null) {
|
||||
log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType);
|
||||
longNames.add(longName);
|
||||
}
|
||||
}
|
||||
if (!longNames.isEmpty() || !shortNames.isEmpty()) {
|
||||
log.debug("Registering longNames='{}' shortNames='{}'", longNames, shortNames);
|
||||
Class<?> parameterType = mp.getParameterType();
|
||||
OptionSpec optionSpec = builder.withOption();
|
||||
optionSpec.type(parameterType);
|
||||
optionSpec.longNames(longNames.toArray(new String[0]));
|
||||
optionSpec.shortNames(shortNames.toArray(new Character[0]));
|
||||
optionSpec.position(mp.getParameterIndex());
|
||||
optionSpec.description(so.description());
|
||||
if (so.arity() != OptionArity.NONE) {
|
||||
optionSpec.arity(so.arity());
|
||||
}
|
||||
else {
|
||||
if (ClassUtils.isAssignable(boolean.class, parameterType)) {
|
||||
optionSpec.arity(OptionArity.ZERO);
|
||||
}
|
||||
else if (ClassUtils.isAssignable(Boolean.class, parameterType)) {
|
||||
optionSpec.arity(OptionArity.ZERO);
|
||||
}
|
||||
else {
|
||||
optionSpec.arity(OptionArity.EXACTLY_ONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(so.defaultValue())) {
|
||||
optionSpec.defaultValue(so.defaultValue());
|
||||
}
|
||||
else if (ClassUtils.isAssignable(boolean.class, parameterType)){
|
||||
optionSpec.required(false);
|
||||
optionSpec.defaultValue("false");
|
||||
}
|
||||
else {
|
||||
if (optionAnn.isPresent()) {
|
||||
boolean requiredDeduce = optionAnn.getBoolean("required");
|
||||
optionSpec.required(requiredDeduce);
|
||||
}
|
||||
}
|
||||
|
||||
OptionValues ovAnn = mp.getParameterAnnotation(OptionValues.class);
|
||||
if (ovAnn != null && StringUtils.hasText(ovAnn.ref())) {
|
||||
CompletionProvider cr = this.applicationContext.getBean(ovAnn.ref(), CompletionProvider.class);
|
||||
if (cr != null) {
|
||||
optionSpec.completion(ctx -> cr.apply(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
String longName = mp.getParameterName();
|
||||
Class<?> parameterType = mp.getParameterType();
|
||||
if (longName != null) {
|
||||
log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType);
|
||||
OptionSpec optionSpec = builder.withOption();
|
||||
optionSpec.longNames(longName);
|
||||
optionSpec.type(parameterType);
|
||||
optionSpec.required();
|
||||
optionSpec.position(mp.getParameterIndex());
|
||||
if (ClassUtils.isAssignable(boolean.class, parameterType)) {
|
||||
optionSpec.arity(OptionArity.ZERO);
|
||||
}
|
||||
else if (ClassUtils.isAssignable(Boolean.class, parameterType)) {
|
||||
optionSpec.arity(OptionArity.ZERO);
|
||||
}
|
||||
else {
|
||||
optionSpec.arity(OptionArity.EXACTLY_ONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class MethodCommandExceptionResolver implements CommandExceptionResolver {
|
||||
|
||||
Object bean;
|
||||
ExceptionResolverMethodResolver exceptionResolverMethodResolver;
|
||||
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception ex) {
|
||||
Method exceptionHandlerMethod = exceptionResolverMethodResolver.resolveMethodByThrowable(ex);
|
||||
if (exceptionHandlerMethod == null) {
|
||||
return null;
|
||||
}
|
||||
InvocableShellMethod invocable = new InvocableShellMethod(bean, exceptionHandlerMethod);
|
||||
try {
|
||||
Object invoke = invocable.invoke(null, ex);
|
||||
if (invoke instanceof CommandHandlingResult) {
|
||||
return (CommandHandlingResult)invoke;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.boot.context.TypeExcludeFilter;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.CommandScan;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link ImportBeanDefinitionRegistrar} for {@link CommandScan @CommandScan}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandScanRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
private final Environment environment;
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
CommandScanRegistrar(Environment environment, ResourceLoader resourceLoader) {
|
||||
this.environment = environment;
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
|
||||
Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
|
||||
scan(registry, packagesToScan);
|
||||
}
|
||||
|
||||
private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
|
||||
AnnotationAttributes attributes = AnnotationAttributes
|
||||
.fromMap(metadata.getAnnotationAttributes(CommandScan.class.getName()));
|
||||
String[] basePackages = attributes.getStringArray("basePackages");
|
||||
Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
|
||||
Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
|
||||
for (Class<?> basePackageClass : basePackageClasses) {
|
||||
packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
|
||||
}
|
||||
if (packagesToScan.isEmpty()) {
|
||||
packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
|
||||
}
|
||||
packagesToScan.removeIf((candidate) -> !StringUtils.hasText(candidate));
|
||||
return packagesToScan;
|
||||
}
|
||||
|
||||
private void scan(BeanDefinitionRegistry registry, Set<String> packages) {
|
||||
CommandRegistrationBeanRegistrar registrar = new CommandRegistrationBeanRegistrar(registry);
|
||||
ClassPathScanningCandidateComponentProvider scanner = getScanner(registry);
|
||||
|
||||
for (String basePackage : packages) {
|
||||
for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) {
|
||||
register(registrar, candidate.getBeanClassName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ClassPathScanningCandidateComponentProvider getScanner(BeanDefinitionRegistry registry) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
|
||||
scanner.setEnvironment(this.environment);
|
||||
scanner.setResourceLoader(this.resourceLoader);
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(Command.class));
|
||||
TypeExcludeFilter typeExcludeFilter = new TypeExcludeFilter();
|
||||
typeExcludeFilter.setBeanFactory((BeanFactory) registry);
|
||||
scanner.addExcludeFilter(typeExcludeFilter);
|
||||
return scanner;
|
||||
}
|
||||
|
||||
private void register(CommandRegistrationBeanRegistrar registrar, String className) throws LinkageError {
|
||||
try {
|
||||
register(registrar, ClassUtils.forName(className, null));
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void register(CommandRegistrationBeanRegistrar registrar, Class<?> type) {
|
||||
if (!isComponent(type)) {
|
||||
registrar.register(type);
|
||||
}
|
||||
registrar.register(type);
|
||||
}
|
||||
|
||||
private boolean isComponent(Class<?> type) {
|
||||
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(Component.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.shell.command.annotation.EnableCommand;
|
||||
|
||||
/**
|
||||
* {@link ImportBeanDefinitionRegistrar} for {@link EnableCommand @EnableCommands}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class EnableCommandRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
CommandRegistrationBeanRegistrar beanRegistrar = new CommandRegistrationBeanRegistrar(registry);
|
||||
getTypes(metadata).forEach(beanRegistrar::register);
|
||||
}
|
||||
|
||||
private Set<Class<?>> getTypes(AnnotationMetadata metadata) {
|
||||
return metadata.getAnnotations().stream(EnableCommand.class)
|
||||
.flatMap((annotation) -> Arrays.stream(annotation.getClassArray(MergedAnnotation.VALUE)))
|
||||
.filter((type) -> void.class != type)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.completion;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
|
||||
/**
|
||||
* Interface resolving completion proposals.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CompletionProvider extends Function<CompletionContext, List<CompletionProposal>> {
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
* Copyright 2022-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.
|
||||
@@ -15,17 +15,13 @@
|
||||
*/
|
||||
package org.springframework.shell.completion;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
|
||||
/**
|
||||
* Interface resolving completion proposals.
|
||||
* Interface resolving completion proposals. This is same as
|
||||
* {@link CompletionProvider} but mean to be autowired globally.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
* @see CompletionProvider
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CompletionResolver extends Function<CompletionContext, List<CompletionProposal>> {
|
||||
public interface CompletionResolver extends CompletionProvider {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CommandAnnotationUtilsTests {
|
||||
|
||||
private static MergedAnnotation<Command> hiddenTrue = MergedAnnotations.from(HiddenTrue.class).get(Command.class);
|
||||
private static MergedAnnotation<Command> hiddenFalse = MergedAnnotations.from(HiddenFalse.class).get(Command.class);
|
||||
private static MergedAnnotation<Command> hiddenDefault = MergedAnnotations.from(HiddenDefault.class)
|
||||
.get(Command.class);
|
||||
|
||||
@Command(hidden = true)
|
||||
private static class HiddenTrue {
|
||||
}
|
||||
|
||||
@Command(hidden = false)
|
||||
private static class HiddenFalse {
|
||||
}
|
||||
|
||||
@Command()
|
||||
private static class HiddenDefault {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHidden() {
|
||||
assertThat(CommandAnnotationUtils.deduceHidden(hiddenTrue, hiddenDefault)).isTrue();
|
||||
assertThat(CommandAnnotationUtils.deduceHidden(hiddenTrue, hiddenFalse)).isTrue();
|
||||
assertThat(CommandAnnotationUtils.deduceHidden(hiddenTrue, hiddenTrue)).isTrue();
|
||||
|
||||
assertThat(CommandAnnotationUtils.deduceHidden(hiddenFalse, hiddenDefault)).isFalse();
|
||||
assertThat(CommandAnnotationUtils.deduceHidden(hiddenFalse, hiddenFalse)).isFalse();
|
||||
assertThat(CommandAnnotationUtils.deduceHidden(hiddenFalse, hiddenTrue)).isTrue();
|
||||
}
|
||||
|
||||
private static MergedAnnotation<Command> commandDefault = MergedAnnotations.from(CommandDefault.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> commandValues1 = MergedAnnotations.from(CommandValues1.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> commandValues2 = MergedAnnotations.from(CommandValues2.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> commandValues3 = MergedAnnotations.from(CommandValues3.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> commandValues4 = MergedAnnotations.from(CommandValues4.class)
|
||||
.get(Command.class);
|
||||
|
||||
@Command
|
||||
private static class CommandDefault {
|
||||
}
|
||||
|
||||
@Command(command = { "one", "two" })
|
||||
private static class CommandValues1 {
|
||||
}
|
||||
|
||||
@Command(command = { "three", "four" })
|
||||
private static class CommandValues2 {
|
||||
}
|
||||
|
||||
@Command(command = { " five", "six ", " seven " })
|
||||
private static class CommandValues3 {
|
||||
}
|
||||
|
||||
@Command(command = { " eight nine " })
|
||||
private static class CommandValues4 {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommand() {
|
||||
assertThat(CommandAnnotationUtils.deduceCommand(commandDefault, commandDefault)).isEmpty();
|
||||
assertThat(CommandAnnotationUtils.deduceCommand(commandDefault, commandValues1))
|
||||
.isEqualTo(new String[] { "one", "two" });
|
||||
assertThat(CommandAnnotationUtils.deduceCommand(commandValues1, commandValues2))
|
||||
.isEqualTo(new String[] { "one", "two", "three", "four" });
|
||||
assertThat(CommandAnnotationUtils.deduceCommand(commandDefault, commandValues3))
|
||||
.isEqualTo(new String[] { "five", "six", "seven" });
|
||||
assertThat(CommandAnnotationUtils.deduceCommand(commandDefault, commandValues4))
|
||||
.isEqualTo(new String[] { "eight", "nine" });
|
||||
}
|
||||
|
||||
private static MergedAnnotation<Command> aliasDefault = MergedAnnotations.from(AliasDefault.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> aliasValues1 = MergedAnnotations.from(AliasValues1.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> aliasValues2 = MergedAnnotations.from(AliasValues2.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> aliasValues3 = MergedAnnotations.from(AliasValues3.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> aliasValues4 = MergedAnnotations.from(AliasValues4.class)
|
||||
.get(Command.class);
|
||||
|
||||
@Command
|
||||
private static class AliasDefault {
|
||||
}
|
||||
|
||||
@Command(alias = { "one", "two" })
|
||||
private static class AliasValues1 {
|
||||
}
|
||||
|
||||
@Command(alias = { "three", "four" })
|
||||
private static class AliasValues2 {
|
||||
}
|
||||
|
||||
@Command(alias = { " five", "six ", " seven " })
|
||||
private static class AliasValues3 {
|
||||
}
|
||||
|
||||
@Command(alias = { " eight nine " })
|
||||
private static class AliasValues4 {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAlias() {
|
||||
assertThat(CommandAnnotationUtils.deduceAlias(aliasDefault, aliasDefault)).isEmpty();
|
||||
assertThat(CommandAnnotationUtils.deduceAlias(aliasDefault, aliasValues1))
|
||||
.isEqualTo(new String[] { "one", "two" });
|
||||
assertThat(CommandAnnotationUtils.deduceAlias(aliasValues1, aliasValues2))
|
||||
.isEqualTo(new String[] { "one", "two", "three", "four" });
|
||||
assertThat(CommandAnnotationUtils.deduceAlias(aliasDefault, aliasValues3))
|
||||
.isEqualTo(new String[] { "five", "six", "seven" });
|
||||
assertThat(CommandAnnotationUtils.deduceAlias(aliasDefault, aliasValues4))
|
||||
.isEqualTo(new String[] { "eight", "nine" });
|
||||
}
|
||||
|
||||
private static MergedAnnotation<Command> groupValue1 = MergedAnnotations.from(GroupValues1.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> groupValue2 = MergedAnnotations.from(GroupValues2.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> groupDefault = MergedAnnotations.from(GroupDefault.class)
|
||||
.get(Command.class);
|
||||
|
||||
@Command(group = "group1")
|
||||
private static class GroupValues1 {
|
||||
}
|
||||
|
||||
@Command(group = "group2")
|
||||
private static class GroupValues2 {
|
||||
}
|
||||
|
||||
@Command()
|
||||
private static class GroupDefault {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGroup() {
|
||||
assertThat(CommandAnnotationUtils.deduceGroup(groupDefault, groupDefault)).isEqualTo("");
|
||||
assertThat(CommandAnnotationUtils.deduceGroup(groupDefault, groupValue1)).isEqualTo("group1");
|
||||
assertThat(CommandAnnotationUtils.deduceGroup(groupValue1, groupDefault)).isEqualTo("group1");
|
||||
assertThat(CommandAnnotationUtils.deduceGroup(groupValue1, groupValue2)).isEqualTo("group2");
|
||||
}
|
||||
|
||||
private static MergedAnnotation<Command> descriptionValue1 = MergedAnnotations.from(DescriptionValues1.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> descriptionValue2 = MergedAnnotations.from(DescriptionValues2.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> descriptionDefault = MergedAnnotations.from(DescriptionDefault.class)
|
||||
.get(Command.class);
|
||||
|
||||
@Command(description = "description1")
|
||||
private static class DescriptionValues1 {
|
||||
}
|
||||
|
||||
@Command(description = "description2")
|
||||
private static class DescriptionValues2 {
|
||||
}
|
||||
|
||||
@Command()
|
||||
private static class DescriptionDefault {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDescription() {
|
||||
assertThat(CommandAnnotationUtils.deduceDescription(descriptionDefault, descriptionDefault)).isEqualTo("");
|
||||
assertThat(CommandAnnotationUtils.deduceDescription(descriptionDefault, descriptionValue1))
|
||||
.isEqualTo("description1");
|
||||
assertThat(CommandAnnotationUtils.deduceDescription(descriptionValue1, descriptionDefault))
|
||||
.isEqualTo("description1");
|
||||
assertThat(CommandAnnotationUtils.deduceDescription(descriptionValue1, descriptionValue2))
|
||||
.isEqualTo("description2");
|
||||
}
|
||||
|
||||
private static MergedAnnotation<Command> interactionModeDefault = MergedAnnotations
|
||||
.from(InteractionModeDefault.class).get(Command.class);
|
||||
private static MergedAnnotation<Command> interactionModeAll = MergedAnnotations.from(InteractionModeAll.class)
|
||||
.get(Command.class);
|
||||
private static MergedAnnotation<Command> interactionModeInteractive = MergedAnnotations
|
||||
.from(InteractionModeInteractive.class).get(Command.class);
|
||||
private static MergedAnnotation<Command> interactionModeNoninteractive = MergedAnnotations
|
||||
.from(InteractionModeNoninteractive.class).get(Command.class);
|
||||
|
||||
@Command()
|
||||
private static class InteractionModeDefault {
|
||||
}
|
||||
|
||||
@Command(interactionMode = InteractionMode.ALL)
|
||||
private static class InteractionModeAll {
|
||||
}
|
||||
|
||||
@Command(interactionMode = InteractionMode.INTERACTIVE)
|
||||
private static class InteractionModeInteractive {
|
||||
}
|
||||
|
||||
@Command(interactionMode = InteractionMode.NONINTERACTIVE)
|
||||
private static class InteractionModeNoninteractive {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInteractionMode() {
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeDefault, interactionModeDefault))
|
||||
.isNull();
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeAll, interactionModeDefault))
|
||||
.isEqualTo(InteractionMode.ALL);
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeInteractive, interactionModeDefault))
|
||||
.isEqualTo(InteractionMode.INTERACTIVE);
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeNoninteractive, interactionModeDefault))
|
||||
.isEqualTo(InteractionMode.NONINTERACTIVE);
|
||||
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeDefault, interactionModeAll))
|
||||
.isEqualTo(InteractionMode.ALL);
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeDefault, interactionModeInteractive))
|
||||
.isEqualTo(InteractionMode.INTERACTIVE);
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeDefault, interactionModeNoninteractive))
|
||||
.isEqualTo(InteractionMode.NONINTERACTIVE);
|
||||
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeAll, interactionModeInteractive))
|
||||
.isEqualTo(InteractionMode.INTERACTIVE);
|
||||
assertThat(CommandAnnotationUtils.deduceInteractionMode(interactionModeAll, interactionModeNoninteractive))
|
||||
.isEqualTo(InteractionMode.NONINTERACTIVE);
|
||||
|
||||
assertThat(
|
||||
CommandAnnotationUtils.deduceInteractionMode(interactionModeInteractive, interactionModeNoninteractive))
|
||||
.isEqualTo(InteractionMode.NONINTERACTIVE);
|
||||
assertThat(
|
||||
CommandAnnotationUtils.deduceInteractionMode(interactionModeNoninteractive, interactionModeInteractive))
|
||||
.isEqualTo(InteractionMode.INTERACTIVE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
class CommandRegistrationBeanRegistrarTests {
|
||||
|
||||
private final BeanDefinitionRegistry registry = new DefaultListableBeanFactory();
|
||||
private final CommandRegistrationBeanRegistrar registrar = new CommandRegistrationBeanRegistrar(registry);
|
||||
|
||||
@Test
|
||||
void registerWhenNotAlreadyRegisteredAddBeanDefinition() {
|
||||
String beanName = BeanCommand.class.getName();
|
||||
this.registrar.register(BeanCommand.class);
|
||||
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
|
||||
assertThat(definition).isNotNull();
|
||||
assertThat(definition.getBeanClassName()).isEqualTo(BeanCommand.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerWhenNoAnnotationThrowsException() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.registrar.register(NoAnnotationCommand.class))
|
||||
.withMessageContaining("No Command annotation found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerWhenNotAlreadyRegisteredAddMethodBeanDefinition() {
|
||||
String beanName = BeanWithMethodCommand.class.getName();
|
||||
this.registrar.register(BeanWithMethodCommand.class);
|
||||
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
|
||||
assertThat(definition).isNotNull();
|
||||
definition = this.registry.getBeanDefinition(beanName + "/method");
|
||||
assertThat(definition).isNotNull();
|
||||
assertThat(definition.getBeanClassName()).isEqualTo(CommandRegistrationFactoryBean.class.getName());
|
||||
}
|
||||
|
||||
@Command
|
||||
static class BeanCommand {
|
||||
}
|
||||
|
||||
@Command
|
||||
static class BeanWithMethodCommand {
|
||||
|
||||
@Command
|
||||
void method() {
|
||||
}
|
||||
}
|
||||
|
||||
static class NoAnnotationCommand {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.command.annotation.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.Option;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CommandRegistrationFactoryBeanTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
|
||||
private final static String BEAN = "commandBean";
|
||||
private final static String FACTORYBEAN = "commandRegistrationFactoryBean";
|
||||
private final static String FACTORYBEANREF = "&" + FACTORYBEAN;
|
||||
|
||||
@Test
|
||||
void hiddenOnClassLevel() {
|
||||
configCommon(HiddenOnClassBean.class, new HiddenOnClassBean())
|
||||
.run((context) -> {
|
||||
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
|
||||
CommandRegistrationFactoryBean.class);
|
||||
assertThat(fb).isNotNull();
|
||||
CommandRegistration registration = fb.getObject();
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.isHidden()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Command(hidden = true)
|
||||
private static class HiddenOnClassBean {
|
||||
|
||||
@Command
|
||||
void command(){
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandCommonThings() {
|
||||
configCommon(OnBothClassAndMethod.class, new OnBothClassAndMethod())
|
||||
.run((context) -> {
|
||||
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
|
||||
CommandRegistrationFactoryBean.class);
|
||||
assertThat(fb).isNotNull();
|
||||
CommandRegistration registration = fb.getObject();
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getCommand()).isEqualTo("one two");
|
||||
assertThat(registration.getAliases()).hasSize(1);
|
||||
assertThat(registration.getAliases().get(0).getCommand()).isEqualTo("three four");
|
||||
assertThat(registration.getGroup()).isEqualTo("group2");
|
||||
});
|
||||
}
|
||||
|
||||
@Command(command = "one", alias = "three", group = "group1")
|
||||
private static class OnBothClassAndMethod {
|
||||
|
||||
@Command(command = "two", alias = "four", group = "group2")
|
||||
void command(){
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void setsRequiredOption() {
|
||||
configCommon(RequiredOption.class, new RequiredOption(), "command1", new Class[] { String.class })
|
||||
.run((context) -> {
|
||||
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
|
||||
CommandRegistrationFactoryBean.class);
|
||||
assertThat(fb).isNotNull();
|
||||
CommandRegistration registration = fb.getObject();
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isTrue();
|
||||
});
|
||||
configCommon(RequiredOption.class, new RequiredOption(), "command2", new Class[] { String.class })
|
||||
.run((context) -> {
|
||||
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
|
||||
CommandRegistrationFactoryBean.class);
|
||||
assertThat(fb).isNotNull();
|
||||
CommandRegistration registration = fb.getObject();
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Command
|
||||
private static class RequiredOption {
|
||||
|
||||
@Command
|
||||
void command1(@Option(required = true) String arg) {
|
||||
}
|
||||
|
||||
@Command
|
||||
void command2(@Option(required = false) String arg) {
|
||||
}
|
||||
}
|
||||
|
||||
private <T> ApplicationContextRunner configCommon(Class<T> type, T bean) {
|
||||
return configCommon(type, bean, "command", new Class[0]);
|
||||
}
|
||||
|
||||
private <T> ApplicationContextRunner configCommon(Class<T> type, T bean, String method, Class<?>[] parameters) {
|
||||
return this.contextRunner
|
||||
.withBean(BEAN, type, () -> bean)
|
||||
.withBean(FACTORYBEAN, CommandRegistrationFactoryBean.class, () -> new CommandRegistrationFactoryBean(), bd -> {
|
||||
bd.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_BEAN_TYPE, type);
|
||||
bd.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_BEAN_NAME, BEAN);
|
||||
bd.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_METHOD_NAME, method);
|
||||
bd.getPropertyValues().add(CommandRegistrationFactoryBean.COMMAND_METHOD_PARAMETERS, parameters);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2022 the original author or authors.
|
||||
* Copyright 2017-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.
|
||||
@@ -18,10 +18,11 @@ package org.springframework.shell.samples;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.Banner.Mode;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.annotation.CommandScan;
|
||||
import org.springframework.shell.jline.PromptProvider;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,7 @@ import org.springframework.shell.jline.PromptProvider;
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@CommandScan
|
||||
public class SpringShellSample {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.samples.e2e;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
public class AliasCommands {
|
||||
|
||||
@Command(command = BaseE2ECommands.ANNO, alias = BaseE2ECommands.ANNO, group = BaseE2ECommands.GROUP)
|
||||
public static class AliasCommandsAnnotation extends BaseE2ECommands {
|
||||
|
||||
@Command(command = "alias-1", alias = "aliasfor-1")
|
||||
public String testAlias1Annotation() {
|
||||
return "Hello from alias command";
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public static class AliasCommandsRegistration extends BaseE2ECommands {
|
||||
|
||||
@Bean
|
||||
public CommandRegistration testAlias1Registration(CommandRegistration.BuilderSupplier builder) {
|
||||
return builder.get()
|
||||
.command(REG, "alias-1")
|
||||
.group(GROUP)
|
||||
.withAlias()
|
||||
.command(REG, "aliasfor-1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(ctx -> {
|
||||
return "Hello from alias command";
|
||||
})
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,8 @@ abstract class BaseE2ECommands {
|
||||
static final String GROUP = "E2E Commands";
|
||||
static final String REG = "e2e reg";
|
||||
static final String LEGACY_ANNO = "e2e anno ";
|
||||
// TODO: anno should become anno-legacy and annox to anno
|
||||
static final String ANNO = "e2e annox ";
|
||||
|
||||
@Autowired
|
||||
private CommandRegistration.BuilderSupplier builder;
|
||||
|
||||
@@ -23,6 +23,10 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.Option;
|
||||
import org.springframework.shell.command.annotation.OptionValues;
|
||||
import org.springframework.shell.completion.CompletionProvider;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
@@ -42,7 +46,6 @@ public class InteractiveCompletionCommands {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
Test1ValuesProvider test1ValuesProvider() {
|
||||
return new Test1ValuesProvider();
|
||||
@@ -54,6 +57,34 @@ public class InteractiveCompletionCommands {
|
||||
}
|
||||
}
|
||||
|
||||
@Command(command = BaseE2ECommands.ANNO, group = BaseE2ECommands.GROUP)
|
||||
public static class Annotation extends BaseE2ECommands {
|
||||
|
||||
@Command(command = "interactive-completion-1")
|
||||
public String testRequiredValueAnnotation(
|
||||
@Option(longNames = "arg1", required = true) @OptionValues(ref = "test1CompletionProvider") String arg1,
|
||||
@Option(longNames = "arg2", required = true) @OptionValues(ref = "test2CompletionProvider") String arg2
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
@Bean
|
||||
CompletionProvider test1CompletionProvider() {
|
||||
return ctx -> {
|
||||
Test1ValuesProvider test1ValuesProvider = new Test1ValuesProvider();
|
||||
return test1ValuesProvider.complete(ctx);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
CompletionProvider test2CompletionProvider() {
|
||||
return ctx -> {
|
||||
Test1ValuesProvider test1ValuesProvider = new Test1ValuesProvider();
|
||||
return test1ValuesProvider.complete(ctx);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public static class Registration extends BaseE2ECommands {
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.shell.samples.e2e;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.Option;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
@@ -39,6 +41,17 @@ public class RequiredValueCommands {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
}
|
||||
@Command(command = BaseE2ECommands.ANNO, group = BaseE2ECommands.GROUP)
|
||||
public static class Annotation extends BaseE2ECommands {
|
||||
|
||||
@Command(command = "required-value")
|
||||
public String testRequiredValueAnnotation(
|
||||
@Option(longNames = "arg1", required = true, description = "Desc arg1")
|
||||
String arg1
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public static class Registration extends BaseE2ECommands {
|
||||
|
||||
@@ -18,20 +18,25 @@ package org.springframework.shell.samples.e2e;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import org.springframework.shell.command.annotation.EnableCommand;
|
||||
import org.springframework.shell.samples.AbstractSampleTests;
|
||||
import org.springframework.shell.samples.e2e.RequiredValueCommands.Annotation;
|
||||
import org.springframework.shell.samples.e2e.RequiredValueCommands.LegacyAnnotation;
|
||||
import org.springframework.shell.samples.e2e.RequiredValueCommands.Registration;
|
||||
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
@ContextConfiguration(classes = {LegacyAnnotation.class, Registration.class})
|
||||
@EnableCommand(Annotation.class)
|
||||
class RequiredValueCommandsTests extends AbstractSampleTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"e2e anno required-value,false",
|
||||
"e2e annox required-value,false",
|
||||
"e2e reg required-value,false",
|
||||
"e2e anno required-value,true",
|
||||
"e2e annox required-value,true",
|
||||
"e2e reg required-value,true"
|
||||
})
|
||||
void shouldRequireOption(String command, boolean interactive) {
|
||||
|
||||
Reference in New Issue
Block a user