package com.linkedin.databus.core; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; 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.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; import org.apache.log4j.PropertyConfigurator; /* * * Copyright 2013 LinkedIn Corp. All rights reserved * * 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. * */ /** * Base CLI implementation. Provides common options. * Default options supported: * <preformat> * -h,--help Prints command-line options info * -l,--log_props <property_file> Log4j properties to use * -q,--quiet turn off logging * -v verbose * -vv more verbose * -vvv most verbose * </preformat> * */ public class BaseCli { /** Possible verbosities for logging - get around the problem that the int <--> Level mapping * is not consistent */ protected static final Level[] VERBOSITIES = { Level.OFF, Level.ERROR, Level.WARN, Level.INFO, Level.DEBUG, Level.ALL }; /** * Encapsulates the information to be printed as part of the help screen for a CLI tool. * The structure is: * <pre> * usage * header * options (generate by Apache CLI) * footer * </pre> */ public static class CliHelp { private final String _className; private final String _usage; private final String _header; private final String _footer; public CliHelp(String className, String usage, String header, String footer) { super(); _usage = usage; _header = header; _footer = footer; _className = className; } public String getUsage() { return _usage; } public String getHeader() { return _header; } public String getFooter() { return _footer; } public String getClassName() { return _className; } } public static interface HeaderFooterBuilder { HeaderFooterBuilder add(String s); HeaderFooterBuilder addLine(); HeaderFooterBuilder addLine(String ln); HeaderFooterBuilder addSection(String sectionName); CliHelpBuilder finish(); @Override String toString(); } public static class StdHeaderFooterBuilder implements HeaderFooterBuilder { protected final StringBuilder _s = new StringBuilder(); protected final CliHelpBuilder _parent; public StdHeaderFooterBuilder(CliHelpBuilder parent) { super(); _parent = parent; } @Override public HeaderFooterBuilder addLine(String ln) { _s.append(ln); _s.append("\n"); return this; } @Override public HeaderFooterBuilder addSection(String sectionName) { addLine(); addLine(sectionName.toUpperCase()); for (int i = 0; i< sectionName.length(); ++i) add("-"); addLine(); return this; } @Override public String toString() { return _s.toString(); } /** * @see com.linkedin.databus.core.BaseCli.HeaderFooterBuilder#add(java.lang.String) */ @Override public HeaderFooterBuilder add(String s) { _s.append(s); return this; } /** * @see com.linkedin.databus.core.BaseCli.HeaderFooterBuilder#finishSection() */ @Override public CliHelpBuilder finish() { return _parent; } /** * @see com.linkedin.databus.core.BaseCli.HeaderFooterBuilder#addLine() */ @Override public HeaderFooterBuilder addLine() { //the default Apache Cli HelpFormatter does not like completely blank lines :( return addLine("\177"); } } public static class CliHelpBuilder { private String _usage; private HeaderFooterBuilder _header; private HeaderFooterBuilder _footer; private String _className = "<class>"; public CliHelpBuilder className(String className) { _className = className; _header = new StdHeaderFooterBuilder(this); _footer = new StdHeaderFooterBuilder(this); return this; } public CliHelpBuilder className(Class<?> clazz) { return className(clazz.getName()); } public String className() { return _className; } public HeaderFooterBuilder startHeader() { return _header; } public CliHelpBuilder headerBuilder(HeaderFooterBuilder builder) { _header = builder; return this; } public HeaderFooterBuilder startFooter() { return _footer; } public CliHelpBuilder footerBuilder(HeaderFooterBuilder builder) { _footer = builder; return this; } public String header() { return _header.toString(); } public String footer() { return _footer.toString(); } public CliHelpBuilder usage(String usage) { _usage = usage; return this; } public String usage() { return _usage; } public CliHelp build() { if (null == _usage) { _usage = "java " + _className + " [options]"; } return new CliHelp(_className, _usage, null != _header ? _header.toString() : "", null != _footer ? _footer.toString() : ""); } } protected static final String CMD_LINE_PROPS_OPT_LONG_NAME = "cmdline_props"; protected static final char CMD_LINE_PROPS_OPT_CHAR = 'c'; protected static final String HELP_OPT_LONG_NAME = "help"; protected static final char HELP_OPT_CHAR = 'h'; protected static final String LOG4J_PROPS_OPT_LONG_NAME = "log_props"; protected static final char LOG4J_PROPS_OPT_CHAR = 'l'; protected static final String PROPS_FILE_OPT_LONG_NAME = "props_file"; protected static final char PROPS_FILE_OPT_CHAR = 'p'; protected static final String QUIET_OPT_LONG_NAME = "quiet"; protected static final char QUIET_OPT_CHAR = 'q'; protected static final String VERBOSE1_OPT_LONG_NAME = "v"; protected static final String VERBOSE2_OPT_LONG_NAME = "vv"; protected static final String VERBOSE3_OPT_LONG_NAME = "vvv"; private final CliHelp _help; final protected Options _cliOptions; final protected HelpFormatter _helpFormatter; final protected Logger _log; protected CommandLine _cmd; protected int _defaultLogLevelIdx = 3; //Level.INFO final protected Properties _configProps = new Properties(System.getProperties()); public BaseCli(String usage, Logger log) { this(new CliHelpBuilder().usage(usage).build(), log); } public BaseCli(CliHelp help, Logger log) { _log = null == log ? Logger.getLogger(getClass()) : log; _help = help; _cliOptions = new Options(); _helpFormatter = new HelpFormatter(); _helpFormatter.setWidth(150); } public static String createDefaultUsageString(String className) { return "java " + className + " [options]"; } public static String createDefaultUsageString(Class<?> clazz) { return createDefaultUsageString(clazz.getName()); } public Properties getConfigProps() { return _configProps; } protected String getProgramName() { return _help.getClassName(); } protected void printError(String message, boolean printHelp) { System.err.println(getProgramName() + ": " + message); if (printHelp) { System.out.println(); printCliHelp(); } } private void updatePropsFromCmdLine(String cmdLinePropString) { String[] cmdLinePropSplit = cmdLinePropString.split(";"); for(String s : cmdLinePropSplit) { String[] onePropSplit = s.split("="); if (onePropSplit.length != 2) { _log.error("CMD line property setting " + s + "is not valid!"); } else { _log.info("CMD line Property overwride: " + s); _configProps.put(onePropSplit[0], onePropSplit[1]); } } } /** * Creates command-line options */ @SuppressWarnings("static-access") protected void constructCommandLineOptions() { _cliOptions.addOption( OptionBuilder.withLongOpt(HELP_OPT_LONG_NAME) .withDescription("Prints command-line options info") .create(HELP_OPT_CHAR)); _cliOptions.addOption( OptionBuilder.withLongOpt(CMD_LINE_PROPS_OPT_LONG_NAME) .withDescription("Command-line override for config properties; a " + "semicolon-separated list of key=vale.") .hasArg() .withArgName("Semicolon_separated_properties") .create(CMD_LINE_PROPS_OPT_CHAR)); _cliOptions.addOption( OptionBuilder.withLongOpt(LOG4J_PROPS_OPT_LONG_NAME) .withDescription("Log4j properties to use") .hasArg() .withArgName("property_file") .create(LOG4J_PROPS_OPT_CHAR)); _cliOptions.addOption( OptionBuilder.withLongOpt(PROPS_FILE_OPT_LONG_NAME) .withDescription("Config properties file to use") .hasArg() .withArgName("property_file") .create(PROPS_FILE_OPT_CHAR)); _cliOptions.addOption( OptionBuilder.withLongOpt(QUIET_OPT_LONG_NAME) .withDescription("quiet (no logging)") .create(QUIET_OPT_CHAR)); _cliOptions.addOption( OptionBuilder.withDescription("verbose") .create(VERBOSE1_OPT_LONG_NAME)); _cliOptions.addOption( OptionBuilder.withDescription("more verbose") .create(VERBOSE2_OPT_LONG_NAME)); _cliOptions.addOption( OptionBuilder.withDescription("most verbose") .create(VERBOSE3_OPT_LONG_NAME)); } /** * Parses the command line arguments * @param cliArgs the command line arguments * @return true iff parsing was successful */ public boolean processCommandLineArgs(String[] cliArgs) { constructCommandLineOptions(); CommandLineParser cliParser = new GnuParser(); _cmd = null; try { _cmd = cliParser.parse(_cliOptions, cliArgs); } catch (ParseException pe) { printError("failed to parse command-line options: " + pe.toString(), true); return false; } if (_cmd.hasOption(HELP_OPT_CHAR)) { printCliHelp(); return false; } int verbosityInc = 0; if (_cmd.hasOption(VERBOSE3_OPT_LONG_NAME)) { //-vvv is always Level.ALL verbosityInc = VERBOSITIES.length; } else if (_cmd.hasOption(VERBOSE2_OPT_LONG_NAME)) { verbosityInc = 2; } else if (_cmd.hasOption(VERBOSE1_OPT_LONG_NAME)) { verbosityInc = 1; } Level effectiveLevel = VERBOSITIES[Math.min(_defaultLogLevelIdx + verbosityInc, VERBOSITIES.length - 1)]; if (_cmd.hasOption(QUIET_OPT_CHAR)) { effectiveLevel = Level.OFF; } Logger.getRootLogger().setLevel(effectiveLevel); if (_cmd.hasOption(LOG4J_PROPS_OPT_CHAR)) { String log4jPropFile = _cmd.getOptionValue(LOG4J_PROPS_OPT_CHAR); PropertyConfigurator.configure(log4jPropFile); _log.debug("Using custom logging settings from file " + log4jPropFile); } else { PatternLayout defaultLayout = new PatternLayout("%d{ISO8601} +%r [%t] (%p) {%c{1}} %m%n"); ConsoleAppender defaultAppender = new ConsoleAppender(defaultLayout); Logger.getRootLogger().removeAllAppenders(); Logger.getRootLogger().addAppender(defaultAppender); _log.debug("Using default logging settings"); } processProperties(); return true; } private void processProperties() { if (_cmd.hasOption(PROPS_FILE_OPT_CHAR)) { for (String propFile: _cmd.getOptionValues(PROPS_FILE_OPT_CHAR)) { _log.info("Loading container config from properties file " + propFile); FileInputStream fis = null; try { fis = new FileInputStream(propFile); _configProps.load(fis); } catch (Exception e) { _log.error("error processing properties; ignoring:" + e.getMessage()); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { //not much to do -- ignore } } } } } else { _log.info("Using system properties for container config"); } if (_cmd.hasOption(CMD_LINE_PROPS_OPT_CHAR)) { String cmdLinePropString = _cmd.getOptionValue(CMD_LINE_PROPS_OPT_CHAR); updatePropsFromCmdLine(cmdLinePropString); } } /** Print out the help for this program */ public void printCliHelp() { _helpFormatter.printHelp(_help.getUsage(), _help.getHeader(), _cliOptions, _help.getFooter()); } /** Gets out the usage for this program*/ public String getUsage() { return _help.getUsage(); } /** For testing */ public static void main(String[] args) { BasicConfigurator.configure(); BaseCli cli = new BaseCli(new CliHelpBuilder().className(BaseCli.class) .startHeader() .addSection("Description") .addLine("Description Line 1") .addLine("Description Line 2") .addSection("Options") .finish() .startFooter() .addSection("Examples") .addLine("* Example Line 1") .addLine("\177\t Example Line 2") .addSection("Notes") .addLine("Be careful") .finish() .build(), null); cli.processCommandLineArgs(args); } }