All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.shell.command.CommandRegistration Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.shell.command;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.shell.Availability;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.context.InteractionMode;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
 * Interface defining a command registration endpoint.
 *
 * @author Janne Valkealahti
 */
public interface CommandRegistration {

	/**
	 * Gets a command for this registration.
	 *
	 * @return command
	 */
	String getCommand();

	/**
	 * Gets an {@link InteractionMode}.
	 *
	 * @return the interaction mode
	 */
	InteractionMode getInteractionMode();

	/**
	 * Get group for a command.
	 *
	 * @return the group
	 */
	String getGroup();

	/**
	 * Get description for a command.
	 *
	 * @return the description
	 */
	String getDescription();

	/**
	 * Get {@link Availability} for a command
	 *
	 * @return the availability
	 */
	Availability getAvailability();

	/**
	 * Gets target info.
	 *
	 * @return the target info
	 */
	TargetInfo getTarget();

	/**
	 * Gets an options.
	 *
	 * @return the options
	 */
	List getOptions();

	/**
	 * Gets an aliases.
	 *
	 * @return the aliases
	 */
	List getAliases();

	/**
	 * Gets an exit code.
	 *
	 * @return the exit code
	 */
	CommandExitCode getExitCode();

	/**
	 * Gets a new instance of a {@link Builder}.
	 *
	 * @return a new builder instance
	 */
	public static Builder builder() {
		return new DefaultBuilder();
	}

	/**
	 * Spec defining an option.
	 */
	public interface OptionSpec {

		/**
		 * Define long option names.
		 *
		 * @param names the long option names
		 * @return option spec for chaining
		 */
		OptionSpec longNames(String... names);

		/**
		 * Define short option names.
		 *
		 * @param names the long option names
		 * @return option spec for chaining
		 */
		OptionSpec shortNames(Character... names);

		/**
		 * Define a type for an option.
		 *
		 * @param type the type
		 * @return option spec for chaining
		 */
		OptionSpec type(Type type);

		/**
		 * Define a {@code description} for an option.
		 *
		 * @param description the option description
		 * @return option spec for chaining
		 */
		OptionSpec description(String description);

		/**
		 * Define if option is required.
		 *
		 * @param required the required flag
		 * @return option spec for chaining
		 */
		OptionSpec required(boolean required);

		/**
		 * Define option to be required. Syntatic sugar calling
		 * {@link #required(boolean)} with {@code true}.
		 *
		 * @return option spec for chaining
		 */
		OptionSpec required();

		/**
		 * Define a {@code defaultValue} for an option.
		 *
		 * @param defaultValue the option default value
		 * @return option spec for chaining
		 */
		OptionSpec defaultValue(String defaultValue);

		/**
		 * Define an optional hint for possible positional mapping.
		 *
		 * @param position the position
		 * @return option spec for chaining
		 */
		OptionSpec position(Integer position);

		/**
		 * Define an {@code arity} for an option.
		 *
		 * @param min the min arity
		 * @param max the max arity
		 * @return option spec for chaining
		 */
		OptionSpec arity(int min, int max);

		/**
		 * Define an {@code arity} for an option.
		 *
		 * @param arity the arity
		 * @return option spec for chaining
		 */
		OptionSpec arity(OptionArity arity);

		/**
		 * Define a {@code label} for an option.
		 *
		 * @param label the label
		 * @return option spec for chaining
		 */
		OptionSpec label(String label);

		/**
		 * Define a {@code completion function} for an option.
		 *
		 * @param completion the completion function
		 * @return option spec for chaining
		 */
		OptionSpec completion(CompletionResolver completion);

		/**
		 * Return a builder for chaining.
		 *
		 * @return a builder for chaining
		 */
		Builder and();
	}

	public enum OptionArity {
		ZERO,
		ZERO_OR_ONE,
		EXACTLY_ONE,
		ZERO_OR_MORE,
		ONE_OR_MORE
	}

	/**
	 * Encapsulates info for {@link TargetSpec}.
	 */
	public interface TargetInfo {

		/**
		 * Get target type
		 *
		 * @return the target type
		 */
		TargetType getTargetType();

