All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.broadinstitute.hellbender.Main Maven / Gradle / Ivy

The newest version!
package org.broadinstitute.hellbender;

import com.google.cloud.storage.StorageException;
import htsjdk.samtools.util.StringUtil;
import org.broadinstitute.barclay.argparser.*;
import org.broadinstitute.hellbender.cmdline.*;
import org.broadinstitute.hellbender.exceptions.PicardNonZeroExitException;
import org.broadinstitute.hellbender.exceptions.UserException;
import org.broadinstitute.hellbender.utils.ClassUtils;
import org.broadinstitute.hellbender.utils.Utils;
import org.broadinstitute.hellbender.utils.config.ConfigFactory;
import org.broadinstitute.hellbender.utils.runtime.RuntimeUtils;

import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
 * This is the main class of Hellbender and is the way of executing individual command line programs.
 *
 * CommandLinePrograms are listed in a single command line interface based on the java package specified to instanceMain.
 *
 * If you want your own single command line program, extend this class and override if required:
 *
 * - {@link #getPackageList()} to return a list of java packages in which to search for classes that extend CommandLineProgram.
 * - {@link #getClassList()} to return a list of single classes to include (e.g. required input pre-processing tools).
 * - {@link #getCommandLineName()} for the name of the toolkit.
 * - {@link #handleResult(Object)} for handle the result of the tool.
 * - {@link #handleNonUserException(Exception)} for handle non {@link UserException}.
 * - {@link #parseArgsForConfigSetup(String[])} for pulling command-line configuration options out and initializing the {@link org.broadinstitute.hellbender.utils.config.GATKConfig}
 *
 * Note: If any of the previous methods was overrided, {@link #main(String[])} should be implemented to instantiate your class
 * and call {@link #mainEntry(String[])} to make the changes effective.
 */
public class Main {

    static {
        /**
         * The very first thing that any GATK application does is forces the JVM locale into US English, so that we don't have
         * to think about number formatting issues.
         */
        Utils.forceJVMLocaleToUSEnglish();

        // Turn off the Picard legacy parser and opt in to Barclay syntax for Picard tools. This should be replaced
        // with a config setting once PR https://github.com/broadinstitute/gatk/pull/3447 is merged.
        System.setProperty("picard.useLegacyParser", "false");
    }

    /**
     * Provides ANSI colors for the terminal output *
     */
    private static final String KNRM = "\u001B[0m"; // reset
    private static final String RED = "\u001B[31m";
    private static final String GREEN = "\u001B[32m";
    private static final String CYAN = "\u001B[36m";
    private static final String WHITE = "\u001B[37m";
    private static final String BOLDRED = "\u001B[1m\u001B[31m";

    /**
     * exit value when an issue with the commandline is detected, ie CommandLineException.
     * This is the same value as picard uses.
     */
    private static final int COMMANDLINE_EXCEPTION_EXIT_VALUE = 1;

    /**
     * Exit value when an unrecoverable {@link UserException} occurs.
     */
    public static final int USER_EXCEPTION_EXIT_VALUE = 2;

    /**
     * Exit value used when a Picard tool returns a non-zero exit code (the actual value is displayed on the command line)
     */
    public static final int PICARD_TOOL_EXCEPTION = 4;

    /**
     * exit value when any unrecoverable exception other than {@link UserException} occurs
     */
    private static final int ANY_OTHER_EXCEPTION_EXIT_VALUE = 3;

    /**
     * exit value when an out of memory error occurs
     */
    private static final int OUT_OF_MEMORY_EXIT_VALUE = 137;
    
    private static final String STACK_TRACE_ON_USER_EXCEPTION_PROPERTY = "GATK_STACKTRACE_ON_USER_EXCEPTION";

    /**
     * Prints the given message (may be null) to the provided stream, adding adornments and formatting.
     */
    protected static void printDecoratedExceptionMessage(final PrintStream ps, final Exception e, String prefix){
        Utils.nonNull(ps, "stream");
        Utils.nonNull(e, "exception");
        ps.println("***********************************************************************");
        ps.println();
        ps.println(prefix + e.getMessage());
        ps.println();
        ps.println("***********************************************************************") ;
    }

    /**
     * The packages we wish to include in our command line.
     */
    protected List getPackageList() {
        final List packageList = new ArrayList<>();
        packageList.addAll(Arrays.asList("org.broadinstitute.hellbender"));
        packageList.addAll(Arrays.asList("picard"));
        return packageList;
    }


    /**
     * Reads from the given command-line arguments, pulls out configuration options,
     * and initializes the configuration for this instance of Main.
     *
     * Suggested use for this is to handle downstream project configuration options and overrides.
     * For example this would allow:
     *
     *      Custom command-line arguments for use in tools
     *      Custom config file loading and initialization
     */
    protected void parseArgsForConfigSetup(final String[] args) {
        ConfigFactory.getInstance().initializeConfigurationsFromCommandLineArgs(args, "--" + StandardArgumentDefinitions.GATK_CONFIG_FILE_OPTION);
    }

    /**
     * The single classes we wish to include in our command line.
     */
    protected List> getClassList() {
        return Collections.emptyList();
    }

    /** Returns the command line that will appear in the usage. */
    protected String getCommandLineName() {
        return "";
    }

    /**
     * The main method.
     * 

* Give a list of java packages in which to search for classes that extend CommandLineProgram and a list of single CommandLineProgram classes. * Those will be included on the command line. * * This method is not intended to be used outside of the GATK framework and tests. * */ public Object instanceMain(final String[] args, final List packageList, final List> classList, final String commandLineName) { final CommandLineProgram program = setupConfigAndExtractProgram(args, packageList, classList, commandLineName); return runCommandLineProgram(program, args); } /** * Run the given command line program with the raw arguments from the command line * @param rawArgs these are the raw arguments from the command line, the first will be stripped off * @return the result of running {program} with the given args, possibly null */ protected static Object runCommandLineProgram(final CommandLineProgram program, final String[] rawArgs) { if (null == program) return null; // no program found! This will happen if help was specified with no other arguments // we can lop off the first two arguments but it requires an array copy or alternatively we could update CLP to remove them // in the constructor do the former in this implementation. final String[] mainArgs = Arrays.copyOfRange(rawArgs, 1, rawArgs.length); return program.instanceMain(mainArgs); } /** * Set up the configuration file store and create the {@link CommandLineProgram} to run. * @param args Argument array passed into this invocation of {@link Main}. * @param packageList List of packages to include in the command-line. * @param classList List of single classes to include in the command-line. * @param commandLineName The command-line name as it appears in the usage. * @return The {@link CommandLineProgram} to run from this invocation of {@link Main}. */ protected CommandLineProgram setupConfigAndExtractProgram(final String[] args, final List packageList, final List> classList, final String commandLineName ){ // Parse our config file path from our arguments and initialize the configuration file. // Note: this must be here because the command-line invocation inserts into here: parseArgsForConfigSetup(args); // Get our command-line program: return extractCommandLineProgram(args, packageList, classList, commandLineName); } /** * This method is not intended to be used outside of the GATK framework and tests. */ public Object instanceMain(final String[] args) { return instanceMain(args, getPackageList(), getClassList(), getCommandLineName()); } /** * The entry point to the toolkit from commandline: it uses {@link #instanceMain(String[])} to run the command line * program and handle the returned object with {@link #handleResult(Object)}, and exit with 0. * If any error occurs, it handles the exception (if non-user exception, through {@link #handleNonUserException(Exception)}) * and exit with the concrete error exit value. * * Note: this is the only method that is allowed to call System.exit (because gatk tools may be run from test harness etc) */ protected final void mainEntry(final String[] args) { CommandLineProgram program = null; try { program = setupConfigAndExtractProgram(args, getPackageList(), getClassList(), getCommandLineName()); final Object result = runCommandLineProgram(program, args); handleResult(result); //no explicit System.exit(0) since that causes issues when running in Yarn containers } catch (final CommandLineException e){ if (program != null) { System.err.println(program.getUsage()); } handleUserException(e); System.exit(COMMANDLINE_EXCEPTION_EXIT_VALUE); } catch (final PicardNonZeroExitException e) { // a Picard tool returned a non-zero exit code handleResult(e.getToolReturnCode()); System.exit(PICARD_TOOL_EXCEPTION); } catch (final UserException e){ handleUserException(e); System.exit(USER_EXCEPTION_EXIT_VALUE); } catch (final StorageException e) { handleStorageException(e); System.exit(ANY_OTHER_EXCEPTION_EXIT_VALUE); } catch (final OutOfMemoryError e) { handleNonUserException(e); System.exit(OUT_OF_MEMORY_EXIT_VALUE); } catch (final Exception e){ handleNonUserException(e); System.exit(ANY_OTHER_EXCEPTION_EXIT_VALUE); } } /** * Handle the result returned for a tool. Default implementation prints a message with the string value of the object if it is not null. * @param result the result of the tool (may be null) */ protected void handleResult(final Object result) { if (result != null) { System.out.println("Tool returned:\n" + result); } } /** * Handle an exception that was likely caused by user error. * This includes {@link UserException} and {@link CommandLineException} * * Default implementation produces a pretty error message * and a stack trace iff {@link #printStackTraceOnUserExceptions()} * * @param e the exception to handle */ protected void handleUserException(Exception e) { printDecoratedExceptionMessage(System.err, e, "A USER ERROR has occurred: "); if(printStackTraceOnUserExceptions()) { e.printStackTrace(); } else { System.err.println(String.format( "Set the system property %s (--java-options '-D%s=true') to print the stack trace.", STACK_TRACE_ON_USER_EXCEPTION_PROPERTY, STACK_TRACE_ON_USER_EXCEPTION_PROPERTY)); } } /** * Handle any exception that does not come from the user. Default implementation prints the stack trace. * @param exception the exception to handle (never an {@link UserException}). */ protected void handleNonUserException(final Exception exception) { exception.printStackTrace(); } /** * Handle any Error that does not come from the user. Default implementation prints the stack trace. * @param error the Error to handle (never an {@link UserException}). */ protected void handleNonUserException(final Error error) { error.printStackTrace(); } /** * Handle any exception that does not come from the user. Default implementation prints the stack trace. * @param exception the exception to handle (never an {@link UserException}). */ protected void handleStorageException(final StorageException exception) { // HTTP error code System.err.println("code: " + exception.getCode()); // user-friendly message System.err.println("message: " + exception.getMessage()); // short reason code, eg. "invalidArgument" System.err.println("reason: " + exception.getReason()); // eg. the name of the argument that was invalid System.err.println("location: " + exception.getLocation()); // true indicates the server thinks the same request may succeed later System.err.println("retryable: " + exception.isRetryable()); exception.printStackTrace(); } /** The entry point to GATK from commandline. It calls {@link #mainEntry(String[])} from this instance. */ public static void main(final String[] args) { new Main().mainEntry(args); } private static boolean printStackTraceOnUserExceptions() { return "true".equals(System.getenv(STACK_TRACE_ON_USER_EXCEPTION_PROPERTY)) || Boolean.getBoolean(STACK_TRACE_ON_USER_EXCEPTION_PROPERTY); } /** * Returns the command line program specified, or prints the usage and exits with exit code 1 * */ private CommandLineProgram extractCommandLineProgram( final String[] args, final List packageList, final List> classList, final String commandLineName ) { /** Get the set of classes that are our command line programs **/ final ClassFinder classFinder = new ClassFinder(); for (final String pkg : packageList) { classFinder.find(pkg, picard.cmdline.CommandLineProgram.class); classFinder.find(pkg, CommandLineProgram.class); } String missingAnnotationClasses = ""; final Set> toCheck = classFinder.getClasses(); toCheck.addAll(classList); final Map> simpleNameToClass = new LinkedHashMap<>(); for (final Class clazz : toCheck) { if (clazz.equals(PicardCommandLineProgramExecutor.class) || clazz.equals(CommandLineArgumentValidator.class)) { continue; } // No interfaces, synthetic, primitive, local, or abstract classes. if (ClassUtils.canMakeInstances(clazz)) { final CommandLineProgramProperties property = getProgramProperty(clazz); // Check for missing annotations if (null == property) { if (missingAnnotationClasses.isEmpty()) missingAnnotationClasses += clazz.getSimpleName(); else missingAnnotationClasses += ", " + clazz.getSimpleName(); } else { /** We should check for missing annotations later **/ if (simpleNameToClass.containsKey(clazz.getSimpleName())) { throw new RuntimeException("Simple class name collision: " + clazz.getName()); } simpleNameToClass.put(clazz.getSimpleName(), clazz); } } } if (!missingAnnotationClasses.isEmpty()) { throw new RuntimeException("The following classes are missing the required CommandLineProgramProperties annotation: " + missingAnnotationClasses); } final Set> classes = new LinkedHashSet<>(); classes.addAll(simpleNameToClass.values()); if (args.length < 1 || args[0].equals("-h") || args[0].equals("--help")) { printUsage(System.out, classes, commandLineName); } else if ( Arrays.stream(args).anyMatch(arg -> arg.equals("-" + SpecialArgumentsCollection.VERSION_FULLNAME) || arg.equals("--" + SpecialArgumentsCollection.VERSION_FULLNAME)) ) { printVersionInfo(System.out); } else { if (simpleNameToClass.containsKey(args[0])) { final Class clazz = simpleNameToClass.get(args[0]); try { final Object commandLineProgram = clazz.getDeclaredConstructor().newInstance(); // wrap Picard CommandLinePrograms in a PicardCommandLineProgramExecutor return commandLineProgram instanceof picard.cmdline.CommandLineProgram ? new PicardCommandLineProgramExecutor((picard.cmdline.CommandLineProgram) commandLineProgram) : (CommandLineProgram) commandLineProgram; } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { throw new RuntimeException(e); } } printUsage(System.err, classes, commandLineName); throw new UserException(getUnknownCommandMessage(classes, args[0])); } return null; } public static CommandLineProgramProperties getProgramProperty(Class clazz) { return clazz.getAnnotation(CommandLineProgramProperties.class); } private static class SimpleNameComparator implements Comparator>, Serializable { private static final long serialVersionUID = 1096632824580028876L; @Override public int compare(final Class aClass, final Class bClass) { return RuntimeUtils.toolDisplayName(aClass).compareTo(RuntimeUtils.toolDisplayName(bClass)); } } private void printUsage(final PrintStream destinationStream, final Set> classes, final String commandLineName) { final StringBuilder builder = new StringBuilder(); builder.append(BOLDRED + "USAGE: " + commandLineName + " " + GREEN + "" + BOLDRED + " [-h]\n\n" + KNRM) .append(BOLDRED + "Available Programs:\n" + KNRM); /** Group CommandLinePrograms by CommandLineProgramGroup **/ final Map, CommandLineProgramGroup> programGroupClassToProgramGroupInstance = new LinkedHashMap<>(); final Map>> programsByGroup = new TreeMap<>(CommandLineProgramGroup.comparator); final Map, CommandLineProgramProperties> programsToProperty = new LinkedHashMap<>(); for (final Class clazz : classes) { // Get the command line property for this command line program final CommandLineProgramProperties property = getProgramProperty(clazz); if (null == property) { throw new RuntimeException(String.format("The class '%s' is missing the required CommandLineProgramProperties annotation.", clazz.getSimpleName())); } else if (!property.omitFromCommandLine()) { // only if they are not omit from the command line programsToProperty.put(clazz, property); // Get the command line program group for the command line property // NB: we want to minimize the number of times we make a new instance, hence programGroupClassToProgramGroupInstance CommandLineProgramGroup programGroup = programGroupClassToProgramGroupInstance.get(property.programGroup()); if (null == programGroup) { try { programGroup = property.programGroup().getDeclaredConstructor().newInstance(); } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { throw new RuntimeException(e); } programGroupClassToProgramGroupInstance.put(property.programGroup(), programGroup); } List> programs = programsByGroup.get(programGroup); if (null == programs) { programsByGroup.put(programGroup, programs = new ArrayList<>()); } programs.add(clazz); } } /** Print out the programs in each group **/ for (final Map.Entry>> entry : programsByGroup.entrySet()) { final CommandLineProgramGroup programGroup = entry.getKey(); builder.append(WHITE + "--------------------------------------------------------------------------------------\n" + KNRM); builder.append(String.format("%s%-48s %-45s%s\n", RED, programGroup.getName() + ":", programGroup.getDescription(), KNRM)); final List> sortedClasses = new ArrayList<>(); sortedClasses.addAll(entry.getValue()); Collections.sort(sortedClasses, new SimpleNameComparator()); for (final Class clazz : sortedClasses) { final CommandLineProgramProperties clpProperties = programsToProperty.get(clazz); if (null == clpProperties) { throw new RuntimeException(String.format("Unexpected error: did not find the CommandLineProgramProperties annotation for '%s'", clazz.getSimpleName())); } builder.append(getDisplaySummaryForTool(clazz, clpProperties)); } builder.append(String.format("\n")); } builder.append(WHITE + "--------------------------------------------------------------------------------------\n" + KNRM); destinationStream.println(builder.toString()); } /** * Return a summary string for a command line tool suitable for display. * @param toolClass tool class * @param clpProperties {@CommandLineProgramProperties} for the tool * @return */ protected String getDisplaySummaryForTool(final Class toolClass, final CommandLineProgramProperties clpProperties) { final BetaFeature betaFeature = toolClass.getAnnotation(BetaFeature.class); final ExperimentalFeature experimentalFeature = toolClass.getAnnotation(ExperimentalFeature.class); final StringBuilder builder = new StringBuilder(); final String summaryLine; if (experimentalFeature != null) { summaryLine = String.format("%s%s %s%s", RED, "(EXPERIMENTAL Tool)", CYAN, clpProperties.oneLineSummary()); } else if (betaFeature != null) { summaryLine = String.format("%s%s %s%s", RED, "(BETA Tool)", CYAN, clpProperties.oneLineSummary()); } else { summaryLine = String.format("%s%s", CYAN, clpProperties.oneLineSummary()); } final String annotatedToolName = getDisplayNameForToolClass(toolClass); if (toolClass.getSimpleName().length() >= 45) { builder.append(String.format("%s %s %s%s\n", GREEN, annotatedToolName, summaryLine, KNRM)); } else { builder.append(String.format("%s %-45s%s%s\n", GREEN, annotatedToolName, summaryLine, KNRM)); } return builder.toString(); } /** * @return A display name to be used for the tool who's class is {@code clazz}. */ protected String getDisplayNameForToolClass(final Class clazz) { return RuntimeUtils.toolDisplayName(clazz); } /** * Get deprecation message for a tool * @param toolName command specified by the user * @return deprecation message string, or null if none */ public String getToolDeprecationMessage(final String toolName) { return DeprecatedToolsRegistry.getToolDeprecationInfo(toolName); } /** * When a command does not match any known command, searches for a deprecation message, if any, or for similar * commands. * @return returns an error message including the closes match if relevant. */ public String getUnknownCommandMessage(final Set> classes, final String command) { final String deprecationMessage = getToolDeprecationMessage(command); if (deprecationMessage != null) { return deprecationMessage; } return getSuggestedAlternateCommand(classes, command); } /** * similarity floor for matching in getUnknownCommandMessage * */ private static final int HELP_SIMILARITY_FLOOR = 7; private static final int MINIMUM_SUBSTRING_LENGTH = 5; /** * When a command does not match any known command, searches for similar commands, using the same method as GIT * * @return returns an error message including the closes match if relevant. */ public String getSuggestedAlternateCommand(final Set> classes, final String command) { final Map, Integer> distances = new LinkedHashMap<>(); int bestDistance = Integer.MAX_VALUE; int bestN = 0; // Score against all classes for (final Class clazz : classes) { final String name = clazz.getSimpleName(); final int distance; if (name.equals(command)) { throw new RuntimeException("Command matches: " + command); } if (name.startsWith(command) || (MINIMUM_SUBSTRING_LENGTH <= command.length() && name.contains(command))) { distance = 0; } else { distance = StringUtil.levenshteinDistance(command, name, 0, 2, 1, 4); } distances.put(clazz, distance); if (distance < bestDistance) { bestDistance = distance; bestN = 1; } else if (distance == bestDistance) { bestN++; } } // Upper bound on the similarity score if (0 == bestDistance && bestN == classes.size()) { bestDistance = HELP_SIMILARITY_FLOOR + 1; } StringBuilder message = new StringBuilder(); // Output similar matches message.append(String.format("'%s' is not a valid command.", command)); message.append(System.lineSeparator()); if (bestDistance < HELP_SIMILARITY_FLOOR) { message.append(String.format("Did you mean %s?", (bestN < 2) ? "this" : "one of these")); message.append(System.lineSeparator()); for (final Class clazz : classes) { if (bestDistance == distances.get(clazz)) { message.append(String.format(" %s", clazz.getSimpleName())); } } } return message.toString(); } /** * Prints version information for the toolkit, including versions of important libraries. * Subclasses may override to change this behavior. */ protected void printVersionInfo(PrintStream out){ final String toolkitName = RuntimeUtils.getToolkitName(this.getClass()); final String version = RuntimeUtils.getVersion(this.getClass()); System.out.println(String.format("%s v%s", toolkitName, version)); final Manifest manifest = RuntimeUtils.getManifest(this.getClass()); if( manifest != null){ final Attributes manifestAttributes = manifest.getMainAttributes(); final String htsjdkVersion = manifestAttributes.getValue("htsjdk-Version"); final String picardVersion = manifestAttributes.getValue("Picard-Version"); out.println("HTSJDK Version: " + (htsjdkVersion != null ? htsjdkVersion : "unknown")); out.println("Picard Version: " + (picardVersion != null ? picardVersion : "unknown")); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy