Skip to content

Commit

Permalink
Merge pull request #511 from tgallagher2017/master
Browse files Browse the repository at this point in the history
Add capability to set Parameterized Parser at runtime.
  • Loading branch information
cbeust authored Jan 11, 2022
2 parents 947cd54 + 2c0ddfd commit f109a8e
Show file tree
Hide file tree
Showing 10 changed files with 546 additions and 1 deletion.
24 changes: 24 additions & 0 deletions src/main/java/com/beust/jcommander/IParameterizedParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.beust.jcommander;

import java.util.List;

/**
* Thin interface allows the Parameterized parsing mechanism, which reflects an object to find the
* JCommander annotations, to be replaced at runtime for cases where the source code cannot
* be directly annotated with JCommander annotations, but may have other annotations such as
* JSON annotations that can be used to reflect as JCommander parameters.
*
* @author Tim Gallagher
*/
public interface IParameterizedParser {

/**
* Parses the given object for any command line related annotations and returns the list of
* JCommander Parameterized definitions.
*
* @param annotatedObj the object that contains the annotations.
* @return non-null List but may be empty
*/
List<Parameterized> parseArg(Object annotatedObj);

}
9 changes: 8 additions & 1 deletion src/main/java/com/beust/jcommander/JCommander.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package com.beust.jcommander;

