Java tutorial
/* * 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 { * @CommandLine("t") * 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("com.example.config.messages") * .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; } }