/** * Copyright (C) 2012-2013 Selventa, Inc. * * This file is part of the OpenBEL Framework. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The OpenBEL Framework is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the OpenBEL Framework. If not, see <http://www.gnu.org/licenses/>. * * Additional Terms under LGPL v3: * * This license does not authorize you and you are prohibited from using the * name, trademarks, service marks, logos or similar indicia of Selventa, Inc., * or, in the discretion of other licensors or authors of the program, the * name, trademarks, service marks, logos or similar indicia of such authors or * licensors, in any marketing or advertising materials relating to your * distribution of the program or any covered product. This restriction does * not waive or limit your obligation to keep intact all copyright notices set * forth in the program as delivered to you. * * If you distribute the program in whole or in part, or any modified version * of the program, and you assume contractual liability to the recipient with * respect to the program or modified version, then you will indemnify the * authors and licensors of the program for any liabilities that these * contractual assumptions directly impose on those licensors and authors. */ package org.openbel.framework.core; import static java.lang.String.format; import static java.lang.System.err; import static java.lang.System.out; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.openbel.framework.common.BELUtilities.noItems; import static org.openbel.framework.common.PathConstants.SLF4J_LOGGER_FILE; import static org.openbel.framework.common.Strings.DEBUG_HELP; import static org.openbel.framework.common.Strings.ERROR_INIT_LOGGING; import static org.openbel.framework.common.Strings.SYSCFG_READ_FAILURE; import static org.openbel.framework.common.Strings.VERBOSE_HELP; import static org.openbel.framework.common.cfg.SystemConfiguration.createSystemConfiguration; import static org.openbel.framework.common.cfg.SystemConfiguration.getSystemConfiguration; import static org.openbel.framework.common.enums.ExitCode.GENERAL_FAILURE; import static org.openbel.framework.common.enums.ExitCode.PARSE_ERROR; import static org.openbel.framework.common.enums.ExitCode.SUCCESS; import static org.openbel.framework.core.StandardOptions.LONG_OPT_DEBUG; import static org.openbel.framework.core.StandardOptions.LONG_OPT_SYSCFG; import static org.openbel.framework.core.StandardOptions.LONG_OPT_VERBOSE; import static org.openbel.framework.core.StandardOptions.SHORT_OPT_VERBOSE; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.List; import java.util.ListIterator; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.openbel.framework.common.BELRuntimeException; import org.openbel.framework.common.Reportable; import org.openbel.framework.common.SimpleOutput; import org.openbel.framework.common.cfg.SystemConfiguration; import org.openbel.framework.common.enums.BELFrameworkVersion; import org.openbel.framework.common.enums.ExitCode; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.Context; import ch.qos.logback.core.joran.spi.JoranException; /** * Base class for BEL command-line applications. * <p> * <ol> * <li>The only call to {@link System#exit(int)} should be done in * {@link #systemExit(ExitCode)}.</li> * <li>Command-line applications should support output/error redirection to * streams beyond {@code stdout} and {@code stderr}.</li> * <li>Command-line applications should needs to be constructed before any * logging statement are executed.</li> * </ol> * </p> * <p> * BEL command-line applications are built using a mix of Java and XML-centric * container configuration mechanisms (for more information, see <a href= * "http://static.springsource.org/spring/docs/3.0.x/reference/beans.html">The * IoC container</a>). * </p> * <p> * This approach has a number of advantages: * <ul> * <li>It is simple to create new command-line applications</li> * <li>Deriving from command-line applications is feasible and straightforward</li> * <li>Unmatched debugging capabilities</li> * <li>Execution times can be very quick, providing typical command-line * execution speeds</li> * </ul> * </p> */ public abstract class CommandLineApplication { /** * Error, warnings, and outputs. */ protected Reportable reportable; private final CommandLineParser cliParser; private final Options cliOptions; private final String[] cliArgs; /** * System configuration; may be null. * * @see #initializeSystemConfiguration() * @see #attemptSystemConfiguration() */ protected SystemConfiguration syscfg; /** * The command line, post argument processing. */ private CommandLine commandLine; /** * Defines the {@link Logger} that can be used by the concrete * {@link CommandLineApplication}. */ private final Logger cliLogger; /** * Creates a command-line application with the provided command-line * arguments to be parsed. * * @param args Command-line arguments, derived from {@code main} */ public CommandLineApplication(final String[] args) { this(args, true); } protected CommandLineApplication(final String[] args, final boolean checkForHelp) { cliParser = new AntelopeParser(); cliOptions = new Options(); this.cliArgs = args; List<Option> opts = getCommandLineOptions(); if (opts != null) { for (final Option opt : opts) { cliOptions.addOption(opt); } } addOption(new Option(null, LONG_OPT_DEBUG, false, DEBUG_HELP)); addOption(new Option(SHORT_OPT_VERBOSE, LONG_OPT_VERBOSE, false, VERBOSE_HELP)); addOption("h", "help", "Shows command line options."); try { // TODO -- parse() should NOT be stopping at non-options // (false arg) commandLine = cliParser.parse(cliOptions, cliArgs, false); } catch (ParseException e) { err.println(e.getMessage()); printHelp(cliOptions, false, err); bail(PARSE_ERROR); } if (checkForHelp && hasOption('h')) { printHelp(cliOptions, true, out); } ILoggerFactory lf = LoggerFactory.getILoggerFactory(); JoranConfigurator jc = new JoranConfigurator(); jc.setContext((Context) lf); try { InputStream is = getClass().getResourceAsStream(SLF4J_LOGGER_FILE); jc.doConfigure(is); } catch (JoranException e) { String msg = format(ERROR_INIT_LOGGING, e.getMessage()); err.println(msg); } cliLogger = lf.getLogger(Logger.ROOT_LOGGER_NAME); ((ch.qos.logback.classic.Logger) cliLogger).detachAppender("console"); reportable = null; } protected void setReportable(Reportable reportable) { if (reportable != null) { this.reportable = reportable; } else { SimpleOutput so = new SimpleOutput(); so.setErrorStream(err); so.setOutputStream(out); this.reportable = so; } } protected Reportable getReportable() { return reportable; } /** * Returns the non-null application name. * * @return Non-null string */ public abstract String getApplicationName(); /** * Returns the non-null application <i>short name</i>. * * @return Non-null string */ public abstract String getApplicationShortName(); /** * Returns the non-null application description. * * @return Non-null string */ public abstract String getApplicationDescription(); /** * Returns a detailed description of the application's command-line. * <p> * Optional arguments should be wrapped in {@code [] brackets}. For example: * {@code [-o somearg]}. Arguments that can occur more than once should be * followed with ellipses ({@code ...}), for example: {@code [-f FILE...]}. * </p> * * @return Non-null string */ public abstract String getUsage(); /** * Returns the command-line arguments. * * @return Command-line arguments */ protected final String[] getCommandLineArguments() { return cliArgs; } /** * Returns the command-line application {@link Logger} instance. * * @return {@link Logger}, the command-line application logger */ protected final Logger getLogger() { return cliLogger; } /** * Returns the number of command-line arguments, or {@code 0} if none are * available. * * @return int */ public final int getNumberOfCommandLineArgs() { if (commandLine == null) { return 0; } List<?> args = commandLine.getArgList(); if (args == null) { return 0; } return args.size(); } /** * Returns the extraneous command-line arguments. * * @return Non-null list of strings, may be empty */ protected final List<String> getExtraneousArguments() { if (commandLine == null) { return emptyList(); } String[] args = commandLine.getArgs(); if (noItems(args)) { return emptyList(); } return asList(args); } /** * Returns {@code true} if extraneous arguments are present, {@code false} * otherwise. * * @return boolean */ protected final boolean hasExtraneousArguments() { return !getExtraneousArguments().isEmpty(); } /** * Returns {@code true} if the option specified by {@code name} has been * set, {@code false} otherwise. * * @param name Single character option name * @return boolean */ protected final boolean hasOption(final char name) { return commandLine.hasOption(name); } /** * Returns {@code true} if the option specified by {@code name} has been * set, {@code false} otherwise. * * @param name string name * @return boolean */ protected final boolean hasOption(final String name) { return commandLine.hasOption(name); } /** * Returns the option value for the option specified by {@code name}. * * @param name Single character option name * @return String, which may be null */ protected final String getOptionValue(final char name) { return commandLine.getOptionValue(name); } /** * Returns the option values for the option specified by {@code name}. * * @param name Option name * @return String[] */ protected final String[] getOptionValues(final String name) { return commandLine.getOptionValues(name); } /** * Returns the option value for the option specified by {@code name}. * * @param name Option string name * @return String, which may be null */ protected final String getOptionValue(final String name) { return commandLine.getOptionValue(name); } /** * Returns the ordered, parsed command-line options. * * @return {@link Option}[] the ordered options array */ protected final Option[] getOptions() { return commandLine.getOptions(); } /** * Adds a command-line option. This is only useful before calling * {@link #start() start}. * * @param shortOpt Short, one character option (e.g., {@code -t}) * @param longOpt Long, one or two word option (e.g., {@code --long-option}) * @param desc Option description (e.g., {@code does something great}) */ public final void addOption(String shortOpt, String longOpt, String desc) { cliOptions.addOption(new Option(shortOpt, longOpt, false, desc)); } /** * Adds a command-line option. This is only useful before calling * {@link #start() start}. * * @param shortOpt Short, one character option (e.g., {@code -t}) * @param desc Option description (e.g., {@code does something great}) */ public final void addOption(String shortOpt, String desc) { cliOptions.addOption(new Option(shortOpt, desc)); } /** * Adds a command-line option. This is only useful before calling * {@link #start() start}. * * @param shortOpt Short, one character option (e.g., {@code -t}) * @param desc Option description (e.g., {@code does something great}) * @param hasArg boolean {@code true} if the option requires an argument, * {@code false} otherwise */ public final void addOption(String shortOpt, String desc, boolean hasArg) { cliOptions.addOption(new Option(shortOpt, hasArg, desc)); } /** * Adds a command-line option. This is only useful before calling * {@link #start() start}. * * @param shortOpt Short, one character option (e.g., {@code -t}) * @param longOpt Long, one or two word option (e.g., {@code --long-option}) * @param desc Option description (e.g., {@code does something great}) * @param hasArg boolean {@code true} if the option requires an argument, * {@code false} otherwise */ public final void addOption(String shortOpt, String longOpt, String desc, boolean hasArg) { cliOptions.addOption(new Option(shortOpt, longOpt, hasArg, desc)); } /** * Adds a command-line option. This is only useful before calling * {@link #start() start}. * * @param o Option */ public final void addOption(final Option o) { cliOptions.addOption(o); } /** * Adds a command-line option group. This is only useful before calling * {@link #start() start}. * * @param o Option group */ public final void addOptionGroup(final OptionGroup o) { cliOptions.addOptionGroup(o); } /** * Returns command-line options used by the application. * <p> * Options specifies by subclasses should adhere to the convention of a * single-character short option with an optional long option. * </p> * * @return List of options, may be null or empty * @see #addOption(String, String, String, boolean) */ public abstract List<Option> getCommandLineOptions(); /** * Prints out application information. */ protected final void printApplicationInfo(final String applicationName) { final StringBuilder bldr = new StringBuilder(); bldr.append("\n"); bldr.append(BELFrameworkVersion.VERSION_LABEL).append(": ") .append(applicationName).append("\n"); bldr.append("Copyright (c) 2011-2012, Selventa. All Rights Reserved.\n"); bldr.append("\n"); reportable.output(bldr.toString()); } /** * Prints out application information, using the value returned * by {@link #getApplicationName() getApplicationName} as the printed * application name. * @see #printApplicationInfo(String) */ protected final void printApplicationInfo() { printApplicationInfo(getApplicationName()); } /** * Prints usage information to {@code stdout}. */ public void printUsage() { printUsage(out); } /** * Prints usage information to the provided output stream. * * @param os Non-null output stream */ public void printUsage(final OutputStream os) { final StringBuilder bldr = new StringBuilder(); bldr.append("Usage: "); bldr.append(getUsage()); bldr.append("\n"); bldr.append("Try '--help' for more information.\n"); PrintWriter pw = new PrintWriter(os); pw.write(bldr.toString()); pw.close(); } /** * Write command-line help to standard out and invokes * {@link #printHelp(Options, boolean)}. * * @param exit Exit flag */ protected void printHelp(boolean exit) { printHelp(cliOptions, exit); } /** * Write command-line help to {@link Reportable#outputStream()}. * * @param o Options * @param exit Exit flag */ private void printHelp(Options o, boolean exit) { printHelp(o, exit, reportable.outputStream()); } /** * Write command-line help to the provided stream. If {@code exit} is * {@code true}, exit with status code {@link #EXIT_FAILURE}. * * @param o Options * @param exit Exit flag */ private void printHelp(Options o, boolean exit, OutputStream os) { final PrintWriter pw = new PrintWriter(os); final String syntax = getUsage(); String header = getApplicationName(); header = header.concat("\nOptions:"); HelpFormatter hf = new HelpFormatter(); hf.printHelp(pw, 100, syntax, header, o, 2, 2, null, false); pw.flush(); if (exit) { bail(SUCCESS); } } /** * Exits, with {@link ExitCode#SUCCESS}. */ protected final void end() { bail(SUCCESS); } /** * Prints the provided message to {@code stderr}, and invokes * {@link #bail(ExitCode)} with {@link ExitCode#GENERAL_FAILURE}. * * @param msg Message */ protected final void fatal(final String msg) { reportable.error(msg); bail(GENERAL_FAILURE); } /** * Invokes {@link #exit(ExitCode) exit} with the provided status code. * * @param e Exit code */ protected final void bail(final ExitCode e) { if (e.isFailure()) { final StringBuilder bldr = new StringBuilder(); bldr.append("\n"); bldr.append(getApplicationShortName()); bldr.append(" failed with error "); bldr.append(e); bldr.append("."); err.println(bldr.toString()); } systemExit(e); } /** * The <b>ONLY</b> call to {@link System#exit(int)}! * * @param e Exit code */ protected final static void systemExit(final ExitCode e) { System.exit(e.getValue()); } /** * Initializes the system configuration from either {@link #LONG_OPT_SYSCFG} * or system defaults. * <p> * This is an alternative means of initializing the system configuration for * applications interested in handling the possibility of an * {@link IOException}. * </p> * * @throws IOException Thrown if an I/O error occurs during initialization * of the system configuration * @see #initializeSystemConfiguration() */ protected final void attemptSystemConfiguration() throws IOException { if (hasOption(LONG_OPT_SYSCFG)) { String syscfgLoc = getOptionValue(LONG_OPT_SYSCFG); createSystemConfiguration(new File(syscfgLoc)); } else { createSystemConfiguration(); } syscfg = getSystemConfiguration(); } /** * Initializes the system configuration from either {@link #SHRT_OPT_SYSCFG} * or system defaults. * <p> * This method will initialize the system configuration or die trying. * </p> * * @throws IOException Thrown if an I/O error occurs during initialization * of the system configuration * @see #attemptSystemConfiguration() */ protected final void initializeSystemConfiguration() { try { attemptSystemConfiguration(); } catch (IOException e) { // Can't recover from this final String err = SYSCFG_READ_FAILURE; throw new BELRuntimeException(err, ExitCode.UNRECOVERABLE_ERROR, e); } } /** * This class is a commons-cli hack to change the default behavior of * command-line argument parsing. The library will throw * UnrecognizedOptionExceptions. We need unrecognized options to be ignored, * due to the way "belc" behaves. */ public static class AntelopeParser extends GnuParser { /** * {@inheritDoc} */ // Suppressing commons-cli use of raw types @SuppressWarnings("rawtypes") @Override protected void processOption(String arg0, ListIterator arg1) throws ParseException { boolean hasOption = getOptions().hasOption(arg0); if (!hasOption) { return; } super.processOption(arg0, arg1); } } }