-
Notifications
You must be signed in to change notification settings - Fork 337
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #511 from tgallagher2017/master
Add capability to set Parameterized Parser at runtime.
- Loading branch information
Showing
10 changed files
with
546 additions
and
1 deletion.
There are no files selected for viewing
24 changes: 24 additions & 0 deletions
24
src/main/java/com/beust/jcommander/IParameterizedParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
src/main/java/com/beust/jcommander/parser/DefaultParameterizedParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
256 changes: 256 additions & 0 deletions
256
...est/java/com/beust/jcommander/parameterized/parser/JsonAnnotationParameterizedParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
|
||
} |
47 changes: 47 additions & 0 deletions
47
src/test/java/com/beust/jcommander/parameterized/parser/JsonCommandClassExample_01.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} |
Oops, something went wrong.