		/**
		 * Get the bean.
		 *
		 * @return the bean
		 */
		Object getBean();

		/**
		 * Get the bean method
		 *
		 * @return the bean method
		 */
		Method getMethod();

		/**
		 * Get the function
		 *
		 * @return the function
		 */
		Function getFunction();

		/**
		 * Get the consumer
		 *
		 * @return the consumer
		 */
		Consumer getConsumer();

		static TargetInfo of(Object bean, Method method) {
			return new DefaultTargetInfo(TargetType.METHOD, bean, method, null, null);
		}

		static TargetInfo of(Function function) {
			return new DefaultTargetInfo(TargetType.FUNCTION, null, null, function, null);
		}

		static TargetInfo of(Consumer consumer) {
			return new DefaultTargetInfo(TargetType.CONSUMER, null, null, null, consumer);
		}

		enum TargetType {
			METHOD, FUNCTION, CONSUMER;
		}

		static class DefaultTargetInfo implements TargetInfo {

			private final TargetType targetType;
			private final Object bean;
			private final Method method;
			private final Function function;
			private final Consumer consumer;

			public DefaultTargetInfo(TargetType targetType, Object bean, Method method,
					Function function, Consumer consumer) {
				this.targetType = targetType;
				this.bean = bean;
				this.method = method;
				this.function = function;
				this.consumer = consumer;
			}

			@Override
			public TargetType getTargetType() {
				return targetType;
			}

			@Override
			public Object getBean() {
				return bean;
			}

			@Override
			public Method getMethod() {
				return method;
			}

			@Override
			public Function getFunction() {
				return function;
			}

			@Override
			public Consumer getConsumer() {
				return consumer;
			}
		}
	}

	/**
	 * Spec defining a target.
	 */
	public interface TargetSpec {

		/**
		 * Register a method target.
		 *
		 * @param bean the bean
		 * @param method the method
		 * @param paramTypes the parameter types
		 * @return a target spec for chaining
		 */
		TargetSpec method(Object bean, String method, @Nullable Class... paramTypes);

		/**
		 * Register a method target.
		 *
		 * @param bean the bean
		 * @param method the method
		 * @return a target spec for chaining
		 */
		TargetSpec method(Object bean, Method method);

		/**
		 * Register a function target.
		 *
		 * @param function the function to register
		 * @return a target spec for chaining
		 */
		TargetSpec function(Function function);

		/**
		 * Register a consumer target.
		 *
		 * @param consumer the consumer to register
		 * @return a target spec for chaining
		 */
		TargetSpec consumer(Consumer consumer);

		/**
		 * Return a builder for chaining.
		 *
		 * @return a builder for chaining
		 */
		Builder and();
	}

	/**
	 * Spec defining an alias.
	 */
	public interface AliasSpec {

		/**
		 * Define commands for an alias.
		 *
		 * @param commands the commands
		 * @return a target spec for chaining
		 */
		AliasSpec command(String... commands);

		/**
		 * Define group for an alias.
		 *
		 * @param group the group
		 * @return a target spec for chaining
		 */
		AliasSpec group(String group);

		/**
		 * Return a builder for chaining.
		 *
		 * @return a builder for chaining
		 */
		Builder and();
	}

	/**
	 * Spec defining an exit code.
	 */
	public interface ExitCodeSpec {

		/**
		 * Define mapping from exception to code.
		 *
		 * @param e the exception
		 * @param code the exit code
		 * @return a target spec for chaining
		 */
		ExitCodeSpec map(Class e, int code);

		/**
		 *
		 * @param function
		 * @return
		 */
		ExitCodeSpec map(Function function);

		/**
		 * Return a builder for chaining.
		 *
		 * @return a builder for chaining
		 */
		Builder and();
	}

	/**
	 * Builder interface for {@link CommandRegistration}.
	 */
	public interface Builder {

		/**
		 * Define commands this registration uses. Essentially defines a full set of
		 * main and sub commands. It doesn't matter if full command is defined in one
		 * string or multiple strings as "words" are splitted and trimmed with
		 * whitespaces. You will get result of {@code command subcommand1 subcommand2, ...}.
		 *
		 * @param commands the commands
		 * @return builder for chaining
		 */
		Builder command(String... commands);

