package picard.cmdline;
import htsjdk.samtools.util.Log;
import htsjdk.samtools.util.StringUtil;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/*
* The MIT License
*
* Copyright (c) 2014 The Broad Institute
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* This is the main class of Picard 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 give instanceMain a new list of java packages in which to
* search for classes that extend CommandLineProgram.
*
*/
public class PicardCommandLine {
private static final Log log = Log.getInstance(PicardCommandLine.class);
private static String initializeColor(final String color) {
if (CommandLineDefaults.COLOR_STATUS) return color;
else return "";
}
/** Provides ANSI colors for the terminal output **/
private final static String KNRM = initializeColor("\u001B[0m"); // reset
private final static String KBLD = initializeColor("\u001B[1m"); // Bold
private final static String KRED = initializeColor("\u001B[31m");
private final static String KGRN = initializeColor("\u001B[32m");
private final static String KYEL = initializeColor("\u001B[33m");
private final static String KBLU = initializeColor("\u001B[34m");
private final static String KMAG = initializeColor("\u001B[35m");
private final static String KCYN = initializeColor("\u001B[36m");
private final static String KWHT = initializeColor("\u001B[37m");
private final static String KBLDRED = initializeColor("\u001B[1m\u001B[31m");
/** The name of this unified command line program **/
private final static String COMMAND_LINE_NAME = PicardCommandLine.class.getSimpleName();
/** The packages we wish to include in our command line **/
protected static List<String> getPackageList() {
final List<String> packageList = new ArrayList<String>();
packageList.add("picard");
return packageList;
}
/**
* The main method.
*
* Give a list of java packages in which to search for classes that extend CommandLineProgram. Those will be included
* on the command line.
* **/
protected int instanceMain(final String[] args, final List<String> packageList, final String commandLineName) {
final CommandLineProgram program = extractCommandLineProgram(args, packageList, commandLineName);
if (null == program) return 1; // no program found!
// 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(args, 1, args.length);
return program.instanceMain(mainArgs);
}
/** For testing **/
protected int instanceMain(final String[] args) {
return instanceMain(args, getPackageList(), COMMAND_LINE_NAME);
}
/** Override this if you want to include different java packages to search for classes that extend CommandLineProgram. **/
public static void main(final String[] args) {
System.exit(new PicardCommandLine().instanceMain(args, getPackageList(), COMMAND_LINE_NAME));
}
/** Returns the command line program specified, or prints the usage and exits with exit code 1 **/
private static CommandLineProgram extractCommandLineProgram(final String[] args, final List<String> packageList, 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, CommandLineProgram.class);
}
String missingAnnotationClasses = "";
final Map<String, Class<?>> simpleNameToClass = new HashMap<String, Class<?>>();
for (final Class clazz : classFinder.getClasses()) {
// No interfaces, synthetic, primitive, local, or abstract classes.
if (!clazz.isInterface() && !clazz.isSynthetic() && !clazz.isPrimitive() && !clazz.isLocalClass()
&& !Modifier.isAbstract(clazz.getModifiers())) {
final CommandLineProgramProperties property = getProgramProperty(clazz);
// Check for missing annotations
if (null == property) {
if (missingAnnotationClasses.isEmpty()) missingAnnotationClasses += clazz.getSimpleName();
else missingAnnotationClasses += ", " + clazz.getSimpleName();
}
else if (!property.omitFromCommandLine()) { /** We should check for missing annotations later **/
if (simpleNameToClass.containsKey(clazz.getSimpleName())) {
throw new RuntimeException("Simple class name collision: " + clazz.getSimpleName());
}
simpleNameToClass.put(clazz.getSimpleName(), clazz);
}
}
}
if (!missingAnnotationClasses.isEmpty()) {
throw new RuntimeException("The following classes are missing the required CommandLineProgramProperties annotation: " + missingAnnotationClasses);
}
final Set<Class<?>> classes = new HashSet<Class<?>>();
classes.addAll(simpleNameToClass.values());
if (args.length < 1) {
printUsage(classes, commandLineName);
} else {
if (args[0].equals("-h")) {
printUsage(classes, commandLineName);
} else if(args[0].equals("--list-commands")) {
printCommandList(classes);
} else {
if (simpleNameToClass.containsKey(args[0])) {
final Class clazz = simpleNameToClass.get(args[0]);
try {
return (CommandLineProgram)clazz.newInstance();
} catch (final InstantiationException e) {
throw new RuntimeException(e);
} catch (final IllegalAccessException e) {
throw new RuntimeException(e);
}
}
printUsage(classes, commandLineName);
printUnknown(classes, args[0]);
}
}
return null;
}
public static CommandLineProgramProperties getProgramProperty(Class clazz) {
return (CommandLineProgramProperties)clazz.getAnnotation(CommandLineProgramProperties.class);
}
private static class SimpleNameComparator implements Comparator<Class> {
@Override
public int compare(final Class aClass, final Class bClass) {
return aClass.getSimpleName().compareTo(bClass.getSimpleName());
}
}
private static void printCommandList(final Set<Class<?>> classes) {
printUsage(classes, null, true, true);
}
private static void printUsage(final Set<Class<?>> classes, final String commandLineName) {
printUsage(classes, commandLineName, false, false);
}
private static void printUsage(final Set<Class<?>> classes, final String commandLineName, boolean commandListOnly, boolean toStdout) {
final StringBuilder builder = new StringBuilder();
if (!commandListOnly) {
builder.append(KBLDRED + "USAGE: " + commandLineName + " " + KGRN + "<program name>" + KBLDRED + " [-h]\n\n" + KNRM);
builder.append(KBLDRED + "Available Programs:\n" + KNRM);
}
/** Group CommandLinePrograms by CommandLineProgramGroup **/
final Map<Class<? extends CommandLineProgramGroup>, CommandLineProgramGroup> programGroupClassToProgramGroupInstance = new HashMap<Class<? extends CommandLineProgramGroup>, CommandLineProgramGroup>();
final Map<CommandLineProgramGroup, List<Class>> programsByGroup = new TreeMap<CommandLineProgramGroup, List<Class>>(CommandLineProgramGroup.comparator);
final Map<Class, CommandLineProgramProperties> programsToProperty = new HashMap<Class, CommandLineProgramProperties>();
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()));
}
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().newInstance();
} catch (final InstantiationException e) {
throw new RuntimeException(e);
} catch (final IllegalAccessException e) {
throw new RuntimeException(e);
}
programGroupClassToProgramGroupInstance.put(property.programGroup(), programGroup);
}
List<Class> programs = programsByGroup.get(programGroup);
if (null == programs) {
programsByGroup.put(programGroup, programs = new ArrayList<Class>());
}
programs.add(clazz);
}
/** Print out the programs in each group **/
for (final Map.Entry<CommandLineProgramGroup, List<Class>> entry : programsByGroup.entrySet()) {
final CommandLineProgramGroup programGroup = entry.getKey();
if (!commandListOnly) {
builder.append(KWHT + "--------------------------------------------------------------------------------------\n" + KNRM);
builder.append(String.format("%s%-48s %-45s%s\n", KRED, programGroup.getName() + ":", programGroup.getDescription(), KNRM));
}
final List<Class> sortedClasses = new ArrayList<Class>();
sortedClasses.addAll(entry.getValue());
Collections.sort(sortedClasses, new SimpleNameComparator());
for (final Class clazz : sortedClasses) {
final CommandLineProgramProperties property = programsToProperty.get(clazz);
if (null == property) {
throw new RuntimeException(String.format("Unexpected error: did not find the CommandLineProgramProperties annotation for '%s'", clazz.getSimpleName()));
}
if (!commandListOnly) {
if (clazz.getSimpleName().length() >= 45) {
builder.append(String.format("%s %s %s%s%s\n", KGRN, clazz.getSimpleName(), KCYN, property.usageShort(), KNRM));
} else {
builder.append(String.format("%s %-45s%s%s%s\n", KGRN, clazz.getSimpleName(), KCYN, property.usageShort(), KNRM));
}
}
else {
builder.append(clazz.getSimpleName() + "\n");
}
}
if (!commandListOnly) builder.append(String.format("\n"));
}
if (!commandListOnly) builder.append(KWHT + "--------------------------------------------------------------------------------------\n\n" + KNRM);
if (toStdout) {
System.out.print(builder.toString());
}
else {
System.err.print(builder.toString());
}
}
/** similarity floor for matching in printUnknown **/
private final static int HELP_SIMILARITY_FLOOR = 7;
private final static int MINIMUM_SUBSTRING_LENGTH = 5;
/** When a command does not match any known command, searches for similar commands, using the same method as GIT **/
public static void printUnknown(final Set<Class<?>> classes, final String command) {
final Map<Class, Integer> distances = new HashMap<Class, Integer>();
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;
}
// Output similar matches
System.err.println(String.format("'%s' is not a valid command. See PicardCommandLine -h for more information.", command));
if (bestDistance < HELP_SIMILARITY_FLOOR) {
System.err.println(String.format("Did you mean %s?", (bestN < 2) ? "this" : "one of these"));
for (final Class clazz : classes) {
if (bestDistance == distances.get(clazz)) {
System.err.println(String.format(" %s", clazz.getSimpleName()));
}
}
}
}
}