All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unkrig.commons.util.CommandLineOptions Maven / Gradle / Ivy

Go to download

A versatile Java(TM) library that implements many useful container and utility classes.

There is a newer version: 1.1.12
Show newest version

/*
 * de.unkrig.commons - A general-purpose Java class library
 *
 * Copyright (c) 2015, Arno Unkrig
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. The name of the author may not be used to endorse or promote products derived from this software without
 *       specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package de.unkrig.commons.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;

import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.ObjectUtil;
import de.unkrig.commons.lang.protocol.ProducerUtil;
import de.unkrig.commons.lang.protocol.ProducerUtil.FromArrayProducer;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.Notations;
import de.unkrig.commons.text.Notations.Phrase;
import de.unkrig.commons.text.StringStream;
import de.unkrig.commons.text.StringStream.UnexpectedElementException;
import de.unkrig.commons.text.pattern.Glob;
import de.unkrig.commons.text.pattern.Pattern2;
import de.unkrig.commons.text.pattern.PatternUtil;
import de.unkrig.commons.util.CommandLineOptionException.ArgumentConversionFailed;
import de.unkrig.commons.util.CommandLineOptionException.ConflictingOptions;
import de.unkrig.commons.util.CommandLineOptionException.DuplicateOption;
import de.unkrig.commons.util.CommandLineOptionException.OptionArgumentMissing;
import de.unkrig.commons.util.CommandLineOptionException.RequiredOptionGroupMissing;
import de.unkrig.commons.util.CommandLineOptionException.RequiredOptionMissing;
import de.unkrig.commons.util.CommandLineOptionException.UnrecognizedOption;
import de.unkrig.commons.util.annotation.CommandLineOption;
import de.unkrig.commons.util.annotation.CommandLineOptionGroup;
import de.unkrig.commons.util.annotation.RegexFlags;

/**
 * Parses "command line options" from the {@code args} of your {@code main()} method and configures a Java bean
 * accordingly.
 *
 * @see #parse(String[], Object)
 */
public final
class CommandLineOptions {

    static { AssertionUtil.enableAssertionsForThisClass(); }

    /**
     * Detects a "command line option", i.e. a "-" followed by at least one character.
     */
    private static final Pattern REGEX_OPTION = Pattern.compile("-.+");