		/**
		 * Define {@link InteractionMode} for a command.
		 *
		 * @param mode the interaction mode
		 * @return builder for chaining
		 */
		Builder interactionMode(InteractionMode mode);

		/**
		 * Define a description text for a command.
		 *
		 * @param description the description text
		 * @return builder for chaining
		 */
		Builder description(String description);

		/**
		 * Define an {@link Availability} suppliear for a command.
		 *
		 * @param availability the availability
		 * @return builder for chaining
		 */
		Builder availability(Supplier availability);

		/**
		 * Define a group for a command.
		 *
		 * @param group the group
		 * @return builder for chaining
		 */
		Builder group(String group);

		/**
		 * Define an option what this command should user for. Can be used multiple
		 * times.
		 *
		 * @return option spec for chaining
		 */
		OptionSpec withOption();

		/**
		 * Define a target what this command should execute
		 *
		 * @return target spec for chaining
		 */
		TargetSpec withTarget();

		/**
		 * Define an alias what this command should execute
		 *
		 * @return alias spec for chaining
		 */
		AliasSpec withAlias();

		/**
		 * Define an exit code what this command should execute
		 *
		 * @return exit code spec for chaining
		 */
		ExitCodeSpec withExitCode();

		/**
		 * Builds a {@link CommandRegistration}.
		 *
		 * @return a command registration
		 */
		CommandRegistration build();
	}

	static class DefaultOptionSpec implements OptionSpec {

		private BaseBuilder builder;
		private String[] longNames;
		private Character[] shortNames;
		private ResolvableType type;
		private String description;
		private boolean required;
		private String defaultValue;
		private Integer position;
		private Integer arityMin;
		private Integer arityMax;
		private String label;
		private CompletionResolver completion;

		DefaultOptionSpec(BaseBuilder builder) {
			this.builder = builder;
		}

		@Override
		public OptionSpec longNames(String... names) {
			this.longNames = names;
			return this;
		}

		@Override
		public OptionSpec shortNames(Character... names) {
			this.shortNames = names;
			return this;
		}

		@Override
		public OptionSpec type(Type type) {
			this.type = ResolvableType.forType(type);
			return this;
		}

		@Override
		public OptionSpec description(String description) {
			this.description = description;
			return this;
		}

		@Override
		public OptionSpec required(boolean required) {
			this.required = required;
			return this;
		}

		@Override
		public OptionSpec required() {
			return required(true);
		}

		@Override
		public OptionSpec defaultValue(String defaultValue) {
			this.defaultValue = defaultValue;
			return this;
		}

		@Override
		public OptionSpec position(Integer position) {
			this.position = position;
			return this;
		}

		@Override
		public OptionSpec arity(int min, int max) {
			Assert.isTrue(min > -1, "arity min must be 0 or more");
			Assert.isTrue(max >= min, "arity max must be equal more than min");
			this.arityMin = min;
			this.arityMax = max;
			return this;
		}

		@Override
		public OptionSpec arity(OptionArity arity) {
			switch (arity) {
				case ZERO:
					this.arityMin = 0;
					this.arityMax = 0;
					break;
				case ZERO_OR_ONE:
					this.arityMin = 0;
					this.arityMax = 1;
					break;
				case EXACTLY_ONE:
					this.arityMin = 1;
					this.arityMax = 1;
					break;
				case ZERO_OR_MORE:
					this.arityMin = 0;
					this.arityMax = Integer.MAX_VALUE;
					break;
				case ONE_OR_MORE:
					this.arityMin = 1;
					this.arityMax = Integer.MAX_VALUE;
					break;
				default:
					this.arityMin = 0;
					this.arityMax = 0;
					break;
			}
			return this;
		}

		@Override
		public OptionSpec label(String label) {
			this.label = label;
			return this;
		}

		@Override
		public OptionSpec completion(CompletionResolver completion) {
			this.completion = completion;
			return this;
		}

