Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using method arguments to specify the command line interface #139

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions args4j/src/org/kohsuke/args4j/MethodCmdLineParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package org.kohsuke.args4j;

import org.kohsuke.args4j.spi.ArgumentImpl;
import org.kohsuke.args4j.spi.ArraySetter;
import org.kohsuke.args4j.spi.RestOfArgumentsHandler;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
* A variation of {@link CmdLineParser} that can parse {@link Argument} and {@link Option}
* annotations on arguments of a {@link Method}, as opposed to fields of a dedicated bean
*
* Example usage:
* <code>
* class Main {
* public static void main(String[] args) {
* MethodCmdLineParser.invoke(Main.class, args)
* }
*
* public static void typesafeMain(
* @Argument(usage="...") File arg1,
* @Option(name="flag", usage="...") boolean flag,
* Integer alsoAnArgument,
* @Nullable String optionalArgument) {
* ...
* }
* }
* </code>
*/
public class MethodCmdLineParser extends CmdLineParser {

public MethodCmdLineParser(
Method method,
Object[] output,
ParserProperties parserProperties) {
super(null, parserProperties);

Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] parametersAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterTypes.length; i++) {
ArraySetter arraySetter = new ArraySetter(output, i, parameterTypes[i]);
Option optionAnnotation = findAnnotation(parametersAnnotations[i], Option.class);
if (optionAnnotation != null) {
addOption(arraySetter, optionAnnotation);
} else {
addArgument(
arraySetter,
getArgumentAnnotation(i, parametersAnnotations, parameterTypes));
}
}
}

public static Object invoke(Method method, String... args) throws CmdLineException {
return invoke(method, ParserProperties.defaults(), args);
}

public static Object invoke(
Method method,
ParserProperties parserProperties,
String... args) throws CmdLineException {
Object[] values = new Object[method.getParameterTypes().length];

new MethodCmdLineParser(method, values, parserProperties)
.parseArgument(args);

try {
method.setAccessible(true);
return method.invoke(null, values);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}

private static ArgumentImpl getArgumentAnnotation(
int i,
Annotation[][] parametersAnnotations,
Class<?>[] parameterTypes) {
Annotation[] annotations = parametersAnnotations[i];
boolean explicitlyNullable = findAnnotation(annotations, "Nullable") != null;
boolean explicitlyNotNull = findAnnotation(annotations, "NotNull") != null
|| findAnnotation(annotations, "NonNull") != null;
try {
Class<?> type = parameterTypes[i];
ArgumentImpl argument =
ArgumentImpl.copyOf(findAnnotation(annotations, Argument.class));
argument = argument != null ? argument : new ArgumentImpl();
argument.index = i;
if (explicitlyNullable) {
argument.required = false;
} else if (explicitlyNotNull) {
argument.required = true;
} else {
// leave as is
}
if (isLast(i, parameterTypes) && type.equals(String.class)) {
argument.handler = RestOfArgumentsHandler.class;
}
return argument;
} catch (ClassNotFoundException c) {
throw new RuntimeException(c);
}
}

private static Annotation findAnnotation(Annotation[] parameterAnnotations, String simpleName) {
for (Annotation annotation : parameterAnnotations) {
if (annotation.annotationType().getSimpleName().equals(simpleName)) {
return annotation;
}
}
return null;
}

private static <T extends Annotation> T findAnnotation(Annotation[] parameterAnnotations, Class<T> c) {
for (Annotation annotation : parameterAnnotations) {
if (c.isInstance(annotation)) {
return (T) annotation;
}
}
return null;
}

private static boolean isLast(int i, Object[] array) {
return i + 1 == array.length;
}

public static Method getMethod(Class c, String methodName) {
for (Method method : c.getDeclaredMethods()) {
if (method.getName().equals(methodName)) {
return method;
}
}
return null;
}

public static Object invoke(Class hasSinglePublicStaticMethod, String... args) throws CmdLineException {
return invoke(getMainMethod(hasSinglePublicStaticMethod), args);
}

public static Method getMainMethod(Class hasSinglePublicStaticMethod) {
Method stringMain = null;
Method result = null;
for (Method current : hasSinglePublicStaticMethod.getDeclaredMethods()) {
int modifiers = current.getModifiers();
if (!Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
continue;
}
if (current.getName().equals("main") &&
Arrays.equals(current.getParameterTypes(), new Class[]{String[].class})) {
stringMain = current;
continue;
}
if (result != null) {
throw new IllegalArgumentException("Multiple suitable methods found:\n" + result + "\n" + current);
}
result = current;
}
if (result == null) {
result = stringMain;
}
if (result == null) {
throw new IllegalArgumentException("No suitable method found in " + hasSinglePublicStaticMethod);
}
return result;
}
}
22 changes: 21 additions & 1 deletion args4j/src/org/kohsuke/args4j/spi/ArgumentImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,27 @@
* @author Jan Materne
*/
public class ArgumentImpl extends AnnotationImpl implements Argument {

public ArgumentImpl() throws ClassNotFoundException {
this(new ConfigElement());
}

public ArgumentImpl(ConfigElement ce) throws ClassNotFoundException {
super(Argument.class,ce);
}
}

