ubc.pavlab.aspiredb.cli.AbstractCLI.java Source code

Java tutorial

Introduction

Here is the source code for ubc.pavlab.aspiredb.cli.AbstractCLI.java

Source

/*
 * 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");
        }

    }

}