		@Override
		public Builder and() {
			return builder;
		}

		public String[] getLongNames() {
			return longNames;
		}

		public Character[] getShortNames() {
			return shortNames;
		}

		public ResolvableType getType() {
			return type;
		}

		public String getDescription() {
			return description;
		}

		public boolean isRequired() {
			return required;
		}

		public String getDefaultValue() {
			return defaultValue;
		}

		public Integer getPosition() {
			return position;
		}

		public Integer getArityMin() {
			return arityMin;
		}

		public Integer getArityMax() {
			return arityMax;
		}

		public String getLabel() {
			return label;
		}

		public CompletionResolver getCompletion() {
			return completion;
		}
	}

	static class DefaultTargetSpec implements TargetSpec {

		private BaseBuilder builder;
		private Object bean;
		private Method method;
		private Function function;
		private Consumer consumer;

		DefaultTargetSpec(BaseBuilder builder) {
			this.builder = builder;
		}

		@Override
		public TargetSpec method(Object bean, Method method) {
			this.bean = bean;
			this.method = method;
			return this;
		}

		@Override
		public TargetSpec method(Object bean, String method, Class... paramTypes) {
			this.bean = bean;
			this.method = ReflectionUtils.findMethod(bean.getClass(), method,
					ObjectUtils.isEmpty(paramTypes) ? null : paramTypes);
			return this;
		}

		@Override
		public TargetSpec function(Function function) {
			this.function = function;
			return this;
		}

		@Override
		public TargetSpec consumer(Consumer consumer) {
			this.consumer = consumer;
			return this;
		}

		@Override
		public Builder and() {
			return builder;
		}
	}

	static class DefaultAliasSpec implements AliasSpec {

		private BaseBuilder builder;
		private String[] commands;
		private String group;

		DefaultAliasSpec(BaseBuilder builder) {
			this.builder = builder;
		}

		@Override
		public AliasSpec command(String... commands) {
			Assert.notNull(commands, "commands must be set");
			this.commands = Arrays.asList(commands).stream()
				.flatMap(c -> Stream.of(c.split(" ")))
				.filter(c -> StringUtils.hasText(c))
				.map(c -> c.trim())
				.collect(Collectors.toList())
				.toArray(new String[0]);
			return this;
		}

		@Override
		public AliasSpec group(String group) {
			this.group = group;
			return this;
		}

		@Override
		public Builder and() {
			return builder;
		}
	}

	static class DefaultExitCodeSpec implements ExitCodeSpec {

		private BaseBuilder builder;
		private final List> functions = new ArrayList<>();

		DefaultExitCodeSpec(BaseBuilder builder) {
			this.builder = builder;
		}

		@Override
		public ExitCodeSpec map(Class e, int code) {
			Function f = t -> {
				if (ObjectUtils.nullSafeEquals(t.getClass(), e)) {
					return code;
				}
				return 0;
			};
			this.functions.add(f);
			return this;
		}

		@Override
		public ExitCodeSpec map(Function function) {
			this.functions.add(function);
			return this;
		}

		@Override
		public Builder and() {
			return builder;
		}
	}

	static class DefaultCommandRegistration implements CommandRegistration {

		private String command;
		private InteractionMode interactionMode;
		private String group;
		private String description;
		private Supplier availability;
		private List optionSpecs;
		private DefaultTargetSpec targetSpec;
		private List aliasSpecs;
		private DefaultExitCodeSpec exitCodeSpec;

		public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group,
				String description, Supplier availability, List optionSpecs,
				DefaultTargetSpec targetSpec, List aliasSpecs, DefaultExitCodeSpec exitCodeSpec) {
			this.command = commandArrayToName(commands);
			this.interactionMode = interactionMode;
			this.group = group;
			this.description = description;
			this.availability = availability;
			this.optionSpecs = optionSpecs;
			this.targetSpec = targetSpec;
			this.aliasSpecs = aliasSpecs;
			this.exitCodeSpec = exitCodeSpec;
		}

		@Override
		public String getCommand() {
			return command;
		}

		@Override
		public InteractionMode getInteractionMode() {
			return interactionMode;
		}

