001: package org.kohsuke.args4j;
002:
003: import java.io.File;
004: import java.io.OutputStream;
005: import java.io.OutputStreamWriter;
006: import java.io.PrintWriter;
007: import java.io.Writer;
008: import java.lang.reflect.Constructor;
009: import java.lang.reflect.Field;
010: import java.lang.reflect.InvocationTargetException;
011: import java.lang.reflect.Method;
012: import java.util.ArrayList;
013: import java.util.Collections;
014: import java.util.Comparator;
015: import java.util.HashMap;
016: import java.util.HashSet;
017: import java.util.List;
018: import java.util.Map;
019: import java.util.ResourceBundle;
020: import java.util.Set;
021: import java.util.TreeMap;
022:
023: import org.kohsuke.args4j.spi.BooleanOptionHandler;
024: import org.kohsuke.args4j.spi.DoubleOptionHandler;
025: import org.kohsuke.args4j.spi.EnumOptionHandler;
026: import org.kohsuke.args4j.spi.FileOptionHandler;
027: import org.kohsuke.args4j.spi.IntOptionHandler;
028: import org.kohsuke.args4j.spi.OptionHandler;
029: import org.kohsuke.args4j.spi.Parameters;
030: import org.kohsuke.args4j.spi.Setter;
031: import org.kohsuke.args4j.spi.StringOptionHandler;
032:
033: /**
034: * Command line argument owner.
035: *
036: * <p>
037: * For a typical usage, see <a href="https://args4j.dev.java.net/source/browse/args4j/args4j/examples/SampleMain.java?view=markup">this example</a>.
038: *
039: * @author
040: * Kohsuke Kawaguchi (kk@kohsuke.org)
041: */
042: public class CmdLineParser {
043: /**
044: * Option bean instance.
045: */
046: private final Object bean;
047:
048: /**
049: * Discovered {@link OptionHandler}s for options.
050: */
051: private final List<OptionHandler> options = new ArrayList<OptionHandler>();
052:
053: /**
054: * Discovered {@link OptionHandler}s for arguments.
055: */
056: private final List<OptionHandler> arguments = new ArrayList<OptionHandler>();
057:
058: private boolean parsingOptions = true;
059: private OptionHandler currentOptionHandler = null;
060:
061: /**
062: * The length of a usage line.
063: * If the usage message is longer than this value, the parser
064: * wraps the line. Defaults to 80.
065: */
066: private int usageWidth = 80;
067:
068: /**
069: * Creates a new command line owner that
070: * parses arguments/options and set them into
071: * the given object.
072: *
073: * @param bean
074: * instance of a class annotated by {@link Option} and {@link Argument}.
075: * this object will receive values.
076: *
077: * @throws IllegalAnnotationError
078: * if the option bean class is using args4j annotations incorrectly.
079: */
080: public CmdLineParser(Object bean) {
081: this .bean = bean;
082:
083: // recursively process all the methods/fields.
084: for (Class c = bean.getClass(); c != null; c = c
085: .getSuperclass()) {
086: for (Method m : c.getDeclaredMethods()) {
087: Option o = m.getAnnotation(Option.class);
088: if (o != null) {
089: addOption(new MethodSetter(bean, m), o);
090: }
091: Argument a = m.getAnnotation(Argument.class);
092: if (a != null) {
093: addArgument(new MethodSetter(bean, m), a);
094: }
095: }
096:
097: for (Field f : c.getDeclaredFields()) {
098: Option o = f.getAnnotation(Option.class);
099: if (o != null) {
100: addOption(createFieldSetter(f), o);
101: }
102: Argument a = f.getAnnotation(Argument.class);
103: if (a != null) {
104: addArgument(createFieldSetter(f), a);
105: }
106: }
107: }
108: for (int i = 0; i < arguments.size(); ++i) {
109: if (arguments.get(i) == null) {
110: throw new IllegalAnnotationError(
111: "No argument annotation for index " + i);
112: }
113: }
114:
115: // for display purposes, we like the arguments in argument order, but the options in alphabetical order
116: Collections.sort(options, new Comparator<OptionHandler>() {
117: public int compare(OptionHandler o1, OptionHandler o2) {
118: return o1.option.toString().compareTo(
119: o2.option.toString());
120: }
121: });
122: }
123:
124: private Setter createFieldSetter(Field f) {
125: if (List.class.isAssignableFrom(f.getType()))
126: return new MultiValueFieldSetter(bean, f);
127: //else if(Map.class.isAssignableFrom(f.getType()))
128: // return new MapFieldSetter(bean,f);
129: else
130: return new FieldSetter(bean, f);
131: }
132:
133: private void addArgument(Setter setter, Argument a) {
134: OptionHandler h = createOptionHandler(new OptionDef(a, setter
135: .isMultiValued()), setter);
136: int index = a.index();
137: // make sure the argument will fit in the list
138: while (index >= arguments.size()) {
139: arguments.add(null);
140: }
141: if (arguments.get(index) != null) {
142: throw new IllegalAnnotationError("Argument with index "
143: + index + " is used more than once");
144: }
145: arguments.set(index, h);
146: }
147:
148: private void addOption(Setter setter, Option o) {
149: OptionHandler h = createOptionHandler(new NamedOptionDef(o,
150: setter.isMultiValued()), setter);
151: checkOptionNotInMap(o.name());
152: for (String alias : o.aliases()) {
153: checkOptionNotInMap(alias);
154: }
155: options.add(h);
156: }
157:
158: private void checkOptionNotInMap(String name)
159: throws IllegalAnnotationError {
160: if (findOptionByName(name) != null) {
161: throw new IllegalAnnotationError("Option name " + name
162: + " is used more than once");
163: }
164: }
165:
166: /**
167: * Creates an {@link OptionHandler} that handles the given {@link Option} annotation
168: * and the {@link Setter} instance.
169: */
170: protected OptionHandler createOptionHandler(OptionDef o,
171: Setter setter) {
172:
173: Constructor<? extends OptionHandler> handlerType;
174: Class<? extends OptionHandler> h = o.handler();
175: if (h == OptionHandler.class) {
176: // infer the type
177:
178: // enum is the special case
179: Class t = setter.getType();
180: if (Enum.class.isAssignableFrom(t))
181: return new EnumOptionHandler(this , o, setter, t);
182:
183: handlerType = handlerClasses.get(t);
184: if (handlerType == null)
185: throw new IllegalAnnotationError(
186: "No OptionHandler is registered to handle " + t);
187: } else {
188: handlerType = getConstructor(h);
189: }
190:
191: try {
192: return handlerType.newInstance(this , o, setter);
193: } catch (InstantiationException e) {
194: throw new IllegalAnnotationError(e);
195: } catch (IllegalAccessException e) {
196: throw new IllegalAnnotationError(e);
197: } catch (InvocationTargetException e) {
198: throw new IllegalAnnotationError(e);
199: }
200: }
201:
202: /**
203: * Formats a command line example into a string.
204: *
205: * See {@link #printExample(ExampleMode, ResourceBundle)} for more details.
206: *
207: * @param mode
208: * must not be null.
209: * @return
210: * always non-null.
211: */
212: public String printExample(ExampleMode mode) {
213: return printExample(mode, null);
214: }
215:
216: /**
217: * Formats a command line example into a string.
218: *
219: * <p>
220: * This method produces a string like " -d <dir> -v -b",
221: * which is useful for printing a command line example, perhaps
222: * as a part of the usage screen.
223: *
224: *
225: * @param mode
226: * One of the {@link ExampleMode} constants. Must not be null.
227: * This determines what option should be a part of the returned string.
228: * @param rb
229: * If non-null, meta variables (<dir> in the above example)
230: * is treated as a key to this resource bundle, and the associated
231: * value is printed. See {@link Option#metaVar()}. This is to support
232: * localization.
233: *
234: * Passing <tt>null</tt> would print {@link Option#metaVar()} directly.
235: * @return
236: * always non-null. If there's no option, this method returns
237: * just the empty string "". Otherwise, this method returns a
238: * string that contains a space at the beginning (but not at the end.)
239: * This allows you to do something like:
240: *
241: * <pre>System.err.println("java -jar my.jar"+parser.printExample(REQUIRED)+" arg1 arg2");</pre>
242: */
243: public String printExample(ExampleMode mode, ResourceBundle rb) {
244: StringBuilder buf = new StringBuilder();
245:
246: for (OptionHandler h : options) {
247: OptionDef option = h.option;
248: if (option.usage().length() == 0)
249: continue; // ignore
250: if (!mode.print(option))
251: continue;
252:
253: buf.append(' ');
254: buf.append(h.getNameAndMeta(rb));
255: }
256:
257: return buf.toString();
258: }
259:
260: /**
261: * Prints the list of options and their usages to the screen.
262: *
263: * <p>
264: * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)}
265: * so that you can do {@code printUsage(System.err)}.
266: */
267: public void printUsage(OutputStream out) {
268: printUsage(new OutputStreamWriter(out), null);
269: }
270:
271: /**
272: * Prints the list of options and their usages to the screen.
273: *
274: * @param rb
275: * if this is non-null, {@link Option#usage()} is treated
276: * as a key to obtain the actual message from this resource bundle.
277: */
278: public void printUsage(Writer out, ResourceBundle rb) {
279: PrintWriter w = new PrintWriter(out);
280: // determine the length of the option + metavar first
281: int len = 0;
282: for (OptionHandler h : arguments) {
283: int curLen = getPrefixLen(h, rb);
284: len = Math.max(len, curLen);
285: }
286: for (OptionHandler h : options) {
287: int curLen = getPrefixLen(h, rb);
288: len = Math.max(len, curLen);
289: }
290:
291: // then print
292: for (OptionHandler h : arguments) {
293: printOption(w, h, len, rb);
294: }
295: for (OptionHandler h : options) {
296: printOption(w, h, len, rb);
297: }
298:
299: w.flush();
300: }
301:
302: private void printOption(PrintWriter w, OptionHandler h, int len,
303: ResourceBundle rb) {
304: int descriptionWidth = usageWidth - len - 4; // 3 for " : " + 1 for left-most SP
305:
306: String usage = h.option.usage();
307: if (usage.length() == 0)
308: return; // ignore
309:
310: String nameAndMeta = h.getNameAndMeta(rb);
311: w.print(' ');
312: w.print(nameAndMeta);
313: for (int i = nameAndMeta.length(); i < len; ++i) {
314: w.print(' ');
315: }
316: w.print(" : ");
317:
318: if (rb != null)
319: usage = rb.getString(usage);
320:
321: while (usage != null && usage.length() > 0) {
322: int idx = usage.indexOf('\n');
323: if (idx >= 0 && idx <= descriptionWidth) {
324: w.println(usage.substring(0, idx));
325: usage = usage.substring(idx + 1);
326: if (usage.length() > 0)
327: indent(w, len + 4);
328: continue;
329: }
330: if (usage.length() <= descriptionWidth) {
331: w.println(usage);
332: break;
333: }
334:
335: w.println(usage.substring(0, descriptionWidth));
336: usage = usage.substring(descriptionWidth);
337: indent(w, len + 4);
338: }
339: }
340:
341: private int getPrefixLen(OptionHandler h, ResourceBundle rb) {
342: if (h.option.usage().length() == 0)
343: return 0;
344:
345: return h.getNameAndMeta(rb).length();
346: }
347:
348: private void indent(PrintWriter w, int i) {
349: for (; i > 0; i--)
350: w.print(' ');
351: }
352:
353: /**
354: * Essentially a pointer over a {@link String} array.
355: * Can move forward, can look ahead.
356: */
357: private class CmdLineImpl implements Parameters {
358: private final String[] args;
359: private int pos;
360:
361: CmdLineImpl(String[] args) {
362: this .args = args;
363: pos = 0;
364: }
365:
366: protected boolean hasMore() {
367: return pos < args.length;
368: }
369:
370: protected String getCurrentToken() {
371: return args[pos];
372: }
373:
374: private void proceed(int n) {
375: pos += n;
376: }
377:
378: public String getParameter(int idx) throws CmdLineException {
379: if (pos + idx >= args.length)
380: throw new CmdLineException(Messages.MISSING_OPERAND
381: .format(getOptionName()));
382: return args[pos + idx];
383: }
384: }
385:
386: private String getOptionName() {
387: return currentOptionHandler.option.toString();
388: }
389:
390: /**
391: * Parses the command line arguments and set them to the option bean
392: * given in the constructor.
393: *
394: * @param args arguments to parse
395: *
396: * @throws CmdLineException
397: * if there's any error parsing arguments, or if
398: * {@link Option#required() required} option was not given.
399: */
400: public void parseArgument(final String... args)
401: throws CmdLineException {
402: CmdLineImpl cmdLine = new CmdLineImpl(args);
403:
404: Set<OptionHandler> present = new HashSet<OptionHandler>();
405: int argIndex = 0;
406:
407: while (cmdLine.hasMore()) {
408: String arg = cmdLine.getCurrentToken();
409: if (isOption(arg)) {
410: boolean isKeyValuePair = arg.indexOf('=') != -1;
411: // parse this as an option.
412: currentOptionHandler = isKeyValuePair ? findOptionHandler(arg)
413: : findOptionByName(arg);
414:
415: if (currentOptionHandler == null) {
416: // TODO: insert dynamic handler processing
417: throw new CmdLineException(
418: Messages.UNDEFINED_OPTION.format(arg));
419: }
420:
421: // known option; skip its name
422: cmdLine.proceed(1);
423: } else {
424: if (argIndex >= arguments.size()) {
425: Messages msg = arguments.size() == 0 ? Messages.NO_ARGUMENT_ALLOWED
426: : Messages.TOO_MANY_ARGUMENTS;
427: throw new CmdLineException(msg.format(arg));
428: }
429:
430: // known argument
431: currentOptionHandler = arguments.get(argIndex);
432: if (!currentOptionHandler.option.isMultiValued())
433: argIndex++;
434: }
435: int diff = currentOptionHandler.parseArguments(cmdLine);
436: cmdLine.proceed(diff);
437: present.add(currentOptionHandler);
438: }
439:
440: // make sure that all mandatory options are present
441: for (OptionHandler handler : options)
442: if (handler.option.required() && !present.contains(handler))
443: throw new CmdLineException(
444: Messages.REQUIRED_OPTION_MISSING
445: .format(handler.option.toString()));
446:
447: // make sure that all mandatory arguments are present
448: for (OptionHandler handler : arguments)
449: if (handler.option.required() && !present.contains(handler))
450: throw new CmdLineException(
451: Messages.REQUIRED_ARGUMENT_MISSING
452: .format(handler.option.toString()));
453: }
454:
455: private OptionHandler findOptionHandler(String name) {
456: OptionHandler handler = findOptionByName(name);
457: if (handler == null) {
458: // Have not found by its name, maybe its a property?
459: // Search for parts of the name (=prefix) - most specific first
460: for (int i = name.length(); i > 1; i--) {
461: String prefix = name.substring(0, i);
462: Map<String, OptionHandler> possibleHandlers = filter(
463: options, prefix);
464: handler = possibleHandlers.get(prefix);
465: if (handler != null)
466: return handler;
467: }
468: }
469: return handler;
470: }
471:
472: private OptionHandler findOptionByName(String name) {
473: for (OptionHandler h : options) {
474: NamedOptionDef option = (NamedOptionDef) h.option;
475: if (name.equals(option.name())) {
476: return h;
477: }
478: for (String alias : option.aliases()) {
479: if (name.equals(alias)) {
480: return h;
481: }
482: }
483: }
484: return null;
485: }
486:
487: private Map<String, OptionHandler> filter(List<OptionHandler> opt,
488: String keyFilter) {
489: Map<String, OptionHandler> rv = new TreeMap<String, OptionHandler>();
490: for (OptionHandler h : opt) {
491: if (opt.toString().startsWith(keyFilter))
492: rv.put(opt.toString(), h);
493: }
494: return rv;
495: }
496:
497: /**
498: * Returns true if the given token is an option
499: * (as opposed to an argument.)
500: */
501: protected boolean isOption(String arg) {
502: return parsingOptions && arg.startsWith("-");
503: }
504:
505: /**
506: * All {@link OptionHandler}s known to the {@link CmdLineParser}.
507: *
508: * Constructors of {@link OptionHandler}-derived class keyed by their supported types.
509: */
510: private static final Map<Class, Constructor<? extends OptionHandler>> handlerClasses = Collections
511: .synchronizedMap(new HashMap<Class, Constructor<? extends OptionHandler>>());
512:
513: /**
514: * Registers a user-defined {@link OptionHandler} class with args4j.
515: *
516: * <p>
517: * This method allows users to extend the behavior of args4j by writing
518: * their own {@link OptionHandler} implementation.
519: *
520: * @param valueType
521: * The specified handler is used when the field/method annotated by {@link Option}
522: * is of this type.
523: * @param handlerClass
524: * This class must have the constructor that has the same signature as
525: * {@link OptionHandler#OptionHandler(CmdLineParser, NamedOptionDef, Setter)}.
526: */
527: public static void registerHandler(Class valueType,
528: Class<? extends OptionHandler> handlerClass) {
529: if (valueType == null || handlerClass == null)
530: throw new IllegalArgumentException();
531: if (!OptionHandler.class.isAssignableFrom(handlerClass))
532: throw new IllegalArgumentException(
533: "Not an OptionHandler class");
534:
535: Constructor<? extends OptionHandler> c = getConstructor(handlerClass);
536: handlerClasses.put(valueType, c);
537: }
538:
539: private static Constructor<? extends OptionHandler> getConstructor(
540: Class<? extends OptionHandler> handlerClass) {
541: try {
542: return handlerClass.getConstructor(CmdLineParser.class,
543: OptionDef.class, Setter.class);
544: } catch (NoSuchMethodException e) {
545: throw new IllegalArgumentException(handlerClass
546: + " does not have the proper constructor");
547: }
548: }
549:
550: static {
551: registerHandler(Boolean.class, BooleanOptionHandler.class);
552: registerHandler(boolean.class, BooleanOptionHandler.class);
553: registerHandler(File.class, FileOptionHandler.class);
554: registerHandler(Integer.class, IntOptionHandler.class);
555: registerHandler(int.class, IntOptionHandler.class);
556: registerHandler(Double.class, DoubleOptionHandler.class);
557: registerHandler(double.class, DoubleOptionHandler.class);
558: registerHandler(String.class, StringOptionHandler.class);
559: // enum is a special case
560: //registerHandler(Map.class,MapOptionHandler.class);
561: }
562:
563: public void setUsageWidth(int usageWidth) {
564: this .usageWidth = usageWidth;
565: }
566:
567: public void stopOptionParsing() {
568: parsingOptions = false;
569: }
570:
571: /**
572: * Prints a single-line usage to the screen.
573: *
574: * <p>
575: * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)}
576: * so that you can do {@code printUsage(System.err)}.
577: */
578: public void printSingleLineUsage(OutputStream out) {
579: printSingleLineUsage(new OutputStreamWriter(out), null);
580: }
581:
582: /**
583: * Prints a single-line usage to the screen.
584: *
585: * @param rb
586: * if this is non-null, {@link Option#usage()} is treated
587: * as a key to obtain the actual message from this resource bundle.
588: */
589: public void printSingleLineUsage(Writer w, ResourceBundle rb) {
590: PrintWriter pw = new PrintWriter(w);
591: for (OptionHandler h : arguments) {
592: printSingleLineOption(pw, h, rb);
593: }
594: for (OptionHandler h : options) {
595: printSingleLineOption(pw, h, rb);
596: }
597: pw.flush();
598: }
599:
600: private void printSingleLineOption(PrintWriter pw, OptionHandler h,
601: ResourceBundle rb) {
602: pw.print(' ');
603: if (!h.option.required())
604: pw.print('[');
605: pw.print(h.getNameAndMeta(rb));
606: if (h.option.isMultiValued()) {
607: pw.print(" ...");
608: }
609: if (!h.option.required())
610: pw.print(']');
611: }
612: }
|