Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.tomitribe.crest.cmds.CmdMethod Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tomitribe.crest.cmds;
import org.tomitribe.crest.api.Command;
import org.tomitribe.crest.api.CrestAnnotation;
import org.tomitribe.crest.api.Default;
import org.tomitribe.crest.api.Defaults;
import org.tomitribe.crest.api.Err;
import org.tomitribe.crest.api.Exit;
import org.tomitribe.crest.api.In;
import org.tomitribe.crest.api.NotAService;
import org.tomitribe.crest.api.Option;
import org.tomitribe.crest.api.Options;
import org.tomitribe.crest.api.Out;
import org.tomitribe.crest.api.Required;
import org.tomitribe.crest.api.interceptor.CrestInterceptor;
import org.tomitribe.crest.api.interceptor.ParameterMetadata;
import org.tomitribe.crest.cmds.processors.Commands;
import org.tomitribe.crest.cmds.processors.Help;
import org.tomitribe.crest.cmds.processors.Item;
import org.tomitribe.crest.cmds.processors.OptionParam;
import org.tomitribe.crest.cmds.processors.Param;
import org.tomitribe.crest.cmds.targets.SimpleBean;
import org.tomitribe.crest.cmds.targets.Substitution;
import org.tomitribe.crest.cmds.targets.Target;
import org.tomitribe.crest.cmds.utils.CommandLine;
import org.tomitribe.crest.contexts.DefaultsContext;
import org.tomitribe.crest.contexts.SystemPropertiesDefaultsContext;
import org.tomitribe.crest.environments.Environment;
import org.tomitribe.crest.help.CommandJavadoc;
import org.tomitribe.crest.help.Document;
import org.tomitribe.crest.help.DocumentFormatter;
import org.tomitribe.crest.help.DocumentParser;
import org.tomitribe.crest.interceptor.internal.InternalInterceptor;
import org.tomitribe.crest.interceptor.internal.InternalInterceptorInvocationContext;
import org.tomitribe.crest.javadoc.Javadoc;
import org.tomitribe.crest.javadoc.JavadocParser;
import org.tomitribe.crest.term.Screen;
import org.tomitribe.crest.val.BeanValidationImpl;
import org.tomitribe.util.IO;
import org.tomitribe.util.Join;
import org.tomitribe.util.editor.Converter;
import org.tomitribe.util.reflect.Parameter;
import org.tomitribe.util.reflect.Reflection;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.BEAN_OPTION;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.INTERNAL;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.OPTION;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.SERVICE;
import static org.tomitribe.crest.help.DocumentParser.parseOptionDescription;
/**
* @version $Revision$ $Date$
*/
public class CmdMethod implements Cmd {
private static final String[] NO_PREFIX = {""};
private static final Join.NameCallback STRING_NAME_CALLBACK = new Join.NameCallback() {
@Override
public String getName(final String object) {
if (object.startsWith("-")) {
return object;
}
if (object.length() > 1) {
return "--" + object;
}
return "-" + object;
}
};
private final Target target;
private final Method method;
private final String name;
private final List parameters;
private final Class>[] interceptors;
private final DefaultsContext defaultsFinder;
private final Spec spec = new Spec();
private final BeanValidationImpl beanValidation;
private volatile List parameterMetadatas;
public class Spec {
private final Map options = new TreeMap<>();
private final Map aliases = new TreeMap<>();
private final List arguments = new LinkedList<>();
public Map getOptions() {
return Collections.unmodifiableMap(options);
}
public Map getAliases() {
return Collections.unmodifiableMap(aliases);
}
public List getArguments() {
return Collections.unmodifiableList(arguments);
}
}
public CmdMethod(final Method method, final DefaultsContext defaultsFinder,
final BeanValidationImpl beanValidation) {
this(method, new SimpleBean(null), defaultsFinder, beanValidation);
}
public CmdMethod(final Method method, final Target target, final DefaultsContext defaultsFinder,
final BeanValidationImpl beanValidation) {
this.target = target;
this.method = method;
this.defaultsFinder = defaultsFinder;
this.name = Commands.name(method);
this.beanValidation = beanValidation;
final List parameters = buildParams(null, NO_PREFIX, null, Reflection.params(method));
this.parameters = Collections.unmodifiableList(parameters);
this.interceptors = getInterceptors(method);
validate();
}
private Class>[] getInterceptors(final Method method) {
final List> interceptors = new ArrayList<>();
/*
* We add the interceptors in the order we see them and ultimately reflection determines the order
*/
for (final Annotation methodAnnotation : method.getDeclaredAnnotations()) {
if (methodAnnotation instanceof Command) {
final Command command = (Command) methodAnnotation;
if (command.interceptedBy() != null) {
Collections.addAll(interceptors, command.interceptedBy());
}
continue;
}
if (methodAnnotation instanceof CrestInterceptor) {
final CrestInterceptor crestInterceptor = (CrestInterceptor) methodAnnotation;
if (crestInterceptor.value() != null && !crestInterceptor.value().equals(Object.class)) {
interceptors.add(crestInterceptor.value());
} else {
throw new IllegalArgumentException("Use of @CrestInterceptor on an @Command method " +
"requires the class value to be supplied. Please specify the interceptor class on method: " + method);
}
}
final Annotation[] annotations = methodAnnotation.annotationType().getAnnotations();
for (final Annotation annotation : annotations) {
if (annotation instanceof CrestInterceptor) {
final CrestInterceptor crestInterceptor = (CrestInterceptor) annotation;
if (crestInterceptor.value() != null && !crestInterceptor.value().equals(Object.class)) {
interceptors.add(crestInterceptor.value());
} else {
interceptors.add(methodAnnotation.annotationType());
}
}
}
}
return interceptors.toArray(new Class[0]);
}
private List buildParams(final String globalDescription, final String[] inPrefixes,
final Defaults.DefaultMapping[] defaultsMapping, final Iterable params) {
final String[] prefixes = inPrefixes == null ? NO_PREFIX : inPrefixes;
final List parameters = new ArrayList<>();
for (final Parameter parameter : params) {
if (parameter.isAnnotationPresent(Option.class)) {
final Option option = parameter.getAnnotation(Option.class);
final Options options = parameter.getType().getAnnotation(Options.class);
if (options != null) {
final Defaults defaultMappings = parameter.getAnnotation(Defaults.class);
final Defaults.DefaultMapping[] directMapping = parameter.getDeclaredAnnotationsByType(Defaults.DefaultMapping.class);
final ComplexParam complexParam = new ComplexParam(
option.value(), option.description(),
directMapping != null ? directMapping : defaultMappings.value(),
parameter, options.nillable());
parameters.add(complexParam);
} else {
if (parameter.isAnnotationPresent(Defaults.class)) {
throw new IllegalArgumentException("Simple option doesnt support @Defaults, use @Default please");
}
final String shortName = option.value()[0];
final String mainOption = prefixes[0] + shortName;
String def = null;
String description = option.description();
if (defaultsMapping != null) {
for (final Defaults.DefaultMapping mapping : defaultsMapping) {
if (mapping.name().equals(shortName)) {
def = mapping.value();
if (!mapping.description().isEmpty()) {
def = mapping.description();
}
break;
}
}
}
final OptionParam optionParam = new OptionParam(parameter, mainOption, def, (globalDescription != null ? globalDescription : "") + description);
final OptionParam existing = spec.options.put(mainOption, optionParam);
if (existing != null) {
throw new IllegalArgumentException("Duplicate option: " + mainOption);
}
for (int i = 1; i < prefixes.length; i++) {
final String key = prefixes[i] + optionParam.getName();
final OptionParam existingAlias = spec.aliases.put(key, optionParam);
if (existingAlias != null) {
throw new IllegalArgumentException("Duplicate alias: " + key);
}
}
for (int i = 1; i < option.value().length; i++) {
final String alias = option.value()[i];
for (final String prefix : prefixes) {
final String fullAlias = prefix + alias;
final OptionParam existingAlias = spec.aliases.put(fullAlias, optionParam);
if (existingAlias != null) {
throw new IllegalArgumentException("Duplicate alias: " + fullAlias);
}
}
}
parameters.add(optionParam);
}
} else if (parameter.getType().isAnnotationPresent(Options.class)) {
final ComplexParam complexParam = new ComplexParam(null, null, null, parameter, parameter.getType().getAnnotation(Options.class).nillable());
parameters.add(complexParam);
} else {
final Param e = new Param(parameter);
spec.arguments.add(e);
parameters.add(e);
}
}
parameterMetadatas = buildApiParameterViews(parameters);
return parameters;
}
private class ComplexParam extends Param {
private final List parameters;
private final Constructor> constructor;
private final boolean nullable;
private ComplexParam(final String[] prefixes, final String globalDescription,
final Defaults.DefaultMapping[] defaults, final Parameter parent, final boolean nullable) {
super(parent);
this.constructor = selectConstructor(parent);
this.parameters = Collections.unmodifiableList(buildParams(globalDescription, prefixes, defaults, Reflection.params(constructor)));
this.nullable = nullable;
}
private Constructor> selectConstructor(final Parameter parent) {
final List> constructors = Arrays.asList(parent.getType().getConstructors());
constructors.sort(Comparator.comparing(Object::toString));
if (constructors.size() == 1) {
return constructors.get(0);
}
final Constructor> annotatedConstructor = constructors.stream()
.filter(this::isAnnotated)
.findFirst()
.orElse(null);
if (annotatedConstructor != null) {
return annotatedConstructor;
}
return constructors.get(0);
}
private boolean isAnnotated(final Constructor> constructor) {
for (final Annotation[] annotations : constructor.getParameterAnnotations()) {
for (final Annotation annotation : annotations) {
final Class extends Annotation> type = annotation.annotationType();
if (Option.class.equals(type)) return true;
if (Default.class.equals(type)) return true;
if (Required.class.equals(type)) return true;
if (Out.class.equals(type)) return true;
if (In.class.equals(type)) return true;
if (Err.class.equals(type)) return true;
}
}
return false;
}
public Value convert(final Arguments arguments, final Needed needed) {
final List converted = CmdMethod.this.convert(arguments, needed, parameters);
if (nullable) {
boolean allNull = true;
for (final Value val : converted) {
if (val.isProvided()) {
allNull = false;
break;
}
}
if (allNull) {
return new Value(null, false);
}
}
try {
final Object[] args = toArgs(converted).toArray(new Object[converted.size()]);
if (beanValidation != null) {
beanValidation.validateParameters(constructor, args);
}
return new Value(constructor.newInstance(args), true);
} catch (InvocationTargetException e) {
throw toRuntimeException(e.getCause());
} catch (Exception e) {
throw toRuntimeException(e);
}
}
}
public CmdMethod(final Method method, final Target target, final BeanValidationImpl beanValidation) {
this(method, target, new SystemPropertiesDefaultsContext(), beanValidation);
}
public Method getMethod() {
return method;
}
public List getArgumentParameters() {
return Collections.unmodifiableList(spec.arguments);
}
private void validate() {
for (final Param param : spec.arguments) {
if (param.isAnnotationPresent(Default.class)) {
throw new IllegalArgumentException("@Default only usable with @Option parameters.");
}
if (!param.isListable() && param.isAnnotationPresent(Required.class)) {
throw new IllegalArgumentException("@Required only usable with @Option parameters and lists.");
}
}
}
/**
* Returns a single line description of the command
*/
@Override
public String getUsage() {
String commandName = name;
final Class> declaringClass = method.getDeclaringClass();
final Map commands = Commands.get(declaringClass);
if (commands.size() == 1 && commands.values().iterator().next() instanceof CmdGroup) {
final CmdGroup cmdGroup = (CmdGroup) commands.values().iterator().next();
commandName = cmdGroup.getName() + " " + name;
}
final String usage = usage();
if (usage != null) {
if (!usage.startsWith(commandName)) {
return commandName + " " + usage;
} else {
return usage;
}
}
final List args = new ArrayList<>();
for (final Param parameter : spec.arguments) {
boolean skip = Environment.class.isAssignableFrom(parameter.getType());
for (final Annotation a : parameter.getAnnotations()) {
final CrestAnnotation crestAnnotation = a.annotationType().getAnnotation(CrestAnnotation.class);
if (crestAnnotation != null) {
skip = crestAnnotation.skipUsage();
break;
}
}
if (!skip) {
skip = parameter.getAnnotation(NotAService.class) == null &&
Environment.ENVIRONMENT_THREAD_LOCAL.get().findService(parameter.getType()) != null;
}
if (skip) {
continue;
}
args.add(parameter.getDisplayType().replace("[]", "..."));
}
return String.format("%s %s %s", commandName, args.size() == method.getParameterTypes().length ? "" : "[options]",
Join.join(" ", args)).trim();
}
private String usage() {
final Command command = method.getAnnotation(Command.class);
if (command == null) {
return null;
}
if ("".equals(command.usage())) {
return null;
}
return command.usage();
}
@Override
public String getName() {
return name;
}
@Override
public Object exec(final Map, InternalInterceptor> globalInterceptors, final String... rawArgs) {
final List list;
try {
list = parse(rawArgs);
} catch (final Exception e) {
/*
* If any exception in the chain was annotated with @Exit
* then we want to let that exception handle the error message
*/
final RuntimeException handled = getExitCode(e);
if (handled != null) {
final Exit exit = handled.getClass().getAnnotation(Exit.class);
if (exit.help()) {
reportWithHelp(e);
}
throw handled;
}
reportWithHelp(e);
throw toRuntimeException(e);
}
return exec(globalInterceptors, list);
}
private RuntimeException getExitCode(final Throwable e) {
if (e == null) return null;
if (e instanceof RuntimeException && e.getClass().isAnnotationPresent(Exit.class)) {
return (RuntimeException) e;
}
return getExitCode(e.getCause());
}
public Object exec(final Map, InternalInterceptor> globalInterceptors, final List list) {
if (interceptors == null || interceptors.length == 0) {
return doInvoke(list);
}
return new InternalInterceptorInvocationContext(globalInterceptors, interceptors, name, parameterMetadatas, method, list) {
@Override
protected Object doInvoke(final List parameters) {
return CmdMethod.this.doInvoke(parameters);
}
}.proceed();
}
private List buildApiParameterViews(final List parameters) {
final List parameterMetadatas = new ArrayList<>();
for (final Param param : parameters) {
// precompute all values to get a fast runtime immutable structure
final ParameterMetadata.ParamType type = OptionParam.class.isInstance(param) ? OPTION :
(ComplexParam.class.isInstance(param) ? BEAN_OPTION :
(Environment.class.isAssignableFrom(param.getType()) || param.getAnnotation(In.class) != null
|| param.getAnnotation(Out.class) != null || param.getAnnotation(Err.class) != null ? INTERNAL :
(Environment.ENVIRONMENT_THREAD_LOCAL.get().findService(param.getType()) != null ? SERVICE : ParameterMetadata.ParamType.PLAIN)));
if (type == INTERNAL) { // some pre runtime checks
if (param.isAnnotationPresent(In.class)) {
if (InputStream.class != param.getType()) {
throw new IllegalArgumentException("@In only supports InputStream injection");
}
} else if (param.isAnnotationPresent(Out.class)) {
if (PrintStream.class != param.getType()) {
throw new IllegalArgumentException("@Out only supports PrintStream injection");
}
} else if (param.isAnnotationPresent(Err.class)) {
if (PrintStream.class != param.getType()) {
throw new IllegalArgumentException("@Err only supports PrintStream injection");
}
}
}
final String name = type == ParameterMetadata.ParamType.OPTION ? OptionParam.class.cast(param).getName() : null;
final List nested = type == BEAN_OPTION ? buildApiParameterViews(ComplexParam.class.cast(param).parameters) : null;
final ParameterMetadata parameterMetadata = new ParameterMetadata() {
@Override
public ParamType getType() {
return type;
}
@Override
public String getName() {
return name;
}
@Override
public List getNested() {
return nested;
}
@Override
public Type getReflectType() {
return param.getGenericType();
}
@Override
public boolean isListable() {
return param.isListable();
}
@Override
public Class> getComponentType() {
return param.getListableType();
}
@Override
public String toString() {
return getType() + ": " + getReflectType() + ", name=" + getName() + ", nested=" + getNested();
}
};
param.setApiView(parameterMetadata);
parameterMetadatas.add(parameterMetadata);
}
return unmodifiableList(parameterMetadatas);
}
protected Object doInvoke(final List list) {
final Object[] args;
try {
args = list.toArray();
if (beanValidation != null) {
beanValidation.validateParameters(target.getInstance(method), method, args);
}
} catch (final Exception e) {
reportWithHelp(e);
throw toRuntimeException(e);
}
try {
return target.invoke(method, args);
} catch (final InvocationTargetException e) {
final Throwable cause = e.getCause();
final Exit exit = cause.getClass().getAnnotation(Exit.class);
if (exit != null && exit.help()) {
reportWithHelp(cause);
}
throw new CommandFailedException(cause, getName());
} catch (final Throwable e) {
throw toRuntimeException(e);
}
}
private void reportWithHelp(final Throwable e) {
final PrintStream err = Environment.ENVIRONMENT_THREAD_LOCAL.get().getError();
if (beanValidation == null) {
err.println(e.getMessage());
} else {
for (final String message : beanValidation.messages(e).orElseGet(() -> singletonList(e.getMessage()))) {
err.println(message);
}
}
help(err);
throw new HelpPrintedException(e);
}
public static RuntimeException toRuntimeException(final Throwable e) {
if (e instanceof RuntimeException) {
return (RuntimeException) e;
}
return new IllegalArgumentException(e);
}
public Map getOptionParameters() {
return Collections.unmodifiableMap(spec.options);
}
public Spec getSpec() {
return spec;
}
@Override
public void manual(final PrintStream out) {
final CommandJavadoc commandJavadoc = CommandJavadoc.getCommandJavadocs(method, name);
if (commandJavadoc == null) {
help(out);
return;
}
final Javadoc javadoc = JavadocParser.parse(commandJavadoc.getJavadoc());
if (javadoc.isEmpty()){
help(out);
return;
}
final Document.Builder manual = Document.builder()
.heading("NAME")
.paragraph(name)
.heading("SYNOPSIS")
.paragraph(getUsage());
{
final Document description = DocumentParser.parser(javadoc.getContent());
if (description.getElements().size() > 0) {
manual.heading("DESCRIPTION")
.inline(description);
}
}
if (spec.getOptions().size() > 0) {
manual.heading("OPTIONS");
final List- items = Help.getItems(method, name, spec.options.values(), commandJavadoc);
for (final Item item : items) {
final Document.Builder description = Document.builder();
if (item.getDescription() != null) {
description.inline(parseOptionDescription(item.getDescription()));
}
if (has(item.getNote())) {
final String notes = Join.join(". ", item.getNote());
description.paragraph(notes);
}
manual.element(new org.tomitribe.crest.help.Option(item.getFlag(), description.build()));
}
}
if (javadoc.getDeprecated() != null) {
manual.heading("Deprecated");
final Javadoc.Deprecated deprecated = javadoc.getDeprecated();
if (deprecated.getContent() == null || deprecated.getContent().length() == 0) {
manual.paragraph("Command has been marked deprecated.");
} else {
manual.paragraph(deprecated.getContent());
}
}
if (has(javadoc.getSees())) {
manual.heading("SEE ALSO");
javadoc.getSees().forEach(see -> manual.paragraph(see.getContent()));
}
if (has(javadoc.getAuthors())) {
manual.heading("AUTHORS");
javadoc.getAuthors().forEach(author -> manual.paragraph(author.getContent()));
}
final Environment environment = Environment.ENVIRONMENT_THREAD_LOCAL.get();
final boolean color = !environment.getEnv().containsKey("NOCOLOR");
final int guess = Screen.guessWidth();
int width = guess > 0 ? guess : 100;
// Man pages seem to look like this, it looks nice
if (width > 120) width -= 7;
final DocumentFormatter formatter = new DocumentFormatter(width, color);
final String format = formatter.format(manual.build());
final boolean less = !environment.getEnv().containsKey("NOLESS");
if (!less) {
out.print(format);
} else {
try {
final File tempFile = File.createTempFile("help-", ".txt");
tempFile.deleteOnExit();
IO.copy(IO.read(format), tempFile);
final Process process = new ProcessBuilder("less", "-r")
.inheritIO()
.redirectInput(tempFile)
.start();
final int exit = process.waitFor();
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (InterruptedException e) {
Thread.interrupted();
throw new IllegalStateException(e);
}
}
}
public static boolean has(final List> list) {
return list != null && list.size() > 0;
}
@Override
public void help(final PrintStream out) {
out.println();
out.print("Usage: ");
out.println(getUsage());
out.println();
Help.optionHelp(method, getName(), spec.options.values(), out);
}
public List
parse(final String... rawArgs) {
return convert(new Arguments(rawArgs));
}
public class Needed {
private int count;
public Needed(int count) {
this.count = count;
}
}
private List convert(final Arguments args) {
final Needed needed = new Needed(spec.arguments.size());
final List converted = convert(args, needed, parameters);
if (!args.list.isEmpty()) {
throw new IllegalArgumentException("Excess arguments: " + Join.join(", ", args.list));
}
if (!args.options.isEmpty()) {
throw new IllegalArgumentException("Unknown arguments: " + Join.join(", ", STRING_NAME_CALLBACK, args.options.keySet()));
}
return toArgs(converted);
}
private List toArgs(final List converted) {
final List objects = new ArrayList<>(converted.size());
for (final Value v : converted) {
objects.add(v.getValue());
}
return objects;
}
private List convert(Arguments args, Needed needed, List parameters1) {
/**
* Here we iterate over the method's parameters and convert strings into their equivalent Option or Arg value.
*
* The result is a List of objects that matches perfectly the available of arguments required to pass into the
* java.lang.reflect.Method.invoke() method.
*
* Thus, iteration order is very significant in this loop.
*/
final List converted = new ArrayList<>(args.options.size() /*approx but better than nothing*/);
final Environment environment = Environment.ENVIRONMENT_THREAD_LOCAL.get();
for (final Param parameter : parameters1) {
final ParameterMetadata apiView = parameter.getApiView();
switch (apiView.getType()) {
case INTERNAL: {
if (parameter.isAnnotationPresent(In.class)) {
converted.add(new Value(environment.getInput(), false));
needed.count--;
} else if (parameter.isAnnotationPresent(Out.class)) {
converted.add(new Value(environment.getOutput(), false));
needed.count--;
} else if (parameter.isAnnotationPresent(Err.class)) {
converted.add(new Value(environment.getError(), false));
needed.count--;
} else if (Environment.class.isAssignableFrom(parameter.getType())) {
converted.add(new Value(environment, false));
needed.count--;
}
break;
}
case SERVICE:
converted.add(new Value(environment.findService(parameter.getType()), false));
break;
case PLAIN:
if (!args.list.isEmpty()) {
needed.count--;
converted.add(fillPlainParameter(args, needed, parameter));
} else {
throw new MissingArgumentException(parameter.getDisplayType().replace("[]", "..."));
}
break;
case BEAN_OPTION:
converted.add(ComplexParam.class.cast(parameter).convert(args, needed));
break;
case OPTION:
converted.add(fillOptionParameter(args, parameter, apiView.getName()));
break;
default:
throw new IllegalStateException("Unsupported ParamType: " + apiView.getType());
}
}
return converted;
}
private Value fillOptionParameter(final Arguments args, final Param parameter, final String name) {
final String value = args.options.remove(name);
if (parameter.isListable()) {
return convert(parameter, OptionParam.getSeparatedValues(value), name);
}
final Object convert;
try {
convert = Converter.convert(value, parameter.getType(), name);
} catch (final IllegalArgumentException e) {
/*
* If something we attempted to conver threw an exception that
* was annotated with @Exit, then we should unwrap it and throw
* that exact exception so the help system can handle it properly.
*/
if (e.getCause() != null &&
e.getCause() instanceof RuntimeException &&
e.getCause().getClass().isAnnotationPresent(Exit.class)) {
throw (RuntimeException) e.getCause();
}
throw e;
}
return new Value(convert, value != null && !value.equals(OptionParam.class.cast(parameter).getDefaultValue()));
}
private Value fillPlainParameter(final Arguments args, final Needed needed, final Param parameter) {
if (parameter.isListable()) {
final List glob = new ArrayList<>(args.list.size());
for (int i = args.list.size(); i > needed.count; i--) {
glob.add(args.list.remove(0));
}
return convert(parameter, glob, null);
} else {
final String value = args.list.remove(0);
return new Value(Converter.convert(value, parameter.getType(), parameter.getDisplayType().replace("[]", "...")), value != null);
}
}
private static Value convert(final Param parameter, final List values, final String name) {
final Class> type = parameter.getListableType();
if (parameter.isAnnotationPresent(Required.class) && values.isEmpty()) {
if (parameter instanceof OptionParam) {
final OptionParam optionParam = (OptionParam) parameter;
throw new IllegalArgumentException(String.format("--%s must be specified at least once",
optionParam.getName()));
} else {
throw new IllegalArgumentException(String.format("Argument for %s requires at least one value",
parameter.getDisplayType().replace("[]", "...")));
}
}
final String description = name == null ? "[" + type.getSimpleName() + "]" : name;
if (Enum.class.isAssignableFrom(type) && isBoolean(values)) {
final boolean all = "true".equals(values.get(0));
values.clear();
if (all) {
final Class extends Enum> elementType = (Class extends Enum>) type;
final EnumSet extends Enum> enums = EnumSet.allOf(elementType);
for (final Enum e : enums) {
values.add(e.name());
}
}
}
if (parameter.getType().isArray()) {
final Object array = Array.newInstance(type, values.size());
int i = 0;
for (final String string : values) {
Array.set(array, i++, Converter.convert(string, type, description));
}
return new Value(array, !values.isEmpty());
}
final Collection collection = instantiate((Class extends Collection>) parameter.getType());
for (final String string : values) {
collection.add(Converter.convert(string, type, description));
}
return new Value(collection, !collection.isEmpty());
}
private static boolean isBoolean(final List values) {
if (values.size() != 1) {
return false;
}
if ("true".equals(values.get(0))) {
return true;
}
if ("false".equals(values.get(0))) {
return true;
}
return false;
}
public static Collection instantiate(final Class extends Collection> aClass) {
if (aClass.isInterface()) {
// Sub iterfaces listed first
// Sets
if (NavigableSet.class.isAssignableFrom(aClass)) {
return new TreeSet<>();
}
if (SortedSet.class.isAssignableFrom(aClass)) {
return new TreeSet<>();
}
if (Set.class.isAssignableFrom(aClass)) {
return new LinkedHashSet<>();
}
// Queues
if (Deque.class.isAssignableFrom(aClass)) {
return new LinkedList<>();
}
if (Queue.class.isAssignableFrom(aClass)) {
return new LinkedList<>();
}
// Lists
if (List.class.isAssignableFrom(aClass)) {
return new ArrayList<>();
}
// Collection
if (Collection.class.isAssignableFrom(aClass)) {
return new LinkedList<>();
}
// Iterable
if (Iterable.class.isAssignableFrom(aClass)) {
return new LinkedList<>();
}
throw new IllegalStateException("Unsupported Collection type: " + aClass.getName());
}
if (Modifier.isAbstract(aClass.getModifiers())) {
throw new IllegalStateException("Unsupported Collection type: " + aClass.getName() + " - Type is Abstract");
}
try {
final Constructor extends Collection> constructor = aClass.getConstructor();
return constructor.newInstance();
} catch (final NoSuchMethodException e) {
throw new IllegalStateException("Unsupported Collection type: " + aClass.getName() + " - No default "
+ "constructor");
} catch (final Exception e) {
throw new IllegalStateException("Cannot construct java.util.Collection type: " + aClass.getName(), e);
}
}
public Map getDefaults() {
final Map options = new HashMap<>();
for (final OptionParam parameter : spec.options.values()) {
options.put(parameter.getName(), parameter.getDefaultValue());
}
return options;
}
private class Arguments {
private final List list = new ArrayList<>();
private final Map options = new HashMap<>();
private Arguments(final String[] rawArgs) {
final Map defaults = getDefaults();
final Map supplied = new HashMap<>();
final List invalid = new ArrayList<>();
final Set repeated = new HashSet<>();
// Read in and apply the options specified on the command line
for (final String arg : rawArgs) {
if (arg.startsWith("--")) {
getCommand("--", arg, defaults, supplied, invalid, repeated);
} else if (arg.startsWith("-")) {
getCommand("-", arg, defaults, supplied, invalid, repeated);
} else {
this.list.add(arg);
}
}
checkInvalid(invalid);
checkRequired(supplied);
checkRepeated(repeated);
interpret(defaults);
this.options.putAll(defaults);
this.options.putAll(supplied);
}
private void getCommand(final String defaultPrefix,
final String arg,
final Map defaults,
final Map supplied,
final List invalid,
final Set repeated) {
String name;
String value;
String prefix = defaultPrefix;
if (arg.indexOf('=') > 0) {
name = arg.substring(arg.indexOf(prefix) + prefix.length(), arg.indexOf('='));
if (!defaults.containsKey(name) && !spec.aliases.containsKey(name)) {
name = arg.substring(0, arg.indexOf('='));
prefix = "";
}
value = arg.substring(arg.indexOf('=') + 1);
} else {
if (arg.startsWith("--no-")) {
name = arg.substring(5);
value = "false";
} else {
name = arg.substring(prefix.length());
value = "true";
}
}
if ("-".equals(prefix)) {
// reject -del=true
if (arg.indexOf('=') > -1 && name.length() > 1) {
invalid.add(prefix + name);
return;
}
final Set opts = new HashSet<>();
for (final String opt : name.split("(?!^)")) {
opts.add(opt);
}
for (final String opt : opts) {
processOption(prefix, opt, value, defaults, supplied, invalid, repeated);
}
}
// reject --d and --d=true
if ("--".equals(prefix)) {
if (name.length() == 1) {
invalid.add(prefix + name);
return;
}
processOption(prefix, name, value, defaults, supplied, invalid, repeated);
}
if (prefix.isEmpty()) {
processOption(prefix, name, value, defaults, supplied, invalid, repeated);
}
}
private void processOption(final String prefix,
final String optName,
final String optValue,
final Map defaults,
final Map supplied,
final List invalid,
final Set repeated) {
String name = optName;
String value = optValue;
if (!defaults.containsKey(name) && spec.aliases.containsKey(name)) {
// check the options to find see if name is an alias for an option
// if it is, get the actual optionparam name
name = spec.aliases.get(name).getName();
}
if (defaults.containsKey(name)) {
final boolean isList = defaults.get(name) != null && defaults.get(name).startsWith(OptionParam.LIST_TYPE);
final String existing = supplied.get(name);
if (isList) {
if (existing == null) {
value = OptionParam.LIST_TYPE + value;
} else {
value = existing + OptionParam.LIST_SEPARATOR + value;
}
} else if (existing != null) {
repeated.add(name);
}
supplied.put(name, value);
} else {
invalid.add(prefix + name);
}
}
private void interpret(final Map map) {
for (final Map.Entry entry : map.entrySet()) {
if (entry.getValue() == null) {
continue;
}
final String value = Substitution.format(target, method, entry.getValue(), defaultsFinder);
map.put(entry.getKey(), value);
}
}
private void checkInvalid(final List invalid) {
if (!invalid.isEmpty()) {
throw new IllegalArgumentException("Unknown options: " + Join.join(", ", STRING_NAME_CALLBACK, invalid));
}
}
private void checkRequired(final Map supplied) {
final List required = new ArrayList<>();
for (final Param parameter : spec.options.values()) {
if (!parameter.isAnnotationPresent(Required.class)) {
continue;
}
final Option option = parameter.getAnnotation(Option.class);
for (String optionValue : option.value()) {
if (!supplied.containsKey(optionValue)) {
required.add(optionValue);
}
}
}
if (!required.isEmpty()) {
throw new IllegalArgumentException("Required: " + Join.join(", ", STRING_NAME_CALLBACK, required));
}
}
private void checkRepeated(final Set repeated) {
if (!repeated.isEmpty()) {
throw new IllegalArgumentException("Cannot be specified more than once: " + Join.join(", ", repeated));
}
}
}
@Override
public String toString() {
return "Command{" + "name='" + name + '\'' + '}';
}
@Override
public Collection complete(final String buffer, final int cursorPosition) {
final List result = new ArrayList<>();
final String commandLine = buffer.substring(0, cursorPosition);
final String[] args = CommandLine.translateCommandline(commandLine);
if (args != null && args.length > 0) {
final String lastArg = args[args.length - 1];
if (lastArg.startsWith("--")) {
result.addAll(findMatchingOptions(lastArg.substring(2), false));
} else if (lastArg.startsWith("-")) {
result.addAll(findMatchingOptions(lastArg.substring(1), true));
}
}
return result;
}
private Collection findMatcingParametersOptions(String prefix, boolean isIncludeAliasChar) {
final List result = new ArrayList<>();
for (Param param : parameters) {
if (param instanceof OptionParam) {
final OptionParam optionParam = (OptionParam) param;
final String optionParamName = optionParam.getName();
if (optionParamName.startsWith(prefix)) {
if (optionParamName.startsWith("-")) {
result.add(optionParamName);
continue;
}
if (optionParamName.length() > 1) {
result.add("--" + optionParamName);
continue;
}
if (isIncludeAliasChar) {
result.add("-" + optionParamName);
}
}
}
}
return result;
}
private Collection findMatchingAliasOptions(String prefix, boolean isIncludeAliasChar) {
final List result = new ArrayList<>();
for (String alias : spec.aliases.keySet()) {
if (alias.startsWith(prefix)) {
if (alias.startsWith("-")) {
result.add(alias);
} else if (alias.length() > 1) {
result.add("--" + alias);
}
if (isIncludeAliasChar && alias.length() == 1) {
result.add("-" + alias);
}
}
}
return result;
}
private Collection findMatchingOptions(String prefix, boolean isIncludeAliasChar) {
List results = new ArrayList<>();
results.addAll(findMatcingParametersOptions(prefix, isIncludeAliasChar));
results.addAll(findMatchingAliasOptions(prefix, isIncludeAliasChar));
return results;
}
public static final class Value {
private final Object value;
private final boolean provided;
protected Value(final Object value, final boolean provided) {
this.value = value;
this.provided = provided;
}
public Object getValue() {
return value;
}
public boolean isProvided() {
return provided;
}
}
}