View Javadoc

1   package org.kohsuke.args4j;
2   
3   import org.kohsuke.args4j.spi.EnumOptionHandler;
4   import org.kohsuke.args4j.spi.OptionHandler;
5   import org.kohsuke.args4j.spi.Parameters;
6   import org.kohsuke.args4j.spi.Setter;
7   import org.kohsuke.args4j.spi.BooleanOptionHandler;
8   import org.kohsuke.args4j.spi.FileOptionHandler;
9   import org.kohsuke.args4j.spi.StringOptionHandler;
10  import org.kohsuke.args4j.spi.IntOptionHandler;
11  import org.kohsuke.args4j.spi.DoubleOptionHandler;
12  
13  import java.io.OutputStream;
14  import java.io.OutputStreamWriter;
15  import java.io.PrintWriter;
16  import java.io.Writer;
17  import java.io.File;
18  import java.lang.reflect.Constructor;
19  import java.lang.reflect.Field;
20  import java.lang.reflect.InvocationTargetException;
21  import java.lang.reflect.Method;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.ResourceBundle;
27  import java.util.TreeMap;
28  import java.util.Set;
29  import java.util.HashSet;
30  
31  
32  /***
33   * Command line argument owner.
34   *
35   * <p>
36   * For a typical usage, see <a href="https://args4j.dev.java.net/source/browse/args4j/args4j/examples/SampleMain.java?view=markup">this example</a>.
37   *
38   * @author
39   *     Kohsuke Kawaguchi (kk@kohsuke.org)
40   */
41  public class CmdLineParser {
42      /***
43       * Option bean instance.
44       */
45      private final Object bean;
46  
47      /***
48       * Discovered {@link OptionHandler}s keyed by their option names.
49       */
50      private final Map<String,OptionHandler> options = new TreeMap<String,OptionHandler>();
51  
52      /***
53       * {@link Setter} that accepts the arguments.
54       */
55      private Setter argumentSetter;
56  
57      /***
58       * Creates a new command line owner that
59       * parses arguments/options and set them into
60       * the given object.
61       *
62       * @param bean
63       *      instance of a class annotated by {@link Option} and {@link Argument}.
64       *      this object will receive values.
65       *
66       * @throws IllegalAnnotationError
67       *      if the option bean class is using args4j annotations incorrectly.
68       */
69      public CmdLineParser(Object bean) {
70          this.bean = bean;
71  
72          // recursively process all the methods/fields.
73          for( Class c=bean.getClass(); c!=null; c=c.getSuperclass()) {
74              for( Method m : c.getDeclaredMethods() ) {
75                  Option o = m.getAnnotation(Option.class);
76                  if(o!=null) {
77                      addOption(new MethodSetter(bean,m),o);
78                  }
79                  Argument a = m.getAnnotation(Argument.class);
80                  if(a!=null) {
81                      addArgument(new MethodSetter(bean,m));
82                  }
83              }
84  
85              for( Field f : c.getDeclaredFields() ) {
86                  Option o = f.getAnnotation(Option.class);
87                  if(o!=null) {
88                      addOption(createFieldSetter(f),o);
89                  }
90                  Argument a = f.getAnnotation(Argument.class);
91                  if(a!=null) {
92                      addArgument(createFieldSetter(f));
93                  }
94              }
95          }
96      }
97  
98      private Setter createFieldSetter(Field f) {
99          if(List.class.isAssignableFrom(f.getType()))
100             return new MultiValueFieldSetter(bean,f);
101         else
102             return new FieldSetter(bean,f);
103     }
104 
105     private void addArgument(Setter setter) {
106         if(argumentSetter!=null)
107             throw new IllegalAnnotationError("@Argument is used more than once");
108         argumentSetter = setter;
109     }
110 
111     private void addOption(Setter setter, Option o) {
112         OptionHandler h = createOptionHandler(o,setter);
113         if(options.put(h.option.name(),h)!=null) {
114             throw new IllegalAnnotationError("Option name "+h.option.name()+" is used more than once");
115         }
116     }
117 
118     /***
119      * Creates an {@link OptionHandler} that handles the given {@link Option} annotation
120      * and the {@link Setter} instance.
121      */
122     protected OptionHandler createOptionHandler(Option o, Setter setter) {
123 
124         Constructor<? extends OptionHandler> handlerType;
125         Class<? extends OptionHandler> h = o.handler();
126         if(h==OptionHandler.class) {
127             // infer the type
128 
129             // enum is the special case
130             Class t = setter.getType();
131             if(Enum.class.isAssignableFrom(t))
132                 return new EnumOptionHandler(this,o,setter,t);
133 
134             handlerType = handlerClasses.get(t);
135             if(handlerType==null)
136                 throw new IllegalAnnotationError("No OptionHandler is registered to handle "+t);
137         } else {
138             handlerType = getConstructor(h);
139         }
140 
141         try {
142             return handlerType.newInstance(this,o,setter);
143         } catch (InstantiationException e) {
144             throw new IllegalAnnotationError(e);
145         } catch (IllegalAccessException e) {
146             throw new IllegalAnnotationError(e);
147         } catch (InvocationTargetException e) {
148             throw new IllegalAnnotationError(e);
149         }
150     }
151 
152     /***
153      * Formats a command line example into a string.
154      *
155      * See {@link #printExample(ExampleMode, ResourceBundle)} for more details.
156      *
157      * @param mode
158      *      must not be null.
159      * @return
160      *      always non-null.
161      */
162     public String printExample(ExampleMode mode) {
163         return printExample(mode,null);
164     }
165 
166     /***
167      * Formats a command line example into a string.
168      *
169      * <p>
170      * This method produces a string like " -d &lt;dir> -v -b",
171      * which is useful for printing a command line example, perhaps
172      * as a part of the usage screen.
173      *
174      *
175      * @param mode
176      *      One of the {@link ExampleMode} constants. Must not be null.
177      *      This determines what option should be a part of the returned string.
178      * @param rb
179      *      If non-null, meta variables (&lt;dir> in the above example)
180      *      is treated as a key to this resource bundle, and the associated
181      *      value is printed. See {@link Option#metaVar()}. This is to support
182      *      localization.
183      *
184      *      Passing <tt>null</tt> would print {@link Option#metaVar()} directly.
185      * @return
186      *      always non-null. If there's no option, this method returns
187      *      just the empty string "". Otherwise, this method returns a
188      *      string that contains a space at the beginning (but not at the end.)
189      *      This allows you to do something like:
190      *
191      *      <pre>System.err.println("java -jar my.jar"+parser.printExample(REQUIRED)+" arg1 arg2");</pre> 
192      */
193     public String printExample(ExampleMode mode,ResourceBundle rb) {
194         StringBuilder buf = new StringBuilder();
195 
196         for (Map.Entry<String, OptionHandler> e : options.entrySet()) {
197             Option option = e.getValue().option;
198             if(option.usage().length()==0)  continue;   // ignore
199             if(!mode.print(option))         continue;
200 
201             buf.append(' ');
202             buf.append(e.getKey());
203 
204             String metaVar = e.getValue().getMetaVariable(rb);
205             if(metaVar!=null) {
206                 buf.append(' ').append(metaVar);
207             }
208         }
209 
210         return buf.toString();
211     }
212 
213     /***
214      * Prints the list of options and their usages to the screen.
215      *
216      * <p>
217      * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)}
218      * so that you can do {@code printUsage(System.err)}.
219      */
220     public void printUsage(OutputStream out) {
221         printUsage(new OutputStreamWriter(out),null);
222     }
223     /***
224      * Prints the list of options and their usages to the screen.
225      *
226      * @param rb
227      *      if this is non-null, {@link Option#usage()} is treated
228      *      as a key to obtain the actual message from this resource bundle.
229      */
230     public void printUsage(Writer out, ResourceBundle rb) {
231         PrintWriter w = new PrintWriter(out);
232         // determine the length of the option + metavar first
233         int len = 0;
234         for (Map.Entry<String, OptionHandler> e : options.entrySet()) {
235             String usage = e.getValue().option.usage();
236             if(usage.length()==0)   continue;   // ignore
237 
238             String metaVar = e.getValue().getMetaVariable(rb);
239             int metaLen = (metaVar!=null?metaVar.length()+1:0);
240             len = Math.max(len,e.getKey().length()+metaLen);
241         }
242 
243         int descriptionWidth = 72-len-4;    // 3 for " : " + 1 for left-most SP
244 
245         // then print
246         for (Map.Entry<String, OptionHandler> e : options.entrySet()) {
247             String usage = e.getValue().option.usage();
248             if(usage.length()==0)   continue;   // ignore
249 
250             String option = e.getKey();
251             int headLen = option.length();
252             w.print(' ');
253             w.print(option);
254 
255             String metaVar = e.getValue().getMetaVariable(rb);
256             if(metaVar!=null) {
257                 w.print(' ');
258                 w.print(metaVar);
259                 headLen += metaVar.length()+1;
260             }
261             for( ; headLen<len; headLen++ )
262                 w.print(' ');
263             w.print(" : ");
264 
265             if(rb!=null)
266                 usage = rb.getString(usage);
267 
268             while(usage!=null && usage.length()>0) {
269                 int idx = usage.indexOf('\n');
270                 if(idx>=0 && idx<=descriptionWidth) {
271                     w.println(usage.substring(0,idx));
272                     usage = usage.substring(idx+1);
273                     if(usage.length()>0)
274                         indent(w,len+4);
275                     continue;
276                 }
277                 if(usage.length()<=descriptionWidth) {
278                     w.println(usage);
279                     break;
280                 }
281 
282                 w.println(usage.substring(0,descriptionWidth));
283                 usage = usage.substring(descriptionWidth);
284                 indent(w,len+4);
285             }
286         }
287 
288         w.flush();
289     }
290 
291     private void indent(PrintWriter w, int i) {
292         for( ; i>0; i-- )
293             w.print(' ');
294     }
295 
296 
297     /***
298      * Essentially a pointer over a {@link String} array.
299      * Can move forward, can look ahead.
300      */
301     private class CmdLineImpl extends Parameters {
302         private final String[] args;
303         private int pos;
304 
305         CmdLineImpl( String[] args ) {
306             this.args = args;
307             pos = 0;
308         }
309 
310         private boolean hasMore() {
311             return pos<args.length;
312         }
313 
314         private String getCurrentToken() {
315             return args[pos];
316         }
317 
318         private void proceed( int n ) {
319             pos += n;
320         }
321 
322 
323         public String getOptionName() {
324             return getCurrentToken();
325         }
326 
327         public String getParameter(int idx) throws CmdLineException {
328             if( pos+idx+1>=args.length )
329                 throw new CmdLineException(Messages.MISSING_OPERAND.format(getOptionName()));
330             return args[pos+idx+1];
331         }
332 
333         public int getParameterCount() {
334             return args.length-(pos+1);
335         }
336     }
337 
338     /***
339      * Parses the command line arguments and set them to the option bean
340      * given in the constructor.
341      *
342      * @throws CmdLineException
343      *      if there's any error parsing arguments, or if
344      *      {@link Option#required() required} option was not given.
345      */
346     public void parseArgument(final String... args) throws CmdLineException {
347         CmdLineImpl cmdLine = new CmdLineImpl(args);
348 
349         Set<OptionHandler> present = new HashSet<OptionHandler>();
350 
351         while( cmdLine.hasMore() ) {
352             String arg = cmdLine.getOptionName();
353             if( isOption(arg) ) {
354                 // parse this as an option.
355                 OptionHandler handler = options.get(arg);
356                 if(handler!=null) {
357                     // known option
358                     int diff = handler.parseArguments(cmdLine);
359                     cmdLine.proceed(diff+1);
360                     present.add(handler);
361                     continue;
362                 }
363 
364                 // TODO: insert dynamic handler processing
365 
366                 throw new CmdLineException(Messages.UNDEFINED_OPTION.format(arg));
367             } else {
368                 // parse this as arguments
369                 if(argumentSetter==null)
370                     throw new CmdLineException(Messages.NO_ARGUMENT_ALLOWED.format(arg));
371                 argumentSetter.addValue(arg);
372                 cmdLine.proceed(1);
373             }
374         }
375 
376         // make sure that all mandatory options are present
377         for (OptionHandler handler : options.values())
378             if(handler.option.required() && !present.contains(handler))
379                 throw new CmdLineException(Messages.REQUIRED_OPTION_MISSING.format(handler.option.name()));
380     }
381 
382     /***
383      * Returns true if the given token is an option
384      * (as opposed to an argument.)
385      */
386     protected boolean isOption(String arg) {
387         return arg.startsWith("-");
388     }
389 
390 
391     /***
392      * All {@link OptionHandler}s known to the {@link CmdLineParser}.
393      *
394      * Constructors of {@link OptionHandler}-derived class keyed by their supported types.
395      */
396     private static final Map<Class,Constructor<? extends OptionHandler>> handlerClasses =
397             Collections.synchronizedMap(new HashMap<Class,Constructor<? extends OptionHandler>>());
398 
399     /***
400      * Registers a user-defined {@link OptionHandler} class with args4j.
401      *
402      * <p>
403      * This method allows users to extend the behavior of args4j by writing
404      * their own {@link OptionHandler} implementation.
405      *
406      * @param valueType
407      *      The specified handler is used when the field/method annotated by {@link Option}
408      *      is of this type.
409      * @param handlerClass
410      *      This class must have the constructor that has the same signature as
411      *      {@link OptionHandler#OptionHandler(CmdLineParser, Option, Setter)}.
412      */
413     public static void registerHandler( Class valueType, Class<? extends OptionHandler> handlerClass ) {
414         if(valueType==null || handlerClass==null)
415             throw new IllegalArgumentException();
416         if(!OptionHandler.class.isAssignableFrom(handlerClass))
417             throw new IllegalArgumentException("Not an OptionHandler class");
418 
419         Constructor<? extends OptionHandler> c = getConstructor(handlerClass);
420         handlerClasses.put(valueType,c);
421     }
422 
423     private static Constructor<? extends OptionHandler> getConstructor(Class<? extends OptionHandler> handlerClass) {
424         try {
425             return handlerClass.getConstructor(CmdLineParser.class, Option.class, Setter.class);
426         } catch (NoSuchMethodException e) {
427             throw new IllegalArgumentException(handlerClass+" does not have the proper constructor");
428         }
429     }
430 
431     static {
432         registerHandler(Boolean.class,BooleanOptionHandler.class);
433         registerHandler(boolean.class,BooleanOptionHandler.class);
434         registerHandler(File.class,FileOptionHandler.class);
435         registerHandler(Integer.class,IntOptionHandler.class);
436         registerHandler(int.class,IntOptionHandler.class);
437         registerHandler(Double.class, DoubleOptionHandler.class);
438         registerHandler(double.class,DoubleOptionHandler.class);
439         registerHandler(String.class,StringOptionHandler.class);
440         // enum is a special case
441     }
442 }