Drop cli-parser, use simple reflection-based parser instead

The main reason of this change is that major changes are required in how
command line arguments are parsed in kotlinc, and it's much easier to
make them in our own codebase (given that the code is short and simple
enough) than in a third-party library
This commit is contained in:
Alexander Udalov
2017-04-05 12:20:29 +03:00
parent 007408c792
commit 78c0111c6e
16 changed files with 225 additions and 857 deletions
@@ -18,7 +18,6 @@ package org.jetbrains.kotlin.cli.common.arguments;
import com.intellij.util.SmartList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli.Argument;
import java.io.Serializable;
import java.util.List;
@@ -29,69 +28,78 @@ public abstract class CommonCompilerArguments implements Serializable {
public static final String PLUGIN_OPTION_FORMAT = "plugin:<pluginId>:<optionName>=<value>";
@GradleOption(DefaultValues.LanguageVersions.class)
@Argument(value = "language-version", description = "Provide source compatibility with specified language version")
@ValueDescription("<version>")
@Argument(
value = "-language-version",
valueDescription = "<version>",
description = "Provide source compatibility with specified language version"
)
public String languageVersion;
@GradleOption(DefaultValues.LanguageVersions.class)
@Argument(value = "api-version", description = "Allow to use declarations only from the specified version of bundled libraries")
@ValueDescription("<version>")
@Argument(
value = "-api-version",
valueDescription = "<version>",
description = "Allow to use declarations only from the specified version of bundled libraries"
)
public String apiVersion;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "nowarn", description = "Generate no warnings")
@Argument(value = "-nowarn", description = "Generate no warnings")
public boolean suppressWarnings;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "verbose", description = "Enable verbose logging output")
@Argument(value = "-verbose", description = "Enable verbose logging output")
public boolean verbose;
@Argument(value = "version", description = "Display compiler version")
@Argument(value = "-version", description = "Display compiler version")
public boolean version;
@Argument(value = "help", alias = "h", description = "Print a synopsis of standard options")
@Argument(value = "-help", shortName = "-h", description = "Print a synopsis of standard options")
public boolean help;
@Argument(value = "X", description = "Print a synopsis of advanced options")
@Argument(value = "-X", description = "Print a synopsis of advanced options")
public boolean extraHelp;
@Argument(value = "Xno-inline", description = "Disable method inlining")
@Argument(value = "-P", valueDescription = PLUGIN_OPTION_FORMAT, description = "Pass an option to a plugin")
public String[] pluginOptions;
// Advanced options
@Argument(value = "-Xno-inline", description = "Disable method inlining")
public boolean noInline;
// TODO Remove in 1.0
@Argument(value = "Xrepeat", description = "Repeat compilation (for performance analysis)")
@ValueDescription("<count>")
@Argument(
value = "-Xrepeat",
valueDescription = "<count>",
description = "Repeat compilation (for performance analysis)"
)
public String repeat;
@Argument(value = "Xskip-metadata-version-check", description = "Load classes with bad metadata version anyway (incl. pre-release classes)")
@Argument(value = "-Xskip-metadata-version-check", description = "Load classes with bad metadata version anyway (incl. pre-release classes)")
public boolean skipMetadataVersionCheck;
@Argument(value = "Xallow-kotlin-package", description = "Allow compiling code in package 'kotlin'")
@Argument(value = "-Xallow-kotlin-package", description = "Allow compiling code in package 'kotlin'")
public boolean allowKotlinPackage;
@Argument(value = "Xplugin", description = "Load plugins from the given classpath")
@ValueDescription("<path>")
@Argument(value = "-Xplugin", valueDescription = "<path>", description = "Load plugins from the given classpath")
public String[] pluginClasspaths;
@Argument(value = "Xmulti-platform", description = "Enable experimental language support for multi-platform projects")
@Argument(value = "-Xmulti-platform", description = "Enable experimental language support for multi-platform projects")
public boolean multiPlatform;
@Argument(value = "Xno-check-impl", description = "Do not check presence of 'impl' modifier in multi-platform projects")
@Argument(value = "-Xno-check-impl", description = "Do not check presence of 'impl' modifier in multi-platform projects")
public boolean noCheckImpl;
@Argument(value = "Xcoroutines=warn")
@Argument(value = "-Xcoroutines=warn", description = "")
public boolean coroutinesWarn;
@Argument(value = "Xcoroutines=error")
@Argument(value = "-Xcoroutines=error", description = "")
public boolean coroutinesError;
@Argument(value = "Xcoroutines=enable")
@Argument(value = "-Xcoroutines=enable", description = "")
public boolean coroutinesEnable;
@Argument(value = "P", description = "Pass an option to a plugin")
@ValueDescription(PLUGIN_OPTION_FORMAT)
public String[] pluginOptions;
public List<String> freeArgs = new SmartList<>();
public List<String> unknownExtraFlags = new SmartList<>();
@@ -17,8 +17,6 @@
package org.jetbrains.kotlin.cli.common.arguments;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli.Argument;
import static org.jetbrains.kotlin.cli.common.arguments.K2JsArgumentConstants.CALL;
import static org.jetbrains.kotlin.cli.common.arguments.K2JsArgumentConstants.NO_CALL;
@@ -27,52 +25,61 @@ public class K2JSCompilerArguments extends CommonCompilerArguments {
public static final long serialVersionUID = 0L;
@GradleOption(DefaultValues.StringNullDefault.class)
@Argument(value = "output", description = "Output file path")
@ValueDescription("<path>")
@Argument(value = "-output", valueDescription = "<path>", description = "Output file path")
public String outputFile;
@GradleOption(DefaultValues.BooleanTrueDefault.class)
@Argument(value = "no-stdlib", description = "Don't use bundled Kotlin stdlib")
@Argument(value = "-no-stdlib", description = "Don't use bundled Kotlin stdlib")
public boolean noStdlib;
@Argument(value = "libraries", description = "Paths to Kotlin libraries with .meta.js and .kjsm files, separated by system file separator")
@ValueDescription("<path>")
@Argument(
value = "-libraries",
valueDescription = "<path>",
description = "Paths to Kotlin libraries with .meta.js and .kjsm files, separated by system file separator")
public String libraries;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "source-map", description = "Generate source map")
@Argument(value = "-source-map", description = "Generate source map")
public boolean sourceMap;
@GradleOption(DefaultValues.BooleanTrueDefault.class)
@Argument(value = "meta-info", description = "Generate .meta.js and .kjsm files with metadata. Use to create a library")
@Argument(value = "-meta-info", description = "Generate .meta.js and .kjsm files with metadata. Use to create a library")
public boolean metaInfo;
@GradleOption(DefaultValues.JsEcmaVersions.class)
@Argument(value = "target", description = "Generate JS files for specific ECMA version")
@ValueDescription("{ v5 }")
@Argument(value = "-target", valueDescription = "{ v5 }", description = "Generate JS files for specific ECMA version")
public String target;
@GradleOption(DefaultValues.JsModuleKinds.class)
@Argument(value = "module-kind", description = "Kind of a module generated by compiler")
@ValueDescription("{ plain, amd, commonjs, umd }")
@Argument(
value = "-module-kind",
valueDescription = "{ plain, amd, commonjs, umd }",
description = "Kind of a module generated by compiler"
)
public String moduleKind;
@GradleOption(DefaultValues.JsMain.class)
@Nullable
@Argument(value = "main", description = "Whether a main function should be called")
@ValueDescription("{" + CALL + "," + NO_CALL + "}")
@Argument(value = "-main", valueDescription = "{" + CALL + "," + NO_CALL + "}", description = "Whether a main function should be called")
public String main;
@Argument(value = "output-prefix", description = "Path to file which will be added to the beginning of output file")
@ValueDescription("<path>")
@Argument(
value = "-output-prefix",
valueDescription = "<path>",
description = "Path to file which will be added to the beginning of output file"
)
public String outputPrefix;
@Argument(value = "output-postfix", description = "Path to file which will be added to the end of output file")
@ValueDescription("<path>")
@Argument(
value = "-output-postfix",
valueDescription = "<path>",
description = "Path to file which will be added to the end of output file"
)
public String outputPostfix;
// Advanced options
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "Xtypedarrays", description = "Translate primitive arrays to JS typed arrays")
@Argument(value = "-Xtypedarrays", description = "Translate primitive arrays to JS typed arrays")
public boolean typedArrays;
@NotNull
@@ -17,102 +17,117 @@
package org.jetbrains.kotlin.cli.common.arguments;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli.Argument;
import org.jetbrains.kotlin.config.JvmTarget;
public class K2JVMCompilerArguments extends CommonCompilerArguments {
public static final long serialVersionUID = 0L;
@Argument(value = "d", description = "Destination for generated class files")
@ValueDescription("<directory|jar>")
@Argument(value = "-d", valueDescription = "<directory|jar>", description = "Destination for generated class files")
public String destination;
@Argument(value = "classpath", alias = "cp", description = "Paths where to find user class files")
@ValueDescription("<path>")
@Argument(value = "-classpath", shortName = "-cp", valueDescription = "<path>", description = "Paths where to find user class files")
public String classpath;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "include-runtime", description = "Include Kotlin runtime in to resulting .jar")
@Argument(value = "-include-runtime", description = "Include Kotlin runtime in to resulting .jar")
public boolean includeRuntime;
@GradleOption(DefaultValues.StringNullDefault.class)
@Argument(value = "jdk-home", description = "Path to JDK home directory to include into classpath, if differs from default JAVA_HOME")
@ValueDescription("<path>")
@Argument(
value = "-jdk-home",
valueDescription = "<path>",
description = "Path to JDK home directory to include into classpath, if differs from default JAVA_HOME"
)
public String jdkHome;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "no-jdk", description = "Don't include Java runtime into classpath")
@Argument(value = "-no-jdk", description = "Don't include Java runtime into classpath")
public boolean noJdk;
@GradleOption(DefaultValues.BooleanTrueDefault.class)
@Argument(value = "no-stdlib", description = "Don't include Kotlin runtime into classpath")
@Argument(value = "-no-stdlib", description = "Don't include Kotlin runtime into classpath")
public boolean noStdlib;
@GradleOption(DefaultValues.BooleanTrueDefault.class)
@Argument(value = "no-reflect", description = "Don't include Kotlin reflection implementation into classpath")
@Argument(value = "-no-reflect", description = "Don't include Kotlin reflection implementation into classpath")
public boolean noReflect;
@Argument(value = "module", description = "Path to the module file to compile")
@ValueDescription("<path>")
@Argument(value = "-module", valueDescription = "<path>", description = "Path to the module file to compile")
public String module;
@Argument(value = "script", description = "Evaluate the script file")
@Argument(value = "-script", description = "Evaluate the script file")
public boolean script;
@Argument(value = "script-templates", description = "Script definition template classes")
@ValueDescription("<fully qualified class name[,]>")
@Argument(
value = "-script-templates",
valueDescription = "<fully qualified class name[,]>",
description = "Script definition template classes"
)
public String[] scriptTemplates;
@Argument(value = "kotlin-home", description = "Path to Kotlin compiler home directory, used for runtime libraries discovery")
@ValueDescription("<path>")
@Argument(
value = "-kotlin-home",
valueDescription = "<path>",
description = "Path to Kotlin compiler home directory, used for runtime libraries discovery"
)
public String kotlinHome;
@Argument(value = "module-name", description = "Module name")
@Argument(value = "-module-name", description = "Module name")
public String moduleName;
@GradleOption(DefaultValues.JvmTargetVersions.class)
@Argument(value = "jvm-target", description = "Target version of the generated JVM bytecode (1.6 or 1.8), default is 1.6")
@ValueDescription("<version>")
@Argument(
value = "-jvm-target",
valueDescription = "<version>",
description = "Target version of the generated JVM bytecode (1.6 or 1.8), default is 1.6"
)
public String jvmTarget;
@GradleOption(DefaultValues.BooleanFalseDefault.class)
@Argument(value = "java-parameters", description = "Generate metadata for Java 1.8 reflection on method parameters")
@Argument(value = "-java-parameters", description = "Generate metadata for Java 1.8 reflection on method parameters")
public boolean javaParameters;
// Advanced options
@Argument(value = "Xno-call-assertions", description = "Don't generate not-null assertion after each invocation of method returning not-null")
@Argument(value = "-Xno-call-assertions", description = "Don't generate not-null assertion after each invocation of method returning not-null")
public boolean noCallAssertions;
@Argument(value = "Xno-param-assertions", description = "Don't generate not-null assertions on parameters of methods accessible from Java")
@Argument(value = "-Xno-param-assertions", description = "Don't generate not-null assertions on parameters of methods accessible from Java")
public boolean noParamAssertions;
@Argument(value = "Xno-optimize", description = "Disable optimizations")
@Argument(value = "-Xno-optimize", description = "Disable optimizations")
public boolean noOptimize;
@Argument(value = "Xreport-perf", description = "Report detailed performance statistics")
@Argument(value = "-Xreport-perf", description = "Report detailed performance statistics")
public boolean reportPerf;
@Argument(value = "Xmultifile-parts-inherit", description = "Compile multifile classes as a hierarchy of parts and facade")
@Argument(value = "-Xmultifile-parts-inherit", description = "Compile multifile classes as a hierarchy of parts and facade")
public boolean inheritMultifileParts;
@Argument(value = "Xskip-runtime-version-check", description = "Allow Kotlin runtime libraries of incompatible versions in the classpath")
@Argument(value = "-Xskip-runtime-version-check", description = "Allow Kotlin runtime libraries of incompatible versions in the classpath")
public boolean skipRuntimeVersionCheck;
@Argument(value = "Xdump-declarations-to", description = "Path to JSON file to dump Java to Kotlin declaration mappings")
@ValueDescription("<path>")
@Argument(
value = "-Xdump-declarations-to",
valueDescription = "<path>",
description = "Path to JSON file to dump Java to Kotlin declaration mappings"
)
public String declarationsOutputPath;
@Argument(value = "Xsingle-module", description = "Combine modules for source files and binary dependencies into a single module")
@Argument(value = "-Xsingle-module", description = "Combine modules for source files and binary dependencies into a single module")
public boolean singleModule;
@Argument(value = "Xadd-compiler-builtins", description = "Add definitions of built-in declarations to the compilation classpath (useful with -no-stdlib)")
@Argument(value = "-Xadd-compiler-builtins", description = "Add definitions of built-in declarations to the compilation classpath (useful with -no-stdlib)")
public boolean addCompilerBuiltIns;
@Argument(value = "Xload-builtins-from-dependencies", description = "Load definitions of built-in declarations from module dependencies, instead of from the compiler")
@Argument(value = "-Xload-builtins-from-dependencies", description = "Load definitions of built-in declarations from module dependencies, instead of from the compiler")
public boolean loadBuiltInsFromDependencies;
@Argument(value = "Xscript-resolver-environment", description = "Script resolver environment in key-value pairs (the value could be quoted and escaped)")
@ValueDescription("<key=value[,]>")
@Argument(
value = "-Xscript-resolver-environment",
valueDescription = "<key=value[,]>",
description = "Script resolver environment in key-value pairs (the value could be quoted and escaped)"
)
public String[] scriptResolverEnvironment;
// Paths to output directories for friend modules.
@@ -16,16 +16,17 @@
package org.jetbrains.kotlin.cli.common.arguments;
import org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli.Argument;
public class K2MetadataCompilerArguments extends CommonCompilerArguments {
public static final long serialVersionUID = 0L;
@Argument(value = "d", description = "Destination for generated .kotlin_metadata files")
@ValueDescription("<directory|jar>")
@Argument(value = "-d", valueDescription = "<directory|jar>", description = "Destination for generated .kotlin_metadata files")
public String destination;
@Argument(value = "classpath", alias = "cp", description = "Paths where to find library .kotlin_metadata files")
@ValueDescription("<path>")
@Argument(
value = "-classpath",
shortName = "-cp",
valueDescription = "<path>",
description = "Paths where to find library .kotlin_metadata files"
)
public String classpath;
}
@@ -1,28 +0,0 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.cli.common.arguments;
import org.jetbrains.annotations.NotNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface ValueDescription {
@NotNull
String value();
}
@@ -16,14 +16,13 @@
package org.jetbrains.kotlin.cli.common.arguments
import com.intellij.util.xmlb.XmlSerializerUtil
import org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli.Args
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import java.util.*
@JvmOverloads fun <A : CommonCompilerArguments> parseArguments(args: Array<String>, arguments: A, ignoreInvalidArguments: Boolean = false) {
val unparsedArgs = Args.parse(arguments, args, false)
@JvmOverloads
fun <A : CommonCompilerArguments> parseArguments(args: Array<String>, arguments: A, ignoreInvalidArguments: Boolean = false) {
val unparsedArgs = parseCommandLineArguments(args, arguments)
val (unknownExtraArgs, unknownArgs) = unparsedArgs.partition { it.startsWith("-X") }
arguments.unknownExtraFlags = unknownExtraArgs
arguments.freeArgs = if (ignoreInvalidArguments) unknownArgs.filterNot { it.startsWith("-") } else unknownArgs
@@ -0,0 +1,77 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.cli.common.arguments
import java.lang.reflect.Field
annotation class Argument(
val value: String,
val shortName: String = "",
val valueDescription: String = "",
val description: String
)
val Argument.isAdvanced: Boolean
get() = value.startsWith("-X") && value.length > 2
fun <A : CommonCompilerArguments> parseCommandLineArguments(args: Array<String>, result: A): List<String> {
data class ArgumentField(val field: Field, val argumentNames: List<String>)
val fields = result::class.java.fields.mapNotNull { field ->
val argument = field.getAnnotation(Argument::class.java)
if (argument != null)
ArgumentField(field, listOfNotNull(argument.value, argument.shortName.takeUnless(String::isEmpty)))
else null
}
val freeArgs = mutableListOf<String>()
var i = 0
while (i < args.size) {
val arg = args[i++]
val field = fields.firstOrNull { (_, names) -> arg in names }?.field
if (field == null) {
freeArgs.add(arg)
continue
}
val value: Any =
if (field.type == Boolean::class.java) true
else {
if (i == args.size) {
throw IllegalArgumentException("No value passed for argument $arg")
}
args[i++]
}
updateField(field, result, value)
}
return freeArgs
}
private fun <A : CommonCompilerArguments> updateField(field: Field, result: A, value: Any) {
when (field.type) {
Boolean::class.java, String::class.java -> field.set(result, value)
Array<String>::class.java -> {
val newElements = (value as String).split(",").toTypedArray()
@Suppress("UNCHECKED_CAST")
val oldValue = field.get(result) as Array<String>?
field.set(result, if (oldValue != null) arrayOf(*oldValue, *newElements) else newElements)
}
else -> throw IllegalStateException("Unsupported argument type: ${field.type}")
}
}
@@ -1,529 +0,0 @@
/*
* Copyright (c) 2005, Sam Pullara. All Rights Reserved.
* You may modify and redistribute as long as this attribution remains.
*/
package org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.PrintStream;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class Args {
/**
* {@link ValueCreator} building object using a one arg constructor taking a {@link String} object as parameter
*/
public static final ValueCreator FROM_STRING_CONSTRUCTOR = new ValueCreator() {
public Object createValue(Class<?> type, String value) {
Object v = null;
try {
Constructor<?> init = type.getDeclaredConstructor(String.class);
v = init.newInstance(value);
} catch (NoSuchMethodException e) {
// ignore
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert " + value + " to type " + type.getName(), e);
}
return v;
}
};
public static final ValueCreator ENUM_CREATOR = new ValueCreator() {
@SuppressWarnings({"unchecked", "rawtypes"})
public Object createValue(Class type, String value) {
if (Enum.class.isAssignableFrom(type)) {
return Enum.valueOf(type, value);
}
return null;
}
};
private static final List<ValueCreator> DEFAULT_VALUE_CREATORS = Arrays.asList(Args.FROM_STRING_CONSTRUCTOR, Args.ENUM_CREATOR);
private static List<ValueCreator> valueCreators = new ArrayList<Args.ValueCreator>(DEFAULT_VALUE_CREATORS);
/**
* A convenience method for parsing and automatically producing error messages.
*
* @param target Either an instance or a class
* @param args The arguments you want to parse and populate
* @return The list of arguments that were not consumed
*/
public static List<String> parseOrExit(Object target, String[] args) {
try {
return parse(target, args);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
Args.usage(target);
System.exit(1);
throw e;
}
}
public static List<String> parse(Object target, String[] args) {
return parse(target, args, true);
}
/**
* Parse a set of arguments and populate the target with the appropriate values.
*
* @param target
* Either an instance or a class
* @param args
* The arguments you want to parse and populate
* @param failOnExtraFlags
* Throw an IllegalArgumentException if extra flags are present
* @return The list of arguments that were not consumed
*/
public static List<String> parse(Object target, String[] args, boolean failOnExtraFlags) {
List<String> arguments = new ArrayList<String>();
arguments.addAll(Arrays.asList(args));
Class<?> clazz;
if (target instanceof Class) {
clazz = (Class) target;
} else {
clazz = target.getClass();
try {
BeanInfo info = Introspector.getBeanInfo(clazz);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
processProperty(target, pd, arguments);
}
} catch (IntrospectionException e) {
// If its not a JavaBean we ignore it
}
}
// Check fields of 'target' class and its superclasses
for (Class<?> currentClazz = clazz; currentClazz != null; currentClazz = currentClazz.getSuperclass()) {
for (Field field : currentClazz.getDeclaredFields()) {
processField(target, field, arguments);
}
}
if (failOnExtraFlags) {
for (String argument : arguments) {
if (argument.startsWith("-")) {
throw new IllegalArgumentException("Invalid argument: " + argument);
}
}
}
return arguments;
}
private static void processField(Object target, Field field, List<String> arguments) {
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
boolean set = false;
for (Iterator<String> i = arguments.iterator(); i.hasNext(); ) {
String arg = i.next();
String prefix = argument.prefix();
String delimiter = argument.delimiter();
if (arg.startsWith(prefix)) {
Object value;
String name = getName(argument, field);
String alias = getAlias(argument);
arg = arg.substring(prefix.length());
Class<?> type = field.getType();
if (arg.equals(name) || (alias != null && arg.equals(alias))) {
i.remove();
value = consumeArgumentValue(type, argument, i);
if (!set) {
setField(type, field, target, value, delimiter);
} else {
addArgument(type, field, target, value, delimiter);
}
set = true;
}
if (set && !type.isArray()) break;
}
}
if (!set && argument.required()) {
String name = getName(argument, field);
throw new IllegalArgumentException("You must set argument " + name);
}
}
}
private static void addArgument(Class type, Field field, Object target, Object value, String delimiter) {
try {
Object[] os = (Object[]) field.get(target);
Object[] vs = (Object[]) getValue(type, value, delimiter);
Object[] s = (Object[]) Array.newInstance(type.getComponentType(), os.length + vs.length);
System.arraycopy(os, 0, s, 0, os.length);
System.arraycopy(vs, 0, s, os.length, vs.length);
field.set(target, s);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set field " + field, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
}
}
private static void addPropertyArgument(Class type, PropertyDescriptor property, Object target, Object value, String delimiter) {
try {
Object[] os = (Object[]) property.getReadMethod().invoke(target);
Object[] vs = (Object[]) getValue(type, value, delimiter);
Object[] s = (Object[]) Array.newInstance(type.getComponentType(), os.length + vs.length);
System.arraycopy(os, 0, s, 0, os.length);
System.arraycopy(vs, 0, s, os.length, vs.length);
property.getWriteMethod().invoke(target, (Object) s);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set property " + property, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Failed to validate argument " + value + " for " + property);
}
}
private static void processProperty(Object target, PropertyDescriptor property, List<String> arguments) {
Method writeMethod = property.getWriteMethod();
if (writeMethod != null) {
Argument argument = writeMethod.getAnnotation(Argument.class);
if (argument != null) {
boolean set = false;
for (Iterator<String> i = arguments.iterator(); i.hasNext(); ) {
String arg = i.next();
String prefix = argument.prefix();
String delimiter = argument.delimiter();
if (arg.startsWith(prefix)) {
Object value;
String name = getName(argument, property);
String alias = getAlias(argument);
arg = arg.substring(prefix.length());
Class<?> type = property.getPropertyType();
if (arg.equals(name) || (alias != null && arg.equals(alias))) {
i.remove();
value = consumeArgumentValue(type, argument, i);
if (!set) {
setProperty(type, property, target, value, delimiter);
} else {
addPropertyArgument(type, property, target, value, delimiter);
}
set = true;
}
if (set && !type.isArray()) break;
}
}
if (!set && argument.required()) {
String name = getName(argument, property);
throw new IllegalArgumentException("You must set argument " + name);
}
}
}
}
/**
* Generate usage information based on the target annotations.
*
* @param target An instance or class.
*/
public static void usage(Object target) {
usage(System.err, target);
}
/**
* Generate usage information based on the target annotations.
*
* @param errStream A {@link java.io.PrintStream} to print the usage information to.
* @param target An instance or class.
*/
public static void usage(PrintStream errStream, Object target) {
Class<?> clazz;
if (target instanceof Class) {
clazz = (Class) target;
} else {
clazz = target.getClass();
}
errStream.println("Usage: " + clazz.getName());
for (Class<?> currentClazz = clazz; currentClazz != null; currentClazz = currentClazz.getSuperclass()) {
for (Field field : currentClazz.getDeclaredFields()) {
fieldUsage(errStream, target, field);
}
}
try {
BeanInfo info = Introspector.getBeanInfo(clazz);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
propertyUsage(errStream, target, pd);
}
} catch (IntrospectionException e) {
// If its not a JavaBean we ignore it
}
}
private static void fieldUsage(PrintStream errStream, Object target, Field field) {
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
String name = getName(argument, field);
String alias = getAlias(argument);
String prefix = argument.prefix();
String delimiter = argument.delimiter();
String description = argument.description();
makeAccessible(field);
try {
Object defaultValue = field.get(target);
Class<?> type = field.getType();
propertyUsage(errStream, prefix, name, alias, type, delimiter, description, defaultValue);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Could not use thie field " + field + " as an argument field", e);
}
}
}
private static void propertyUsage(PrintStream errStream, Object target, PropertyDescriptor field) {
Method writeMethod = field.getWriteMethod();
if (writeMethod != null) {
Argument argument = writeMethod.getAnnotation(Argument.class);
if (argument != null) {
String name = getName(argument, field);
String alias = getAlias(argument);
String prefix = argument.prefix();
String delimiter = argument.delimiter();
String description = argument.description();
try {
Method readMethod = field.getReadMethod();
Object defaultValue;
if (readMethod == null) {
defaultValue = null;
} else {
defaultValue = readMethod.invoke(target, (Object[]) null);
}
Class<?> type = field.getPropertyType();
propertyUsage(errStream, prefix, name, alias, type, delimiter, description, defaultValue);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Could not use thie field " + field + " as an argument field", e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Could not get default value for " + field, e);
}
}
}
}
private static void propertyUsage(PrintStream errStream, String prefix, String name, String alias, Class<?> type, String delimiter, String description, Object defaultValue) {
StringBuilder sb = new StringBuilder(" ");
sb.append(prefix);
sb.append(name);
if (alias != null) {
sb.append(" (");
sb.append(prefix);
sb.append(alias);
sb.append(")");
}
if (type == Boolean.TYPE || type == Boolean.class) {
sb.append(" [flag] ");
sb.append(description);
} else {
sb.append(" [");
if (type.isArray()) {
String typeName = getTypeName(type.getComponentType());
sb.append(typeName);
sb.append("[");
sb.append(delimiter);
sb.append("]");
} else {
String typeName = getTypeName(type);
sb.append(typeName);
}
sb.append("] ");
sb.append(description);
if (defaultValue != null) {
sb.append(" (");
if (type.isArray()) {
List<Object> list = new ArrayList<Object>();
int len = Array.getLength(defaultValue);
for (int i = 0; i < len; i++) {
list.add(Array.get(defaultValue, i));
}
sb.append(list);
} else {
sb.append(defaultValue);
}
sb.append(")");
}
}
errStream.println(sb);
}
private static String getTypeName(Class<?> type) {
String typeName = type.getName();
int beginIndex = typeName.lastIndexOf(".");
typeName = typeName.substring(beginIndex + 1);
return typeName;
}
static String getName(Argument argument, PropertyDescriptor property) {
String name = argument.value();
if (name.equals("")) {
name = property.getName();
}
return name;
}
private static Object consumeArgumentValue(Class<?> type, Argument argument, Iterator<String> i) {
Object value;
if (type == Boolean.TYPE || type == Boolean.class) {
value = true;
} else {
if (i.hasNext()) {
value = i.next();
i.remove();
} else {
throw new IllegalArgumentException("Must have a value for non-boolean argument " + argument.value());
}
}
return value;
}
static void setProperty(Class<?> type, PropertyDescriptor property, Object target, Object value, String delimiter) {
try {
value = getValue(type, value, delimiter);
property.getWriteMethod().invoke(target, value);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set property " + property, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Failed to validate argument " + value + " for " + property);
}
}
static String getAlias(Argument argument) {
String alias = argument.alias();
if (alias.equals("")) {
alias = null;
}
return alias;
}
static String getName(Argument argument, Field field) {
String name = argument.value();
if (name.equals("")) {
name = field.getName();
}
return name;
}
static void setField(Class<?> type, Field field, Object target, Object value, String delimiter) {
makeAccessible(field);
try {
value = getValue(type, value, delimiter);
field.set(target, value);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set field " + field, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
}
}
private static Object getValue(Class<?> type, Object value, String delimiter) throws NoSuchMethodException {
if (type != String.class && type != Boolean.class && type != Boolean.TYPE) {
String string = (String) value;
if (type.isArray()) {
String[] strings = string.split(delimiter);
type = type.getComponentType();
if (type == String.class) {
value = strings;
} else {
Object[] array = (Object[]) Array.newInstance(type, strings.length);
for (int i = 0; i < array.length; i++) {
array[i] = createValue(type, strings[i]);
}
value = array;
}
} else {
value = createValue(type, string);
}
}
return value;
}
private static Object createValue(Class<?> type, String valueAsString) throws NoSuchMethodException {
for (ValueCreator valueCreator : valueCreators) {
Object createdValue = valueCreator.createValue(type, valueAsString);
if (createdValue != null) {
return createdValue;
}
}
throw new IllegalArgumentException(String.format("cannot instanciate any %s object using %s value", type.toString(), valueAsString));
}
private static void makeAccessible(AccessibleObject ao) {
if (ao instanceof Member) {
Member member = (Member) ao;
if (!Modifier.isPublic(member.getModifiers())) {
ao.setAccessible(true);
}
}
}
/**
* Creates a {@link ValueCreator} object able to create object assignable from given type,
* using a static one arg method which name is the the given one taking a String object as parameter
*
* @param compatibleType the base assignable for which this object will try to invoke the given method
* @param methodName the name of the one arg method taking a String as parameter that will be used to built a new value
* @return null if the object could not be created, the value otherwise
*/
public static ValueCreator byStaticMethodInvocation(Class<?> compatibleType, String methodName) {
return new ValueCreator() {
public Object createValue(Class<?> type, String value) {
Object v = null;
if (compatibleType.isAssignableFrom(type)) {
try {
Method m = type.getMethod(methodName, String.class);
return m.invoke(null, value);
} catch (NoSuchMethodException e) {
// ignore
} catch (Exception e) {
throw new IllegalArgumentException(String.format("could not invoke %s#%s to create an obejct from %s", type.toString(), methodName, value));
}
}
return v;
}
};
}
/**
* Allows external extension of the valiue creators.
*
* @param vc another value creator to take into account for trying to create values
*/
public static void registerValueCreator(ValueCreator vc) {
valueCreators.add(vc);
}
/**
* Cleanup of registered ValueCreators (mainly for tests)
*/
public static void resetValueCreators() {
valueCreators.clear();
valueCreators.addAll(DEFAULT_VALUE_CREATORS);
}
public static interface ValueCreator {
/**
* Creates a value object of the given type using the given string value representation;
*
* @param type the type to create an instance of
* @param value the string represented value of the object to create
* @return null if the object could not be created, the value otherwise
*/
public Object createValue(Class<?> type, String value);
}
}
@@ -1,41 +0,0 @@
/*
* Copyright (c) 2005, Sam Pullara. All Rights Reserved.
* You may modify and redistribute as long as this attribution remains.
*/
package org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Argument {
/**
* This is the actual command line argument itself
*/
String value() default "";
/**
* If this is true, then the argument must be set or the parse will fail
*/
boolean required() default false;
/**
* This is the prefix expected for the argument
*/
String prefix() default "-";
/**
* Each argument can have an alias
*/
String alias() default "";
/**
* A description of the argument that will appear in the usage method
*/
String description() default "";
/**
* A delimiter for arguments that are multi-valued.
*/
String delimiter() default ",";
}
@@ -1,111 +0,0 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.cli.common.parser.com.sampullara.cli;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Properties;
/**
* Created by IntelliJ IDEA.
* <p/>
* User: samp
* Date: Nov 11, 2007
* Time: 3:42:27 PM
*/
public class PropertiesArgs {
/**
* Parse properties instead of String arguments. Any additional arguments need to be passed some other way.
* This is often used in a second pass when the property filename is passed on the command line. Because of
* required properties you must be careful to set them all in the property file.
*
* @param target Either an instance or a class
* @param arguments The properties that contain the arguments
*/
public static void parse(Object target, Properties arguments) {
Class clazz;
if (target instanceof Class) {
clazz = (Class) target;
} else {
clazz = target.getClass();
}
for (Field field : clazz.getDeclaredFields()) {
processField(target, field, arguments);
}
try {
BeanInfo info = Introspector.getBeanInfo(clazz);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
processProperty(target, pd, arguments);
}
} catch (IntrospectionException e) {
// If its not a JavaBean we ignore it
}
}
private static void processField(Object target, Field field, Properties arguments) {
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
String name = Args.getName(argument, field);
String alias = Args.getAlias(argument);
Class type = field.getType();
Object value = arguments.get(name);
if (value == null && alias != null) {
value = arguments.get(alias);
}
if (value != null) {
if (type == Boolean.TYPE || type == Boolean.class) {
value = true;
}
Args.setField(type, field, target, value, argument.delimiter());
} else {
if (argument.required()) {
throw new IllegalArgumentException("You must set argument " + name);
}
}
}
}
private static void processProperty(Object target, PropertyDescriptor property, Properties arguments) {
Method writeMethod = property.getWriteMethod();
if (writeMethod != null) {
Argument argument = writeMethod.getAnnotation(Argument.class);
if (argument != null) {
String name = Args.getName(argument, property);
String alias = Args.getAlias(argument);
Object value = arguments.get(name);
if (value == null && alias != null) {
value = arguments.get(alias);
}
if (value != null) {
Class type = property.getPropertyType();
if (type == Boolean.TYPE || type == Boolean.class) {
value = true;
}
Args.setProperty(type, property, target, value, argument.delimiter());
} else {
if (argument.required()) {
throw new IllegalArgumentException("You must set argument " + name);
}
}
}
}
}
}