    /**
     * Detects a "compact command lime option", like "-lar" for the UNIX "ls" command.
     * 
*
{@code group(1)}:
The first option letter (for "-lar": "l")
*
{@code group(2)}:
The following option letters (for "-lar": "ar")
*
*/ private static final Pattern REGEX_COMPACT_OPTIONS = Pattern.compile("-([^\\-])(.*)"); private CommandLineOptions() {} /** * Invokes methods of the target object based on the args. *

* All public methods of the target (including those declared by superclasses) are regarded candidates iff * they are annotated with the {@link de.unkrig.commons.util.annotation.CommandLineOption} annotation. *

*

* The possible "names" of the command line option are derived from the {@link * de.unkrig.commons.util.annotation.CommandLineOption#name() name} element of the {@link * de.unkrig.commons.util.annotation.CommandLineOption} annotation, or, if that is missing, from the method name * (see examples below). *

*

* When an element of args equals such a name, then the following elements in args are * converted to match the parameter types of the method (see example code below). After that, the method is * invoked with the arguments. *

*

* Parsing terminates iff either *

*
    *
  • The args are exhausted
  • *
  • The special arg {@code "--"} is reached (which is consumed)
  • *
  • The special arg {@code "-"} is reached (which is notconsumed)
  • *
  • A "normal" command line arguments appears, i.e. one that does not start with "-"
  • *
*

* Example: *

*
     *   public
     *   class MyMain {
     *
     *       // ...
     *
     *       // This one maps to "-font-size <x>" and "--font-size <x>".
     *       @CommandLineOption public static void
     *       setFontSize(double size) { System.out.println("fontSize=" + size); }
     *
     *       // This one maps to "-help" and "--help".
     *       @CommandLineOption public static void
     *       help() {}
     *
     *       // This one maps to "-alpha" and "--alpha".
     *       @CommandLineOption(name = "alpha") public static void
     *       method1() {}
     *
     *       // This one maps to "-beta" and "--gamma".
     *       @CommandLineOption(name = { "-beta", "--gamma" }) public static void
     *       method2() {}
     *
     *       // This one maps to "-foo <a> <b> <c>". (A single dashes may be used instead of the double dash.)
     *       @CommandLineOption public static void
     *       foo(int one, String two, java.util.Pattern three) {}
     *
     *       // This one maps to "--person [ --name <>name> ] [ --age <age> ]". (Single dashes may be used
     *       // instead of the double dashes.)
     *       @CommandLineOption public static void
     *       addPerson(Person p) {}
     *
     *       public static
     *       class Person {
     *           public void setName(String name) {}
     *           public void setAge(int age) {}
     *       }
     *   }
     *
     *   ...
     *
     *   final MyMain main = new MyMain();
     *
     *   String[] args = { "--font-size", "17.5", "a", "b" };
     *   System.out.println(Arrays.toString(args));           // Prints "[--font-size, 17.5, a, b]".
     *
     *   args = MainBean.parseCommandLineOptions(args, main); // Prints "fontSize=17.5".
     *
     *   System.out.println(Arrays.toString(args);            // Prints "[a, b]".
     * 
* *

Option cardinality

* *

* To enforce that a particular command line option must be given a specific number of times, use the * {@link de.unkrig.commons.util.annotation.CommandLineOption#cardinality() cardinality()} element of the {@link * de.unkrig.commons.util.annotation.CommandLineOption @CommandLineOption} annotation: *

*
     *   @CommandLineOption(cardinality = CommandLineOption.Cardinality.MANDATORY)
     *   setColor(String color) {
     *       this.color = color;
     *   }
     * 
*

* For this example, it is now guaranteed that {@link #parse(String[], Object)} will invoke the "{@code * setColor()}" method exactly once. *

*

* The default value for the command line option cardinality is {@link * de.unkrig.commons.util.annotation.CommandLineOption.Cardinality#OPTIONAL}. *

* *

Option groups

* *

* To enforce that a particular number of a set of command line options must be given, use the {@link * de.unkrig.commons.util.annotation.CommandLineOption#group() group()} element of the {@link * de.unkrig.commons.util.annotation.CommandLineOption @CommandLineOption} annotation: *

*
     *   @CommandLineOption(group = Sources.class)
     *   setFile(File file) {
     *       this.file = file;
     *   }
     *
     *   @CommandLineOption(group = Sources.class)
     *   setStdin() {
     *       this.stdin = true;
     *   }
     *
     *   // This interface solely serves as the "connector" between the related command line options; it is (typically)
     *   // not used otherwise.
     *   @CommandLineOptionGroup(cardinality = CommandLineOptionGroup.Cardinality.EXACTLY_ONE)
     *   interface Sources {}
     * 
*

* For this example, it is now guaranteed that {@link #parse(String[], Object)} will invoke exactly one of the * methods "{@code setFile()}" and "{@code setStdin()}". *

*

* The default value for the command line option group cardinality is {@link * de.unkrig.commons.util.annotation.CommandLineOptionGroup.Cardinality#ZERO_OR_ONE ZERO_OR_ONE}. *

* * @return The args, less the elements that were parsed as command line * options * @throws CommandLineOptionException An error occurred during the parsing; typically a command-line application * would print the message of the exception to STDERR and call "{@code * System.exit(1)}" */ public static String[] parse(String[] args, Object target) throws CommandLineOptionException { StringStream ss = new StringStream(ProducerUtil.fromArray(args)); Parser p = new Parser(target.getClass()); // Parse all command line options and apply them to the "target". p.parseOptions(ss, target); // Consume the optional separator "--" between the command line options and the "normal" command line // arguments. if (!ss.peekRead("--") && ss.peek(CommandLineOptions.REGEX_OPTION)) { throw new UnrecognizedOption(AssertionUtil.notNull(ss.group(0))); } // Return the remaining command line arguments. return ss.readRest(); } private static class Parser { // CONFIGURATION /** Option name => {@link CommandLineOption} */ private final Map allOptions = new HashMap(); /** The options that must appear at most once. */ private final Set singularOptions = new IdentityHashSet(); /** The options that must appear at least once. */ private final List requiredOptions = new ArrayList(); /** The option groups with cardinality 1 or less. */ private final Set singularOptionGroups = new IdentityHashSet(); /** The option groups with cardinality 0 or more. */ private final List requiredOptionGroups = new ArrayList(); private final Map optionToGroup = new IdentityHashMap(); private final Map optionToMethod = new IdentityHashMap(); // PARSING STATE /** Options that were parsed so far. */ private final Set actualOptions = new IdentityHashSet(); /** Options groups that were parsed so far. */ private final Set actualOptionGroups = new IdentityHashSet(); /** * Analyzes the targetClass for any command line option setters. * * @throws AssertionError A {@link CommandLineOption @CommandLineOption} refers to a type that is not * annotated with {@link CommandLineOptionGroup @CommandLineOptionGroup} * @throws AssertionError Two methods map to the same name */ Parser(Class targetClass) { // Identify all command line options that apply to the target class, and populate the "methodCache". for (Method m : targetClass.getMethods()) { CommandLineOption option = CommandLineOptions.getOption(m); if (option == null) continue; this.optionToMethod.put(option, m); // Determine the "names" of the command-line option - either from the "name" element of the // "@CommandLineOption" annotation, or from the method name. String[] names = option.name(); if (names.length == 0) { String n = m.getName(); if (n.startsWith("set")) { n = n.substring(3); } else if (n.startsWith("add")) { n = n.substring(3); } names = new String[] { Notations.fromCamelCase(n).toLowerCaseHyphenated() }; } for (String name : names) { for (String name2 : ( name.startsWith("-") ? new String[] { name } : new String[] { "-" + name, "--" + name } )) { CommandLineOption prev = this.allOptions.put(name2, option); assert prev == null : "Two methods map to option \"" + name2 + "\""; } } // Process the option cardinality. { CommandLineOption.Cardinality c = option.cardinality(); if (c == CommandLineOption.Cardinality.MANDATORY || c == CommandLineOption.Cardinality.OPTIONAL) { this.singularOptions.add(option); } if ( c == CommandLineOption.Cardinality.MANDATORY || c == CommandLineOption.Cardinality.ONCE_OR_MORE ) { this.requiredOptions.add(option); } } // Process the group cardinality. GROUP_CARDINALITY: { Class group = option.group(); if (group == Object.class) break GROUP_CARDINALITY; CommandLineOptionGroup optionGroup = group.getAnnotation(CommandLineOptionGroup.class); if (optionGroup == null) { throw new AssertionError( "Group class \"" + group + "\" lacks the \"@CommandLineOptionGroup\" annotation" ); } this.optionToGroup.put(option, optionGroup); CommandLineOptionGroup.Cardinality c = optionGroup.cardinality(); if ( c == CommandLineOptionGroup.Cardinality.EXACTLY_ONE || c == CommandLineOptionGroup.Cardinality.ZERO_OR_ONE ) this.singularOptionGroups.add(optionGroup); if ( c == CommandLineOptionGroup.Cardinality.EXACTLY_ONE || c == CommandLineOptionGroup.Cardinality.ONE_OR_MORE ) this.requiredOptionGroups.add(optionGroup); } } } /** * Parses tokens on the ss that map to options of the target. * * @return Whether the next token(s) on ss were parsed as an option of * the target * @throws CommandLineOptionException An error occurred during the parsing * @throws EX ss threw an exception */ private void parseOptions(StringStream ss, Object target) throws EX, CommandLineOptionException { // Parse as many command line options as possible. while (this.parseNextOption(ss, target)); // Verify that all "required" options were parsed. for (CommandLineOption option : this.requiredOptions) { if (!this.actualOptions.contains(option)) { throw new RequiredOptionMissing(option, this.optionNames(option)); } } for (CommandLineOptionGroup optionGroup : this.requiredOptionGroups) { if (!this.actualOptionGroups.contains(optionGroup)) { throw new RequiredOptionGroupMissing(optionGroup, this.optionNames(optionGroup)); } } } /** * @return All names of the option */ private String[] optionNames(CommandLineOption option) { List result = new ArrayList(); for (Entry e : this.allOptions.entrySet()) { final String optionName = e.getKey(); final CommandLineOption o = e.getValue(); if (o != option) continue; result.add(optionName); } return result.toArray(new String[result.size()]); } /** * @return All names of all options that are members of the group */ private String[] optionNames(CommandLineOptionGroup optionGroup) { List result = new ArrayList(); for (Entry e : this.allOptions.entrySet()) { final String optionName = e.getKey(); final CommandLineOption option = e.getValue(); if (option.group().getAnnotation(CommandLineOptionGroup.class) != optionGroup) continue; result.add(optionName); } return result.toArray(new String[result.size()]); } /** * Parses an option of the target iff the next token on the ss maps to an option of the * target. * * @return Whether the next token(s) on ss were parsed as an option of the * target * @throws CommandLineOptionException An error occurred during the parsing * @throws EX ss threw an exception */ private boolean parseNextOption(StringStream ss, Object target) throws CommandLineOptionException, EX { if (ss.atEnd()) return false; // Special option "--" indicates the end of the command line options. if (ss.peek("--")) return false; // "-" is not a command line option, but a normal command line argument (typically means "STDIN" or // "STDOUT"). if (ss.peek("-")) return false; // Is it a "verbose" (non-compact) option? if (this.parseNextVerboseOption(ss, target)) return true; // Iff the first letter after "-" can be interpreted as a single-letter option, then interpret ALL letters // as such. // This feature is called "compact command line options": E.g. "-lar" is equivalent with "-l -a -r". // Even options with arguments are possible, e.g. "-abc AA BB CC" in place of "-a AA -b BB -c CC". COMPACT_OPTIONS: if (ss.peek(CommandLineOptions.REGEX_COMPACT_OPTIONS)) { char firstOptionLetter = AssertionUtil.notNull(ss.group(1)).charAt(0); String followingOptionLetters = AssertionUtil.notNull(ss.group(2)); { String firstOptionName = "-" + firstOptionLetter; CommandLineOption firstOption = this.getOption(firstOptionName, target.getClass()); if (firstOption == null) break COMPACT_OPTIONS; // Only now that we have verified that the first letter can be interpreted as a single-letter // option, we consume the token. try { ss.read(); } catch (UnexpectedElementException e) { throw new AssertionError(); } this.applyCommandLineOption(firstOptionName, firstOption, ss, target); } for (int i = 0; i < followingOptionLetters.length(); i++) { String optionName = "-" + followingOptionLetters.charAt(i); CommandLineOption optionMethod = this.getOption(optionName, target.getClass()); if (optionMethod == null) { throw new UnrecognizedOption(optionName); } this.applyCommandLineOption(optionName, optionMethod, ss, target); } return true; } return false; } /** * @return Whether the next tokens on ss could be parsed as an option for * the target * @throws CommandLineOptionException An error occurred during the parsing * @throws EX ss threw an exception */ private boolean parseNextVerboseOption(StringStream ss, Object target) throws CommandLineOptionException, EX { if (ss.atEnd()) return false; String optionName; try { optionName = ss.peek(); } catch (UnexpectedElementException uee) { throw new AssertionError(uee); } CommandLineOption option = this.getOption(optionName, target.getClass()); if (option == null) return false; try { ss.read(); } catch (UnexpectedElementException uee) { throw new AssertionError(uee); } this.applyCommandLineOption( optionName, // optionName option, // option ss, // stringStream target // target ); return true; } /** * Determines the method of {@code target.getClass()} that is applicable for the given option. The * rules for matching are as follows: *
    *
  • The method must be annotated with {@link CommandLineOption @CommandLineOption}.
  • *
  • * If that annotation has the "{@code name}" element: *
      *
    • * If the {@code name} value starts with "-": *
        *
      • * The option equals the {@code name} value. *
      • *
      *
    • *
    • * Otherwise, if the {@code name} value does not start with "-": *
        *
      • * The option equals {@code "-"+name} or {@code "--"+name} *
      • *
      *
    • *
    * Example: *
    * @CommandLineOption(name = { "alpha", "-beta" }) public void method() { ... *
    * matches if option is {@code "-alpha"}, {@code "--alpha"} and {@code "-beta"}. *
  • *
  • * Otherwise, if that annotation does not have the "{@code name}" element: *
      *
    • * The method name, with an optional "set" or "add" prefix removed, then converted to {@link * Phrase#toLowerCaseHyphenated() lower-case-hyphenated}, then prefixed with "-" and "--", equals the * option. *
      * Example: Method name "setFooBar()" matches if option is "-foo-bar" or "--foo-bar". *
    • *
    *
  • *
* * @return {@code Null} iff there is no applicable method */ @Nullable public CommandLineOption getOption(String optionName, Class targetClass) { return this.allOptions.get(optionName); } private Method methodFor(CommandLineOption option) { return AssertionUtil.notNull(this.optionToMethod.get(option), option.toString()); } /** * Parses the command line option's arguments from the args and invokes the method. *

* The arguments for the method are parsed from the command line, see {@link #getArgument(String[], int[], * Annotation[], Class)}. *

*

* Iff the method is a {@code varargs} method, then all remaining arguments are converted to an * array. *

* * @throws CommandLineOptionException An error occurred during the parsing * @throws EX The stringStream threw an exception */ public void applyCommandLineOption( String optionName, CommandLineOption option, StringStream stringStream, @Nullable Object target ) throws CommandLineOptionException, EX { if (this.singularOptions.contains(option) && this.actualOptions.contains(option)) { throw new DuplicateOption(option, optionName, this.optionNames(option)); } CommandLineOptionGroup optionGroup = this.optionToGroup.get(option); if (optionGroup != null) { if (this.singularOptionGroups.contains(optionGroup) && this.actualOptionGroups.contains(optionGroup)) { throw new ConflictingOptions(optionGroup, option, optionName); } } Method method = this.methodFor(option); Class[] methodParametersTypes = method.getParameterTypes(); Annotation[][] methodParametersAnnotations = method.getParameterAnnotations(); assert methodParametersTypes.length == methodParametersAnnotations.length; // Convert the command line option arguments into method call arguments. Object[] methodArgs = new Object[methodParametersTypes.length]; for (int i = 0; i < methodArgs.length; i++) { try { methodArgs[i] = this.getArgument( stringStream, // stringStream methodParametersAnnotations[i], // annotations methodParametersTypes[i] // targetType ); } catch (UnexpectedElementException uee) { throw new OptionArgumentMissing(option, optionName, i); } } // Now that the "methodArgs" array is filled, invoke the method. try { method.invoke(target, methodArgs); } catch (Exception e) { throw new AssertionError(e); } this.actualOptions.add(option); if (optionGroup != null) this.actualOptionGroups.add(optionGroup); } /** * Creates and returns an object of the given targetType from tokens from ss. *
    *
  • * For an array target type, all remaining tokens from ss are parsed as array elements. *
  • *
  • * For a bean target type (a class that has only the zero-arg constructor), the bean is instantiated and as * many tokens from ss as possible are parsed as options of the bean. *
  • *
  • * For any other target type, the next token from ss is converted to the target type, as * described {@link ObjectUtil#fromString(String, Class) here}. *
  • *
* * @throws CommandLineOptionException An error occurred during the parsing * @throws UnexpectedElementException The ss has too few tokens * @throws EX The ss threw an exception * @throws AssertionError Bean creation failed */ private Object getArgument(StringStream ss, Annotation[] annotations, Class targetType) throws CommandLineOptionException, UnexpectedElementException, EX { // Special case: The target type is an array type. (This handles also the case of a VARARGS method.) if (targetType.isArray()) { final Class componentType = targetType.getComponentType(); // Treat all remaining arguments as array elements (even those starting with a dash!). List elements = new ArrayList(); while (!ss.atEnd()) { elements.add(this.getArgument(ss, annotations, componentType)); } int size = elements.size(); Object result = Array.newInstance(componentType, size); if (componentType.isPrimitive()) { // Can't use "System.arraycopy()" to copy to array of primitive. for (int i = 0; i < size; i++) Array.set(result, i, elements.get(i)); } else { System.arraycopy(elements.toArray(), 0, result, 0, size); } return result; } // Special case: Target type "java.util.Pattern". if (targetType == Pattern.class) { // Use "Pattern2", so that the Pattern2.WILDCARD flag can also be used. return Pattern2.compile(ss.read(), CommandLineOptions.getRegexFlags(annotations)); } // Special case: Target type "de.unkrig.commons.text.pattern.Glob". if (targetType == Glob.class) { return Glob.compile(ss.read(), CommandLineOptions.getRegexFlags(annotations)); } // Special case: Target type is a Java bean. Constructor[] cs = targetType.getConstructors(); if (cs.length == 1 && cs[0].getParameterTypes().length == 0) { Object bean; try { bean = cs[0].newInstance(); } catch (Exception e) { throw new AssertionError(e); } // Invoke as many setter methods on the bean as possible. new Parser(targetType).parseOptions(ss, bean); return bean; } // Fall back to "ObjectUtil.fromString()". String arg = ss.read(); try { return ObjectUtil.fromString(arg, targetType); } catch (IllegalArgumentException iae) { throw new ArgumentConversionFailed(arg, targetType, iae); } } } /** * Checks for a {@link RegexFlags} annotation and returns its value. */ private static int getRegexFlags(Annotation[] annotations) { for (Annotation a : annotations) { if (a.annotationType() == RegexFlags.class) { return ((RegexFlags) a).value(); } } return 0; } /** * Determines the method of targetClass that is applicable for the given optionName. * * @return {@code null} iff there is no * applicable method * @see de.unkrig.commons.util.CommandLineOptions.Parser#getOption(String, Class) */ @Nullable public static Method getMethodForOption(String optionName, Class targetClass) { Parser parser = new Parser(targetClass); CommandLineOption option = parser.getOption(optionName, targetClass); if (option == null) return null; return parser.methodFor(option); } /** * Parses the command line option's arguments from the args and invokes the method. *

* Iff the method is a {@code varargs} method, then all remaining arguments are converted to an array. *

* * @param optionArgumentIndex The position of the first option argument in args * @return The position of the last option argument in args plus one * @throws CommandLineOptionException An error occurred during the parsing * @throws AssertionError The method is not annotated with {@link * CommandLineOption @CommandLineOption} */ public static int applyCommandLineOption( String optionName, Method method, String[] args, int optionArgumentIndex, @Nullable Object target ) throws CommandLineOptionException { CommandLineOption option = CommandLineOptions.getOption(method); assert option != null; Class targetClass = target != null ? target.getClass() : method.getDeclaringClass(); FromArrayProducer fap = ProducerUtil.fromArray(args, optionArgumentIndex, args.length); new Parser(targetClass).applyCommandLineOption( optionName, option, new StringStream(fap), target ); return fap.index(); } /** * @return The singleton {@link CommandLineOption} for the method, or {@code null} iff the * method has no {@link CommandLineOption} annotation */ @Nullable private static CommandLineOption getOption(Method method) { synchronized (CommandLineOptions.METHOD_TO_OPTION) { CommandLineOption option = CommandLineOptions.METHOD_TO_OPTION.get(method); if (option != null) return option; if (CommandLineOptions.METHOD_TO_OPTION.containsKey(method)) return null; option = method.getAnnotation(CommandLineOption.class); CommandLineOptions.METHOD_TO_OPTION.put(method, option); return option; } } private static final Map METHOD_TO_OPTION = new WeakHashMap(); /** * Reads (and decodes) the contents of a resource, replaces all occurrences of * "${system-property-name}" with the value of the designated system property, * and writes the result to the printStream. *

* The resource is found through the baseClass's class loader and the name {@code * "}package-name{@code /}simple-class-name{@code .}relativeResourceName{@code * "}, where all periods in the package-name have been replaced with slashes. *

*

* To ensure that the resource is decoded with the same charset as it was encoded, you should not use the JVM * charset (which could be different in the build environment and the runtime environment). *

* * @param baseClass Poses the base for the resource name * @param relativeResourceName The name of the resource, relative to the baseClass * @param resourceCharset The charset to use for decoding the contents of the resource; {@code null} for the * JVM default charset * @throws FileNotFoundException The resource could not be found */ public static void printResource( Class baseClass, String relativeResourceName, @Nullable Charset resourceCharset, PrintStream printStream ) throws IOException { String resourceName = baseClass.getSimpleName() + "." + relativeResourceName; InputStream is = baseClass.getResourceAsStream(resourceName); if (is == null) throw new FileNotFoundException(resourceName); try { Writer w = new OutputStreamWriter(printStream); PatternUtil.replaceSystemProperties( resourceCharset == null ? new InputStreamReader(is) : new InputStreamReader(is, resourceCharset), w ); w.flush(); } finally { is.close(); } } }