Skip to content

Commit f109a8e

Browse files
authored
Merge pull request #511 from tgallagher2017/master
Add capability to set Parameterized Parser at runtime.
2 parents 947cd54 + 2c0ddfd commit f109a8e

10 files changed

Lines changed: 546 additions & 1 deletion
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.beust.jcommander;
2+
3+
import java.util.List;
4+
5+
/**
6+
* Thin interface allows the Parameterized parsing mechanism, which reflects an object to find the
7+
* JCommander annotations, to be replaced at runtime for cases where the source code cannot
8+
* be directly annotated with JCommander annotations, but may have other annotations such as
9+
* JSON annotations that can be used to reflect as JCommander parameters.
10+
*
11+
* @author Tim Gallagher
12+
*/
13+
public interface IParameterizedParser {
14+
15+
/**
16+
* Parses the given object for any command line related annotations and returns the list of
17+
* JCommander Parameterized definitions.
18+
*
19+
* @param annotatedObj the object that contains the annotations.
20+
* @return non-null List but may be empty
21+
*/
22+
List<Parameterized> parseArg(Object annotatedObj);
23+
24+
}

src/main/java/com/beust/jcommander/JCommander.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package com.beust.jcommander;
2020

21+
import com.beust.jcommander.parser.DefaultParameterizedParser;
2122
import com.beust.jcommander.FuzzyMap.IKey;
2223
import com.beust.jcommander.converters.*;
2324
import com.beust.jcommander.internal.*;
@@ -46,6 +47,8 @@
4647
*/
4748
public class JCommander {
4849
public static final String DEBUG_PROPERTY = "jcommander.debug";
50+
51+
protected IParameterizedParser parameterizedParser = new DefaultParameterizedParser();
4952

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

270+
public void setParameterizedParser(IParameterizedParser parameterizedParser) {
271+
this.parameterizedParser = parameterizedParser;
272+
}
273+
267274
/**
268275
* Disables expanding {@code @file}.
269276
*
@@ -598,7 +605,7 @@ public void createDescriptions() {
598605
private void addDescription(Object object) {
599606
Class<?> cls = object.getClass();
600607

601-
List<Parameterized> parameterizeds = Parameterized.parseArg(object);
608+
List<Parameterized> parameterizeds = parameterizedParser.parseArg(object);
602609
for (Parameterized parameterized : parameterizeds) {
603610
WrappedParameter wp = parameterized.getWrappedParameter();
604611
if (wp != null && wp.getParameter() != null) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.beust.jcommander.parser;
2+
3+
import com.beust.jcommander.IParameterizedParser;
4+
import com.beust.jcommander.Parameterized;
5+
import java.util.List;
6+
7+
/**
8+
* Pulled from the JCommander where is reflects the object to determine the Parameter annotations.
9+
*
10+
* @author Tim Gallagher
11+
*/
12+
public class DefaultParameterizedParser implements IParameterizedParser {
13+
14+
/**
15+
* Wraps the default parser.
16+
*
17+
* @param annotatedObj an instance of the object with Parameter related annotations.
18+
*
19+
* @author Tim Gallagher
20+
*/
21+
@Override
22+
public List<Parameterized> parseArg(Object annotatedObj) {
23+
return Parameterized.parseArg(annotatedObj);
24+
}
25+
26+
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package com.beust.jcommander.parameterized.parser;
2+
3+
import com.beust.jcommander.IParameterizedParser;
4+
import com.beust.jcommander.Parameter;
5+
import com.beust.jcommander.Parameterized;
6+
import com.beust.jcommander.ParametersDelegate;
7+
import com.beust.jcommander.WrappedParameter;
8+
import com.beust.jcommander.converters.CommaParameterSplitter;
9+
import com.beust.jcommander.converters.NoConverter;
10+
import com.beust.jcommander.internal.Sets;
11+
import com.beust.jcommander.validators.NoValidator;
12+
import com.beust.jcommander.validators.NoValueValidator;
13+
import com.fasterxml.jackson.annotation.JsonProperty;
14+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
15+
import java.lang.reflect.Field;
16+
import java.util.Collections;
17+
import java.util.HashMap;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Set;
21+
import sun.reflect.annotation.AnnotationParser;
22+
23+
/**
24+
* Provides building JCommander Parameters based on @ComponentInput and @ComponentConfiguration as
25+
* opposed to using JCommander @Parameter.
26+
*
27+
* @author Cedric Beust <cedric@beust.com>
28+
* @author Tim Gallagher
29+
*/
30+
public class JsonAnnotationParameterizedParser implements IParameterizedParser {
31+
32+
public static final String PREFIX_MARKER = "prefix:";
33+
34+
/**
35+
* This is the standard prefix like "--" or "-"
36+
*/
37+
protected final String paramPrefix;
38+
39+
/**
40+
* When a class has a member class and is annotated with JsonProperty, then there couple be fields
41+
* that are the same name, for example 'version'. This map allows the parser to define a Parameter
42+
* with a prefix in order to avoid the collision.
43+
*/
44+
protected final Map<Class, String> classPrefixes = new HashMap<>();
45+
46+
/**
47+
* This used in auto generation of the prefixes from the JsonDescription. If this, prefix
48+
* separator value is not in the descriptions prefix definition, then it will be added. The
49+
* default is '.'
50+
*/
51+
protected String prefixSeparator = ".";
52+
53+
public JsonAnnotationParameterizedParser() {
54+
this("");
55+
}
56+
57+
public JsonAnnotationParameterizedParser(String paramPrefix) {
58+
this.paramPrefix = paramPrefix;
59+
}
60+
61+
public void addClassPrefix(Class clazz, String prefix) {
62+
this.classPrefixes.put(clazz, prefix);
63+
}
64+
65+
public void setPrefixSeparator(String separator) {
66+
this.prefixSeparator = separator == null ? "" : separator;
67+
}
68+
69+
/**
70+
* Recursive handler for describing the set of classes while using the setOfClasses parameter as a
71+
* collector
72+
*
73+
* @param inputClass the class to analyze
74+
* @param setOfClasses the set collector to collect the results
75+
*/
76+
private void describeClassTree(Class<?> inputClass, Set<Class<?>> setOfClasses) {
77+
// can't map null class
78+
if (inputClass == null) {
79+
return;
80+
}
81+
82+
// don't further analyze a class that has been analyzed already
83+
if (Object.class.equals(inputClass) || setOfClasses.contains(inputClass)) {
84+
return;
85+
}
86+
87+
// add to analysis set
88+
setOfClasses.add(inputClass);
89+
90+
// perform super class analysis
91+
describeClassTree(inputClass.getSuperclass(), setOfClasses);
92+
93+
// perform analysis on interfaces
94+
for (Class<?> hasInterface : inputClass.getInterfaces()) {
95+
describeClassTree(hasInterface, setOfClasses);
96+
}
97+
}
98+
99+
/**
100+
* Given an object return the set of classes that it extends or implements.
101+
*
102+
* @param arg object to describe
103+
* @return set of classes that are implemented or extended by that object
104+
*/
105+
private Set<Class<?>> describeClassTree(Class<?> inputClass) {
106+
if (inputClass == null) {
107+
return Collections.emptySet();
108+
}
109+
110+
// create result collector
111+
Set<Class<?>> classes = Sets.newLinkedHashSet();
112+
113+
// describe tree
114+
describeClassTree(inputClass, classes);
115+
116+
return classes;
117+
}
118+
119+
@Override
120+
public List<Parameterized> parseArg(Object arg) {
121+
List<Parameterized> result = Parameterized.parseArg(arg);
122+
123+
Class<?> rootClass = arg.getClass();
124+
125+
// get the list of types that are extended or implemented by the root class
126+
// and all of its parent types
127+
Set<Class<?>> types = describeClassTree(rootClass);
128+
129+
// analyze each type
130+
for (Class<?> curClazz : types) {
131+
// check fields
132+
for (Field field : curClazz.getDeclaredFields()) {
133+
JsonProperty fieldAnnotation = (JsonProperty) field.getAnnotation(JsonProperty.class);
134+
JsonPropertyDescription descrAnnotation = (JsonPropertyDescription) field.getAnnotation(JsonPropertyDescription.class);
135+
MyDelegate myDelegate = (MyDelegate) field.getAnnotation(MyDelegate.class);
136+
if (fieldAnnotation != null) {
137+
// this is a map of annotation field names uses to create the Parameter annotation
138+
// at runtime
139+
Map<String, Object> map = new HashMap<>();
140+
141+
/*
142+
* For primitive and their derived types, we can use the Parameter annotation, but for
143+
* other user classes, we need to add a delegate
144+
*/
145+
if (isPrimitiveOrString(field) || myDelegate == null) {
146+
/*
147+
* create standard Parameter
148+
*/
149+
String name = fieldAnnotation.value();
150+
map.put("names", new String[]{name});
151+
map.put("required", fieldAnnotation.required());
152+
map.put("descriptionKey", "");
153+
// all variable types, even Boolean require 1 following parameter.
154+
//if (field.getType() == Boolean.class || field.getType() == boolean.class) {
155+
map.put("arity", 1);
156+
map.put("variableArity", (field.getType() == List.class));
157+
map.put("password", false);
158+
map.put("converter", NoConverter.class);
159+
map.put("listConverter", NoConverter.class);
160+
map.put("hidden", false);
161+
map.put("validateWith", new Class[]{NoValidator.class});
162+
map.put("validateValueWith", new Class[]{NoValueValidator.class});
163+
map.put("splitter", CommaParameterSplitter.class);
164+
map.put("echoInput", true);
165+
map.put("help", false);
166+
map.put("forceNonOverwritable", false);
167+
map.put("order", -1);
168+
map.put("description", descrAnnotation != null ? descrAnnotation.value() : "");
169+
170+
Parameter param = (Parameter) AnnotationParser.annotationForMap(Parameter.class, map);
171+
result.add(new Parameterized(new WrappedParameter(param), null, field, null));
172+
} else {
173+
/*
174+
* Create ParametersDelegate
175+
*/
176+
ParametersDelegate param = (ParametersDelegate) AnnotationParser.annotationForMap(ParametersDelegate.class, map);
177+
result.add(new Parameterized(null, param, field, null));
178+
}
179+
}
180+
}
181+
182+
/*
183+
* This section would be for completeness and although it is not tested it is left here
184+
* as a template to use the JsonSetter (or JsonGetter) methods as ways to define parameters
185+
* at runtime.
186+
*/
187+
// // check methods
188+
// for (Method method : curClazz.getDeclaredMethods()) {
189+
// // these only work on setMethods
190+
// if (!method.getName().startsWith("set")) {
191+
// continue;
192+
// }
193+
//
194+
// JsonSetter jsonSetterAnnotation = (JsonSetter) method.getAnnotation(JsonSetter.class);
195+
// if (jsonSetterAnnotation != null) {
196+
// Map<String, Object> map = new HashMap<String, Object>();
197+
//
198+
// /*
199+
// * For primitive and their derived types, we can use the Parameter annotation, but for
200+
// * other user classes, we need to add a delegate
201+
// */
202+
// /*
203+
// * create standard Parameter
204+
// */
205+
// String name = jsonSetterAnnotation.value();
206+
// map.put("names", new String[]{name});
207+
// map.put("required", false);
208+
// //
209+
// // TODO SET THE DEFAULT VALUE BASED ON THE values() OR valuesEnum()
210+
// // map.put("default", annotation.defaultValue());
211+
// //
212+
// map.put("descriptionKey", "");
213+
// // get the parameter type
214+
// Class[] paramTypes = method.getParameterTypes();
215+
// // there should only be one for a
216+
// // all variable types, even Boolean require 1 following parameter.
217+
// //if (paramTypes[0] == Boolean.class || paramTypes[0] == boolean.class) {
218+
// map.put("arity", 1);
219+
// //}
220+
// map.put("variableArity", (paramTypes[0] == List.class));
221+
// map.put("password", false);
222+
// map.put("converter", NoConverter.class);
223+
// map.put("listConverter", NoConverter.class);
224+
// map.put("hidden", false);
225+
// map.put("validateWith", new Class[]{NoValidator.class});
226+
// map.put("validateValueWith", new Class[]{NoValueValidator.class});
227+
// map.put("splitter", CommaParameterSplitter.class);
228+
// map.put("echoInput", true);
229+
// map.put("help", false);
230+
// map.put("forceNonOverwritable", false);
231+
// map.put("order", -1);
232+
// map.put("description", "");
233+
//
234+
// Parameter param = (Parameter) AnnotationParser.annotationForMap(Parameter.class, map);
235+
// result.add(new Parameterized(new WrappedParameter(param), null, null, method));
236+
// }
237+
// }
238+
}
239+
240+
return result;
241+
}
242+
243+
/**
244+
* Basic check for primitive or Java class that should be used directly.
245+
*
246+
* @param field non-null java Field
247+
* @return true if Java primitive or part of the Java or Sun package.
248+
*/
249+
public boolean isPrimitiveOrString(Field field) {
250+
Class type = field.getType();
251+
String name = type.getName();
252+
253+
return type.isPrimitive() || name.startsWith("java") || name.startsWith("sun");
254+
}
255+
256+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.beust.jcommander.parameterized.parser;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
5+
6+
/**
7+
* This is an arbitrary class to test Parameter values using JSON annotations instead with
8+
* JCommander annotations.
9+
*
10+
* @author Tim Gallagher
11+
*/
12+
public class JsonCommandClassExample_01 {
13+
14+
public static final String PARAM_VERSION = "version";
15+
16+
/**
17+
* In this example, the JsonProperty annotation does not include enough values to allow us to
18+
* indicate a delegated object for JCommander, as the JCommander annotations do.
19+
* However, because the object is not a java.lang or other Java supplied objects, we can assumed
20+
* it is delegated.
21+
*
22+
* A more precise way to do this, is to introduce new annotation, MyDelegate in this case,
23+
* to simulate JCommander but does not require JCommander for your low level libraries.
24+
* For example, lets say you have a REST component, you could use the JsonProperty and other
25+
* JSON annotations for that service, but add a new annotation so that when you want to pull
26+
* that service component out and into a command line app, you could use that new Annotation
27+
* within the context of JCommander. Here we use a very simple MyDelegate. But there is no
28+
* reason why you can't add more data to it.
29+
*/
30+
@JsonProperty(
31+
value = "subCommands"
32+
)
33+
@MyDelegate
34+
public final JsonCommandClassExample_02 subCommands = new JsonCommandClassExample_02();
35+
36+
/**
37+
* In this example, the JsonProperty does not have any description, but there is a
38+
* JsonPropertyDescription annotation that we can take advantage of.
39+
*/
40+
@JsonProperty(
41+
value = PARAM_VERSION,
42+
required = true
43+
)
44+
@JsonPropertyDescription("Version of the software to run. eg. \"v38.1.0\"")
45+
public String version;
46+
47+
}

0 commit comments

Comments
 (0)