public static ArgumentImpl copyOf(Argument toClone) throws ClassNotFoundException {
if (toClone == null) {
return null;
}
ConfigElement configElement = new ConfigElement();
Class<? extends OptionHandler> handler = toClone.handler();
configElement.handler = handler == null ? null : handler.getName();
configElement.usage = toClone.usage();
configElement.metavar = toClone.metaVar();
configElement.multiValued = toClone.multiValued();
configElement.required = toClone.required();
configElement.hidden = toClone.hidden();
return new ArgumentImpl(configElement);
}
}
39 changes: 39 additions & 0 deletions args4j/src/org/kohsuke/args4j/spi/ArraySetter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.kohsuke.args4j.spi;

import org.kohsuke.args4j.CmdLineException;

import java.lang.reflect.AnnotatedElement;

public class ArraySetter implements Setter {

private final int index;
private final Class type;
private final Object[] values;

public ArraySetter(Object[] values, int index, Class type) {
this.index = index;
this.type = type;
this.values = values;
}

public void addValue(Object value) throws CmdLineException {
Object old = values[index];
values[index] = old == null ? value : old + " " + value;
}

public Class getType() {
return type;
}

public boolean isMultiValued() {
return getType().isArray();
}

public FieldSetter asFieldSetter() {
return null;
}

public AnnotatedElement asAnnotatedElement() {
return null;
}
}
80 changes: 80 additions & 0 deletions args4j/test/org/kohsuke/args4j/MethodCmdLineParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.kohsuke.args4j;

import org.junit.Assert;
import org.junit.Test;

import java.io.File;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class MethodCmdLineParserTest {

public static String method(
String a1, // Implicit @Argument
@Argument(usage = "location. Must exist") File a2,
@Argument(usage = "number. Must be 2 to 7") Integer a3,
@Argument(usage = "optional via @Nullable") @Nullable String a4,
@Option(name = "-f", usage = "a (boolean) flag") Boolean flag,
@Option(name = "-v", usage = "omitted optional") String omitted) {
String values = toStringWithType(a1) +
toStringWithType(a2) +
toStringWithType(a3) +
toStringWithType(flag);
Assert.assertTrue(values, flag);
Assert.assertNotNull(values, a1);
Assert.assertNotNull(values, a2);
Assert.assertNull(values, a4);
return values;
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface Nullable {}

private static class ClassWithMain {
private static boolean mainInvoked = false;

public static void typesafeMain(File arg1, int arg2, String arg3) {
mainInvoked = true;
Assert.assertNotNull(arg1);
Assert.assertNotEquals(arg2, 0);
Assert.assertNotNull(arg3);
}

public static void main(String[] args) throws CmdLineException {
MethodCmdLineParser.invoke(ClassWithMain.class, args);
}
}


private static String toStringWithType(Object o) {
return getClassName(o) + ": " + o +"\n";
}

private static String getClassName(Object o) {
return o == null ? "NULL" : o.getClass().getSimpleName();
}

@Test
public void testInvoke() throws Exception {
Object result = MethodCmdLineParser.invoke(
MethodCmdLineParserTest.class,
"-f",
"Hello",
"/tmp",
"3");
// assert no exception
}

@Test
public void testInvokeClass() throws Exception {
ClassWithMain.mainInvoked = false;

ClassWithMain.main(new String[]{ "/tmp", "3", "rest1", "rest2" });
// assert no exception

Assert.assertTrue(ClassWithMain.mainInvoked);
}
}