import com.beust.jcommander.parser.DefaultParameterizedParser;
import com.beust.jcommander.FuzzyMap.IKey;
import com.beust.jcommander.converters.*;
import com.beust.jcommander.internal.*;
Expand Down Expand Up @@ -46,6 +47,8 @@
*/
public class JCommander {
public static final String DEBUG_PROPERTY = "jcommander.debug";

protected IParameterizedParser parameterizedParser = new DefaultParameterizedParser();

/**
* A map to look up parameter description per option name.
Expand Down Expand Up @@ -264,6 +267,10 @@ public JCommander(Object object, String... args) {
parse(args);
}

public void setParameterizedParser(IParameterizedParser parameterizedParser) {
this.parameterizedParser = parameterizedParser;
}

/**
* Disables expanding {@code @file}.
*
Expand Down Expand Up @@ -598,7 +605,7 @@ public void createDescriptions() {
private void addDescription(Object object) {
Class<?> cls = object.getClass();

List<Parameterized> parameterizeds = Parameterized.parseArg(object);
List<Parameterized> parameterizeds = parameterizedParser.parseArg(object);
for (Parameterized parameterized : parameterizeds) {
WrappedParameter wp = parameterized.getWrappedParameter();
if (wp != null && wp.getParameter() != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.beust.jcommander.parser;

import com.beust.jcommander.IParameterizedParser;
import com.beust.jcommander.Parameterized;
import java.util.List;

/**
* Pulled from the JCommander where is reflects the object to determine the Parameter annotations.
*
* @author Tim Gallagher
*/
public class DefaultParameterizedParser implements IParameterizedParser {

/**
* Wraps the default parser.
*
* @param annotatedObj an instance of the object with Parameter related annotations.
*
* @author Tim Gallagher
*/
@Override
public List<Parameterized> parseArg(Object annotatedObj) {
return Parameterized.parseArg(annotatedObj);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package com.beust.jcommander.parameterized.parser;

import com.beust.jcommander.IParameterizedParser;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameterized;
import com.beust.jcommander.ParametersDelegate;
import com.beust.jcommander.WrappedParameter;
import com.beust.jcommander.converters.CommaParameterSplitter;
import com.beust.jcommander.converters.NoConverter;
import com.beust.jcommander.internal.Sets;
import com.beust.jcommander.validators.NoValidator;
import com.beust.jcommander.validators.NoValueValidator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import sun.reflect.annotation.AnnotationParser;

/**
* Provides building JCommander Parameters based on @ComponentInput and @ComponentConfiguration as
* opposed to using JCommander @Parameter.
*
* @author Cedric Beust <[email protected]>
* @author Tim Gallagher
*/
public class JsonAnnotationParameterizedParser implements IParameterizedParser {

public static final String PREFIX_MARKER = "prefix:";

/**
* This is the standard prefix like "--" or "-"
*/
protected final String paramPrefix;

/**
* When a class has a member class and is annotated with JsonProperty, then there couple be fields
* that are the same name, for example 'version'. This map allows the parser to define a Parameter
* with a prefix in order to avoid the collision.
*/
protected final Map<Class, String> classPrefixes = new HashMap<>();

/**
* This used in auto generation of the prefixes from the JsonDescription. If this, prefix
* separator value is not in the descriptions prefix definition, then it will be added. The
* default is '.'
*/
protected String prefixSeparator = ".";

public JsonAnnotationParameterizedParser() {
this("");
}

public JsonAnnotationParameterizedParser(String paramPrefix) {
this.paramPrefix = paramPrefix;
}

public void addClassPrefix(Class clazz, String prefix) {
this.classPrefixes.put(clazz, prefix);
}

public void setPrefixSeparator(String separator) {
this.prefixSeparator = separator == null ? "" : separator;
}

/**
* Recursive handler for describing the set of classes while using the setOfClasses parameter as a
* collector
*
* @param inputClass the class to analyze
* @param setOfClasses the set collector to collect the results
*/
private void describeClassTree(Class<?> inputClass, Set<Class<?>> setOfClasses) {
// can't map null class
if (inputClass == null) {
return;
}

// don't further analyze a class that has been analyzed already
if (Object.class.equals(inputClass) || setOfClasses.contains(inputClass)) {
return;
}

// add to analysis set
setOfClasses.add(inputClass);

// perform super class analysis
describeClassTree(inputClass.getSuperclass(), setOfClasses);

// perform analysis on interfaces
for (Class<?> hasInterface : inputClass.getInterfaces()) {
describeClassTree(hasInterface, setOfClasses);
}
}

/**
* Given an object return the set of classes that it extends or implements.
*
* @param arg object to describe
* @return set of classes that are implemented or extended by that object
*/
private Set<Class<?>> describeClassTree(Class<?> inputClass) {
if (inputClass == null) {
return Collections.emptySet();
}

// create result collector
Set<Class<?>> classes = Sets.newLinkedHashSet();

// describe tree
describeClassTree(inputClass, classes);

return classes;
}

@Override
public List<Parameterized> parseArg(Object arg) {
List<Parameterized> result = Parameterized.parseArg(arg);

Class<?> rootClass = arg.getClass();

// get the list of types that are extended or implemented by the root class
// and all of its parent types
Set<Class<?>> types = describeClassTree(rootClass);

// analyze each type
for (Class<?> curClazz : types) {
// check fields
for (Field field : curClazz.getDeclaredFields()) {
JsonProperty fieldAnnotation = (JsonProperty) field.getAnnotation(JsonProperty.class);
JsonPropertyDescription descrAnnotation = (JsonPropertyDescription) field.getAnnotation(JsonPropertyDescription.class);
MyDelegate myDelegate = (MyDelegate) field.getAnnotation(MyDelegate.class);
if (fieldAnnotation != null) {
// this is a map of annotation field names uses to create the Parameter annotation
// at runtime
Map<String, Object> map = new HashMap<>();

/*
* For primitive and their derived types, we can use the Parameter annotation, but for
* other user classes, we need to add a delegate
*/
if (isPrimitiveOrString(field) || myDelegate == null) {
/*
* create standard Parameter
*/
String name = fieldAnnotation.value();
map.put("names", new String[]{name});
map.put("required", fieldAnnotation.required());
map.put("descriptionKey", "");
// all variable types, even Boolean require 1 following parameter.
//if (field.getType() == Boolean.class || field.getType() == boolean.class) {
map.put("arity", 1);
map.put("variableArity", (field.getType() == List.class));
map.put("password", false);
map.put("converter", NoConverter.class);
map.put("listConverter", NoConverter.class);
map.put("hidden", false);
map.put("validateWith", new Class[]{NoValidator.class});
map.put("validateValueWith", new Class[]{NoValueValidator.class});
map.put("splitter", CommaParameterSplitter.class);
map.put("echoInput", true);
map.put("help", false);
map.put("forceNonOverwritable", false);
map.put("order", -1);
map.put("description", descrAnnotation != null ? descrAnnotation.value() : "");

Parameter param = (Parameter) AnnotationParser.annotationForMap(Parameter.class, map);
result.add(new Parameterized(new WrappedParameter(param), null, field, null));
} else {
/*
* Create ParametersDelegate
*/
ParametersDelegate param = (ParametersDelegate) AnnotationParser.annotationForMap(ParametersDelegate.class, map);
result.add(new Parameterized(null, param, field, null));
}
}
}

/*
* This section would be for completeness and although it is not tested it is left here
* as a template to use the JsonSetter (or JsonGetter) methods as ways to define parameters
* at runtime.
*/
// // check methods
// for (Method method : curClazz.getDeclaredMethods()) {
// // these only work on setMethods
// if (!method.getName().startsWith("set")) {
// continue;
// }
//
// JsonSetter jsonSetterAnnotation = (JsonSetter) method.getAnnotation(JsonSetter.class);
// if (jsonSetterAnnotation != null) {
// Map<String, Object> map = new HashMap<String, Object>();
//
// /*
// * For primitive and their derived types, we can use the Parameter annotation, but for
// * other user classes, we need to add a delegate
// */
// /*
// * create standard Parameter
// */
// String name = jsonSetterAnnotation.value();
// map.put("names", new String[]{name});
// map.put("required", false);
// //
// // TODO SET THE DEFAULT VALUE BASED ON THE values() OR valuesEnum()
// // map.put("default", annotation.defaultValue());
// //
// map.put("descriptionKey", "");
// // get the parameter type
// Class[] paramTypes = method.getParameterTypes();
// // there should only be one for a
// // all variable types, even Boolean require 1 following parameter.
// //if (paramTypes[0] == Boolean.class || paramTypes[0] == boolean.class) {
// map.put("arity", 1);
// //}
// map.put("variableArity", (paramTypes[0] == List.class));
// map.put("password", false);
// map.put("converter", NoConverter.class);
// map.put("listConverter", NoConverter.class);
// map.put("hidden", false);
// map.put("validateWith", new Class[]{NoValidator.class});
// map.put("validateValueWith", new Class[]{NoValueValidator.class});
// map.put("splitter", CommaParameterSplitter.class);
// map.put("echoInput", true);
// map.put("help", false);
// map.put("forceNonOverwritable", false);
// map.put("order", -1);
// map.put("description", "");
//
// Parameter param = (Parameter) AnnotationParser.annotationForMap(Parameter.class, map);
// result.add(new Parameterized(new WrappedParameter(param), null, null, method));
// }
// }
}

return result;
}

/**
* Basic check for primitive or Java class that should be used directly.
*
* @param field non-null java Field
* @return true if Java primitive or part of the Java or Sun package.
*/
public boolean isPrimitiveOrString(Field field) {
Class type = field.getType();
String name = type.getName();

return type.isPrimitive() || name.startsWith("java") || name.startsWith("sun");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.beust.jcommander.parameterized.parser;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;

/**
* This is an arbitrary class to test Parameter values using JSON annotations instead with
* JCommander annotations.
*
* @author Tim Gallagher
*/
public class JsonCommandClassExample_01 {

public static final String PARAM_VERSION = "version";

/**
* In this example, the JsonProperty annotation does not include enough values to allow us to
* indicate a delegated object for JCommander, as the JCommander annotations do.
* However, because the object is not a java.lang or other Java supplied objects, we can assumed
* it is delegated.
*
* A more precise way to do this, is to introduce new annotation, MyDelegate in this case,
* to simulate JCommander but does not require JCommander for your low level libraries.
* For example, lets say you have a REST component, you could use the JsonProperty and other
* JSON annotations for that service, but add a new annotation so that when you want to pull
* that service component out and into a command line app, you could use that new Annotation
* within the context of JCommander. Here we use a very simple MyDelegate. But there is no
* reason why you can't add more data to it.
*/
@JsonProperty(
value = "subCommands"
)
@MyDelegate
public final JsonCommandClassExample_02 subCommands = new JsonCommandClassExample_02();

/**
* In this example, the JsonProperty does not have any description, but there is a
* JsonPropertyDescription annotation that we can take advantage of.
*/
@JsonProperty(
value = PARAM_VERSION,
required = true
)
@JsonPropertyDescription("Version of the software to run. eg. \"v38.1.0\"")
public String version;

}
Loading

0 comments on commit f109a8e

Please sign in to comment.