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.Method;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.regex.Pattern;

import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.ObjectUtil;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.Notations;
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.annotation.CommandLineOption;
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(); }

    private CommandLineOptions() {}

    /**
     * Sets the target's properties from the args.
     * 

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

*

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

*

* 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. After that, the method is invoked with the arguments, * and the elements are removed from args. *

*

* 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)
  • *
*

* Example: *

*
     *   public
     *   class MyMain {
     *
     *       // ...
     *
     *       @CommandLineOption
     *       public static void
     *       setWidth(double width) {
     *           System.out.println("width=" + width);
     *       }
     *   }
     *
     *   ...
     *
     *   final MyMain main = new MyMain();
     *
     *   String[] args = { "--width", "17.5", "a", "b" };
     *   System.out.println(Arrays.toString(args);            // Prints "[--width, 17.5, a, b]".
     *
     *   args = MainBean.parseCommandLineOptions(args, main); // Prints "width=17.5".
     *
     *   System.out.println(Arrays.toString(args);            // Prints "[a, b]".
     * 
* * @return The args, less the elements that were parsed as "command line * options" * @throws IllegalArgumentException An error occurred with parsing; typically a command-line application would * print the message of the exception to STDERR and call "{@code System.exit(1)}" * @throws RuntimeException */ public static String[] parse(String[] args, Object target) { // Now examine the "args" as far as they can be identified as "command line options". int idx = 0; while (idx < args.length) { String arg = args[idx]; // Special option "--" indicates the end of the command line options. if ("--".equals(arg)) { idx++; break; } // "-" is not a command line option, but a normal command line argument (typically means "STDIN" or // "STDOUT"). if ("-".equals(arg)) { break; } Method method = CommandLineOptions.getMethodForOption(arg, target.getClass()); if (method != null) { // A "normal" command line option, e.g. "-o foo.txt". idx = CommandLineOptions.applyCommandLineOption( arg, // optionName method, // method args, // args idx + 1, // optionaArgumentIndex target // target ); } else if (arg.length() >= 2 && arg.charAt(0) == '-' && arg.charAt(1) != '-') { // Process "compact single-letter" command line options, e.g. "-lar" for "-l -a -r". // Event options with arguments are possible, e.g. "-abc AA BB CC" for "-a AA -b BB -c CC". idx++; for (int i = 1; i < arg.length(); i++) { String optionName = "-" + arg.charAt(i); method = CommandLineOptions.getMethodForOption(optionName, target.getClass()); if (method == null) { throw new IllegalArgumentException(( "Unrecognized command line option \"" + arg + "\"; try \"--help\"" )); } idx = CommandLineOptions.applyCommandLineOption( optionName, method, args, idx, // optionArgumentIndex target ); } } else { // See whether "arg" is a normal command line argument (e.g. a file name), or rather an unrecognized // command line option. if (args[idx].startsWith("-")) { throw new IllegalArgumentException(( "Unrecognized command line option \"" + args[idx] + "\"; try \"--help\"" )); } // "Arg" is probably a normal command line argument (e.g. a file name) return idx == 0 ? args : Arrays.copyOfRange(args, idx, args.length); } } // "Args" was completely parse as command line options. return new String[0]; } /** * Determines the method of {@code target.getClass()} that is applicable for the given option. * * @return {@code Null} iff there is no applicable method */ @Nullable public static Method getMethodForOption(String option, Class targetClass) { for (Method m : targetClass.getMethods()) { CommandLineOption clo = m.getAnnotation(CommandLineOption.class); if (clo == null) continue; // Determine the "names" of the command-line option - either from the "name" element of the // "@CommandLineOption" annotation, or from the method name. String[] names = clo.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 } )) { if (name2.equals(option)) return m; } } } return null; } /** * 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 IllegalArgumentException There are less option arguments available than the method requires */ public static int applyCommandLineOption( String optionName, Method method, String[] args, int optionArgumentIndex, @Nullable Object target ) { 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]; if (methodParametersTypes.length >= 1 && method.isVarArgs()) { final Class componentType = methodParametersTypes[methodArgs.length - 1].getComponentType(); // The VARARGS case: Process the first n-1 args as usual... if (optionArgumentIndex + methodParametersTypes.length - 1 > args.length) { throw new IllegalArgumentException( "Command line option \"" + optionName + "\" requires at least " + (methodParametersTypes.length - 1) + "\" arguments, but only " + (args.length - optionArgumentIndex) + " are available on the command line" ); } for (int i = 0; i < methodArgs.length - 1; i++) { methodArgs[i] = CommandLineOptions.fromString( args[optionArgumentIndex++], // text methodParametersTypes[i], // targetType methodParametersAnnotations[i] // annotations ); } // ... then parse the all remaining args into the array that is the last method args. int arrayLength = args.length - optionArgumentIndex; Object array = Array.newInstance(componentType, arrayLength); for (int i = 0; i < arrayLength; i++) { Array.set(array, i, CommandLineOptions.fromString( args[optionArgumentIndex++], // text componentType, // targetType methodParametersAnnotations[methodArgs.length - 1] // annotations )); } methodArgs[methodArgs.length - 1] = array; } else { // This is the non-VARARGS case. if (optionArgumentIndex + methodParametersTypes.length > args.length) { throw new IllegalArgumentException( "Command line option \"" + optionName + "\" requires " + methodParametersTypes.length + "\" arguments, but only " + (args.length - optionArgumentIndex) + " are available on the command line" ); } for (int i = 0; i < methodArgs.length; i++) { methodArgs[i] = CommandLineOptions.fromString( args[optionArgumentIndex++], // text methodParametersTypes[i], // targetType methodParametersAnnotations[i] // annotations ); } } // Now that the "methodArgs" array is filled, invoke the method. try { method.invoke(target, methodArgs); } catch (Exception e) { throw new AssertionError(e); } return optionArgumentIndex; } /** * An extended version of {@link ObjectUtil#fromString(String, Class)} that also supports {@link Pattern2} and * {@link Glob}. */ @SuppressWarnings("unchecked") private static T fromString(String text, Class targetType, Annotation[] annotations) { // Use "Pattern2", so that the Pattern2.WILDCARD flag can also be used. if (targetType == Pattern.class) { return (T) Pattern2.compile(text, CommandLineOptions.getRegexFlags(annotations)); } if (targetType == Glob.class) { return (T) Glob.compile(text, CommandLineOptions.getRegexFlags(annotations)); } return ObjectUtil.fromString(text, targetType); } /** * 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; } /** * Reads (and decodes) the contents of a resource, replaces all occurrences of * "${system-property-name}" with the value of the system property, and writes * the result to a {@code PrintStream}. *

* The resource is found through the clasS's class loader, and the name {@code * "}package-name{@code /}simple-class-name{@code .}name{@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 charset 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 clasS, String name, @Nullable Charset charset, PrintStream ps) throws IOException { String resourceName = clasS.getSimpleName() + "." + name; InputStream is = clasS.getResourceAsStream(resourceName); if (is == null) throw new FileNotFoundException(resourceName); try { Writer w = new OutputStreamWriter(ps); PatternUtil.replaceSystemProperties( resourceName, charset == null ? new InputStreamReader(is) : new InputStreamReader(is, charset), w ); w.flush(); } finally { is.close(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy