org.codeseed.common.config.ext.CommandLineHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.codeseed.common.config.ext.CommandLineHelper.java

Source

/*
 * Copyright 2013 Jeremy Gustie
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codeseed.common.config.ext;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.collect.Iterables.getLast;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.annotation.Nullable;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.Parser;
import org.apache.commons.cli.PosixParser;
import org.codeseed.common.config.Configuration;
import org.codeseed.common.config.PropertySource;
import org.codeseed.common.config.PropertySources;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;

/**
 * A wrapper to help minimize boilerplate when dealing with command line
 * arguments. Typical usage involves creating a resource bundle and associating
 * configuration methods with the command line options:
 *
 * Given a configuration interface:
 *
 * <pre>
 * public interface MyConfig extends Configuration {
 *     &#064;CommandLine(&quot;t&quot;)
 *     String test();
 * }
 * </pre>
 *
 * You should have the bundle {@code com/example/config/messages.properties}:
 *
 * <pre>
 * # Uncomment to change default values
 * # syntaxPrefix=Usage\:
 * # header=Options\:
 * # footer=
 * # help.description=show this help
 * t.description=test value
 * </pre>
 *
 * And you can construct a property source using the code:
 *
 * <pre>
 * public static void main(String[] args) {
 *     PropertySource cmd = CommandLineHelper.posix(&quot;com.example.config.messages&quot;)
 *             .withHelpOption()
 *             .withOptions(MyConfig.class)
 *             .parse(args);
 * }
 * </pre>
 *
 * Which would produce the following output when executed as {@code test -h}:
 *
 * <pre>
 * Usage: test [-h] [-t]
 * Options:
 * -h, --help  show this help
 * -t          test value
 * </pre>
 *
 * @author Jeremy Gustie
 */
public class CommandLineHelper {

    /**
     * The command line parser for this helper.
     */
    private final Parser parser;

    /**
     * The resource bundle used for help and usage messages.
     */
    private final ResourceBundle bundle;

    /**
     * The options builder that scans for annotations.
     */
    private final CommandLineOptionsBuilder options;

    /**
     * A help option that can be used to show a help menu.
     */
    @Nullable
    private Option helpOption;

    /**
     * The writer to print everything out to.
     */
    private PrintWriter err = new PrintWriter(System.err, true);

    /**
     * The terminal width to print everything.
     */
    private int terminalWidth = guessTerminalWidth();

    /**
     * Creates a new POSIX command line parser with the supplied bundle.
     *
     * @param bundleName
     *            the name of the resource bundle to use
     * @return a new command line helper
     */
    public static CommandLineHelper posix(String bundleName) {
        return new CommandLineHelper(new PosixParser(), bundleName);
    }

    /**
     * Creates a new POSIX command line parser with the supplied bundle.
     *
     * @param bundleType
     *            the type of the resource bundle to use
     * @return a new command line helper
     * @see #posix(String)
     */
    public static CommandLineHelper posix(Class<? extends ResourceBundle> bundleType) {
        return posix(bundleType.getName());
    }

    /**
     * Creates a new GNU command line parser with the supplied bundle.
     *
     * @param bundleName
     *            the name of the resource bundle to use
     * @return a new command line helper
     */
    public static CommandLineHelper gnu(String bundleName) {
        return new CommandLineHelper(new GnuParser(), bundleName);
    }

    /**
     * Creates a new GNU command line parser with the supplied bundle.
     *
     * @param bundleType
     *            the type of the resource bundle to use
     * @return a new command line helper
     * @see #gnu(String)
     */
    public static CommandLineHelper gnu(Class<? extends ResourceBundle> bundleType) {
        return gnu(bundleType.getName());
    }

    private CommandLineHelper(Parser parser, String bundleName) {
        this.parser = checkNotNull(parser);
        this.bundle = ResourceBundle.getBundle(bundleName, Locale.getDefault());
        this.options = CommandLineOptionsBuilder.newBuilder().bundle(bundle);
    }

    /**
     * Scan the supplied interface for annotations.
     *
     * @param configuration
     *            the configuration interface to find command line options from
     * @return this helper
     * @see CommandLineOptionsBuilder#addFrom(Class)
     */
    public CommandLineHelper withOptions(Class<? extends Configuration> configuration) {
        options.addFrom(configuration);
        return this;
    }