		@Override
		public String getGroup() {
			return group;
		}

		@Override
		public String getDescription() {
			return description;
		}

		@Override
		public Availability getAvailability() {
			return availability != null ? availability.get() : Availability.available();
		}

		@Override
		public List getOptions() {
			return optionSpecs.stream()
				.map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(),
						o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(),
						o.getLabel(), o.getCompletion()))
				.collect(Collectors.toList());
		}

		@Override
		public TargetInfo getTarget() {
			if (targetSpec.bean != null) {
				return TargetInfo.of(targetSpec.bean, targetSpec.method);
			}
			if (targetSpec.function != null) {
				return TargetInfo.of(targetSpec.function);
			}
			if (targetSpec.consumer != null) {
				return TargetInfo.of(targetSpec.consumer);
			}
			throw new IllegalArgumentException("No bean, function or consumer defined");
		}

		@Override
		public List getAliases() {
			return this.aliasSpecs.stream()
				.map(spec -> {
					return CommandAlias.of(commandArrayToName(spec.commands), spec.group);
				})
				.collect(Collectors.toList());
		}

		@Override
		public CommandExitCode getExitCode() {
			if (this.exitCodeSpec == null) {
				return CommandExitCode.of();
			}
			else {
				return CommandExitCode.of(exitCodeSpec.functions);
			}
		}

		private static String commandArrayToName(String[] commands) {
			return Arrays.asList(commands).stream()
				.flatMap(c -> Stream.of(c.split(" ")))
				.filter(c -> StringUtils.hasText(c))
				.map(c -> c.trim())
				.collect(Collectors.joining(" "));
		}
	}

	static class DefaultBuilder extends BaseBuilder {

	}

	static class BaseBuilder implements Builder {

		private String[] commands;
		private InteractionMode interactionMode = InteractionMode.ALL;
		private String group;
		private String description;
		private Supplier availability;
		private List optionSpecs = new ArrayList<>();
		private List aliasSpecs = new ArrayList<>();
		private DefaultTargetSpec targetSpec;
		private DefaultExitCodeSpec exitCodeSpec;

		@Override
		public Builder command(String... commands) {
			Assert.notNull(commands, "commands must be set");
			this.commands = Arrays.asList(commands).stream()
				.flatMap(c -> Stream.of(c.split(" ")))
				.filter(c -> StringUtils.hasText(c))
				.map(c -> c.trim())
				.collect(Collectors.toList())
				.toArray(new String[0]);
			return this;
		}

		@Override
		public Builder interactionMode(InteractionMode mode) {
			this.interactionMode = mode != null ? mode : InteractionMode.ALL;
			return this;
		}

		@Override
		public Builder description(String description) {
			this.description = description;
			return this;
		}

		@Override
		public Builder group(String group) {
			this.group = group;
			return this;
		}

		@Override
		public Builder availability(Supplier availability) {
			this.availability = availability;
			return this;
		}

		@Override
		public OptionSpec withOption() {
			DefaultOptionSpec spec = new DefaultOptionSpec(this);
			optionSpecs.add(spec);
			return spec;
		}

		@Override
		public TargetSpec withTarget() {
			DefaultTargetSpec spec = new DefaultTargetSpec(this);
			targetSpec = spec;
			return spec;
		}

		@Override
		public AliasSpec withAlias() {
			DefaultAliasSpec spec = new DefaultAliasSpec(this);
			this.aliasSpecs.add(spec);
			return spec;
		}

		@Override
		public ExitCodeSpec withExitCode() {
			DefaultExitCodeSpec spec = new DefaultExitCodeSpec(this);
			this.exitCodeSpec = spec;
			return spec;
		}

		@Override
		public CommandRegistration build() {
			Assert.notNull(commands, "command cannot be empty");
			Assert.notNull(targetSpec, "target cannot be empty");
			Assert.state(!(targetSpec.bean != null && targetSpec.function != null), "only one target can exist");
			return new DefaultCommandRegistration(commands, interactionMode, group, description, availability,
					optionSpecs, targetSpec, aliasSpecs, exitCodeSpec);
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy