Java tutorial
/* * The aspiredb project * * Copyright (c) 2012 University of British Columbia * * 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 ubc.pavlab.aspiredb.cli; import gemma.gsec.authentication.ManualAuthenticationService; import java.io.File; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import org.apache.commons.cli.AlreadySelectedException; import org.apache.commons.cli.BasicParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.MissingArgumentException; import org.apache.commons.cli.MissingOptionException; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.cli.UnrecognizedOptionException; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; import org.springframework.beans.factory.BeanFactory; import org.springframework.security.core.context.SecurityContextHolder; import ubc.pavlab.aspiredb.server.util.ConfigUtils; /** * Base Command Line Interface. Provides some default functionality. * <p> * To use this, in your concrete subclass, implement a main method. You must implement buildOptions and processOptions * to handle any application-specific options (they can be no-ops). * <p> * To facilitate testing of your subclass, your main method must call a non-static 'doWork' method, that will be exposed * for testing. In that method call processCommandline. You should return any non-null return value from * processCommandLine. * * @author pavlidis * @version $Id: AbstractCLI.java,v 1.7 2013/06/11 22:30:58 anton Exp $ */ public abstract class AbstractCLI { public enum ErrorCode { NORMAL, MISSING_OPTION, INVALID_OPTION, MISSING_ARGUMENT, FATAL_ERROR, AUTHENTICATION_ERROR } protected static final String THREADS_OPTION = "threads"; protected static final String AUTO_OPTION_NAME = "auto"; private static final char PASSWORD_CONSTANT = 'p'; private static final char USERNAME_OPTION = 'u'; private static final char PORT_OPTION = 'P'; private static final char HOST_OPTION = 'H'; private static final char VERBOSITY_OPTION = 'v'; private static final String HEADER = "Options:"; public static final String FOOTER = "The aspiredb project, Copyright (c) " + (Calendar.getInstance().get(Calendar.YEAR)) + " University of British Columbia."; private static final int DEFAULT_PORT = 3306; private static int DEFAULT_VERBOSITY = 4; // info. protected static Log log = LogFactory.getLog(AbstractCLI.class); protected Options options = new Options(); private CommandLine commandLine; /* support for convenience options */ private String DEFAULT_HOST = "localhost"; private int verbosity = DEFAULT_VERBOSITY; // corresponds to "Error". private Map<Logger, Level> originalLoggingLevels = new HashMap<Logger, Level>(); protected int numThreads = 1; protected String host = DEFAULT_HOST; protected int port = DEFAULT_PORT; protected String username; protected String password; /** * Date used to identify which endities to run the tool on (e.g., those which were run less recently than mDate). To * enable call addDateOption. */ protected String mDate = null; /** * Automatically identify which entities to run the tool on. To enable call addAutoOption. */ protected boolean autoSeek = false; // needs to be concurrently modifiable. protected Collection<Object> errorObjects = Collections.synchronizedSet(new HashSet<Object>()); protected Collection<Object> successObjects = Collections.synchronizedSet(new HashSet<Object>()); protected Option passwordOpt; protected Option usernameOpt; public AbstractCLI() { this.buildStandardOptions(); this.buildOptions(); this.addUserNameAndPasswordOptions(); } /** * @param opt * @return * @see org.apache.commons.cli.Options#addOption(org.apache.commons.cli.Option) */ public final Options addOption(Option opt) { return this.options.addOption(opt); } /** * @param opt * @param hasArg * @param description * @return * @see org.apache.commons.cli.Options#addOption(java.lang.String, boolean, java.lang.String) */ public final Options addOption(String opt, boolean hasArg, String description) { return this.options.addOption(opt, hasArg, description); } /** * @param opt * @param longOpt * @param hasArg * @param description * @return * @see org.apache.commons.cli.Options#addOption(java.lang.String, java.lang.String, boolean, java.lang.String) */ public final Options addOption(String opt, String longOpt, boolean hasArg, String description) { return this.options.addOption(opt, longOpt, hasArg, description); } /** * @param group * @return * @see org.apache.commons.cli.Options#addOptionGroup(org.apache.commons.cli.OptionGroup) */ public final Options addOptionGroup(OptionGroup group) { return this.options.addOptionGroup(group); } public List<?> getArgList() { return commandLine.getArgList(); } public String[] getArgs() { return commandLine.getArgs(); } /** * @param opt * @return * @see org.apache.commons.cli.Options#getOption(java.lang.String) */ public final Option getOption(String opt) { return this.options.getOption(opt); } /** * @param opt * @return * @see org.apache.commons.cli.Options#getOptionGroup(org.apache.commons.cli.Option) */ public final OptionGroup getOptionGroup(Option opt) { return this.options.getOptionGroup(opt); } public Object getOptionObject(char opt) { return commandLine.getOptionObject(opt); } /** * @return * @see org.apache.commons.cli.Options#getOptions() */ public final Collection<?> getOptions() { return this.options.getOptions(); } public String getOptionValue(char opt) { return commandLine.getOptionValue(opt); } public String getOptionValue(char opt, String defaultValue) { return commandLine.getOptionValue(opt, defaultValue); } public String getOptionValue(String opt) { return commandLine.getOptionValue(opt); } public String getOptionValue(String opt, String defaultValue) { return commandLine.getOptionValue(opt, defaultValue); } public String[] getOptionValues(char opt) { return commandLine.getOptionValues(opt); } public String[] getOptionValues(String opt) { return commandLine.getOptionValues(opt); } /** * @return * @see org.apache.commons.cli.Options#getRequiredOptions() */ public final List<?> getRequiredOptions() { return this.options.getRequiredOptions(); } public abstract String getShortDesc(); public boolean hasOption(char opt) { return commandLine.hasOption(opt); } public boolean hasOption(String opt) { return commandLine.hasOption(opt); } /** check username and password. */ void authenticate(BeanFactory ctx) { /* * Allow security settings (authorization etc) in a given context to be passed into spawned threads */ SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL); ManualAuthenticationService manAuthentication = ctx.getBean(ManualAuthenticationService.class); if (hasOption('u') && hasOption('p')) { username = getOptionValue('u'); password = getOptionValue('p'); if (StringUtils.isBlank(username)) { System.err.println("Not authenticated. Username was blank"); log.debug("Username=" + username); bail(ErrorCode.AUTHENTICATION_ERROR); } if (StringUtils.isBlank(password)) { System.err.println("Not authenticated. You didn't enter a password"); bail(ErrorCode.AUTHENTICATION_ERROR); } boolean success = manAuthentication.validateRequest(username, password); if (!success) { System.err.println("Not authenticated. Make sure you entered a valid username (got '" + username + "') and/or password"); bail(ErrorCode.AUTHENTICATION_ERROR); } else { log.info("Logged in as " + username); } } else { log.info("Logging in as anonymous guest with limited privileges"); // manAuthentication.authenticateAnonymously(); } } /** * You must implement the handling for this option. */ protected void addAutoOption() { OptionBuilder.withArgName(AUTO_OPTION_NAME); OptionBuilder .withDescription("Attempt to process entities that need processing based on workflow criteria."); Option autoSeekOption = OptionBuilder.create(AUTO_OPTION_NAME); addOption(autoSeekOption); } protected void addDateOption() { OptionBuilder.hasArg(); OptionBuilder.withArgName("mdate"); OptionBuilder.withDescription("Constrain to run only on entities with analyses older than the given date. " + "For example, to run only on entities that have not been analyzed in the last 10 days, use '-10d'. " + "If there is no record of when the analysis was last run, it will be run."); Option dateOption = OptionBuilder.create("mdate"); addOption(dateOption); } /** * Convenience method to add a standard pair of options to intake a host name and port number. * * * @param hostRequired Whether the host name is required * @param portRequired Whether the port is required */ protected void addHostAndPortOptions(boolean hostRequired, boolean portRequired) { OptionBuilder.withArgName("host"); OptionBuilder.withLongOpt("host"); OptionBuilder.hasArg(); OptionBuilder.withDescription("Hostname to use (Default = " + DEFAULT_HOST + ")"); Option hostOpt = OptionBuilder.create(HOST_OPTION); hostOpt.setRequired(hostRequired); OptionBuilder.withArgName("port"); OptionBuilder.withLongOpt("port"); OptionBuilder.hasArg(); OptionBuilder.withDescription("Port to use on host (Default = " + DEFAULT_PORT + ")"); Option portOpt = OptionBuilder.create(PORT_OPTION); portOpt.setRequired(portRequired); options.addOption(hostOpt); options.addOption(portOpt); } /** * Convenience method to add an option for parallel processing option. */ protected void addThreadsOption() { OptionBuilder.withArgName("numThreads"); OptionBuilder.hasArg(); OptionBuilder.withDescription("Number of threads to use for batch processing."); Option threadsOpt = OptionBuilder.create(THREADS_OPTION); options.addOption(threadsOpt); } /** * Add required user name and password options. */ protected void addUserNameAndPasswordOptions() { /* * Changed to make it so password is not required. */ this.addUserNameAndPasswordOptions(false); } /** * Convenience method to add a standard pair of (required) options to intake a user name and password, optionally * required */ protected void addUserNameAndPasswordOptions(boolean required) { OptionBuilder.withArgName("user"); OptionBuilder.withLongOpt("user"); OptionBuilder.hasArg(); OptionBuilder.withDescription("User name for accessing the system (optional for some tools)"); this.usernameOpt = OptionBuilder.create(USERNAME_OPTION); usernameOpt.setRequired(required); OptionBuilder.withArgName("passwd"); OptionBuilder.withLongOpt("password"); OptionBuilder.hasArg(); OptionBuilder.withDescription("Password for accessing the system (optional for some tools)"); this.passwordOpt = OptionBuilder.create(PASSWORD_CONSTANT); passwordOpt.setRequired(required); options.addOption(usernameOpt); options.addOption(passwordOpt); } /** * Stop exeucting the CLI. */ protected void bail(ErrorCode errorCode) { // do something, but not System.exit. log.debug("Bailing with error code " + errorCode); resetLogging(); throw new IllegalStateException(errorCode.toString()); } /** * Implement this method to add options to your command line, using the OptionBuilder. * * @see OptionBuilder */ protected abstract void buildOptions(); protected void buildStandardOptions() { log.debug("Creating standard options"); Option helpOpt = new Option("h", "help", false, "Print this message"); Option testOpt = new Option("testing", false, "Use the test environment"); Option logOpt = new Option("v", "verbosity", true, "Set verbosity level (0=silent, 5=very verbose; default is " + DEFAULT_VERBOSITY + ")"); OptionBuilder.hasArg(); OptionBuilder.withArgName("logger"); OptionBuilder.withDescription( "Set the selected logger to the verbosity level after the equals sign. For example, '-logger=org.hibernate.SQL=4'"); Option otherLogOpt = OptionBuilder.create("logger"); options.addOption(otherLogOpt); options.addOption(logOpt); options.addOption(helpOpt); options.addOption(testOpt); } /** * @param args * @return * @throws Exception */ protected abstract Exception doWork(String[] args); protected final double getDoubleOptionValue(char option) { try { return Double.parseDouble(commandLine.getOptionValue(option)); } catch (NumberFormatException e) { System.out.println(invalidOptionString("" + option) + ", not a valid double"); bail(ErrorCode.INVALID_OPTION); } return 0.0; } protected final double getDoubleOptionValue(String option) { try { return Double.parseDouble(commandLine.getOptionValue(option)); } catch (NumberFormatException e) { System.out.println(invalidOptionString(option) + ", not a valid double"); bail(ErrorCode.INVALID_OPTION); } return 0.0; } /** * @param c * @return */ protected final String getFileNameOptionValue(char c) { String fileName = commandLine.getOptionValue(c); File f = new File(fileName); if (!f.canRead()) { System.out.println(invalidOptionString("" + c) + ", cannot read from file"); bail(ErrorCode.INVALID_OPTION); } return fileName; } /** * @param c * @return */ protected final String getFileNameOptionValue(String c) { String fileName = commandLine.getOptionValue(c); File f = new File(fileName); if (!f.canRead()) { System.out.println(invalidOptionString("" + c) + ", cannot read from file"); bail(ErrorCode.INVALID_OPTION); } return fileName; } protected final int getIntegerOptionValue(char option) { try { return Integer.parseInt(commandLine.getOptionValue(option)); } catch (NumberFormatException e) { System.out.println(invalidOptionString("" + option) + ", not a valid integer"); bail(ErrorCode.INVALID_OPTION); } return 0; } protected final int getIntegerOptionValue(String option) { try { return Integer.parseInt(commandLine.getOptionValue(option)); } catch (NumberFormatException e) { System.out.println(invalidOptionString(option) + ", not a valid integer"); bail(ErrorCode.INVALID_OPTION); } return 0; } protected String getLogger() { return "set your own logger"; } /** * @param command The name of the command as used at the command line. */ protected void printHelp(String command) { HelpFormatter h = new HelpFormatter(); h.printHelp(command + " [options]", HEADER, options, FOOTER); } /** * This must be called in your main method. It triggers parsing of the command line and processing of the options. * Check the error code to decide whether execution of your program should proceed. * * @param args * @return Exception; null if nothing went wrong. * @throws ParseException */ protected final Exception processCommandLine(String commandName, String[] args) { /* COMMAND LINE PARSER STAGE */ BasicParser parser = new BasicParser(); System.err.println("ASPIREdb version " + ConfigUtils.getAppVersion()); if (args == null) { printHelp(commandName); return new Exception("No arguments"); } try { commandLine = parser.parse(options, args); } catch (ParseException e) { if (e instanceof MissingOptionException) { System.err.println("Required option(s) were not supplied: " + e.getMessage()); } else if (e instanceof AlreadySelectedException) { System.err.println("The option(s) " + e.getMessage() + " were already selected"); } else if (e instanceof MissingArgumentException) { System.err.println("Missing argument: " + e.getMessage()); } else if (e instanceof UnrecognizedOptionException) { System.err.println("Unrecognized option: " + e.getMessage()); } else { e.printStackTrace(); } printHelp(commandName); if (log.isDebugEnabled()) { log.debug(e); } return e; } /* INTERROGATION STAGE */ if (commandLine.hasOption('h')) { printHelp(commandName); return new Exception("Help selected"); } processStandardOptions(); processOptions(); return null; } /** * Implement this to provide processing of options. It is called at the end of processCommandLine. */ protected abstract void processOptions(); /** * Call in 'buildOptions' to force users to provide a user name and password. */ protected void requireLogin() { if (this.passwordOpt != null) { this.passwordOpt.setRequired(true); } if (this.usernameOpt != null) { this.usernameOpt.setRequired(true); } } /** * This is needed for CLIs that run in tests, so the logging settings get reset. */ protected void resetLogging() { for (Logger log4jLogger : this.originalLoggingLevels.keySet()) { log4jLogger.setLevel(this.originalLoggingLevels.get(log4jLogger)); } } /** * Print out a summary of what the program did. Useful when analyzing lists of experiments etc. Use the * 'successObjects' and 'errorObjects' * * @param errorObjects * @param successObjects */ protected void summarizeProcessing() { if (successObjects.size() > 0) { StringBuilder buf = new StringBuilder(); buf.append("\n---------------------\n Processed:\n"); for (Object object : successObjects) { buf.append(" " + object + "\n"); } buf.append("---------------------\n"); log.info(buf); } else { log.error("No objects processed successfully!"); } if (errorObjects.size() > 0) { StringBuilder buf = new StringBuilder(); buf.append("\n---------------------\n Errors occurred during the processing of:\n"); for (Object object : errorObjects) { buf.append(" " + object + "\n"); } buf.append("---------------------\n"); log.error(buf); } } /** * Wait for completion. */ protected void waitForThreadPoolCompletion(Collection<Thread> threads) { while (true) { boolean anyAlive = false; for (Thread k : threads) { if (k.isAlive()) { anyAlive = true; } } if (!anyAlive) { break; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * Set up logging according to the user-selected (or default) verbosity level. */ private void configureLogging(String loggerName, int v) { Logger log4jLogger = LogManager.exists(loggerName); if (log4jLogger == null) { try { log4jLogger = LogManager.getLogger(Class.forName(loggerName)); } catch (ClassNotFoundException e) { log.warn("ClassNotFound: " + loggerName); } if (log4jLogger == null) { log.warn("No logger of name '" + loggerName + "'"); return; } } this.originalLoggingLevels.put(log4jLogger, log4jLogger.getLevel()); switch (v) { case 0: log4jLogger.setLevel(Level.OFF); break; case 1: log4jLogger.setLevel(Level.FATAL); break; case 2: log4jLogger.setLevel(Level.ERROR); break; case 3: log4jLogger.setLevel(Level.WARN); break; case 4: log4jLogger.setLevel(Level.INFO); break; case 5: log4jLogger.setLevel(Level.DEBUG); break; default: throw new RuntimeException("Verbosity must be from 0 to 5"); } log.debug("Logging level is at " + log4jLogger.getEffectiveLevel()); } private String invalidOptionString(String option) { return "Invalid value '" + commandLine.getOptionValue(option) + " for option " + option; } /** * Somewhat annoying: This causes subclasses to be unable to safely use 'h', 'p', 'u' and 'P' etc for their own * purposes. */ private void processStandardOptions() { if (commandLine.hasOption(HOST_OPTION)) { this.host = commandLine.getOptionValue(HOST_OPTION); } else { this.host = DEFAULT_HOST; } if (commandLine.hasOption(PORT_OPTION)) { this.port = getIntegerOptionValue(PORT_OPTION); } else { this.port = DEFAULT_PORT; } if (commandLine.hasOption(USERNAME_OPTION)) { this.username = commandLine.getOptionValue(USERNAME_OPTION); } if (commandLine.hasOption(PASSWORD_CONSTANT)) { this.password = commandLine.getOptionValue(PASSWORD_CONSTANT); } if (commandLine.hasOption(VERBOSITY_OPTION)) { this.verbosity = getIntegerOptionValue(VERBOSITY_OPTION); if (verbosity < 1 || verbosity > 5) { throw new RuntimeException("Verbosity must be from 1 to 5"); } } PatternLayout layout = new PatternLayout("[Gemma %d] %p [%t] %C.%M(%L) | %m%n"); ConsoleAppender cnslAppndr = new ConsoleAppender(layout); Logger f = LogManager.getRootLogger(); assert f != null; f.addAppender(cnslAppndr); if (commandLine.hasOption("logger")) { String value = getOptionValue("logger"); String[] vals = value.split("="); if (vals.length != 2) { throw new RuntimeException("Logging value must in format [logger]=[value]"); } try { log.info("Setting logging for " + vals[0] + " to " + vals[1]); configureLogging(vals[0], Integer.parseInt(vals[1])); } catch (NumberFormatException e) { throw new RuntimeException("Logging level must be an integer"); } } else { configureLogging(getLogger(), this.verbosity); } if (hasOption("mdate")) { this.mDate = this.getOptionValue("mdate"); } } }