    /**
     * Include a help option. Users will be able to use {@code -h} or
     * {@code --help} to show help from the command line.
     * <p>
     * If the bundle contains the key {@code help.description}, it's value will
     * be used as the description, otherwise the default text "show this help"
     * will be used.
     *
     * @return this helper
     */
    public CommandLineHelper withHelpOption() {
        this.helpOption = new Option("h", "help", false, getString("help.description", "show this help"));
        return this;
    }

    /**
     * Returns a property source from the supplied command line arguments.
     *
     * @param args
     *            the arguments passed from the command line
     * @return a property source backed by the parsed command line arguments
     */
    public PropertySource parse(String[] args) {
        // Construct the command line options
        final Options options = this.options.build();
        if (helpOption != null) {
            options.addOption(helpOption);
        }

        try {
            final CommandLine commandLine = parser.parse(options, args);
            if (helpOption != null && commandLine.hasOption(helpOption.getOpt())) {
                // Display the help
                HelpFormatter help = new HelpFormatter();
                help.setSyntaxPrefix(syntaxPrefix());
                help.setLongOptPrefix(" --");
                help.printHelp(err, terminalWidth, applicationName(), header(), options, 1, 1, footer(), true);
                return helpExit();
            } else {
                // Wrapped the parsed commands in a property source
                return new CommandLinePropertySource(commandLine);
            }
        } catch (ParseException e) {
            // Display the error and usage message
            HelpFormatter help = new HelpFormatter();
            help.printWrapped(err, terminalWidth, e.getMessage());
            help.printUsage(err, terminalWidth, applicationName(), options);
            throw usageExit(e);
        }
    }

    /**
     * Attempts to determine the application name.
     *
     * @return the command used to invoke the application
     */
    protected String applicationName() {
        // Look for the system property and take the first value
        final String javaCommand = System.getProperty("sun.java.command");
        if (javaCommand == null) {
            return Splitter.on(CharMatcher.WHITESPACE).split(javaCommand).iterator().next();
        }

        // Look for the class name of the main method
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        if (stackTrace.length > 0) {
            return getLast(Splitter.on('.').split(stackTrace[stackTrace.length - 1].getClassName()));
        }
        return "app";
    }

    /**
     * Returns the syntax prefix. The default implementation looks up the
     * {@code syntaxPrefix} message, if it fails the default value is "Usage: ".
     *
     * @return prefix to the usage syntax
     */
    protected String syntaxPrefix() {
        return getString("syntaxPrefix", "Usage: ");
    }

    /**
     * Returns the header. The default implementation looks up the
     * {@code header} message, if it fails the default value is "Options:".
     *
     * @return header to the command line options help text
     */
    protected String header() {
        return getString("header", "Options:");
    }

    /**
     * Returns the footer. The default implementation looks up the
     * {@code footer} message, if it fails the default value is "".
     *
     * @return footer to the command line options help text
     */
    protected String footer() {
        return getString("footer", "");
    }

    /**
     * Attempts to lookup a string, if it is not available in the bundle then
     * the default value is used.
     *
     * @param key
     *            the bundle key
     * @param defaultValue
     *            the value to use when silently ignoring missing resources
     * @return the value from the bundle, or the default if not found
     */
    private String getString(String key, String defaultValue) {
        try {
            return bundle.getString(key);
        } catch (MissingResourceException e) {
            return defaultValue;
        }
    }

    /**
     * Exit after showing the help. Technically this won't return because it
     * just exits.
     *
     * @return nothing, won't happen
     */
    protected PropertySource helpExit() {
        System.exit(1);
        return PropertySources.empty();
    }

    /**
     * Exit after showing the usage. Technically this won't return because it
     * just exits.
     *
     * @param t
     *            ignored throwable that is making us exit
     * @return nothing, won't happen
     */
    protected RuntimeException usageExit(Throwable t) {
        System.exit(64);
        return propagate(t);
    }

    /**
     * Return a guess at the width of the current terminal.
     *
     * @return approximate number of columns in the current terminal
     */
    private static int guessTerminalWidth() {
        // Environment variable
        String columns = System.getenv("COLUMNS");
        if (columns != null) {
            return Integer.parseInt(columns);
        }

        // TODO terminfo? (tput cols)

        // Reasonable default
        return 80;
    }

}