From 4f51a02dea94bc669796f3ae9e6ff6d4581a3e5b Mon Sep 17 00:00:00 2001 From: kshakir Date: Mon, 9 Aug 2010 16:42:48 +0000 Subject: [PATCH] Changed logging level to default at INFO instead of WARN. Changes to StingUtils command line for use in Queue, replacing Queue's use of property files. Updates to walkers used in existing QScripts to add @Input/@Output. RMD used in @Required/@Allows now has a new default equal to "any" type. New QueueGATKExtensions.jar generator for auto wrapping walkers as Queue CommandLineFunctions. Added hooks to modify the functions that perform the Scattering and Gathering (setting their jar files, other arguments, etc.) Removed dependency on BroadCore by porting LSF job submitter to scala. Ivy now pulls down module dependencies from maven. git-svn-id: file:///humgen/gsa-scr1/gsa-engineering/svn_contents/trunk@3984 348d0f76-0448-11de-a6fe-93d51630548a --- build.xml | 282 ++++++++---- ivy.xml | 57 ++- .../analyzecovariates/AnalyzeCovariates.java | 3 +- .../sting/commandline/ArgumentDefinition.java | 43 +- .../sting/commandline/ArgumentIOType.java | 24 +- .../sting/commandline/ArgumentMatch.java | 225 ++++++++++ .../sting/commandline/ArgumentMatches.java | 197 --------- .../sting/commandline/ArgumentSource.java | 42 +- .../commandline/ArgumentTypeDescriptor.java | 64 ++- .../sting/commandline/CommandLineProgram.java | 38 +- .../sting/commandline/ParsingEngine.java | 65 +-- .../sting/gatk/CommandLineGATK.java | 5 +- .../sting/gatk/WalkerManager.java | 234 +++++++--- .../arguments/GATKArgumentCollection.java | 24 +- .../sting/gatk/filters/FilterManager.java | 15 +- .../GenotypeWriterArgumentTypeDescriptor.java | 6 +- .../SAMFileWriterArgumentTypeDescriptor.java | 6 +- .../iterators/MergingSamRecordIterator2.java | 125 ------ .../gatk/refdata/tracks/RMDTrackManager.java | 100 +++-- .../tracks/builders/RMDTrackBuilder.java | 3 + .../tracks/builders/RODTrackBuilder.java | 16 +- .../builders/TribbleRMDTrackBuilder.java | 26 +- .../sting/gatk/walkers/ClipReadsWalker.java | 2 +- .../sting/gatk/walkers/PrintReadsWalker.java | 3 +- .../sting/gatk/walkers/RMD.java | 4 +- .../recalibration/CovariateCounterWalker.java | 3 +- .../gatk/ArgumentDefinitionField.java | 334 ++++++++++++++ .../queue/extensions/gatk/ArgumentField.java | 215 ++++++++++ .../gatk/CommandLineProgramManager.java | 43 ++ .../gatk/GATKExtensionsGenerator.java | 232 ++++++++++ .../extensions/gatk/ReadFilterField.java | 46 ++ .../queue/extensions/gatk/RodBindField.java | 125 ++++++ scala/qscript/UnifiedGenotyperExample.scala | 63 +++ scala/qscript/depristo/1kg_table1.scala | 76 ++-- scala/qscript/fullCallingPipeline.q | 402 ++++++----------- scala/qscript/recalibrate.scala | 88 ++-- .../qscript/rpoplin/variantRecalibrator.scala | 86 ++-- .../unifiedgenotyper_example.properties | 7 - scala/qscript/unifiedgenotyper_example.scala | 54 --- .../sting/queue/QArguments.scala | 105 ----- .../sting/queue/QCommandLine.scala | 138 ++++-- .../broadinstitute/sting/queue/QScript.scala | 96 +---- .../sting/queue/QScriptManager.scala | 163 +++++++ .../queue/engine/CommandLineRunner.scala | 20 - .../queue/engine/DispatchJobRunner.scala | 50 ++- .../sting/queue/engine/LsfJobRunner.scala | 77 ++-- .../sting/queue/engine/QGraph.scala | 113 +++-- .../sting/queue/engine/QNode.scala | 5 +- .../sting/queue/engine/ShellJobRunner.scala | 31 ++ .../engine/TopologicalJobScheduler.scala | 15 +- .../extensions/gatk/BamGatherFunction.scala | 17 + .../extensions/gatk/BamIndexFunction.scala | 35 ++ .../gatk/ContigScatterFunction.scala | 8 + .../gatk/IntervalScatterFunction.scala | 16 + .../sting/queue/extensions/gatk/RodBind.scala | 14 + .../queue/function/CommandLineFunction.scala | 406 ++++++++++++++++-- .../queue/function/DispatchFunction.scala | 93 ---- .../queue/function/DispatchWaitFunction.scala | 6 +- .../sting/queue/function/FileProvider.scala | 11 + .../queue/function/InputOutputFunction.scala | 67 --- .../queue/function/IntervalFunction.scala | 8 - .../function/JarCommandLineFunction.scala | 15 + .../queue/function/MappingFunction.scala | 10 +- .../sting/queue/function/QFunction.scala | 9 +- .../queue/function/gatk/GatkFunction.scala | 38 -- .../scattergather/BamGatherFunction.scala | 17 - .../CleanupTempDirsFunction.scala | 15 +- .../scattergather/ContigScatterFunction.scala | 21 - .../CreateTempDirsFunction.scala | 27 +- .../FixMatesGatherFunction.scala | 17 - .../scattergather/GatherFunction.scala | 27 +- .../IntervalScatterFunction.scala | 21 - .../scattergather/ScatterFunction.scala | 28 +- .../ScatterGatherableFunction.scala | 402 +++++++++++++---- .../SimpleTextGatherFunction.scala | 14 +- .../sting/queue/util/ClasspathUtils.scala | 22 +- .../sting/queue/util/CollectionUtils.scala | 57 ++- .../sting/queue/util/CommandLineJob.scala | 51 +++ .../sting/queue/util/IOUtils.scala | 71 ++- .../sting/queue/util/Logging.scala | 22 +- .../sting/queue/util/LsfJob.scala | 142 ++++++ .../sting/queue/util/ProcessController.scala | 360 ++++++++++++++++ .../sting/queue/util/ProcessUtils.scala | 43 -- .../sting/queue/util/ReflectionUtils.scala | 147 +++---- .../ScalaCompoundArgumentTypeDescriptor.scala | 71 +++ .../sting/queue/util/ShellJob.scala | 37 ++ settings/ivysettings.xml | 7 +- .../edu.mit.broad/broad-core-all-2.8.jar | Bin 172746 -> 0 bytes .../edu.mit.broad/broad-core-all-2.8.xml | 8 - .../reflections-0.9.5-svnversion79M_mod2.xml | 9 + 90 files changed, 4533 insertions(+), 2052 deletions(-) create mode 100755 java/src/org/broadinstitute/sting/commandline/ArgumentMatch.java delete mode 100644 java/src/org/broadinstitute/sting/gatk/iterators/MergingSamRecordIterator2.java create mode 100644 java/src/org/broadinstitute/sting/queue/extensions/gatk/ArgumentDefinitionField.java create mode 100644 java/src/org/broadinstitute/sting/queue/extensions/gatk/ArgumentField.java create mode 100644 java/src/org/broadinstitute/sting/queue/extensions/gatk/CommandLineProgramManager.java create mode 100644 java/src/org/broadinstitute/sting/queue/extensions/gatk/GATKExtensionsGenerator.java create mode 100644 java/src/org/broadinstitute/sting/queue/extensions/gatk/ReadFilterField.java create mode 100644 java/src/org/broadinstitute/sting/queue/extensions/gatk/RodBindField.java create mode 100644 scala/qscript/UnifiedGenotyperExample.scala delete mode 100644 scala/qscript/unifiedgenotyper_example.properties delete mode 100644 scala/qscript/unifiedgenotyper_example.scala delete mode 100755 scala/src/org/broadinstitute/sting/queue/QArguments.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/QScriptManager.scala delete mode 100755 scala/src/org/broadinstitute/sting/queue/engine/CommandLineRunner.scala create mode 100755 scala/src/org/broadinstitute/sting/queue/engine/ShellJobRunner.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamGatherFunction.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamIndexFunction.scala create mode 100755 scala/src/org/broadinstitute/sting/queue/extensions/gatk/ContigScatterFunction.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/extensions/gatk/IntervalScatterFunction.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/extensions/gatk/RodBind.scala delete mode 100644 scala/src/org/broadinstitute/sting/queue/function/DispatchFunction.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/function/FileProvider.scala delete mode 100644 scala/src/org/broadinstitute/sting/queue/function/InputOutputFunction.scala delete mode 100644 scala/src/org/broadinstitute/sting/queue/function/IntervalFunction.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/function/JarCommandLineFunction.scala delete mode 100644 scala/src/org/broadinstitute/sting/queue/function/gatk/GatkFunction.scala delete mode 100644 scala/src/org/broadinstitute/sting/queue/function/scattergather/BamGatherFunction.scala delete mode 100755 scala/src/org/broadinstitute/sting/queue/function/scattergather/ContigScatterFunction.scala delete mode 100644 scala/src/org/broadinstitute/sting/queue/function/scattergather/FixMatesGatherFunction.scala delete mode 100644 scala/src/org/broadinstitute/sting/queue/function/scattergather/IntervalScatterFunction.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/util/CommandLineJob.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/util/LsfJob.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/util/ProcessController.scala delete mode 100755 scala/src/org/broadinstitute/sting/queue/util/ProcessUtils.scala create mode 100644 scala/src/org/broadinstitute/sting/queue/util/ScalaCompoundArgumentTypeDescriptor.scala create mode 100755 scala/src/org/broadinstitute/sting/queue/util/ShellJob.scala delete mode 100644 settings/repository/edu.mit.broad/broad-core-all-2.8.jar delete mode 100644 settings/repository/edu.mit.broad/broad-core-all-2.8.xml diff --git a/build.xml b/build.xml index dda03c288..7f69a3a12 100644 --- a/build.xml +++ b/build.xml @@ -7,7 +7,14 @@ + + + + + + + @@ -24,9 +31,7 @@ - - @@ -55,21 +60,28 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + - + + + + + + + + + + + + + - + + + + + + + Building Queue... + + + + + + + + + + + + + + + + Generating Queue GATK extensions... + + + + + Building Queue GATK extensions... + + + + + + + + + + + additionalparam="-build-timestamp "${build.timestamp}" -version-suffix .${build.version} -out ${basedir}/${resource.path}"> - + + + @@ -141,14 +249,20 @@ + + + + + @@ -193,12 +307,46 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -206,12 +354,6 @@ - - - - - - @@ -232,29 +374,57 @@ + + + + + + + + + + + + + + + + + + + + - + - + - + + + + + + + + + @@ -300,46 +470,6 @@ - - - - - - - - - - - - - - - - - - - - Building Queue... - - - - - - - - - - - - - - - - - - - - diff --git a/ivy.xml b/ivy.xml index 3c9e6a4b0..cebf26a86 100644 --- a/ivy.xml +++ b/ivy.xml @@ -8,46 +8,40 @@ - - - - - - - - - - + + + + + + + + + + - - - - - - - - - + - + - + - - - - - + + + + + + + + - - + + - - + @@ -56,5 +50,8 @@ + + + diff --git a/java/src/org/broadinstitute/sting/analyzecovariates/AnalyzeCovariates.java b/java/src/org/broadinstitute/sting/analyzecovariates/AnalyzeCovariates.java index 143c3e6d3..91cbf5f8c 100755 --- a/java/src/org/broadinstitute/sting/analyzecovariates/AnalyzeCovariates.java +++ b/java/src/org/broadinstitute/sting/analyzecovariates/AnalyzeCovariates.java @@ -25,6 +25,7 @@ package org.broadinstitute.sting.analyzecovariates; +import org.broadinstitute.sting.commandline.Input; import org.broadinstitute.sting.gatk.walkers.recalibration.*; import org.broadinstitute.sting.utils.classloader.PackageUtils; import org.broadinstitute.sting.utils.text.XReadLines; @@ -51,7 +52,7 @@ class AnalyzeCovariatesCLP extends CommandLineProgram { // Command Line Arguments ///////////////////////////// - @Argument(fullName = "recal_file", shortName = "recalFile", doc = "The input recal csv file to analyze", required = false) + @Input(fullName = "recal_file", shortName = "recalFile", doc = "The input recal csv file to analyze", required = false) private String RECAL_FILE = "output.recal_data.csv"; @Argument(fullName = "output_dir", shortName = "outputDir", doc = "The directory in which to output all the plots and intermediate data files", required = false) private String OUTPUT_DIR = "analyzeCovariates/"; diff --git a/java/src/org/broadinstitute/sting/commandline/ArgumentDefinition.java b/java/src/org/broadinstitute/sting/commandline/ArgumentDefinition.java index f206aac58..000d540fc 100644 --- a/java/src/org/broadinstitute/sting/commandline/ArgumentDefinition.java +++ b/java/src/org/broadinstitute/sting/commandline/ArgumentDefinition.java @@ -40,6 +40,11 @@ public class ArgumentDefinition { */ public final ArgumentIOType ioType; + /** + * The class of the argument. + */ + public final Class argumentType; + /** * Full name of the argument. Must have a value. */ @@ -70,6 +75,11 @@ public class ArgumentDefinition { */ public final boolean isMultiValued; + /** + * The class of the componentType. Not used for scalars. + */ + public final Class componentType; + /** * Is this argument hidden from the help system? */ @@ -93,35 +103,41 @@ public class ArgumentDefinition { /** * Creates a new argument definition. * @param ioType Whether the argument is an input or an output. + * @param argumentType The class of the field. * @param fullName Full name for this argument definition. * @param shortName Short name for this argument definition. * @param doc Doc string for this argument. * @param required Whether or not this argument is required. * @param isFlag Whether or not this argument should be treated as a flag. * @param isMultiValued Whether or not this argument supports multiple values. + * @param componentType For multivalued arguments the type of the components. * @param isHidden Whether or not this argument should be hidden from the command-line argument system. * @param exclusiveOf Whether this command line argument is mutually exclusive of other arguments. * @param validation A regular expression for command-line argument validation. * @param validOptions is there a particular list of options that's valid for this argument definition? List them if so, otherwise set this to null. */ public ArgumentDefinition( ArgumentIOType ioType, + Class argumentType, String fullName, String shortName, String doc, boolean required, boolean isFlag, boolean isMultiValued, + Class componentType, boolean isHidden, String exclusiveOf, String validation, List validOptions) { this.ioType = ioType; + this.argumentType = argumentType; this.fullName = fullName; this.shortName = shortName; this.doc = doc; this.required = required; this.isFlag = isFlag; this.isMultiValued = isMultiValued; + this.componentType = componentType; this.isHidden = isHidden; this.exclusiveOf = exclusiveOf; this.validation = validation; @@ -131,18 +147,22 @@ public class ArgumentDefinition { /** * Creates a new argument definition. * @param annotation The annotation on the field. + * @param argumentType The class of the field. * @param defaultFullName Default full name for this argument definition. * @param defaultShortName Default short name for this argument definition. * @param isFlag Whether or not this argument should be treated as a flag. * @param isMultiValued Whether or not this argument supports multiple values. + * @param componentType For multivalued arguments the type of the components. * @param isHidden Whether or not this argument should be hidden from the command-line argument system. * @param validOptions is there a particular list of options that's valid for this argument definition? List them if so, otherwise set this to null. */ public ArgumentDefinition( Annotation annotation, + Class argumentType, String defaultFullName, String defaultShortName, boolean isFlag, boolean isMultiValued, + Class componentType, boolean isHidden, List validOptions) { @@ -162,13 +182,15 @@ public class ArgumentDefinition { else shortName = null; - this.ioType = getIOType(annotation); + this.ioType = ArgumentIOType.getIOType(annotation); + this.argumentType = argumentType; this.fullName = fullName; this.shortName = shortName; this.doc = getDoc(annotation); this.required = isRequired(annotation, isFlag); this.isFlag = isFlag; this.isMultiValued = isMultiValued; + this.componentType = componentType; this.isHidden = isHidden; this.exclusiveOf = getExclusiveOf(annotation); this.validation = getValidationRegex(annotation); @@ -178,25 +200,31 @@ public class ArgumentDefinition { /** * Creates a new argument definition. * @param annotation The annotation on the field. + * @param argumentType The class of the field. * @param fieldName Default full name for this argument definition. * @param isFlag Whether or not this argument should be treated as a flag. * @param isMultiValued Whether or not this argument supports multiple values. + * @param componentType For multivalued arguments the type of the components. * @param isHidden Whether or not this argument should be hidden from the command-line argument system. * @param validOptions is there a particular list of options that's valid for this argument definition? List them if so, otherwise set this to null. */ public ArgumentDefinition( Annotation annotation, + Class argumentType, String fieldName, boolean isFlag, boolean isMultiValued, + Class componentType, boolean isHidden, List validOptions) { - this.ioType = getIOType(annotation); + this.ioType = ArgumentIOType.getIOType(annotation); + this.argumentType = argumentType; this.fullName = getFullName(annotation, fieldName); this.shortName = getShortName(annotation); this.doc = getDoc(annotation); this.required = isRequired(annotation, isFlag); this.isFlag = isFlag; this.isMultiValued = isMultiValued; + this.componentType = componentType; this.isHidden = isHidden; this.exclusiveOf = getExclusiveOf(annotation); this.validation = getValidationRegex(annotation); @@ -222,17 +250,6 @@ public class ArgumentDefinition { Utils.equals(shortName,other.shortName); } - /** - * Returns the ArgumentIOType for the annotation. - * @param annotation @Input or @Output - * @return ArgumentIOType.Input, Output, or Unknown - */ - public static ArgumentIOType getIOType(Annotation annotation) { - if (annotation instanceof Input) return ArgumentIOType.INPUT; - if (annotation instanceof Output) return ArgumentIOType.OUTPUT; - return ArgumentIOType.UNKNOWN; - } - /** * A hack to get around the fact that Java doesn't like inheritance in Annotations. * @param annotation to run the method on diff --git a/java/src/org/broadinstitute/sting/commandline/ArgumentIOType.java b/java/src/org/broadinstitute/sting/commandline/ArgumentIOType.java index af516004a..03e3066fb 100644 --- a/java/src/org/broadinstitute/sting/commandline/ArgumentIOType.java +++ b/java/src/org/broadinstitute/sting/commandline/ArgumentIOType.java @@ -24,6 +24,28 @@ package org.broadinstitute.sting.commandline; +import org.broadinstitute.sting.utils.StingException; + +import java.lang.annotation.Annotation; + public enum ArgumentIOType { - INPUT, OUTPUT, UNKNOWN + INPUT(Input.class), OUTPUT(Output.class), ARGUMENT(Argument.class); + + public final Class annotationClass; + + ArgumentIOType(Class annotationClass) { + this.annotationClass = annotationClass; + } + + /** + * Returns the ArgumentIOType for the annotation. + * @param annotation @Input or @Output + * @return ArgumentIOType.Input, Output, or Unknown + */ + public static ArgumentIOType getIOType(Annotation annotation) { + for (ArgumentIOType ioType: ArgumentIOType.values()) + if (ioType.annotationClass.isAssignableFrom(annotation.getClass())) + return ioType; + throw new StingException("Unknown annotation type: " + annotation); + } } diff --git a/java/src/org/broadinstitute/sting/commandline/ArgumentMatch.java b/java/src/org/broadinstitute/sting/commandline/ArgumentMatch.java new file mode 100755 index 000000000..56bedc012 --- /dev/null +++ b/java/src/org/broadinstitute/sting/commandline/ArgumentMatch.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2010 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. + */ + +package org.broadinstitute.sting.commandline; + +import java.util.*; + +/** + * A mapping of all the sites where an argument definition maps to a site on the command line. + */ +public class ArgumentMatch implements Iterable { + /** + * The argument definition that's been matched. + */ + public final ArgumentDefinition definition; + + /** + * The text that's been matched, as it appears in the command line arguments. + */ + public final String label; + + /** + * Maps indicies of command line arguments to values paired with that argument. + */ + public final SortedMap> indices = new TreeMap>(); + + /** + * Create a new argument match, defining its properties later. Used to create invalid arguments. + */ + public ArgumentMatch() { + this.label = null; + this.definition = null; + } + + /** + * A simple way of indicating that an argument with the given label and definition exists at this index. + * @param label Label of the argument match. Must not be null. + * @param definition The associated definition, if one exists. May be null. + * @param index Position of the argument. Must not be null. + */ + public ArgumentMatch( String label, ArgumentDefinition definition, int index ) { + this( label, definition, index, null ); + } + + private ArgumentMatch( String label, ArgumentDefinition definition, int index, String value ) { + this.label = label; + this.definition = definition; + + ArrayList values = new ArrayList(); + if( value != null ) + values.add(value); + indices.put(index,values ); + } + + /** + * Return a string representation of the given argument match, for debugging purposes. + * @return String representation of the match. + */ + public String toString() { + return label; + } + + /** + * Creates an iterator that walks over each individual match at each position of a given argument. + * @return An iterator over the individual matches in this argument. Will not be null. + */ + public Iterator iterator() { + return new Iterator() { + /** + * Iterate over each the available index. + */ + private Iterator indexIterator = null; + + /** + * Iterate over each available token. + */ + private Iterator tokenIterator = null; + + /** + * The next index to return. Null if none remain. + */ + Integer nextIndex = null; + + /** + * The next token to return. Null if none remain. + */ + String nextToken = null; + + { + indexIterator = indices.keySet().iterator(); + prepareNext(); + } + + /** + * Is there a nextToken available to return? + * @return True if there's another token waiting in the wings. False otherwise. + */ + public boolean hasNext() { + return nextToken != null; + } + + /** + * Get the next token, if one exists. If not, throw an IllegalStateException. + * @return The next ArgumentMatch in the series. Should never be null. + */ + public ArgumentMatch next() { + if( nextIndex == null || nextToken == null ) + throw new IllegalStateException( "No more ArgumentMatches are available" ); + + ArgumentMatch match = new ArgumentMatch( label, definition, nextIndex, nextToken ); + prepareNext(); + return match; + } + + /** + * Initialize the next ArgumentMatch to return. If no ArgumentMatches are available, + * initialize nextIndex / nextToken to null. + */ + private void prepareNext() { + if( tokenIterator != null && tokenIterator.hasNext() ) { + nextToken = tokenIterator.next(); + } + else { + nextIndex = null; + nextToken = null; + + // Do a nested loop. While more data is present in the inner loop, grab that data. + // Otherwise, troll the outer iterator looking for more data. + while( indexIterator.hasNext() ) { + nextIndex = indexIterator.next(); + if( indices.get(nextIndex) != null ) { + tokenIterator = indices.get(nextIndex).iterator(); + if( tokenIterator.hasNext() ) { + nextToken = tokenIterator.next(); + break; + } + } + } + } + + } + + /** + * Remove is unsupported in this context. + */ + public void remove() { + throw new UnsupportedOperationException("Cannot remove an argument match from the collection while iterating."); + } + }; + } + + /** + * Merge two ArgumentMatches, so that the values for all arguments go into the + * same data structure. + * @param other The other match to merge into. + */ + public void mergeInto( ArgumentMatch other ) { + indices.putAll(other.indices); + } + + /** + * Associate a value with this merge maapping. + * @param index index of the command-line argument to which this value is mated. + * @param value Text representation of value to add. + */ + public void addValue( int index, String value ) { + if( !indices.containsKey(index) || indices.get(index) == null ) + indices.put(index, new ArrayList() ); + indices.get(index).add(value); + } + + /** + * Does this argument already have a value at the given site? + * Arguments are only allowed to be single-valued per site, and + * flags aren't allowed a value at all. + * @param index Index at which to check for values. + * @return True if the argument has a value at the given site. False otherwise. + */ + public boolean hasValueAtSite( int index ) { + return (indices.get(index) != null && indices.get(index).size() >= 1) || isArgumentFlag(); + } + + /** + * Return the values associated with this argument match. + * @return A collection of the string representation of these value. + */ + public List values() { + List values = new ArrayList(); + for( int index: indices.keySet() ) { + if( indices.get(index) != null ) + values.addAll(indices.get(index)); + } + return values; + } + + /** + * Convenience method returning true if the definition is a flag. + * @return True if definition is known to be a flag; false if not known to be a flag. + */ + private boolean isArgumentFlag() { + return definition != null && definition.isFlag; + } +} diff --git a/java/src/org/broadinstitute/sting/commandline/ArgumentMatches.java b/java/src/org/broadinstitute/sting/commandline/ArgumentMatches.java index 3ee544c5f..03978adac 100755 --- a/java/src/org/broadinstitute/sting/commandline/ArgumentMatches.java +++ b/java/src/org/broadinstitute/sting/commandline/ArgumentMatches.java @@ -192,200 +192,3 @@ public class ArgumentMatches implements Iterable { return new HashSet( argumentMatches.values() ); } } - -/** - * A mapping of all the sites where an argument definition maps to a site on the command line. - */ -class ArgumentMatch implements Iterable { - /** - * The argument definition that's been matched. - */ - public final ArgumentDefinition definition; - - /** - * The text that's been matched, as it appears in the command line arguments. - */ - public final String label; - - /** - * Maps indicies of command line arguments to values paired with that argument. - */ - public final SortedMap> indices = new TreeMap>(); - - /** - * Create a new argument match, defining its properties later. Used to create invalid arguments. - */ - public ArgumentMatch() { - this.label = null; - this.definition = null; - } - - /** - * A simple way of indicating that an argument with the given label and definition exists at this index. - * @param label Label of the argument match. Must not be null. - * @param definition The associated definition, if one exists. May be null. - * @param index Position of the argument. Must not be null. - */ - public ArgumentMatch( String label, ArgumentDefinition definition, int index ) { - this( label, definition, index, null ); - } - - private ArgumentMatch( String label, ArgumentDefinition definition, int index, String value ) { - this.label = label; - this.definition = definition; - - ArrayList values = new ArrayList(); - if( value != null ) - values.add(value); - indices.put(index,values ); - } - - /** - * Return a string representation of the given argument match, for debugging purposes. - * @return String representation of the match. - */ - public String toString() { - return label; - } - - /** - * Creates an iterator that walks over each individual match at each position of a given argument. - * @return An iterator over the individual matches in this argument. Will not be null. - */ - public Iterator iterator() { - return new Iterator() { - /** - * Iterate over each the available index. - */ - private Iterator indexIterator = null; - - /** - * Iterate over each available token. - */ - private Iterator tokenIterator = null; - - /** - * The next index to return. Null if none remain. - */ - Integer nextIndex = null; - - /** - * The next token to return. Null if none remain. - */ - String nextToken = null; - - { - indexIterator = indices.keySet().iterator(); - prepareNext(); - } - - /** - * Is there a nextToken available to return? - * @return True if there's another token waiting in the wings. False otherwise. - */ - public boolean hasNext() { - return nextToken != null; - } - - /** - * Get the next token, if one exists. If not, throw an IllegalStateException. - * @return The next ArgumentMatch in the series. Should never be null. - */ - public ArgumentMatch next() { - if( nextIndex == null || nextToken == null ) - throw new IllegalStateException( "No more ArgumentMatches are available" ); - - ArgumentMatch match = new ArgumentMatch( label, definition, nextIndex, nextToken ); - prepareNext(); - return match; - } - - /** - * Initialize the next ArgumentMatch to return. If no ArgumentMatches are available, - * initialize nextIndex / nextToken to null. - */ - private void prepareNext() { - if( tokenIterator != null && tokenIterator.hasNext() ) { - nextToken = tokenIterator.next(); - } - else { - nextIndex = null; - nextToken = null; - - // Do a nested loop. While more data is present in the inner loop, grab that data. - // Otherwise, troll the outer iterator looking for more data. - while( indexIterator.hasNext() ) { - nextIndex = indexIterator.next(); - if( indices.get(nextIndex) != null ) { - tokenIterator = indices.get(nextIndex).iterator(); - if( tokenIterator.hasNext() ) { - nextToken = tokenIterator.next(); - break; - } - } - } - } - - } - - /** - * Remove is unsupported in this context. - */ - public void remove() { - throw new UnsupportedOperationException("Cannot remove an argument match from the collection while iterating."); - } - }; - } - - /** - * Merge two ArgumentMatches, so that the values for all arguments go into the - * same data structure. - * @param other The other match to merge into. - */ - public void mergeInto( ArgumentMatch other ) { - indices.putAll(other.indices); - } - - /** - * Associate a value with this merge maapping. - * @param index index of the command-line argument to which this value is mated. - * @param value Text representation of value to add. - */ - public void addValue( int index, String value ) { - if( !indices.containsKey(index) || indices.get(index) == null ) - indices.put(index, new ArrayList() ); - indices.get(index).add(value); - } - - /** - * Does this argument already have a value at the given site? - * Arguments are only allowed to be single-valued per site, and - * flags aren't allowed a value at all. - * @param index Index at which to check for values. - * @return True if the argument has a value at the given site. False otherwise. - */ - public boolean hasValueAtSite( int index ) { - return (indices.get(index) != null && indices.get(index).size() >= 1) || isArgumentFlag(); - } - - /** - * Return the values associated with this argument match. - * @return A collection of the string representation of these value. - */ - public List values() { - List values = new ArrayList(); - for( int index: indices.keySet() ) { - if( indices.get(index) != null ) - values.addAll(indices.get(index)); - } - return values; - } - - /** - * Convenience method returning true if the definition is a flag. - * @return True if definition is known to be a flag; false if not known to be a flag. - */ - private boolean isArgumentFlag() { - return definition != null && definition.isFlag; - } -} \ No newline at end of file diff --git a/java/src/org/broadinstitute/sting/commandline/ArgumentSource.java b/java/src/org/broadinstitute/sting/commandline/ArgumentSource.java index 182b1c8a3..635780aa5 100644 --- a/java/src/org/broadinstitute/sting/commandline/ArgumentSource.java +++ b/java/src/org/broadinstitute/sting/commandline/ArgumentSource.java @@ -28,7 +28,7 @@ package org.broadinstitute.sting.commandline; import org.broadinstitute.sting.gatk.walkers.Hidden; import java.lang.reflect.Field; -import java.util.Collection; +import java.util.Arrays; import java.util.List; /** @@ -41,9 +41,9 @@ import java.util.List; */ public class ArgumentSource { /** - * Class to which the field belongs. + * Field into which to inject command-line arguments. */ - public final Class clazz; + public final Field[] parentFields; /** * Field into which to inject command-line arguments. @@ -57,11 +57,19 @@ public class ArgumentSource { /** * Create a new command-line argument target. - * @param clazz Class containing the argument. - * @param field Field containing the argument. Field must be annotated with 'Argument'. + * @param field Field containing the argument. Field must be annotated with 'Input' or 'Output'. */ - public ArgumentSource( Class clazz, Field field ) { - this.clazz = clazz; + public ArgumentSource( Field field ) { + this(new Field[0], field); + } + + /** + * Create a new command-line argument target. + * @param parentFields Parent fields containing the the field. Field must be annotated with 'ArgumentCollection'. + * @param field Field containing the argument. Field must be annotated with 'Input' or 'Output'. + */ + public ArgumentSource( Field[] parentFields, Field field ) { + this.parentFields = parentFields; this.field = field; this.typeDescriptor = ArgumentTypeDescriptor.create( field.getType() ); } @@ -80,7 +88,7 @@ public class ArgumentSource { return false; ArgumentSource otherArgumentSource = (ArgumentSource)other; - return this.clazz.equals(otherArgumentSource.clazz) && this.field.equals(otherArgumentSource.field); + return this.field == otherArgumentSource.field && Arrays.equals(this.parentFields, otherArgumentSource.parentFields); } /** @@ -89,7 +97,7 @@ public class ArgumentSource { */ @Override public int hashCode() { - return clazz.hashCode() ^ field.hashCode(); + return field.hashCode(); } /** @@ -118,18 +126,11 @@ public class ArgumentSource { /** * Parses the specified value based on the specified type. - * @param source The type of value to be parsed. * @param values String representation of all values passed. * @return the parsed value of the object. */ - public Object parse( ArgumentSource source, ArgumentMatches values ) { - Object value = null; - if( !isFlag() ) - value = typeDescriptor.parse( source, values ); - else - value = true; - - return value; + public Object parse( ArgumentMatches values ) { + return typeDescriptor.parse( this, values ); } /** @@ -145,8 +146,7 @@ public class ArgumentSource { * @return True if the argument supports multiple values. */ public boolean isMultiValued() { - Class argumentType = field.getType(); - return Collection.class.isAssignableFrom(argumentType) || field.getType().isArray(); + return typeDescriptor.isMultiValued( this ); } /** @@ -162,6 +162,6 @@ public class ArgumentSource { * @return String representation of the argument source. */ public String toString() { - return clazz.getSimpleName() + ": " + field.getName(); + return field.getDeclaringClass().getSimpleName() + ": " + field.getName(); } } diff --git a/java/src/org/broadinstitute/sting/commandline/ArgumentTypeDescriptor.java b/java/src/org/broadinstitute/sting/commandline/ArgumentTypeDescriptor.java index 02a46b69d..4993ebfe5 100644 --- a/java/src/org/broadinstitute/sting/commandline/ArgumentTypeDescriptor.java +++ b/java/src/org/broadinstitute/sting/commandline/ArgumentTypeDescriptor.java @@ -113,10 +113,26 @@ public abstract class ArgumentTypeDescriptor { return Collections.singletonList(createDefaultArgumentDefinition(source)); } + /** + * Parses an argument source to an object. + * @param source The source used to find the matches. + * @param matches The matches for the source. + * @return The parsed object. + */ public Object parse( ArgumentSource source, ArgumentMatches matches ) { return parse( source, source.field.getType(), matches ); } + /** + * Returns true if the field is a collection or an array. + * @param source The argument source to check. + * @return true if the field is a collection or an array. + */ + public boolean isMultiValued( ArgumentSource source ) { + Class argumentType = source.field.getType(); + return Collection.class.isAssignableFrom(argumentType) || argumentType.isArray(); + } + /** * By default, argument sources create argument definitions with a set of default values. * Use this method to create the one simple argument definition. @@ -125,15 +141,41 @@ public abstract class ArgumentTypeDescriptor { */ protected ArgumentDefinition createDefaultArgumentDefinition( ArgumentSource source ) { return new ArgumentDefinition( getArgumentAnnotation(source), + source.field.getType(), source.field.getName(), source.isFlag(), source.isMultiValued(), + getCollectionComponentType(source.field), source.isHidden(), getValidOptions(source) ); } - public abstract Object parse( ArgumentSource source, Class type, ArgumentMatches matches ); + /** + * Return the component type of a field, or String.class if the type cannot be found. + * @param field The reflected field to inspect. + * @return The parameterized component type, or String.class if the parameterized type could not be found. + * @throws IllegalArgumentException If more than one parameterized type is found on the field. + */ + protected Class getCollectionComponentType( Field field ) { + // If this is a parameterized collection, find the contained type. If blow up if more than one type exists. + if( field.getGenericType() instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType)field.getGenericType(); + if( parameterizedType.getActualTypeArguments().length > 1 ) + throw new IllegalArgumentException("Unable to determine collection type of field: " + field.toString()); + return (Class)parameterizedType.getActualTypeArguments()[0]; + } + else + return String.class; + } + /** + * Parses the argument matches for a class type into an object. + * @param source The original argument source used to find the matches. + * @param type The current class type being inspected. May not match the argument source.field.getType() if this as a collection for example. + * @param matches The argument matches for the argument source, or the individual argument match for a scalar if this is being called to help parse a collection. + * @return The individual parsed object matching the argument match with Class type. + */ + public abstract Object parse( ArgumentSource source, Class type, ArgumentMatches matches ); /** * If the argument source only accepts a small set of options, populate the returned list with @@ -193,6 +235,11 @@ public abstract class ArgumentTypeDescriptor { throw new StingException("ArgumentAnnotation is not present for the argument field: " + source.field.getName()); } + /** + * Returns true if an argument annotation is present + * @param field The field to check for an annotation. + * @return True if an argument annotation is present on the field. + */ @SuppressWarnings("unchecked") public static boolean isArgumentAnnotationPresent(Field field) { for (Class annotation: ARGUMENT_ANNOTATIONS) @@ -235,6 +282,8 @@ class SimpleArgumentTypeDescriptor extends ArgumentTypeDescriptor { @Override public Object parse( ArgumentSource source, Class type, ArgumentMatches matches ) { + if (source.isFlag()) + return true; String value = getArgumentValue( createDefaultArgumentDefinition(source), matches ); // lets go through the types we support @@ -301,7 +350,7 @@ class CompoundArgumentTypeDescriptor extends ArgumentTypeDescriptor { public boolean supports( Class type ) { return ( Collection.class.isAssignableFrom(type) || type.isArray() ); } - + @Override @SuppressWarnings("unchecked") public Object parse( ArgumentSource source, Class type, ArgumentMatches matches ) @@ -319,16 +368,7 @@ class CompoundArgumentTypeDescriptor extends ArgumentTypeDescriptor { else if( java.util.Set.class.isAssignableFrom(type) ) type = java.util.TreeSet.class; } - // If this is a parameterized collection, find the contained type. If blow up if only one type exists. - if( source.field.getGenericType() instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType)source.field.getGenericType(); - if( parameterizedType.getActualTypeArguments().length > 1 ) - throw new IllegalArgumentException("Unable to determine collection type of field: " + source.field.toString()); - componentType = (Class)parameterizedType.getActualTypeArguments()[0]; - } - else - componentType = String.class; - + componentType = getCollectionComponentType( source.field ); ArgumentTypeDescriptor componentArgumentParser = ArgumentTypeDescriptor.create( componentType ); Collection collection; diff --git a/java/src/org/broadinstitute/sting/commandline/CommandLineProgram.java b/java/src/org/broadinstitute/sting/commandline/CommandLineProgram.java index bedca9043..89fd143e2 100644 --- a/java/src/org/broadinstitute/sting/commandline/CommandLineProgram.java +++ b/java/src/org/broadinstitute/sting/commandline/CommandLineProgram.java @@ -43,11 +43,11 @@ public abstract class CommandLineProgram { private static Logger logger = Logger.getRootLogger(); /** the default log level */ - @Input(fullName = "logging_level", + @Argument(fullName = "logging_level", shortName = "l", doc = "Set the minimum level of logging, i.e. setting INFO get's you INFO up to FATAL, setting ERROR gets you ERROR and FATAL level logging.", required = false) - protected String logging_level = "WARN"; + protected String logging_level = "INFO"; /** where to send the output of our logger */ @@ -58,21 +58,21 @@ public abstract class CommandLineProgram { protected String toFile = null; /** do we want to silence the command line output */ - @Input(fullName = "quiet_output_mode", + @Argument(fullName = "quiet_output_mode", shortName = "quiet", doc = "Set the logging to quiet mode, no output to stdout", required = false) protected Boolean quietMode = false; /** do we want to generate debugging information with the logs */ - @Input(fullName = "debug_mode", + @Argument(fullName = "debug_mode", shortName = "debug", doc = "Set the logging file string to include a lot of debugging information (SLOW!)", required = false) protected Boolean debugMode = false; /** this is used to indicate if they've asked for help */ - @Input(fullName = "help", shortName = "h", doc = "Generate this help message", required = false) + @Argument(fullName = "help", shortName = "h", doc = "Generate this help message", required = false) public Boolean help = false; /** our logging output patterns */ @@ -146,6 +146,7 @@ public abstract class CommandLineProgram { * @param clp the command line program to execute * @param args the command line arguments passed in */ + @SuppressWarnings("unchecked") public static void start(CommandLineProgram clp, String[] args) { try { @@ -174,14 +175,14 @@ public abstract class CommandLineProgram { parser.addArgumentSource(clp.getArgumentSourceName(argumentSource), argumentSource); parser.parse(args); - if (isHelpPresent(clp, parser)) + if (isHelpPresent(parser)) printHelpAndExit(clp, parser); parser.validate(); } else { parser.parse(args); - if (isHelpPresent(clp, parser)) + if (isHelpPresent(parser)) printHelpAndExit(clp, parser); parser.validate(); @@ -216,7 +217,7 @@ public abstract class CommandLineProgram { // if they specify a log location, output our data there if (clp.toFile != null) { - FileAppender appender = null; + FileAppender appender; try { appender = new FileAppender(layout, clp.toFile, false); logger.addAppender(appender); @@ -258,7 +259,7 @@ public abstract class CommandLineProgram { */ private static void toErrorLog(CommandLineProgram clp, Exception e) { File logFile = new File("GATK_Error.log"); - PrintStream stream = null; + PrintStream stream; try { stream = new PrintStream(logFile); } catch (Exception e1) { // catch all the exceptions here, if we can't create the file, do the alternate path @@ -279,22 +280,12 @@ public abstract class CommandLineProgram { parser.loadArgumentsIntoObject(obj); } - /** - * a manual way to load argument providing objects into the program - * - * @param clp the command line program - * @param cls the class to load the arguments off of - */ - public void loadAdditionalSource(CommandLineProgram clp, Class cls) { - parser.addArgumentSource(clp.getArgumentSourceName(cls), cls); - } - /** * this function checks the logger level passed in on the command line, taking the lowest * level that was provided. */ private void setupLoggerLevel() { - Level par = Level.WARN; + Level par; if (logging_level.toUpperCase().equals("DEBUG")) { par = Level.DEBUG; } else if (logging_level.toUpperCase().equals("ERROR")) { @@ -316,9 +307,9 @@ public abstract class CommandLineProgram { } /** - * a function used to indicate an error occured in the command line tool + * a function used to indicate an error occurred in the command line tool * - * @param msg + * @param msg message to display */ private static void printExitSystemMsg(final String msg) { System.out.printf("The following error has occurred:%n%n"); @@ -334,12 +325,11 @@ public abstract class CommandLineProgram { /** * Do a cursory search for the given argument. * - * @param clp Instance of the command-line program. * @param parser Parser * * @return True if help is present; false otherwise. */ - private static boolean isHelpPresent(CommandLineProgram clp, ParsingEngine parser) { + private static boolean isHelpPresent(ParsingEngine parser) { return parser.isArgumentPresent("help"); } diff --git a/java/src/org/broadinstitute/sting/commandline/ParsingEngine.java b/java/src/org/broadinstitute/sting/commandline/ParsingEngine.java index 2055faea9..d48123a4d 100755 --- a/java/src/org/broadinstitute/sting/commandline/ParsingEngine.java +++ b/java/src/org/broadinstitute/sting/commandline/ParsingEngine.java @@ -270,26 +270,38 @@ public class ParsingEngine { return; // Target instance into which to inject the value. - List targets = new ArrayList(); - - // Check to see whether the instance itself can be the target. - if( source.clazz.isAssignableFrom(instance.getClass()) ) { - targets.add(instance); - } - - // Check to see whether a contained class can be the target. - targets.addAll(getContainersMatching(instance,source.clazz)); + Collection targets = findTargets( source, instance ); // Abort if no home is found for the object. if( targets.size() == 0 ) throw new StingException("Internal command-line parser error: unable to find a home for argument matches " + argumentMatches); for( Object target: targets ) { - Object value = (argumentMatches.size() != 0) ? source.parse(source,argumentMatches) : source.getDefault(); + Object value = (argumentMatches.size() != 0) ? source.parse(argumentMatches) : source.getDefault(); JVMUtils.setFieldValue(source.field,target,value); } } + /** + * Gets a collection of the container instances of the given type stored within the given target. + * @param source Argument source. + * @param instance Container. + * @return A collection of containers matching the given argument source. + */ + private Collection findTargets(ArgumentSource source, Object instance) { + LinkedHashSet targets = new LinkedHashSet(); + for( Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass() ) { + for( Field field: clazz.getDeclaredFields() ) { + if( field.equals(source.field) ) { + targets.add(instance); + } else if( field.isAnnotationPresent(ArgumentCollection.class) ) { + targets.addAll(findTargets(source, JVMUtils.getFieldValue(field, instance))); + } + } + } + return targets; + } + /** * Prints out the help associated with these command-line argument definitions. * @param applicationDetails Details about the specific GATK-based application being run. @@ -303,15 +315,22 @@ public class ParsingEngine { * @param sourceClass class to act as sources for other arguments. * @return A list of sources associated with this object and its aggregated objects. */ - protected static List extractArgumentSources(Class sourceClass) { + public static List extractArgumentSources(Class sourceClass) { + return extractArgumentSources(sourceClass, new Field[0]); + } + + private static List extractArgumentSources(Class sourceClass, Field[] parentFields) { List argumentSources = new ArrayList(); while( sourceClass != null ) { Field[] fields = sourceClass.getDeclaredFields(); for( Field field: fields ) { if( ArgumentTypeDescriptor.isArgumentAnnotationPresent(field) ) - argumentSources.add( new ArgumentSource(sourceClass,field) ); - if( field.isAnnotationPresent(ArgumentCollection.class) ) - argumentSources.addAll( extractArgumentSources(field.getType()) ); + argumentSources.add( new ArgumentSource(parentFields, field) ); + if( field.isAnnotationPresent(ArgumentCollection.class) ) { + Field[] newParentFields = Arrays.copyOf(parentFields, parentFields.length + 1); + newParentFields[parentFields.length] = field; + argumentSources.addAll( extractArgumentSources(field.getType(), newParentFields) ); + } } sourceClass = sourceClass.getSuperclass(); } @@ -350,24 +369,6 @@ public class ParsingEngine { // No parse results found. return null; } - - /** - * Gets a list of the container instances of the given type stored within the given target. - * @param target Class holding the container. - * @param type Container type. - * @return A list of containers matching the given type. - */ - private List getContainersMatching(Object target, Class type) { - List containers = new ArrayList(); - - Field[] fields = target.getClass().getDeclaredFields(); - for( Field field: fields ) { - if( field.isAnnotationPresent(ArgumentCollection.class) && type.isAssignableFrom(field.getType()) ) - containers.add(JVMUtils.getFieldValue(field,target)); - } - - return containers; - } } /** diff --git a/java/src/org/broadinstitute/sting/gatk/CommandLineGATK.java b/java/src/org/broadinstitute/sting/gatk/CommandLineGATK.java index ce8b23d44..b13c26cb1 100755 --- a/java/src/org/broadinstitute/sting/gatk/CommandLineGATK.java +++ b/java/src/org/broadinstitute/sting/gatk/CommandLineGATK.java @@ -26,7 +26,6 @@ package org.broadinstitute.sting.gatk; import org.broadinstitute.sting.gatk.arguments.GATKArgumentCollection; -import org.broadinstitute.sting.gatk.GATKErrorReport; import org.broadinstitute.sting.utils.text.TextFormattingUtils; import org.broadinstitute.sting.utils.help.ApplicationDetails; import org.broadinstitute.sting.commandline.*; @@ -135,7 +134,7 @@ public class CommandLineGATK extends CommandLineExecutable { * @return A string summarizing the walkers available in this distribution. */ private String getAdditionalHelp() { - String additionalHelp = ""; + String additionalHelp; // If no analysis name is present, fill in extra help on the walkers. WalkerManager walkerManager = GATKEngine.getWalkerManager(); @@ -152,7 +151,7 @@ public class CommandLineGATK extends CommandLineExecutable { private static final int WALKER_INDENT = 3; private static final String FIELD_SEPARATOR = " "; - private String getWalkerHelp(Class walkerType) { + private String getWalkerHelp(Class walkerType) { // Construct a help string to output details on this walker. StringBuilder additionalHelp = new StringBuilder(); Formatter formatter = new Formatter(additionalHelp); diff --git a/java/src/org/broadinstitute/sting/gatk/WalkerManager.java b/java/src/org/broadinstitute/sting/gatk/WalkerManager.java index 588d56a19..d8d4a7861 100755 --- a/java/src/org/broadinstitute/sting/gatk/WalkerManager.java +++ b/java/src/org/broadinstitute/sting/gatk/WalkerManager.java @@ -40,19 +40,10 @@ import org.broadinstitute.sting.utils.help.SummaryTaglet; import java.util.*; /** - * Created by IntelliJ IDEA. - * User: hanna - * Date: Mar 17, 2009 - * Time: 3:14:28 PM - * To change this template use File | Settings | File Templates. + * Plugin manager that also provides various utilities for inspecting Walkers. */ public class WalkerManager extends PluginManager { - /** - * our log, which we want to capture anything from this class - */ - private static Logger logger = Logger.getLogger(WalkerManager.class); - /** * A collection of help text for walkers and their enclosing packages. */ @@ -92,7 +83,7 @@ public class WalkerManager extends PluginManager { public String getPackageDisplayName(String packageName) { // Try to find an override for the display name of this package. String displayNameKey = String.format("%s.%s",packageName,DisplayNameTaglet.NAME); - String displayName = null; + String displayName; if(helpText.containsKey(displayNameKey)) { displayName = helpText.getString(displayNameKey); } @@ -130,6 +121,15 @@ public class WalkerManager extends PluginManager { return helpText.getString(walkerSummary); } + /** + * Gets the summary help text associated with a given walker type. + * @param walker Walker for which to search for help text. + * @return Walker summary description, or "" if none exists. + */ + public String getWalkerSummaryText(Walker walker) { + return getWalkerSummaryText(walker.getClass()); + } + /** * Gets the descriptive help text associated with a given walker type. * @param walkerType Type of walker for which to search for help text. @@ -142,13 +142,34 @@ public class WalkerManager extends PluginManager { return helpText.getString(walkerDescription); } + /** + * Gets the descriptive help text associated with a given walker type. + * @param walker Walker for which to search for help text. + * @return Walker full description, or "" if none exists. + */ + public String getWalkerDescriptionText(Walker walker) { + return getWalkerDescriptionText(walker.getClass()); + } + /** * Retrieves the walker class given a walker name. * @param walkerName Name of the walker. * @return Class representing the walker. */ - public Class getWalkerClassByName(String walkerName) { - return (Class)pluginsByName.get(walkerName); + public Class getWalkerClassByName(String walkerName) { + return pluginsByName.get(walkerName); + } + + /** + * Gets the data source for the provided walker. + * @param walkerClass The class of the walker. + * @return Which type of data source to traverse over...reads or reference? + */ + public static DataSource getWalkerDataSource(Class walkerClass) { + By byDataSource = walkerClass.getAnnotation(By.class); + if( byDataSource == null ) + throw new StingException("Unable to find By annotation for walker class " + walkerClass.getName()); + return byDataSource.value(); } /** @@ -157,21 +178,38 @@ public class WalkerManager extends PluginManager { * @return Which type of data source to traverse over...reads or reference? */ public static DataSource getWalkerDataSource(Walker walker) { - Class walkerClass = walker.getClass(); - By byDataSource = walkerClass.getAnnotation(By.class); - if( byDataSource == null ) - throw new StingException("Unable to find By annotation for walker class " + walkerClass.getName()); - return byDataSource.value(); + return getWalkerDataSource(walker.getClass()); + } + + /** + * Get a list of RODs allowed by the walker. + * @param walkerClass Class of the walker to query. + * @return The list of allowed reference meta data. + */ + public static List getAllowsMetaData(Class walkerClass) { + Allows allowsDataSource = getWalkerAllowed(walkerClass); + if (allowsDataSource == null) + return Collections.emptyList(); + return Arrays.asList(allowsDataSource.referenceMetaData()); + } + + /** + * Get a list of RODs allowed by the walker. + * @param walker Walker to query. + * @return The list of allowed reference meta data. + */ + public static List getAllowsMetaData(Walker walker) { + return getAllowsMetaData(walker.getClass()); } /** * Determine whether the given walker supports the given data source. - * @param walker Walker to query. + * @param walkerClass Class of the walker to query. * @param dataSource Source to check for . * @return True if the walker forbids this data type. False otherwise. */ - public static boolean isAllowed(Walker walker, DataSource dataSource) { - Allows allowsDataSource = getWalkerAllowed(walker); + public static boolean isAllowed(Class walkerClass, DataSource dataSource) { + Allows allowsDataSource = getWalkerAllowed(walkerClass); // Allows is less restrictive than requires. If an allows // clause is not specified, any kind of data is allowed. @@ -182,13 +220,23 @@ public class WalkerManager extends PluginManager { } /** - * Determine whether the given walker supports the given reference ordered data. + * Determine whether the given walker supports the given data source. * @param walker Walker to query. + * @param dataSource Source to check for . + * @return True if the walker forbids this data type. False otherwise. + */ + public static boolean isAllowed(Walker walker, DataSource dataSource) { + return isAllowed(walker.getClass(), dataSource); + } + + /** + * Determine whether the given walker supports the given reference ordered data. + * @param walkerClass Class of the walker to query. * @param rod Source to check. * @return True if the walker forbids this data type. False otherwise. */ - public static boolean isAllowed(Walker walker, RMDTrack rod) { - Allows allowsDataSource = getWalkerAllowed(walker); + public static boolean isAllowed(Class walkerClass, RMDTrack rod) { + Allows allowsDataSource = getWalkerAllowed(walkerClass); // Allows is less restrictive than requires. If an allows // clause is not specified, any kind of data is allowed. @@ -208,6 +256,27 @@ public class WalkerManager extends PluginManager { return false; } + /** + * Determine whether the given walker supports the given reference ordered data. + * @param walker Walker to query. + * @param rod Source to check. + * @return True if the walker forbids this data type. False otherwise. + */ + public static boolean isAllowed(Walker walker, RMDTrack rod) { + return isAllowed(walker.getClass(), rod); + } + + /** + * Determine whether the given walker requires the given data source. + * @param walkerClass Class of the walker to query. + * @param dataSource Source to check for. + * @return True if the walker allows this data type. False otherwise. + */ + public static boolean isRequired(Class walkerClass, DataSource dataSource) { + Requires requiresDataSource = getWalkerRequirements(walkerClass); + return Arrays.asList(requiresDataSource.value()).contains(dataSource); + } + /** * Determine whether the given walker requires the given data source. * @param walker Walker to query. @@ -215,18 +284,26 @@ public class WalkerManager extends PluginManager { * @return True if the walker allows this data type. False otherwise. */ public static boolean isRequired(Walker walker, DataSource dataSource) { - Requires requiresDataSource = getWalkerRequirements(walker); - return Arrays.asList(requiresDataSource.value()).contains(dataSource); + return isRequired(walker.getClass(), dataSource); + } + + /** + * Get a list of RODs required by the walker. + * @param walkerClass Class of the walker to query. + * @return The list of required reference meta data. + */ + public static List getRequiredMetaData(Class walkerClass) { + Requires requiresDataSource = getWalkerRequirements(walkerClass); + return Arrays.asList(requiresDataSource.referenceMetaData()); } /** * Get a list of RODs required by the walker. * @param walker Walker to query. - * @return True if the walker allows this data type. False otherwise. + * @return The list of required reference meta data. */ public static List getRequiredMetaData(Walker walker) { - Requires requiresDataSource = getWalkerRequirements(walker); - return Arrays.asList(requiresDataSource.referenceMetaData()); + return getRequiredMetaData(walker.getClass()); } /** @@ -238,6 +315,19 @@ public class WalkerManager extends PluginManager { return walkerType.isAnnotationPresent(Hidden.class); } + /** + * Extracts filters that the walker has requested be run on the dataset. + * @param walkerClass Class of the walker to inspect for filtering requests. + * @param filterManager Manages the creation of filters. + * @return A non-empty list of filters to apply to the reads. + */ + public static List getReadFilters(Class walkerClass, FilterManager filterManager) { + List filters = new ArrayList(); + for(Class filterType: getReadFilterTypes(walkerClass)) + filters.add(filterManager.createFilterByType(filterType)); + return filters; + } + /** * Extracts filters that the walker has requested be run on the dataset. * @param walker Walker to inspect for filtering requests. @@ -245,10 +335,28 @@ public class WalkerManager extends PluginManager { * @return A non-empty list of filters to apply to the reads. */ public static List getReadFilters(Walker walker, FilterManager filterManager) { - List filters = new ArrayList(); - for(Class filterType: getReadFilterTypes(walker)) - filters.add(filterManager.createFilterByType(filterType)); - return filters; + return getReadFilters(walker.getClass(), filterManager); + } + + /** + * Gets the type of downsampling method requested by the walker. If an alternative + * downsampling method is specified on the command-line, the command-line version will + * be used instead. + * @param walkerClass The class of the walker to interrogate. + * @return The downsampling method, as specified by the walker. Null if none exists. + */ + public static DownsamplingMethod getDownsamplingMethod(Class walkerClass) { + DownsamplingMethod downsamplingMethod = null; + + if( walkerClass.isAnnotationPresent(Downsample.class) ) { + Downsample downsampleParameters = walkerClass.getAnnotation(Downsample.class); + DownsampleType type = downsampleParameters.by(); + Integer toCoverage = downsampleParameters.toCoverage() >= 0 ? downsampleParameters.toCoverage() : null; + Double toFraction = downsampleParameters.toFraction() >= 0.0d ? downsampleParameters.toFraction() : null; + downsamplingMethod = new DownsamplingMethod(type,toCoverage,toFraction); + } + + return downsamplingMethod; } /** @@ -259,17 +367,7 @@ public class WalkerManager extends PluginManager { * @return The downsampling method, as specified by the walker. Null if none exists. */ public static DownsamplingMethod getDownsamplingMethod(Walker walker) { - DownsamplingMethod downsamplingMethod = null; - - if( walker.getClass().isAnnotationPresent(Downsample.class) ) { - Downsample downsampleParameters = walker.getClass().getAnnotation(Downsample.class); - DownsampleType type = downsampleParameters.by(); - Integer toCoverage = downsampleParameters.toCoverage() >= 0 ? downsampleParameters.toCoverage() : null; - Double toFraction = downsampleParameters.toFraction() >= 0.0d ? downsampleParameters.toFraction() : null; - downsamplingMethod = new DownsamplingMethod(type,toCoverage,toFraction); - } - - return downsamplingMethod; + return getDownsamplingMethod(walker.getClass()); } /** @@ -293,26 +391,55 @@ public class WalkerManager extends PluginManager { /** * Utility to get the requires attribute from the walker. * Throws an exception if requirements are missing. - * @param walker Walker to query for required data. + * @param walkerClass Class of the walker to query for required data. * @return Required data attribute. */ - private static Requires getWalkerRequirements(Walker walker) { - Class walkerClass = walker.getClass(); + private static Requires getWalkerRequirements(Class walkerClass) { Requires requiresDataSource = walkerClass.getAnnotation(Requires.class); if( requiresDataSource == null ) throw new StingException( "Unable to find data types required by walker class " + walkerClass.getName()); return requiresDataSource; } + /** + * Utility to get the requires attribute from the walker. + * Throws an exception if requirements are missing. + * @param walker Walker to query for required data. + * @return Required data attribute. + */ + private static Requires getWalkerRequirements(Walker walker) { + return getWalkerRequirements(walker.getClass()); + } + + /** + * Utility to get the forbidden attribute from the walker. + * @param walkerClass Class of the walker to query for required data. + * @return Required data attribute. Null if forbidden info isn't present. + */ + private static Allows getWalkerAllowed(Class walkerClass) { + Allows allowsDataSource = walkerClass.getAnnotation(Allows.class); + return allowsDataSource; + } + /** * Utility to get the forbidden attribute from the walker. * @param walker Walker to query for required data. * @return Required data attribute. Null if forbidden info isn't present. */ private static Allows getWalkerAllowed(Walker walker) { - Class walkerClass = walker.getClass(); - Allows allowsDataSource = walkerClass.getAnnotation(Allows.class); - return allowsDataSource; + return getWalkerAllowed(walker.getClass()); + } + + /** + * Gets the list of filtering classes specified as walker annotations. + * @param walkerClass Class of the walker to inspect. + * @return An array of types extending from SamRecordFilter. Will never be null. + */ + @SuppressWarnings("unchecked") + public static Class[] getReadFilterTypes(Class walkerClass) { + if( !walkerClass.isAnnotationPresent(ReadFilters.class) ) + return new Class[0]; + return walkerClass.getAnnotation(ReadFilters.class).value(); } /** @@ -320,10 +447,7 @@ public class WalkerManager extends PluginManager { * @param walker The walker to inspect. * @return An array of types extending from SamRecordFilter. Will never be null. */ - private static Class[] getReadFilterTypes(Walker walker) { - Class walkerClass = walker.getClass(); - if( !walkerClass.isAnnotationPresent(ReadFilters.class) ) - return new Class[0]; - return walkerClass.getAnnotation(ReadFilters.class).value(); + public static Class[] getReadFilterTypes(Walker walker) { + return getReadFilterTypes(walker.getClass()); } } diff --git a/java/src/org/broadinstitute/sting/gatk/arguments/GATKArgumentCollection.java b/java/src/org/broadinstitute/sting/gatk/arguments/GATKArgumentCollection.java index 90afae069..3be32ec49 100755 --- a/java/src/org/broadinstitute/sting/gatk/arguments/GATKArgumentCollection.java +++ b/java/src/org/broadinstitute/sting/gatk/arguments/GATKArgumentCollection.java @@ -29,6 +29,8 @@ import net.sf.samtools.SAMFileReader; import org.broadinstitute.sting.utils.StingException; import org.broadinstitute.sting.utils.interval.IntervalMergingRule; import org.broadinstitute.sting.commandline.Argument; +import org.broadinstitute.sting.commandline.Input; +import org.broadinstitute.sting.commandline.Output; import org.broadinstitute.sting.gatk.DownsampleType; import org.broadinstitute.sting.utils.interval.IntervalSetRule; import org.simpleframework.xml.*; @@ -64,7 +66,7 @@ public class GATKArgumentCollection { // parameters and their defaults @ElementList(required = false) - @Argument(fullName = "input_file", shortName = "I", doc = "SAM or BAM file(s)", required = false) + @Input(fullName = "input_file", shortName = "I", doc = "SAM or BAM file(s)", required = false) public List samFiles = new ArrayList(); @Element(required = false) @@ -76,19 +78,19 @@ public class GATKArgumentCollection { public List readFilters = new ArrayList(); @ElementList(required = false) - @Argument(fullName = "intervals", shortName = "L", doc = "A list of genomic intervals over which to operate. Can be explicitly specified on the command line or in a file.", required = false) + @Input(fullName = "intervals", shortName = "L", doc = "A list of genomic intervals over which to operate. Can be explicitly specified on the command line or in a file.", required = false) public List intervals = null; @ElementList(required = false) - @Argument(fullName = "excludeIntervals", shortName = "XL", doc = "A list of genomic intervals to exclude from processing. Can be explicitly specified on the command line or in a file.", required = false) + @Input(fullName = "excludeIntervals", shortName = "XL", doc = "A list of genomic intervals to exclude from processing. Can be explicitly specified on the command line or in a file.", required = false) public List excludeIntervals = null; @Element(required = false) - @Argument(fullName = "reference_sequence", shortName = "R", doc = "Reference sequence file", required = false) + @Input(fullName = "reference_sequence", shortName = "R", doc = "Reference sequence file", required = false) public File referenceFile = null; @ElementList(required = false) - @Argument(fullName = "rodBind", shortName = "B", doc = "Bindings for reference-ordered data, in the form ,,", required = false) + @Input(fullName = "rodBind", shortName = "B", doc = "Bindings for reference-ordered data, in the form ,,", required = false) public ArrayList RODBindings = new ArrayList(); @Element(required = false) @@ -100,30 +102,30 @@ public class GATKArgumentCollection { public IntervalSetRule BTIMergeRule = IntervalSetRule.UNION; @Element(required = false) - @Argument(fullName = "DBSNP", shortName = "D", doc = "DBSNP file", required = false) + @Input(fullName = "DBSNP", shortName = "D", doc = "DBSNP file", required = false) public String DBSNPFile = null; @Element(required = false) - @Argument(fullName = "hapmap", shortName = "H", doc = "Hapmap file", required = false) + @Input(fullName = "hapmap", shortName = "H", doc = "Hapmap file", required = false) public String HAPMAPFile = null; @Element(required = false) - @Argument(fullName = "hapmap_chip", shortName = "hc", doc = "Hapmap chip file", required = false) + @Input(fullName = "hapmap_chip", shortName = "hc", doc = "Hapmap chip file", required = false) public String HAPMAPChipFile = null; /** An output file presented to the walker. */ @Element(required = false) - @Argument(fullName = "out", shortName = "o", doc = "An output file presented to the walker. Will overwrite contents if file exists.", required = false) + @Output(fullName = "out", shortName = "o", doc = "An output file presented to the walker. Will overwrite contents if file exists.", required = false) public String outFileName = null; /** An error output file presented to the walker. */ @Element(required = false) - @Argument(fullName = "err", shortName = "e", doc = "An error output file presented to the walker. Will overwrite contents if file exists.", required = false) + @Output(fullName = "err", shortName = "e", doc = "An error output file presented to the walker. Will overwrite contents if file exists.", required = false) public String errFileName = null; /** A joint file for both 'normal' and error output presented to the walker. */ @Element(required = false) - @Argument(fullName = "outerr", shortName = "oe", doc = "A joint file for 'normal' and error output presented to the walker. Will overwrite contents if file exists.", required = false) + @Output(fullName = "outerr", shortName = "oe", doc = "A joint file for 'normal' and error output presented to the walker. Will overwrite contents if file exists.", required = false) public String outErrFileName = null; @Element(required = false) diff --git a/java/src/org/broadinstitute/sting/gatk/filters/FilterManager.java b/java/src/org/broadinstitute/sting/gatk/filters/FilterManager.java index 44f9bdf76..bd899b80c 100644 --- a/java/src/org/broadinstitute/sting/gatk/filters/FilterManager.java +++ b/java/src/org/broadinstitute/sting/gatk/filters/FilterManager.java @@ -30,6 +30,8 @@ import org.broadinstitute.sting.utils.classloader.PluginManager; import net.sf.picard.filter.SamRecordFilter; +import java.util.Collection; + /** * Manage filters and filter options. Any requests for basic filtering classes * should ultimately be made through this class. @@ -38,11 +40,6 @@ import net.sf.picard.filter.SamRecordFilter; * @version 0.1 */ public class FilterManager extends PluginManager { - /** - * our log, which we want to capture anything from this class - */ - private static Logger logger = Logger.getLogger(FilterManager.class); - public FilterManager() { super(SamRecordFilter.class,"filter","Filter"); } @@ -50,10 +47,14 @@ public class FilterManager extends PluginManager { /** * Instantiate a filter of the given type. Along the way, scream bloody murder if * the filter is not available. - * @param filterType - * @return + * @param filterType The type of the filter + * @return The filter */ public SamRecordFilter createFilterByType(Class filterType) { return this.createByName(getName(filterType)); } + + public Collection> getValues() { + return this.pluginsByName.values(); + } } diff --git a/java/src/org/broadinstitute/sting/gatk/io/stubs/GenotypeWriterArgumentTypeDescriptor.java b/java/src/org/broadinstitute/sting/gatk/io/stubs/GenotypeWriterArgumentTypeDescriptor.java index ccbc16d37..2ac9c0314 100644 --- a/java/src/org/broadinstitute/sting/gatk/io/stubs/GenotypeWriterArgumentTypeDescriptor.java +++ b/java/src/org/broadinstitute/sting/gatk/io/stubs/GenotypeWriterArgumentTypeDescriptor.java @@ -158,10 +158,12 @@ public class GenotypeWriterArgumentTypeDescriptor extends ArgumentTypeDescriptor Annotation annotation = this.getArgumentAnnotation(source); return new ArgumentDefinition( annotation, + source.field.getType(), "variants_out", "varout", false, source.isMultiValued(), + getCollectionComponentType(source.field), source.isHidden(), null ); } @@ -173,13 +175,15 @@ public class GenotypeWriterArgumentTypeDescriptor extends ArgumentTypeDescriptor */ private ArgumentDefinition createGenotypeFormatArgumentDefinition(ArgumentSource source) { Annotation annotation = this.getArgumentAnnotation(source); - return new ArgumentDefinition( ArgumentDefinition.getIOType(annotation), + return new ArgumentDefinition( ArgumentIOType.getIOType(annotation), + GenotypeWriterFactory.GENOTYPE_FORMAT.class, "variant_output_format", "vf", "Format to be used to represent variants; default is VCF", false, false, false, + null, source.isHidden(), null, null, diff --git a/java/src/org/broadinstitute/sting/gatk/io/stubs/SAMFileWriterArgumentTypeDescriptor.java b/java/src/org/broadinstitute/sting/gatk/io/stubs/SAMFileWriterArgumentTypeDescriptor.java index 184da8757..7f9802220 100644 --- a/java/src/org/broadinstitute/sting/gatk/io/stubs/SAMFileWriterArgumentTypeDescriptor.java +++ b/java/src/org/broadinstitute/sting/gatk/io/stubs/SAMFileWriterArgumentTypeDescriptor.java @@ -97,10 +97,12 @@ public class SAMFileWriterArgumentTypeDescriptor extends ArgumentTypeDescriptor private ArgumentDefinition createBAMArgumentDefinition(ArgumentSource source) { Annotation annotation = this.getArgumentAnnotation(source); return new ArgumentDefinition( annotation, + source.field.getType(), DEFAULT_ARGUMENT_FULLNAME, DEFAULT_ARGUMENT_SHORTNAME, false, source.isMultiValued(), + getCollectionComponentType(source.field), source.isHidden(), null ); } @@ -112,13 +114,15 @@ public class SAMFileWriterArgumentTypeDescriptor extends ArgumentTypeDescriptor */ private ArgumentDefinition createBAMCompressionArgumentDefinition(ArgumentSource source) { Annotation annotation = this.getArgumentAnnotation(source); - return new ArgumentDefinition( ArgumentDefinition.getIOType(annotation), + return new ArgumentDefinition( ArgumentIOType.getIOType(annotation), + int.class, COMPRESSION_FULLNAME, COMPRESSION_SHORTNAME, "Compression level to use for writing BAM files", false, false, false, + null, source.isHidden(), null, null, diff --git a/java/src/org/broadinstitute/sting/gatk/iterators/MergingSamRecordIterator2.java b/java/src/org/broadinstitute/sting/gatk/iterators/MergingSamRecordIterator2.java deleted file mode 100644 index 7f7b8da13..000000000 --- a/java/src/org/broadinstitute/sting/gatk/iterators/MergingSamRecordIterator2.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2009 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. - */ - -package org.broadinstitute.sting.gatk.iterators; - -import net.sf.picard.PicardException; -import net.sf.picard.sam.ReservedTagConstants; -import net.sf.picard.sam.SamFileHeaderMerger; -import net.sf.picard.util.PeekableIterator; -import net.sf.samtools.*; -import net.sf.samtools.util.CloseableIterator; -import org.apache.log4j.Logger; -import org.broadinstitute.sting.gatk.Reads; -import org.broadinstitute.sting.gatk.arguments.ValidationExclusion; -import org.broadinstitute.sting.utils.StingException; -import org.broadinstitute.sting.utils.Utils; - -import java.lang.reflect.Constructor; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.PriorityQueue; - -// Should replace picard class with the same name -class ComparableSamRecordIterator extends PeekableIterator implements Comparable, StingSAMIterator { - private Reads sourceInfo; - private final Comparator comparator; - private final SAMFileReader reader; - private final SamFileHeaderMerger mHeaderMerger; - - /** - * Constructs an iterator for iteration over the supplied SAM file that will be - * able to compare itself to other ComparableSAMRecordIterator instances using - * the supplied comparator for ordering SAMRecords. - * - * @param sam the SAM file to read records from - * @param comparator the Comparator to use to provide ordering fo SAMRecords - */ - public ComparableSamRecordIterator(SamFileHeaderMerger samHeaderMerger, final SAMFileReader sam, final Comparator comparator) { - super(sam.iterator()); - this.reader = sam; - this.comparator = comparator; - mHeaderMerger = samHeaderMerger; - } - - public ComparableSamRecordIterator(SamFileHeaderMerger samHeaderMerger, final SAMFileReader sam, Iterator iterator, final Comparator comparator) { - super(iterator); // use the provided iterator - this.reader = sam; - this.comparator = comparator; - mHeaderMerger = samHeaderMerger; - } - - public Reads getSourceInfo() { - if (sourceInfo == null) - throw new StingException("Unable to provide source info for the reads. Please upgrade to the new data sharding framework."); - return sourceInfo; - } - - /** - * Returns the reader from which this iterator was constructed. - * - * @return the SAMFileReader - */ - public SAMFileReader getReader() { - return reader; - } - - /** - * Compares this iterator to another comparable iterator based on the next record - * available in each iterator. If the two comparable iterators have different - * comparator types internally an exception is thrown. - * - * @param that another iterator to compare to - * - * @return a negative, 0 or positive number as described in the Comparator interface - */ - public int compareTo(final ComparableSamRecordIterator that) { - if (this.comparator.getClass() != that.comparator.getClass()) { - throw new IllegalStateException("Attempt to compare two ComparableSAMRecordIterators that " + - "have different orderings internally"); - } - - final SAMRecord record = this.peek(); - final SAMRecord record2 = that.peek(); - record.setHeader(mHeaderMerger.getMergedHeader()); - record2.setHeader(mHeaderMerger.getMergedHeader()); - int index, index2; - try { - index = mHeaderMerger.getMergedHeader().getSequenceIndex(record.getReferenceName()); - record.setReferenceIndex(index); - - index2 = mHeaderMerger.getMergedHeader().getSequenceIndex(record2.getReferenceName()); - record2.setReferenceIndex(index2); - } catch (Exception e) { - throw new StingException("MergingSamRecordIterator2: unable to correct the reference index for read " + record.getReadName() + " or record " + record2.getReadName(),e); - } - return comparator.compare(record, record2); - } - - public Iterator iterator() { - return this; - } -} diff --git a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/RMDTrackManager.java b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/RMDTrackManager.java index fb0fd3b25..19a6607fb 100644 --- a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/RMDTrackManager.java +++ b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/RMDTrackManager.java @@ -31,26 +31,28 @@ import org.broadinstitute.sting.utils.classloader.PluginManager; import org.broadinstitute.sting.utils.StingException; import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import java.util.*; /** - * @author aaron - *

- * Class RMDTrackManager - *

- * Find the available track builders, and create the requisite tracks from the command line. + * Find the available track builders, and create the requisite tracks from the command line. + * + * In Tribble RMD tracks have two classes: + * - a Feature that is the model/view for the data + * - a Codec that is the controller to generate the Feature. + * + * In this class, the track types are the Codecs. The track record types are the Features. */ public class RMDTrackManager extends PluginManager { // the input strings we use to create RODs from List inputs = new ArrayList(); // create an active mapping of builder instances, and a map of the name -> class for convenience - Map availableTracks; - Map availableTrackClasses; + /** the tracks that are available to us, associated with their builder */ + Map availableTrackBuilders; + /** the classes names, with their class description (think the Controller Codecs) */ + Map availableTrackTypes; + /** the available track record types (think the Model/View Features) */ + Map availableTrackRecordTypes; /** Create a new track plugin manager. */ public RMDTrackManager() { @@ -65,28 +67,56 @@ public class RMDTrackManager extends PluginManager { * @return a list of RMDTracks, one for each -B option */ public List getReferenceMetaDataSources(List triplets) { - if (availableTracks == null || availableTrackClasses == null) initialize(triplets); + initializeTrackTypes(); + initializeTriplets(triplets); // try and make the tracks given their requests - return createRequestedTrackObjects(availableTracks, availableTrackClasses); + return createRequestedTrackObjects(); + } + + + /** + * Returns a collection of track names that match the record type. + * @param trackRecordType the record type specified in the @RMD annotation + * @return a collection of available track record type names that match the record type + */ + public Collection getTrackRecordTypeNames(Class trackRecordType) { + initializeTrackTypes(); + initializeTrackRecordTypes(); + Set names = new TreeSet(); + for (Map.Entry availableTrackRecordType: availableTrackRecordTypes.entrySet()) { + if (trackRecordType.isAssignableFrom(availableTrackRecordType.getValue())) + names.add(availableTrackRecordType.getKey()); + } + return names; } /** - * initialize our lists of tracks and builders + * initialize our lists of triplets * @param triplets the input to the GATK, as a list of strings passed in through the -B options */ - private void initialize(List triplets) { + private void initializeTriplets(List triplets) { + // NOTE: Method acts as a static. Once the inputs have been passed once they are locked in. + if (inputs.size() > 0 || triplets.size() == 0) + return; + for (String value: triplets) { String[] split = value.split(","); if (split.length != 3) throw new IllegalArgumentException(value + " is not a valid reference metadata track description"); inputs.add(new RMDTriplet(split[0], split[1], split[2])); } + } + + /** + * initialize our lists of tracks and builders + */ + private void initializeTrackTypes() { + if (availableTrackBuilders != null && availableTrackTypes != null) + return; // create an active mapping of builder instances, and a map of the name -> class for convenience - availableTracks = new HashMap(); - availableTrackClasses = new HashMap(); + availableTrackBuilders = new HashMap(); + availableTrackTypes = new HashMap(); createBuilderObjects(); - - } /** @@ -98,8 +128,24 @@ public class RMDTrackManager extends PluginManager { RMDTrackBuilder builder = this.createByName(builderName); Map mapping = builder.getAvailableTrackNamesAndTypes(); for (String name : mapping.keySet()) { - availableTracks.put(name.toUpperCase(), builder); - availableTrackClasses.put(name.toUpperCase(), mapping.get(name)); + availableTrackBuilders.put(name.toUpperCase(), builder); + availableTrackTypes.put(name.toUpperCase(), mapping.get(name)); + } + } + } + + /** + * initialize our list of track record types + */ + private void initializeTrackRecordTypes() { + if (availableTrackRecordTypes != null) + return; + + availableTrackRecordTypes = new HashMap(); + for (RMDTrackBuilder builder : availableTrackBuilders.values()) { + Map mapping = builder.getAvailableTrackNamesAndRecordTypes(); + for (String name : mapping.keySet()) { + availableTrackRecordTypes.put(name.toUpperCase(), mapping.get(name)); } } } @@ -107,22 +153,18 @@ public class RMDTrackManager extends PluginManager { /** * create the requested track objects * - * @param availableTracks the tracks that are available to us, associated with their builder - * @param availableTrackClasses the classes names, with their class description - * * @return a list of the tracks, one for each of the requested input tracks */ - private List createRequestedTrackObjects(Map availableTracks, Map availableTrackClasses) { + private List createRequestedTrackObjects() { // create of live instances of the tracks List tracks = new ArrayList(); // create instances of each of the requested types for (RMDTriplet trip : inputs) { - RMDTrackBuilder b = availableTracks.get(trip.getType().toUpperCase()); + RMDTrackBuilder b = availableTrackBuilders.get(trip.getType().toUpperCase()); if (b == null) throw new StingException("Unable to find track for " + trip.getType()); - tracks.add(b.createInstanceOfTrack(availableTrackClasses.get(trip.getType().toUpperCase()), trip.getName(), new File(trip.getFile()))); + tracks.add(b.createInstanceOfTrack(availableTrackTypes.get(trip.getType().toUpperCase()), trip.getName(), new File(trip.getFile()))); } return tracks; } } - diff --git a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RMDTrackBuilder.java b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RMDTrackBuilder.java index 01c971acb..17b778f45 100644 --- a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RMDTrackBuilder.java +++ b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RMDTrackBuilder.java @@ -44,6 +44,9 @@ public interface RMDTrackBuilder { /** @return a list of all available tracks types we currently have access to create */ public Map getAvailableTrackNamesAndTypes(); + /** @return a list of all available track record types we currently have access to create */ + public Map getAvailableTrackNamesAndRecordTypes(); + /** * create a RMDTrack of the specified type * diff --git a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RODTrackBuilder.java b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RODTrackBuilder.java index dc5de4e20..b04b2dad1 100644 --- a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RODTrackBuilder.java +++ b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/RODTrackBuilder.java @@ -75,15 +75,19 @@ public class RODTrackBuilder implements RMDTrackBuilder { return new RODRMDTrack(targetClass, name, inputFile, createROD(name,targetClass,inputFile)); } - /** @return a map of all available tracks we currently have access to create */ + /** @return a map of all available track types we currently have access to create */ + @Override public Map getAvailableTrackNamesAndTypes() { - Map ret = new HashMap(); - for (String name : Types.keySet()) - ret.put(name, Types.get(name)); - return ret; + return new HashMap(Types); } -/** + /** @return a map of all available track record types we currently have access to create */ + @Override + public Map getAvailableTrackNamesAndRecordTypes() { + return new HashMap(Types); + } + + /** * Helpful function that parses a single triplet of and returns the corresponding ROD with * , of type that reads its input from . * diff --git a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/TribbleRMDTrackBuilder.java b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/TribbleRMDTrackBuilder.java index b5e18e069..955d5111e 100644 --- a/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/TribbleRMDTrackBuilder.java +++ b/java/src/org/broadinstitute/sting/gatk/refdata/tracks/builders/TribbleRMDTrackBuilder.java @@ -35,7 +35,6 @@ import org.broad.tribble.index.IndexFactory; import org.broad.tribble.index.interval.IntervalIndexCreator; import org.broad.tribble.index.linear.LinearIndexCreator; import org.broad.tribble.source.BasicFeatureSource; -import org.broad.tribble.util.LittleEndianInputStream; import org.broad.tribble.util.LittleEndianOutputStream; import org.broadinstitute.sting.gatk.refdata.tracks.TribbleTrack; import org.broadinstitute.sting.gatk.refdata.tracks.RMDTrack; @@ -80,12 +79,20 @@ public class TribbleRMDTrackBuilder extends PluginManager implemen super(FeatureCodec.class, "Codecs", "Codec"); } - /** @return a list of all available tracks we currently have access to create */ + /** @return a list of all available track types we currently have access to create */ @Override public Map getAvailableTrackNamesAndTypes() { + return new HashMap(this.pluginsByName); + } + + /** @return a list of all available track record types we currently have access to create */ + @Override + public Map getAvailableTrackNamesAndRecordTypes() { Map classes = new HashMap(); - for (String c : this.pluginsByName.keySet()) - classes.put(c, this.pluginsByName.get(c)); + for (String name: this.pluginsByName.keySet()) { + FeatureCodec codec = this.createByName(name); + classes.put(name, codec.getFeatureType()); + } return classes; } @@ -115,11 +122,12 @@ public class TribbleRMDTrackBuilder extends PluginManager implemen /** * create a feature reader of the specified type * @param targetClass the target codec type + * @param name the target name * @param inputFile the input file to create the track from (of the codec type) * @return the FeatureReader instance */ public Pair createFeatureReader(Class targetClass, String name, File inputFile) { - Pair pair = null; + Pair pair; if (inputFile.getAbsolutePath().endsWith(".gz")) pair = createBasicFeatureSourceNoAssumedIndex(targetClass, name, inputFile); else @@ -133,6 +141,7 @@ public class TribbleRMDTrackBuilder extends PluginManager implemen * exists. * * @param targetClass the codec class type + * @param name the name of the track * @param inputFile the file to load * @return a feature reader implementation */ @@ -156,6 +165,7 @@ public class TribbleRMDTrackBuilder extends PluginManager implemen /** * create a linear feature reader, where we create the index ahead of time * @param targetClass the target class + * @param name the name of the codec * @param inputFile the tribble file to parse * @return the input file as a FeatureReader */ @@ -264,7 +274,7 @@ public class TribbleRMDTrackBuilder extends PluginManager implemen * @param indexFile the index file location * @param lock the locking object * @return the index object - * @throws IOException + * @throws IOException when unable to create the new index */ private static Index createNewIndex(File inputFile, FeatureCodec codec, boolean onDisk, File indexFile, FSLockWithShared lock) throws IOException { Index index = createIndexInMemory(inputFile, codec); @@ -296,7 +306,7 @@ public class TribbleRMDTrackBuilder extends PluginManager implemen * @param inputFile the input file * @param codec the codec * @return a LinearIndex, given the file location - * @throws IOException + * @throws IOException when unable to create the index in memory */ private static Index createIndexInMemory(File inputFile, FeatureCodec codec) throws IOException { // this can take a while, let them know what we're doing @@ -317,7 +327,7 @@ public class TribbleRMDTrackBuilder extends PluginManager implemen * @param contigList the contig list, in coordinate order, this is allowed to be null * @return a SAMSequenceDictionary, WITHOUT contig sizes */ - private static final SAMSequenceDictionary sequenceSetToDictionary(LinkedHashSet contigList) { + private static SAMSequenceDictionary sequenceSetToDictionary(LinkedHashSet contigList) { SAMSequenceDictionary dict = new SAMSequenceDictionary(); if (contigList == null) return dict; diff --git a/java/src/org/broadinstitute/sting/gatk/walkers/ClipReadsWalker.java b/java/src/org/broadinstitute/sting/gatk/walkers/ClipReadsWalker.java index 83e363b20..acbc708bb 100755 --- a/java/src/org/broadinstitute/sting/gatk/walkers/ClipReadsWalker.java +++ b/java/src/org/broadinstitute/sting/gatk/walkers/ClipReadsWalker.java @@ -487,7 +487,7 @@ public class ClipReadsWalker extends ReadWalker { /** an optional argument to dump the reads out to a BAM file */ - @Argument(fullName = "outputBamFile", shortName = "of", doc = "Write output to this BAM filename instead of STDOUT", required = false) + @Output(fullName = "outputBamFile", shortName = "of", doc = "Write output to this BAM filename instead of STDOUT", required = false) SAMFileWriter outputBamFile = null; @Argument(fullName = "readGroup", shortName = "readGroup", doc="Discard reads not belonging to the specified read group", required = false) String readGroup = null; diff --git a/java/src/org/broadinstitute/sting/gatk/walkers/RMD.java b/java/src/org/broadinstitute/sting/gatk/walkers/RMD.java index 20479a05c..306b3b1d3 100755 --- a/java/src/org/broadinstitute/sting/gatk/walkers/RMD.java +++ b/java/src/org/broadinstitute/sting/gatk/walkers/RMD.java @@ -1,5 +1,7 @@ package org.broadinstitute.sting.gatk.walkers; +import org.broad.tribble.Feature; + import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; @@ -25,5 +27,5 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface RMD { String name(); - Class type(); + Class type() default Feature.class; } diff --git a/java/src/org/broadinstitute/sting/gatk/walkers/recalibration/CovariateCounterWalker.java b/java/src/org/broadinstitute/sting/gatk/walkers/recalibration/CovariateCounterWalker.java index 1480a9ee9..2d72fb439 100755 --- a/java/src/org/broadinstitute/sting/gatk/walkers/recalibration/CovariateCounterWalker.java +++ b/java/src/org/broadinstitute/sting/gatk/walkers/recalibration/CovariateCounterWalker.java @@ -26,6 +26,7 @@ package org.broadinstitute.sting.gatk.walkers.recalibration; import org.broad.tribble.util.variantcontext.VariantContext; +import org.broadinstitute.sting.commandline.Output; import org.broadinstitute.sting.gatk.contexts.AlignmentContext; import org.broadinstitute.sting.gatk.contexts.ReferenceContext; import org.broadinstitute.sting.gatk.datasources.simpleDataSources.ReferenceOrderedDataSource; @@ -85,7 +86,7 @@ public class CovariateCounterWalker extends LocusWalker getAnnotationIOClass() { return argumentDefinition.ioType.annotationClass; } + @Override protected String getDoc() { return escape(argumentDefinition.doc); } + @Override protected String getFullName() { return escape(argumentDefinition.fullName); } + @Override protected String getShortName() { return escape(argumentDefinition.shortName); } + @Override protected boolean isRequired() { return argumentDefinition.required; } + @Override protected String getExclusiveOf() { return escape(argumentDefinition.exclusiveOf); } + @Override protected String getValidation() { return escape(argumentDefinition.validation); } + + protected static final String REQUIRED_TEMPLATE = " + \" %1$s \" + %2$s.format(%3$s)"; + protected static final String REPEAT_TEMPLATE = " + repeat(\" %1$s \", %3$s, format=%2$s)"; + protected static final String OPTIONAL_TEMPLATE = " + optional(\" %1$s \", %3$s, format=%2$s)"; + protected static final String FLAG_TEMPLATE = " + (if (%3$s) \" %1$s \" else \"\")"; + + public final String getCommandLineAddition() { + return String.format(getCommandLineTemplate(), getCommandLineParam(), getCommandLineFormat(), getFieldName()); + } + + protected String getCommandLineParam() { + return (argumentDefinition.shortName != null) + ? "-" + argumentDefinition.shortName + : "--" + argumentDefinition.fullName; + } + + protected String getCommandLineFormat() { + return "\"%s\""; + } + + @Override + protected String getScatterGatherAnnotation() { + return ""; + } + + protected String getCommandLineTemplate() { + return isRequired() ? REQUIRED_TEMPLATE : OPTIONAL_TEMPLATE; + } + + public static List getArgumentFields(Class classType) { + List argumentFields = new ArrayList(); + for (ArgumentSource argumentSource: ParsingEngine.extractArgumentSources(classType)) + for (ArgumentDefinition argumentDefinition: argumentSource.createArgumentDefinitions()) + argumentFields.addAll(getArgumentFields(argumentDefinition)); + return argumentFields; + } + + private static final List intervalFields = Arrays.asList("intervals", "excludeIntervals", "targetIntervals"); + + private static List getArgumentFields(ArgumentDefinition argumentDefinition) { + if (intervalFields.contains(argumentDefinition.fullName) && argumentDefinition.ioType == ArgumentIOType.INPUT) { + boolean scatter = "intervals".equals(argumentDefinition.fullName); + return Arrays.asList( + new IntervalFileArgumentField(argumentDefinition, scatter), + new IntervalStringArgumentField(argumentDefinition)); + + // ROD Bindings are set by the RodBindField + } else if (RodBindField.ROD_BIND_FIELD.equals(argumentDefinition.fullName) && argumentDefinition.ioType == ArgumentIOType.INPUT) { + // TODO: Once everyone is using @Allows and @Requires correctly, we can stop blindly allowing Triplets + return Collections.singletonList(new RodBindArgumentField(argumentDefinition, argumentDefinition.required)); + //return Collections.emptyList(); + + } else if ("input_file".equals(argumentDefinition.fullName) && argumentDefinition.ioType == ArgumentIOType.INPUT) { + return Arrays.asList(new InputArgumentField(argumentDefinition), new IndexFilesField()); + + } else if (argumentDefinition.ioType == ArgumentIOType.INPUT) { + return Collections.singletonList(new InputArgumentField(argumentDefinition)); + + } else if (argumentDefinition.ioType == ArgumentIOType.OUTPUT) { + return Collections.singletonList(new OutputArgumentField(argumentDefinition)); + + } else if (argumentDefinition.isFlag) { + return Collections.singletonList(new FlagArgumentField(argumentDefinition)); + + } else if (argumentDefinition.isMultiValued) { + return Collections.singletonList(new MultiValuedArgumentField(argumentDefinition)); + + } else if (!argumentDefinition.required && useOption(argumentDefinition.argumentType)) { + boolean useFormat = useFormatter(argumentDefinition.argumentType); + List fields = new ArrayList(); + ArgumentField field = new OptionedArgumentField(argumentDefinition, useFormat); + fields.add(field); + if (useFormat) fields.add(new FormatterArgumentField(field)); + return fields; + + } else { + boolean useFormat = useFormatter(argumentDefinition.argumentType); + List fields = new ArrayList(); + ArgumentField field = new DefaultArgumentField(argumentDefinition, useFormat); + fields.add(field); + if (useFormat) fields.add(new FormatterArgumentField(field)); + return fields; + + } + } + + // if (intervalFields.contains(argumentDefinition.fullName) && argumentDefinition.ioType == ArgumentIOType.INPUT) + // Change intervals to an input file, and optionally scatter it. + private static class IntervalFileArgumentField extends InputArgumentField { + private final boolean scatter; + public IntervalFileArgumentField(ArgumentDefinition argumentDefinition, boolean scatter) { + super(argumentDefinition); + this.scatter = scatter; + } + + @Override protected boolean isMultiValued() { return !this.scatter && super.isMultiValued(); } + @Override public boolean isScatter() { return this.scatter; } + @Override protected String getScatterGatherAnnotation() { + return scatter ? String.format("@Scatter(classOf[IntervalScatterFunction])%n") : super.getScatterGatherAnnotation(); + } + + @Override + protected String getExclusiveOf() { + StringBuilder exclusiveOf = new StringBuilder(super.getExclusiveOf()); + if (exclusiveOf.length() > 0) + exclusiveOf.append(","); + exclusiveOf.append(escape(argumentDefinition.fullName)).append("String"); + return exclusiveOf.toString(); + } + } + + // if (intervalFields.contains(argumentDefinition.fullName) && argumentDefinition.ioType == ArgumentIOType.INPUT) + // Change intervals to a string but as an argument. + private static class IntervalStringArgumentField extends ArgumentDefinitionField { + public IntervalStringArgumentField(ArgumentDefinition argumentDefinition) { + super(argumentDefinition); + } + + @SuppressWarnings("unchecked") + @Override protected Class getAnnotationIOClass() { return Argument.class; } + @Override protected Class getInnerType() { return String.class; } + @Override protected String getRawFieldName() { return super.getRawFieldName() + "String"; } + @Override protected String getFullName() { return super.getFullName() + "String"; } + @Override protected String getFieldType() { return "List[String]"; } + @Override protected String getDefaultValue() { return "Nil"; } + @Override public String getCommandLineTemplate() { return REPEAT_TEMPLATE; } + + @Override + protected String getExclusiveOf() { + StringBuilder exclusiveOf = new StringBuilder(super.getExclusiveOf()); + if (exclusiveOf.length() > 0) + exclusiveOf.append(","); + exclusiveOf.append(escape(argumentDefinition.fullName)); + return exclusiveOf.toString(); + } + } + + // if (argumentDefinition.ioType == ArgumentIOType.INPUT) + // Map all inputs to files. Handles multi valued files. + private static class InputArgumentField extends ArgumentDefinitionField { + public InputArgumentField(ArgumentDefinition argumentDefinition) { + super(argumentDefinition); + } + + @Override protected Class getInnerType() { return File.class; } + @Override protected String getFieldType() { return String.format(isMultiValued() ? "List[%s]" : "%s", getRawFieldType()); } + @Override protected String getDefaultValue() { return isMultiValued() ? "Nil" : "_"; } + @Override protected String getCommandLineTemplate() { + return isMultiValued() ? REPEAT_TEMPLATE : super.getCommandLineTemplate(); + } + + protected String getRawFieldType() { return "File"; } + protected boolean isMultiValued() { return argumentDefinition.isMultiValued; } + } + + // if (argumentDefinition.ioType == ArgumentIOType.OUTPUT) + // Map all outputs to files. + private static class OutputArgumentField extends ArgumentDefinitionField { + public OutputArgumentField(ArgumentDefinition argumentDefinition) { + super(argumentDefinition); + } + + @Override protected Class getInnerType() { return File.class; } + @Override protected String getFieldType() { return "File"; } + @Override protected String getDefaultValue() { return "_"; } + + @Override public boolean isGather() { return true; } + @Override protected String getScatterGatherAnnotation() { + return String.format(SAMFileWriter.class.isAssignableFrom(argumentDefinition.argumentType) + ? "@Gather(classOf[BamGatherFunction])%n" + : "@Gather(classOf[org.broadinstitute.sting.queue.function.scattergather.SimpleTextGatherFunction])%n"); + } + } + + // if (argumentDefinition.isFlag) + // Booleans should be set on the commandline only if they are true. + private static class FlagArgumentField extends ArgumentDefinitionField { + public FlagArgumentField(ArgumentDefinition argumentDefinition) { + super(argumentDefinition); + } + + @Override protected Class getInnerType() { return boolean.class; } + @Override protected String getFieldType() { return "Boolean"; } + @Override protected String getDefaultValue() { return "_"; } + @Override protected String getCommandLineTemplate() { return FLAG_TEMPLATE; } + } + + // if (argumentDefinition.isMultiValued) + // Multi value arguments are mapped to List[] and use repeat. + private static class MultiValuedArgumentField extends ArgumentDefinitionField { + public MultiValuedArgumentField(ArgumentDefinition argumentDefinition) { + super(argumentDefinition); + } + + @Override protected Class getInnerType() { return mapType(argumentDefinition.componentType); } + @Override protected String getFieldType() { return String.format("List[%s]", getType(getInnerType())); } + @Override protected String getDefaultValue() { return "Nil"; } + @Override protected String getCommandLineTemplate() { return REPEAT_TEMPLATE; } + } + + // if (!argumentDefinition.required && useOption(argumentDefinition.argumentType)) + // Any optional arguments that are primitives / enums are wrapped in options. + private static class OptionedArgumentField extends ArgumentDefinitionField { + private final boolean useFormatter; + + public OptionedArgumentField(ArgumentDefinition argumentDefinition, boolean useFormatter) { + super(argumentDefinition); + this.useFormatter = useFormatter; + } + + @Override protected Class getInnerType() { return mapType(argumentDefinition.argumentType); } + @Override protected String getFieldType() { return String.format("Option[%s]", getType(getInnerType())); } + @Override protected String getDefaultValue() { return "None"; } + @Override protected String getCommandLineTemplate() { return OPTIONAL_TEMPLATE; } + @Override protected String getCommandLineFormat() { + return this.useFormatter ? getFieldName(this.getRawFieldName() + "Format") : super.getCommandLineFormat(); + } + } + + // Any other @Arguments + private static class DefaultArgumentField extends ArgumentDefinitionField { + private final boolean useFormatter; + + public DefaultArgumentField(ArgumentDefinition argumentDefinition, boolean useFormatter) { + super(argumentDefinition); + this.useFormatter = useFormatter; + } + + @Override protected Class getInnerType() { return mapType(argumentDefinition.argumentType); } + @Override protected String getFieldType() { return getType(getInnerType()); } + @Override protected String getDefaultValue() { return "_"; } + @Override protected String getCommandLineFormat() { + return this.useFormatter ? getFieldName(this.getRawFieldName() + "Format") : super.getCommandLineFormat(); + } + } + + /** + * The other extreme of a NamedRodBindingField, allows the user to specify the track name, track type, and the file. + */ + public static class RodBindArgumentField extends InputArgumentField { + private boolean isRequired; + public RodBindArgumentField(ArgumentDefinition argumentDefinition, boolean isRequired) { + super(argumentDefinition); + this.isRequired = isRequired; + } + + @Override protected boolean isRequired() { return this.isRequired; } + @Override protected String getRawFieldType() { return "RodBind"; } + } + + /** + * Adds optional inputs for the indexes of any bams or sams added to this function. + */ + private static class IndexFilesField extends ArgumentField { + @Override protected Class getAnnotationIOClass() { return Input.class; } + @Override public String getCommandLineAddition() { return ""; } + @Override protected String getDoc() { return "Dependencies on any index files for any bams or sams added to input_files"; } + @Override protected String getFullName() { return "index_files"; } + @Override protected boolean isRequired() { return false; } + @Override protected String getFieldType() { return "List[File]"; } + @Override protected String getDefaultValue() { return "Nil"; } + @Override protected Class getInnerType() { return File.class; } + @Override protected String getRawFieldName() { return "index_files"; } + @Override protected String getFreezeFields() { + return String.format( + "index_files ++= input_file.filter(bam => bam != null && bam.getName.endsWith(\".bam\")).map(bam => new File(bam.getPath + \".bai\"))%n" + + "index_files ++= input_file.filter(sam => sam != null && sam.getName.endsWith(\".sam\")).map(sam => new File(sam.getPath + \".sai\"))%n"); + } + } + + private static class FormatterArgumentField extends ArgumentField { + private final ArgumentField argumentField; + public FormatterArgumentField(ArgumentField argumentField) { + this.argumentField = argumentField; + } + @Override protected Class getAnnotationIOClass() { return Argument.class; } + @Override public String getCommandLineAddition() { return ""; } + @Override protected String getDoc() { return "Format string for " + this.argumentField.getFullName(); } + @Override protected String getFullName() { return this.argumentField.getFullName() + "Format"; } + @Override protected boolean isRequired() { return false; } + @Override protected String getFieldType() { return "String"; } + @Override protected String getDefaultValue() { return "\"%s\""; } + @Override protected Class getInnerType() { return String.class; } + @Override protected String getRawFieldName() { return this.argumentField.getRawFieldName() + "Format"; } + } +} diff --git a/java/src/org/broadinstitute/sting/queue/extensions/gatk/ArgumentField.java b/java/src/org/broadinstitute/sting/queue/extensions/gatk/ArgumentField.java new file mode 100644 index 000000000..ef7f6f729 --- /dev/null +++ b/java/src/org/broadinstitute/sting/queue/extensions/gatk/ArgumentField.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2010, 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. + */ + +package org.broadinstitute.sting.queue.extensions.gatk; + +import net.sf.samtools.SAMFileReader; +import net.sf.samtools.SAMFileWriter; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.broadinstitute.sting.gatk.filters.PlatformUnitFilterHelper; +import org.broadinstitute.sting.utils.genotype.GenotypeWriter; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.util.*; + +public abstract class ArgumentField { + + public Collection getImportStatements() { + List imports = new ArrayList(); + for (Class importClass: getImportClasses()) { + if (!isBuiltIn(importClass)) + imports.add("import " + importClass.getName().replace("$", ".")); + } + return imports; + } + + /** + * Returns true if a class is built in and doesn't need to be imported. + * @param argType The class to check. + * @return true if the class is built in and doesn't need to be imported + */ + private static boolean isBuiltIn(Class argType) { + return argType.isPrimitive() || argType == String.class || Number.class.isAssignableFrom(argType); + } + + /** @return Scala code defining the argument and it's annotation. */ + public final String getArgumentAddition() { + return String.format("%n" + + "/** %s */%n" + + "@%s(fullName=\"%s\", shortName=\"%s\", doc=\"%s\", required=%s, exclusiveOf=\"%s\", validation=\"%s\")%n" + + "%svar %s: %s = %s%n", + getDoc(), + getAnnotationIOClass().getSimpleName(), + getFullName(), + getShortName(), + getDoc(), + isRequired(), + getExclusiveOf(), + getValidation(), + getScatterGatherAnnotation(), getFieldName(), getFieldType(), getDefaultValue()); + } + + /** @return Scala code to append to the command line. */ + public abstract String getCommandLineAddition(); + + // Argument Annotation + + /** @return Documentation for the annotation. */ + protected abstract String getDoc(); + + /** @return Annotation class of the annotation. */ + protected abstract Class getAnnotationIOClass(); + + /** @return Full name for the annotation. */ + protected abstract String getFullName(); + + /** @return Short name for the annotation or "". */ + protected String getShortName() { return ""; } + + /** @return true if the argument is required. */ + protected abstract boolean isRequired(); + + /** @return A comma separated list of arguments that may be substituted for this field. */ + protected String getExclusiveOf() { return ""; } + + /** @return A validation string for the argument. */ + protected String getValidation() { return ""; } + + /** @return A scatter or gather annotation with a line feed, or "". */ + protected String getScatterGatherAnnotation() { return ""; } + + // Scala + + /** @return The scala field type. */ + protected abstract String getFieldType(); + + /** @return The scala default value. */ + protected abstract String getDefaultValue(); + + /** @return The class of the field, or the component type if the scala field is a collection. */ + protected abstract Class getInnerType(); + + /** @return A custom command for overriding freeze. */ + protected String getFreezeFields() { return ""; } + + @SuppressWarnings("unchecked") + protected Collection> getImportClasses() { + return Arrays.asList(this.getInnerType(), getAnnotationIOClass()); + } + + /** @return True if this field uses @Scatter. */ + public boolean isScatter() { return false; } + + /** @return True if this field uses @Gather. */ + public boolean isGather() { return false; } + + /** @return The raw field name, which will be checked against scala build in types. */ + protected abstract String getRawFieldName(); + /** @return The field name checked against reserved words. */ + protected final String getFieldName() { + return getFieldName(this.getRawFieldName()); + } + + /** + * @param rawFieldName The raw field name + * @return The field name checked against reserved words. + */ + protected static String getFieldName(String rawFieldName) { + String fieldName = rawFieldName; + if (!StringUtils.isAlpha(fieldName.substring(0,1))) + fieldName = "_" + fieldName; + if (isReserved(fieldName) || fieldName.contains("-")) + fieldName = "`" + fieldName + "`"; + return fieldName; + } + + /** via http://www.scala-lang.org/sites/default/files/linuxsoft_archives/docu/files/ScalaReference.pdf */ + private static final List reservedWords = Arrays.asList( + "abstract", "case", "catch", "class", "def", + "do", "else", "extends", "false", "final", + "finally", "for", "forSome", "if", "implicit", + "import", "lazy", "match", "new", "null", + "object", "override", "package", "private", "protected", + "return", "sealed", "super", "this", "throw", + "trait", "try", "true", "type", "val", + "var", "while", "with", "yield"); + + protected static boolean isReserved(String word) { + return reservedWords.contains(word); + } + + /** + * On primitive types returns the capitalized scala type. + * @param argType The class to check for options. + * @return the simple name of the class. + */ + protected static String getType(Class argType) { + String type = argType.getSimpleName(); + + if (argType.isPrimitive()) + type = StringUtils.capitalize(type); + + if ("Integer".equals(type)) + type = "Int"; + + return type; + } + + protected static String escape(String string) { + return (string == null) ? "" : StringEscapeUtils.escapeJava(string); + } + + /** + * @param argType The class to check for options. + * @return true if option should be used. + */ + protected static boolean useOption(Class argType) { + return (argType.isPrimitive()) || (Number.class.isAssignableFrom(argType)) || (argType.isEnum()); + } + + /** + * @param argType The class to check for options. + * @return true if option should be used. + */ + protected static boolean useFormatter(Class argType) { + return (argType.equals(Double.class) || argType.equals(Double.TYPE) || + argType.equals(Float.class) || argType.equals(Float.TYPE)); + } + + // TODO: Use an annotation, type descriptor, anything but hardcoding these lists! + + protected static Class mapType(Class clazz) { + if (InputStream.class.isAssignableFrom(clazz)) return File.class; + if (SAMFileReader.class.isAssignableFrom(clazz)) return File.class; + if (OutputStream.class.isAssignableFrom(clazz)) return File.class; + if (GenotypeWriter.class.isAssignableFrom(clazz)) return File.class; + if (SAMFileWriter.class.isAssignableFrom(clazz)) return File.class; + if (PlatformUnitFilterHelper.class.isAssignableFrom(clazz)) return String.class; + return clazz; + } +} diff --git a/java/src/org/broadinstitute/sting/queue/extensions/gatk/CommandLineProgramManager.java b/java/src/org/broadinstitute/sting/queue/extensions/gatk/CommandLineProgramManager.java new file mode 100644 index 000000000..cefca44da --- /dev/null +++ b/java/src/org/broadinstitute/sting/queue/extensions/gatk/CommandLineProgramManager.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010, 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. + */ + +package org.broadinstitute.sting.queue.extensions.gatk; + +import org.broadinstitute.sting.commandline.CommandLineProgram; +import org.broadinstitute.sting.utils.classloader.PluginManager; + +import java.util.Collection; + +/** + * Finds all command line programs. + */ +public class CommandLineProgramManager extends PluginManager { + public CommandLineProgramManager() { + super(CommandLineProgram.class, "CommandLineProgram", "CLP"); + } + + public Collection> getValues() { + return this.pluginsByName.values(); + } +} diff --git a/java/src/org/broadinstitute/sting/queue/extensions/gatk/GATKExtensionsGenerator.java b/java/src/org/broadinstitute/sting/queue/extensions/gatk/GATKExtensionsGenerator.java new file mode 100644 index 000000000..207da8a1f --- /dev/null +++ b/java/src/org/broadinstitute/sting/queue/extensions/gatk/GATKExtensionsGenerator.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2010, 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. + */ + +package org.broadinstitute.sting.queue.extensions.gatk; + +import net.sf.picard.filter.SamRecordFilter; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.broadinstitute.sting.commandline.*; +import org.broadinstitute.sting.gatk.CommandLineGATK; +import org.broadinstitute.sting.gatk.GenomeAnalysisEngine; +import org.broadinstitute.sting.gatk.WalkerManager; +import org.broadinstitute.sting.gatk.filters.FilterManager; +import org.broadinstitute.sting.gatk.io.stubs.GenotypeWriterArgumentTypeDescriptor; +import org.broadinstitute.sting.gatk.io.stubs.OutputStreamArgumentTypeDescriptor; +import org.broadinstitute.sting.gatk.io.stubs.SAMFileReaderArgumentTypeDescriptor; +import org.broadinstitute.sting.gatk.io.stubs.SAMFileWriterArgumentTypeDescriptor; +import org.broadinstitute.sting.gatk.refdata.tracks.RMDTrackManager; +import org.broadinstitute.sting.gatk.walkers.Walker; +import org.broadinstitute.sting.utils.StingException; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.Map.Entry; + +/** + * Generates Queue modules that can be used to run GATK walkers. + * + * ArgumentCollections are flattened into a single module. + */ +public class GATKExtensionsGenerator extends CommandLineProgram { + private static final Logger logger = Logger.getRootLogger(); + public static final String GATK_EXTENSIONS_PACKAGE_NAME = "org.broadinstitute.sting.queue.extensions.gatk"; + private static final String COMMANDLINE_PACKAGE_NAME = GATK_EXTENSIONS_PACKAGE_NAME; + private static final String FILTER_PACKAGE_NAME = GATK_EXTENSIONS_PACKAGE_NAME; + private static final String WALKER_PACKAGE_NAME = GATK_EXTENSIONS_PACKAGE_NAME; + + @Output(fullName="output_directory", shortName="outDir", doc="Directory to output the generated scala", required=true) + public File outputDirectory; + + CommandLineProgramManager clpManager = new CommandLineProgramManager(); + GenomeAnalysisEngine GATKEngine = new GenomeAnalysisEngine(); + WalkerManager walkerManager = new WalkerManager(); + FilterManager filterManager = new FilterManager(); + RMDTrackManager rmdTrackManager = new RMDTrackManager(); + + /** + * Required main method implementation. + * @param argv Command-line arguments. + */ + public static void main(String[] argv) { + try { + start(new GATKExtensionsGenerator(), argv); + System.exit(CommandLineProgram.result); + } catch (Exception e) { + exitSystemWithError(e); + } + } + + @Override + protected Collection getArgumentTypeDescriptors() { + List typeDescriptors = new ArrayList(); + typeDescriptors.add(new GenotypeWriterArgumentTypeDescriptor(GATKEngine)); + typeDescriptors.add(new SAMFileReaderArgumentTypeDescriptor(GATKEngine)); + typeDescriptors.add(new SAMFileWriterArgumentTypeDescriptor(GATKEngine)); + typeDescriptors.add(new OutputStreamArgumentTypeDescriptor(GATKEngine)); + return typeDescriptors; + } + + @Override + protected int execute() { + try { + if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) + throw new StingException("Unable to create output directory: " + outputDirectory); + + for (Class clp: clpManager.getValues()) { + + if (!isGatkProgram(clp)) + continue; + + String clpClassName = clpManager.getName(clp); + + writeClass("org.broadinstitute.sting.queue.function.JarCommandLineFunction", COMMANDLINE_PACKAGE_NAME, clpClassName, + "", ArgumentDefinitionField.getArgumentFields(clp)); + + if (clp == CommandLineGATK.class) { + for (Entry>> walkersByPackage: walkerManager.getWalkerNamesByPackage(false).entrySet()) { + for(Class walkerType: walkersByPackage.getValue()) { + String walkerName = walkerManager.getName(walkerType); + List argumentFields = new ArrayList(); + + argumentFields.addAll(ArgumentDefinitionField.getArgumentFields(walkerType)); + argumentFields.addAll(RodBindField.getRodArguments(walkerType, rmdTrackManager)); + argumentFields.addAll(ReadFilterField.getFilterArguments(walkerType)); + + writeClass(COMMANDLINE_PACKAGE_NAME + "." + clpClassName, WALKER_PACKAGE_NAME, + walkerName, String.format("analysis_type = \"%s\"%n%n", walkerName), argumentFields); + } + } + } + } + + for (Class filter: filterManager.getValues()) { + String filterName = filterManager.getName(filter); + writeFilter(FILTER_PACKAGE_NAME, filterName, ArgumentDefinitionField.getArgumentFields(filter)); + } + + return 0; + } catch (IOException exception) { + logger.error("Error generating queue output.", exception); + return 1; + } + } + + private static final List gatkPackages = Arrays.asList( + "org.broadinstitute.sting.gatk", + "org.broadinstitute.sting.analyzecovariates"); + private boolean isGatkProgram(Class clazz) { + if (clazz.getPackage() == null) + return false; + String classPackage = clazz.getPackage().getName(); + for (String gatkPackage : gatkPackages) + if (classPackage.startsWith(gatkPackage)) + return true; + return false; + } + + private void writeClass(String baseClass, String packageName, String className, String constructor, + List argumentFields) throws IOException { + String content = getContent(CLASS_TEMPLATE, baseClass, packageName, className, constructor, "", argumentFields); + writeFile(packageName + "." + className, content); + } + + private void writeFilter(String packageName, String className, List argumentFields) throws IOException { + String content = getContent(TRAIT_TEMPLATE, "org.broadinstitute.sting.queue.function.CommandLineFunction", + packageName, className, "", String.format(" + \" -read_filter %s\"", className), argumentFields); + writeFile(packageName + "." + className, content); + } + + private void writeFile(String fullClassName, String content) throws IOException { + File outputFile = new File(outputDirectory, fullClassName.replace(".", "/") + ".scala"); + if (outputFile.exists()) { + String existingContent = FileUtils.readFileToString(outputFile); + if (StringUtils.equals(content, existingContent)) + return; + } + FileUtils.writeStringToFile(outputFile, content); + } + + private static String getContent(String scalaTemplate, String baseClass, String packageName, String className, + String constructor, String commandLinePrefix, List argumentFields) { + StringBuilder arguments = new StringBuilder(); + StringBuilder commandLine = new StringBuilder(commandLinePrefix); + + Set importSet = new HashSet(); + boolean isScatter = false; + boolean isGather = false; + List freezeFields = new ArrayList(); + for(ArgumentField argumentField: argumentFields) { + arguments.append(argumentField.getArgumentAddition()); + commandLine.append(argumentField.getCommandLineAddition()); + importSet.addAll(argumentField.getImportStatements()); + freezeFields.add(argumentField.getFreezeFields()); + + isScatter |= argumentField.isScatter(); + isGather |= argumentField.isGather(); + } + + if (isScatter) { + importSet.add("import org.broadinstitute.sting.queue.function.scattergather.ScatterGatherableFunction"); + importSet.add("import org.broadinstitute.sting.queue.function.scattergather.Scatter"); + baseClass += " with ScatterGatherableFunction"; + } + if (isGather) + importSet.add("import org.broadinstitute.sting.queue.function.scattergather.Gather"); + + // Sort the imports so that the are always in the same order. + List sortedImports = new ArrayList(importSet); + Collections.sort(sortedImports); + + StringBuffer freezeFieldOverride = new StringBuffer(); + for (String freezeField: freezeFields) + freezeFieldOverride.append(freezeField); + if (freezeFieldOverride.length() > 0) { + freezeFieldOverride.insert(0, String.format("override def freezeFieldValues = {%nsuper.freezeFieldValues%n")); + freezeFieldOverride.append(String.format("}%n%n")); + } + + // see CLASS_TEMPLATE and TRAIT_TEMPLATE below + return String.format(scalaTemplate, packageName, StringUtils.join(sortedImports, NEWLINE), + className, baseClass, constructor, arguments, freezeFieldOverride, commandLine); + } + + private static final String NEWLINE = String.format("%n"); + + private static final String CLASS_TEMPLATE = "package %s%n"+ + "%s%n" + + "class %s extends %s {%n" + + "%s%s%n" + + "%soverride def commandLine = super.commandLine%s%n" + + "}%n"; + + private static final String TRAIT_TEMPLATE = "package %s%n"+ + "%s%n" + + "trait %s extends %s {%n" + + "%s%s%n" + + "%sabstract override def commandLine = super.commandLine%s%n" + + "}%n"; +} diff --git a/java/src/org/broadinstitute/sting/queue/extensions/gatk/ReadFilterField.java b/java/src/org/broadinstitute/sting/queue/extensions/gatk/ReadFilterField.java new file mode 100644 index 000000000..23eacceae --- /dev/null +++ b/java/src/org/broadinstitute/sting/queue/extensions/gatk/ReadFilterField.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010, 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. + */ + +package org.broadinstitute.sting.queue.extensions.gatk; + +import net.sf.picard.filter.SamRecordFilter; +import org.broadinstitute.sting.gatk.WalkerManager; +import org.broadinstitute.sting.gatk.walkers.Walker; + +import java.util.ArrayList; +import java.util.List; + +public class ReadFilterField { + /** + * Adds an argument for each read filters listed on the walker. + * @param walkerClass the class of the walker + * @return the list of argument fields + */ + public static List getFilterArguments(Class walkerClass) { + List argumentFields = new ArrayList(); + for(Class filter: WalkerManager.getReadFilterTypes(walkerClass)) + argumentFields.addAll(ArgumentDefinitionField.getArgumentFields(filter)); + return argumentFields; + } +} diff --git a/java/src/org/broadinstitute/sting/queue/extensions/gatk/RodBindField.java b/java/src/org/broadinstitute/sting/queue/extensions/gatk/RodBindField.java new file mode 100644 index 000000000..7ae929b93 --- /dev/null +++ b/java/src/org/broadinstitute/sting/queue/extensions/gatk/RodBindField.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010, 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. + */ + +package org.broadinstitute.sting.queue.extensions.gatk; + +import org.broadinstitute.sting.commandline.Input; +import org.broadinstitute.sting.gatk.WalkerManager; +import org.broadinstitute.sting.gatk.refdata.tracks.RMDTrackManager; +import org.broadinstitute.sting.gatk.walkers.RMD; +import org.broadinstitute.sting.gatk.walkers.Walker; + +import java.io.File; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +/** + * Allows user to specify the rod file but locks in the track name and the track type. + */ +public class RodBindField extends ArgumentField { + public static final String ROD_BIND_FIELD = "rodBind"; + + private final String trackName; + private final String typeName; + private final List relatedFields; + private final boolean isRequired; + + public RodBindField(String trackName, String typeName, List relatedFields, boolean isRequired) { + this.trackName = trackName; + this.typeName = typeName; + this.relatedFields = relatedFields; + this.isRequired = isRequired; + } + + @SuppressWarnings("unchecked") + @Override protected Class getAnnotationIOClass() { return Input.class; } + @Override protected Class getInnerType() { return File.class; } + @Override protected String getFullName() { return escape(getRawFieldName()); } + @Override protected String getFieldType() { return "File"; } + @Override protected String getDefaultValue() { return "_"; } + @Override protected String getRawFieldName() { return this.trackName + this.typeName; } + @Override protected String getDoc() { return escape(this.typeName + " " + this.trackName); } + @Override protected boolean isRequired() { return this.isRequired; } + + @Override public String getCommandLineAddition() { + return String.format(this.useOption() + ? " + optional(\" -B %s,%s,\", %s)" + : " + \" -B %s,%s,\" + %s", + this.trackName, this.typeName, getFieldName()); + } + + private boolean useOption() { + return !this.isRequired || (relatedFields.size() > 1); + } + + @Override protected String getExclusiveOf() { + StringBuilder exclusiveOf = new StringBuilder(); + // TODO: Stop allowing the generic "rodBind" triplets to satisfy the requirement after @Requires are fixed. + if (this.isRequired) + exclusiveOf.append(ROD_BIND_FIELD); + for (RodBindField relatedField: relatedFields) + if (relatedField != this) { + if (exclusiveOf.length() > 0) + exclusiveOf.append(","); + exclusiveOf.append(relatedField.getFieldName()); + } + return exclusiveOf.toString(); + } + + public static List getRodArguments(Class walkerClass, RMDTrackManager rmdTrackManager) { + List argumentFields = new ArrayList(); + + List requires = WalkerManager.getRequiredMetaData(walkerClass); + List allows = WalkerManager.getAllowsMetaData(walkerClass); + + for (RMD required: requires) { + List fields = new ArrayList(); + String trackName = required.name(); + if ("*".equals(trackName)) { + // TODO: Add the field triplet for name=* after @Allows and @Requires are fixed on walkers + //fields.add(new RodBindArgumentField(argumentDefinition, true)); + } else { + for (String typeName: rmdTrackManager.getTrackRecordTypeNames(required.type())) + fields.add(new RodBindField(trackName, typeName, fields, true)); + } + argumentFields.addAll(fields); + } + + for (RMD allowed: allows) { + List fields = new ArrayList(); + String trackName = allowed.name(); + if ("*".equals(trackName)) { + // TODO: Add the field triplet for name=* after @Allows and @Requires are fixed on walkers + //fields.add(new RodBindArgumentField(argumentDefinition, false)); + } else { + for (String typeName: rmdTrackManager.getTrackRecordTypeNames(allowed.type())) + fields.add(new RodBindField(trackName, typeName, fields, true)); + } + argumentFields.addAll(fields); + } + + return argumentFields; + } +} diff --git a/scala/qscript/UnifiedGenotyperExample.scala b/scala/qscript/UnifiedGenotyperExample.scala new file mode 100644 index 000000000..714d4a4fd --- /dev/null +++ b/scala/qscript/UnifiedGenotyperExample.scala @@ -0,0 +1,63 @@ +import org.broadinstitute.sting.queue.extensions.gatk._ +import org.broadinstitute.sting.queue.QScript + +class UnifiedGenotyperExample extends QScript { + qscript => + + @Input(doc="gatk jar file") + var gatkJar: File = _ + + @Input(doc="bam files", shortName="I") + var bamFiles: List[File] = Nil + + @Input(doc="interval list", shortName="L") + var intervals: File = _ + + @Input(doc="referenceFile", shortName="R") + var referenceFile: File = _ + + @Argument(doc="filter names", shortName="filter") + var filterNames: List[String] = Nil + + @Argument(doc="filter expressions", shortName="filterExpression") + var filterExpressions: List[String] = Nil + + @Argument(doc="job queue", shortName="queue", required=false) + var jobQueue = "broad" + + trait UnifiedGenotyperArguments extends CommandLineGATK { + this.jobQueue = qscript.jobQueue + this.jarFile = qscript.gatkJar + this.intervals = qscript.intervals + this.reference_sequence = qscript.referenceFile + } + + def script = { + for (bam <- bamFiles) { + val ug = new UnifiedGenotyper with UnifiedGenotyperArguments + val vf = new VariantFiltration with UnifiedGenotyperArguments + val ve = new VariantEval with UnifiedGenotyperArguments + + val pr = new PrintReads with UnifiedGenotyperArguments + pr.input_file :+= bam + pr.outputBamFile = swapExt(bam, "bam", "new.bam") + pr.scatterCount = 2 + pr.setupGatherFunction = { case (f: BamGatherFunction, _) => f.jarFile = new File("/path/to/jar") } + add(pr) + + // Make sure the Sting/shell folder is in your path to use mergeText.sh and splitIntervals.sh. + ug.scatterCount = 3 + ug.input_file :+= bam + ug.out = swapExt(bam, "bam", "unfiltered.vcf") + + vf.rodBind :+= RodBind("vcf", "VCF", ug.out) + vf.out = swapExt(bam, "bam", "filtered.vcf") + + ve.rodBind :+= RodBind("vcf", "VCF", vf.out) + ve.out = swapExt(bam, "bam", "eval") + + //add(ug, vf, ve) + } + + } +} diff --git a/scala/qscript/depristo/1kg_table1.scala b/scala/qscript/depristo/1kg_table1.scala index 89c989678..9434137a1 100755 --- a/scala/qscript/depristo/1kg_table1.scala +++ b/scala/qscript/depristo/1kg_table1.scala @@ -1,8 +1,16 @@ -import org.broadinstitute.sting.queue.QScript._ -// Other imports can be added here +import org.broadinstitute.sting.gatk.contexts.variantcontext.VariantContextUtils.{GenotypeMergeType, VariantMergeType} +import org.broadinstitute.sting.playground.utils.report.VE2ReportFactory.VE2TemplateType +import org.broadinstitute.sting.queue.extensions.gatk._ +import org.broadinstitute.sting.queue.QScript -val UNIVERSAL_GATK_ARGS = " -l INFO " // -L 1 -val unusedArgs = setArgs(args) +class Onekg_table1 extends QScript { + @Argument(doc="stage") + var stage: String = _ + + @Argument(doc="gatkJarFile") + var gatkJarFile: File = _ + +trait UNIVERSAL_GATK_ARGS extends CommandLineGATK { logging_level = "INFO"; jarFile = gatkJarFile } // -L 1 class Target(project: String, snpVCF: String, indelVCF: String, calledGenome: Double, targetGenome: Double, pop: String, pilot : String, bam: String = null) { def reportFile: String = List(pop, pilot, "report").mkString(".") @@ -40,9 +48,9 @@ for ( (pop: String, called) <- p2Targets ) targets ::= new Target("SRP000032", "/humgen/gsa-hpprojects/1kg/releases/pilot_paper_calls/trio/snps/" + pop + ".trio.2010_03.genotypes.vcf.gz", "v1/dindel-v2/"+pop+".trio.2010_06.indel.genotypes.vcf", called, 2.85e9, pop, "pilot2") // pilot 3 -for (POP <- List("CEU", "CHB", "CHD", "JPT", "LWK", "TSI", "YRI")) { - val indels = if ( POP != "LWK" ) "/humgen/gsa-hpprojects/1kg/releases/pilot_paper_calls/exon/indel/"+POP+".exon.2010_06.genotypes.vcf.gz" else null - targets ::= new Target("SRP000033", "/humgen/gsa-hpprojects/1kg/releases/pilot_paper_calls/exon/snps/" + POP + ".exon.2010_03.genotypes.vcf.gz", indels, 1.43e6, 1.43e6, POP, "pilot3", "/humgen/gsa-hpprojects/1kg/1kg_pilot3/useTheseBamsForAnalysis/pilot3.%s.cleaned.bam".format(POP)) +for (pop <- List("CEU", "CHB", "CHD", "JPT", "LWK", "TSI", "YRI")) { + val indels = if ( pop != "LWK" ) "/humgen/gsa-hpprojects/1kg/releases/pilot_paper_calls/exon/indel/"+pop+".exon.2010_06.genotypes.vcf.gz" else null + targets ::= new Target("SRP000033", "/humgen/gsa-hpprojects/1kg/releases/pilot_paper_calls/exon/snps/" + pop + ".exon.2010_03.genotypes.vcf.gz", indels, 1.43e6, 1.43e6, pop, "pilot3", "/humgen/gsa-hpprojects/1kg/1kg_pilot3/useTheseBamsForAnalysis/pilot3.%s.cleaned.bam".format(pop)) } // merged files @@ -57,7 +65,7 @@ val INTERVALS = Map( "pilot3" -> "/humgen/gsa-hpprojects/1kg/1kg_pilot3/documents/CenterSpecificTargetLists/results/p3overlap.targets.b36.interval_list" ) -def setupStage(stage: String) = stage match { +def script = stage match { case "ALL" => // initial pilot1 merge -- autosomes + x for ( (pop: String,called) <- p1Targets ) { @@ -106,36 +114,36 @@ def setupStage(stage: String) = stage match { case _ => throw new Exception("Unknown stage" + stage) } -setupStage(unusedArgs(0)) - -// Populate parameters passed in via -P -setParams - -// Run the pipeline -run - // Using scala anonymous classes -class VariantEval(vcfIn: String, evalOut: String, vcfType: String = "VCF") extends GatkFunction { - @Input(doc="foo") var vcfFile: File = new File(vcfIn) - @Output(doc="foo") var evalFile: File = new File(evalOut) +class VariantEval(vcfIn: String, evalOut: String, vcfType: String = "VCF") extends org.broadinstitute.sting.queue.extensions.gatk.VariantEval with UNIVERSAL_GATK_ARGS { + val vcfFile = new File(vcfIn) + this.rodBind :+= RodBind("eval", vcfType, vcfFile) + this.out = new File(evalOut) + this.DBSNP = new File("/humgen/gsa-hpprojects/GATK/data/dbsnp_129_b36.rod") + this.reportType = Some(VE2TemplateType.Grep) + this.evalModule :+= "CompOverlap" + override def dotString = "VariantEval: " + vcfFile.getName - def commandLine = gatkCommandLine("VariantEval") + UNIVERSAL_GATK_ARGS + "-D /humgen/gsa-hpprojects/GATK/data/dbsnp_129_b36.rod -reportType Grep -B eval,%s,%s -o %s -E CompOverlap".format(vcfType, vcfFile, evalFile) } class StatPop(target: Target) extends CommandLineFunction { @Input(doc="foo") var snpVCF = new File(target.getSNPVCF) @Input(doc="foo") var snpEval = new File(target.getSNPEval) - @Input(doc="foo") var indelVCF = if (target.hasIndelVCF) new File(target.getIndelVCF) else {} + @Input(doc="foo", required=false) var indelVCF: File = if (target.hasIndelVCF) new File(target.getIndelVCF) else { null } @Output(doc="foo") var reportFile: File = new File(target.reportFile) override def dotString = "1kgStats: " + reportFile def commandLine = "python ~/dev/GenomeAnalysisTK/trunk/python/1kgStatsForCalls.py -v -a pilot_data.alignment.index -s pilot_data.sequence.index -r /broad/1KG/DCC/ftp/ -o " + target.reportFile + " " + target.extraArgs + (if (target.hasDOC) " -c " + target.getDOCSummaryFile else "") + " --snpsEval " + target.getSNPEval + (if (target.hasIndelVCF) " --indels " + target.getIndelVCF else "") } -class Combine(vcfsInArg: List[String], vcfOutPath: String) extends GatkFunction { - @Input(doc="foo") var vcfs = vcfsInArg.map((x: String) => new File(x)) - @Output(doc="foo") var vcfFile: File = new File(vcfOutPath) +class Combine(vcfsInArg: List[String], vcfOutPath: String) extends org.broadinstitute.sting.queue.extensions.gatk.CombineVariants with UNIVERSAL_GATK_ARGS { + val vcfs = vcfsInArg.map((x: String) => new File(x)) + val vcfFile = new File(vcfOutPath) + this.variantmergeoption = Some(VariantMergeType.UNION) + this.genotypemergeoption = Some(GenotypeMergeType.PRIORITIZE) + this.out = vcfFile + this.rodBind ++= vcfs.map( input => RodBind(input.getName,"VCF",input) ) + this.rod_priority_list = vcfs.map( _.getName ).mkString(",") override def dotString = "CombineVariants: " + vcfs.map(_.getName).mkString(",") + " => " + vcfFile.getName - def commandLine = gatkCommandLine("CombineVariants") + UNIVERSAL_GATK_ARGS + "-variantMergeOptions UNION -genotypeMergeOptions PRIORITIZE -o %s %s -priority %s".format(vcfFile, vcfs.map( input => " -B %s,VCF,%s".format(input.getName,input)).mkString(""), vcfs.map( _.getName ).mkString(",")) } class MaskStats(pop: String) extends CommandLineFunction { @@ -143,9 +151,19 @@ class MaskStats(pop: String) extends CommandLineFunction { def commandLine = "python ~/dev/GenomeAnalysisTK/trunk/python/maskStats.py masks/" + pop + ".mask.fa.gz -x MT -x Y -o " + outFile } -class DepthOfCoverage(bam: String, docOutPath: String, interval: String) extends GatkFunction { - @Input(doc="foo") var bamFile: File = new File(bam) - @Output(doc="foo") var docFile: File = new File(docOutPath) +class DepthOfCoverage(bam: String, docOutPath: String, interval: String) extends org.broadinstitute.sting.queue.extensions.gatk.DepthOfCoverage with UNIVERSAL_GATK_ARGS { + val bamFile = new File(bam) + this.omitIntervalStatistics = true + this.omitDepthOutputAtEachBase = true + this.minBaseQuality = Some(0) + this.minMappingQuality = Some(0) + this.out = new File(docOutPath) + this.input_file :+= bamFile + if (interval != null) { + this.intervalsString :+= interval + this.excludeIntervalsString ++= List("MT", "Y") + } + override def dotString = "DOC: " + bamFile.getName - def commandLine = gatkCommandLine("DepthOfCoverage") + UNIVERSAL_GATK_ARGS + "-omitIntervals -omitBaseOutput -mbq 0 -mmq 0 -o %s -I %s".format(docFile, bamFile) + (if (interval != null) " -XL MT -XL Y -L " + interval else "") +} } diff --git a/scala/qscript/fullCallingPipeline.q b/scala/qscript/fullCallingPipeline.q index cf09621bb..0e390911c 100755 --- a/scala/qscript/fullCallingPipeline.q +++ b/scala/qscript/fullCallingPipeline.q @@ -1,240 +1,61 @@ -import org.broadinstitute.sting.queue.function.scattergather.{ContigScatterFunction, FixMatesGatherFunction} +import org.broadinstitute.sting.gatk.DownsampleType +import org.broadinstitute.sting.gatk.walkers.genotyper.GenotypeCalculationModel.Model +import org.broadinstitute.sting.queue.extensions.gatk._ import org.broadinstitute.sting.queue.QScript -import org.broadinstitute.sting.queue.QScript._ -// Other imports can be added here -val unparsedArgs = setArgs(args) +class fullCallingPipeline extends QScript { + qscript => -// very slow-to-run fast-to-write parse args function. Only worth changing if using lots of flags with lots of lookups. + @Argument(doc="contigIntervals", shortName="contigIntervals") + var contigIntervals: File = _ -def parseArgs(flag: String): String = { - var retNext: Boolean = false - for ( f <- unparsedArgs ) { - if ( retNext ) { - return f - } else { - if ( f.equals(flag) ) { - retNext = true - } - } - } - return "None" -} + @Argument(doc="numContigs", shortName="numContigs") + var numContigs: Int = _ -///////////////////////////////////////////////// -// step one: we need to create a set of realigner targets, one for each bam file -///////////////////////////////////////////////// -// todo -- make me less of a hack that makes Khalid cry -abstract class GatkFunctionLocal extends GatkFunction { - if ( QScript.inputs("interval_list").size > 0 ) { - this.intervals = QScript.inputs("interval_list").head - } else { - this.intervals = QScript.inputs("interval.list").head - } -} + @Argument(doc="project", shortName="project") + var project: String = _ -class RealignerTargetCreator extends GatkFunctionLocal { - @Gather(classOf[SimpleTextGatherFunction]) - @Output(doc="Realigner targets") - var realignerIntervals: File = _ + @Input(doc="trigger", shortName="trigger", required=false) + var trigger: File = _ - def commandLine = gatkCommandLine("RealignerTargetCreator") + "-o %s".format(realignerIntervals) -} + @Input(doc="refseqTable", shortName="refseqTable") + var refseqTable: File = _ -///////////////////////////////////////////////// -// step two: we need to clean each bam file - gather will fix mates -///////////////////////////////////////////////// + @Input(doc="dbsnpTable", shortName="dbsnpTable") + var dbsnpTable: File = _ -class IndelRealigner extends GatkFunction { - @Input(doc="Intervals to clean") - var intervalsToClean: File = _ - @Scatter(classOf[ContigScatterFunction]) - @Input(doc="Contig intervals") - var contigIntervals: File = _ - @Gather(classOf[FixMatesGatherFunction]) - @Output(doc="Cleaned bam file") - var cleanedBam: File = _ + @Input(doc="Picard FixMateInformation.jar. At the Broad this can be found at /seq/software/picard/current/bin/FixMateInformation.jar. Outside the broad see http://picard.sourceforge.net/") + var picardFixMatesJar: File = _ - this.javaTmpDir = parseArgs("-tmpdir") // todo -- hack, move into script or something + @Input(doc="intervals") + var intervals: File = _ - override def freeze = { - this.intervals = contigIntervals - this.jobQueue = "long" - super.freeze - } + @Input(doc="bam files", shortName="I") + var bamFiles: List[File] = Nil - def commandLine = gatkCommandLine("IndelRealigner") + "--output %s -targetIntervals %s -L %s".format(cleanedBam,intervalsToClean,contigIntervals) -} + @Input(doc="gatk jar") + var gatkJar: File = _ -///////////////////////////////////////////////// -// step three: we need to call (multisample) over all bam files -///////////////////////////////////////////////// - -class UnifiedGenotyper extends GatkFunctionLocal { - @Input(doc="An optional trigger track (trigger emit will be set to 0)",required=false) - var trigger: File = _ - @Input(doc="A list of comparison files for annotation",required=false) - var compTracks: List[(String,File)] = Nil - @Input(doc="Calling confidence level (may change depending on depth and number of samples)") - var callConf: Int = _ - @Gather(classOf[SimpleTextGatherFunction]) - @Output(doc="raw vcf") - var rawVCF: File = _ - - // todo -- add input for comps, triggers, etc - - def commandLine = gatkCommandLine("UnifiedGenotyper") + "-G Standard -A MyHaplotypeScore -varout %s".format(rawVCF) + - " -stand_emit_conf 10 -mmq 20 -mbq 20 -dt EXPERIMENTAL_BY_SAMPLE -dcov 200" + - " -stand_call_conf %d".format(callConf) + - ( if (trigger == null ) "" else " -trig_call_conf %d -trig_emit_conf 0 -B trigger,VCF,%s".format(callConf,trigger) ) + - makeCompString - - def makeCompString = { - var S: String = "" - for ( tup <- compTracks ) { - S += " -B comp%s,VCF,%s".format(tup._1,tup._2) - } - S - } -} - -///////////////////////////////////////////////// -// step four: we need to call indels (multisample) over all bam files -///////////////////////////////////////////////// - -class UnifiedGenotyperIndels extends GatkFunctionLocal { - @Gather(classOf[SimpleTextGatherFunction]) - @Output(doc="indel vcf") - var indelVCF: File = _ - // todo -- add inputs for the indel genotyper - - def commandLine = gatkCommandLine("UnifiedGenotyper") + "-varout %s -gm INDELS".format(indelVCF) -} - -///////////////////////////////////////////////// -// step five: we need to filter variants on cluster and with indel mask -///////////////////////////////////////////////// -class VariantFiltration extends GatkFunctionLocal { - @Input(doc="A VCF file to filter") - var unfilteredVCF: File = _ - @Input(doc="An interval mask to use to filter indels") - var indelMask: File = _ - @Input(doc="Filter names",required=false) - var filterNames: List[String] = Nil - @Input(doc="Filter expressions",required=false) - var filterExpressions: List[String] = Nil - @Output(doc="The input VCF file, but filtered") - var filteredVCF: File = _ - // to do -- snp cluster args? - - def commandLine = gatkCommandLine("VariantFiltration") + "-B variant,VCF,%s -B mask,VCF,%s --maskName NearIndel --clusterWindowSize 20 --clusterSize 7 -o %s".format(unfilteredVCF,indelMask,filteredVCF) + - "%s%s".format(repeat(" -filterName ",filterNames), repeat(" -filterExpression ",filterExpressions)) -} - -///////////////////////////////////////////////// -// step six: we need to generate gaussian clusters with the optimizer -///////////////////////////////////////////////// -class GenerateVariantClusters extends GatkFunctionLocal { - @Input(doc="A VCF that has been filtered for clusters and indels") - var initialFilteredVCF: File = _ - @Output(doc="Variant cluster file generated from input VCF") - var clusterFile: File = _ - // todo -- args for annotations? - // todo -- args for resources (properties file) - - override def freeze = { - // todo -- hacky change in memory limit -- fix this when more official roads to do this are in place - this.memoryLimit = Some(8) - this.jobQueue = "hugemem" - super.freeze - } - - def commandLine = gatkCommandLine("GenerateVariantClusters") + "-an QD -an SB -an MyHaplotypeScore -an HRun " + - "-resources /humgen/gsa-scr1/chartl/sting/R -B input,VCF,%s -clusterFile %s".format(initialFilteredVCF,clusterFile) -} - -///////////////////////////////////////////////// -// step seven: we need to apply gaussian clusters to our variants -///////////////////////////////////////////////// -class ApplyGaussianClusters extends GatkFunctionLocal { - @Input(doc="A VCF file to which to apply clusters") - var inputVCF: File = _ - @Input(doc="A variant cluster file") - var clusterFile: File = _ - @Output(doc="A quality-score recalibrated VCF file") - var recalibratedVCF: File = _ - // todo -- inputs for Ti/Tv expectation and other things - - def commandLine = gatkCommandLine("VariantRecalibrator") + "--target_titv 2.1 -resources /humgen/gsa-scr1/chartl/sting/R " + - "-B input,VCF,%s -clusterFile %s -output %s".format(inputVCF,clusterFile,recalibratedVCF) -} - -///////////////////////////////////////////////// -// step eight: we need to make tranches out of the recalibrated qualities -///////////////////////////////////////////////// -class ApplyVariantCuts extends GatkFunctionLocal { - @Input(doc="A VCF file that has been recalibrated") - var recalibratedVCF: File = _ - @Output(doc="A VCF file that has had tranches marked") - var tranchedVCF: File = _ - @Output(doc="A tranch dat file") - var tranchFile: File = _ - // todo -- fdr inputs, etc - - def commandLine = gatkCommandLine("ApplyVariantCuts") + - "-B input,VCF,%s -outputVCF %s --tranchesFile %s --fdr_filter_level 10.0".format(recalibratedVCF,tranchedVCF,tranchFile) -} - -///////////////////////////////////////////////// -// step nine: we need to annotate variants using the annotator [or maf, for now] -///////////////////////////////////////////////// -class GenomicAnnotator extends GatkFunctionLocal { - @Input(doc="A VCF file to be annotated") - var inputVCF: File = _ - @Input(doc="Refseq input table to use with the annotator") - var refseqTable: File = _ - @Input(doc="Dbsnp input table to use with the annotator") - var dbsnpTable: File = _ - @Gather(classOf[SimpleTextGatherFunction]) - @Output(doc="A genomically annotated VCF file") - var annotatedVCF: File = _ - - def commandLine = gatkCommandLine("GenomicAnnotator") + " -B variant,VCF,%s -B refseq,AnnotatorInputTable,%s -B dbsnp,AnnotatorInputTable,%s -vcf %s -s dbsnp.name,dbsnp.refUCSC,dbsnp.strand,dbsnp.observed,dbsnp.avHet -BTI variant".format(inputVCF,refseqTable,dbsnpTable,annotatedVCF) -} - -///////////////////////////////////////////////// -// step ten: we need to evaluate variants with variant eval -///////////////////////////////////////////////// -class VariantEval extends GatkFunctionLocal { - @Input(doc="An optimized vcf file to evaluate") - var optimizedVCF: File = _ - @Input(doc="A hand-fitlered vcf file to evaluate") - var handFilteredVCF: File = _ - @Output(doc="An evaluation file") - var evalOutput: File = _ - // todo -- make comp tracks command-line arguments or properties - - def commandLine = gatkCommandLine("VariantEval") + "-B evalOptimized,VCF,%s -B evalHandFiltered,VCF,%s -E CountFunctionalClasses -E CompOverlap -E CountVariants -E TiTvVariantEvaluator -o %s".format(optimizedVCF,handFilteredVCF,evalOutput) +trait CommandLineGATKArgs extends CommandLineGATK { + this.intervals = qscript.intervals + this.jarFile = qscript.gatkJar } // ------------ SETUP THE PIPELINE ----------- // // todo -- the unclean and clean pipelines are the same, so the code can be condensed significantly + def script = { + val projectBase: String = qscript.project + val cleanedBase: String = projectBase + ".cleaned" + val uncleanedBase: String = projectBase + ".uncleaned" // there are commands that use all the bam files + var cleanBamFiles = List.empty[File] -val cleanSNPCalls = new UnifiedGenotyper -val uncleanSNPCalls = new UnifiedGenotyper -val cleanIndelCalls = new UnifiedGenotyperIndels -val uncleanIndelCalls = new UnifiedGenotyperIndels - -for ( bam <- inputs("bam") ) { +for ( bam <- bamFiles ) { // put unclean bams in unclean genotypers - uncleanSNPCalls.bamFiles :+= bam - uncleanIndelCalls.bamFiles :+= bam - // in advance, create the extension files val indel_targets = swapExt(bam,"bam","realigner_targets.interval_list") @@ -242,86 +63,129 @@ for ( bam <- inputs("bam") ) { // create the cleaning commands - val targetCreator = new RealignerTargetCreator - targetCreator.bamFiles :+= bam - targetCreator.realignerIntervals = indel_targets + val targetCreator = new RealignerTargetCreator with CommandLineGATKArgs + targetCreator.input_file :+= bam + targetCreator.out = indel_targets - val realigner = new IndelRealigner - realigner.bamFiles = targetCreator.bamFiles - realigner.contigIntervals = new File(parseArgs("-contigIntervals")) - realigner.intervalsToClean = targetCreator.realignerIntervals - realigner.scatterCount = parseArgs("-numContigs").toInt - realigner.cleanedBam = cleaned_bam + val realigner = new IndelRealigner with CommandLineGATKArgs + realigner.input_file = targetCreator.input_file + realigner.intervals = qscript.contigIntervals + //realigner.targetIntervals = targetCreator.out + realigner.targetIntervals = targetCreator.out.getAbsolutePath + realigner.scatterCount = qscript.numContigs + realigner.out = cleaned_bam + realigner.scatterClass = classOf[ContigScatterFunction] + realigner.setupGatherFunction = { case (f: BamGatherFunction, _) => f.jarFile = qscript.picardFixMatesJar } + realigner.jobQueue = "long" // put clean bams in clean genotypers - cleanSNPCalls.bamFiles :+= realigner.cleanedBam - cleanIndelCalls.bamFiles :+= realigner.cleanedBam + cleanBamFiles :+= realigner.out add(targetCreator,realigner) } + endToEnd(uncleanedBase,bamFiles) + endToEnd(cleanedBase,cleanBamFiles) + } -val projectBase: String = parseArgs("-project") -val cleanedBase: String = projectBase + ".cleaned" -val uncleanedBase: String = projectBase + ".uncleaned" - -def endToEnd(base: String, snps: UnifiedGenotyper, indels: UnifiedGenotyperIndels) = { +def endToEnd(base: String, bamFiles: List[File]) = { // step through the un-indel-cleaned graph: // 1a. call snps and indels - snps.rawVCF = new File(base+".vcf") - snps.callConf = 30 - snps.trigger = new File(parseArgs("-trigger")) + val snps = new UnifiedGenotyper with CommandLineGATKArgs + snps.input_file = bamFiles + snps.group :+= "Standard" + snps.annotation :+= "MyHamplotypeScore" + snps.variants_out = new File(base+".vcf") + snps.standard_min_confidence_threshold_for_emitting = Some(10) + snps.min_mapping_quality_score = Some(20) + snps.min_base_quality_score = Some(20) + snps.downsampling_type = Some(DownsampleType.EXPERIMENTAL_BY_SAMPLE) + snps.downsample_to_coverage = Some(200) + // todo -- add input for comps, triggers, etc + if (qscript.trigger != null) { + snps.trigger_min_confidence_threshold_for_calling = Some(30) + snps.rodBind :+= RodBind("trigger", "VCF", qscript.trigger) + } // todo -- hack -- get this from the command line, or properties - snps.compTracks :+= ( "comp1KG_CEU",new File("/humgen/gsa-hpprojects/GATK/data/Comparisons/Unvalidated/1kg_pilot1_projectCalls/CEU.low_coverage.2010_07.sites.hg18.vcf.gz") ) - snps.compTracks :+= ( "comp1KG_ALL",new File(parseArgs("-trigger") ) ) + snps.rodBind :+= RodBind( "comp1KG_CEU", "VCF", new File("/humgen/gsa-hpprojects/GATK/data/Comparisons/Unvalidated/1kg_pilot1_projectCalls/CEU.low_coverage.2010_07.sites.hg18.vcf.gz") ) + + + // TODO: what is the 1KG_ALL track? + //snps.rodBind :+= RodBind( "comp1KG_ALL", "VCF", qscript.trigger ) + + snps.scatterCount = 100 - indels.indelVCF = new File(base+".indels.vcf") + val indels = new UnifiedGenotyper with CommandLineGATKArgs + indels.input_file = bamFiles + indels.variants_out = new File(base+".indels.vcf") + indels.genotype_model = Some(Model.INDELS) indels.scatterCount = 100 + // todo -- add inputs for the indel genotyper // 1b. genomically annotate SNPs -- slow, but scatter it - val annotated = new GenomicAnnotator - annotated.inputVCF = snps.rawVCF - annotated.refseqTable = new File(parseArgs("-refseqTable")) - annotated.dbsnpTable = new File(parseArgs("-dbsnpTable")) - annotated.annotatedVCF = swapExt(snps.rawVCF,".vcf",".annotated.vcf") + val annotated = new GenomicAnnotator with CommandLineGATKArgs + annotated.rodBind :+= RodBind("variant", "VCF", snps.variants_out) + annotated.rodBind :+= RodBind("refseq", "AnnotatorInputTable", qscript.refseqTable) + annotated.rodBind :+= RodBind("dbsnp", "AnnotatorInputTable", qscript.dbsnpTable) + annotated.vcfOutput = swapExt(snps.variants_out,".vcf",".annotated.vcf") + annotated.select :+= "dbsnp.name,dbsnp.refUCSC,dbsnp.strand,dbsnp.observed,dbsnp.avHet" + annotated.rodToIntervalTrackName = "variant" annotated.scatterCount = 100 // 2.a filter on cluster and near indels - val masker = new VariantFiltration - masker.unfilteredVCF = annotated.annotatedVCF - masker.indelMask = indels.indelVCF - masker.filteredVCF = swapExt(annotated.annotatedVCF,".vcf",".indel.masked.vcf") + val masker = new VariantFiltration with CommandLineGATKArgs + masker.rodBind :+= RodBind("variant", "VCF", annotated.vcfOutput) + masker.rodBind :+= RodBind("mask", "VCF", indels.variants_out) + masker.maskName = "NearIndel" + masker.clusterWindowSize = Some(20) + masker.clusterSize = Some(7) + masker.out = swapExt(annotated.vcfOutput,".vcf",".indel.masked.vcf") + // todo -- snp cluster args? // 2.b hand filter with standard filter - val handFilter = new VariantFiltration - handFilter.unfilteredVCF = annotated.annotatedVCF - handFilter.indelMask = indels.indelVCF - handFilter.filterNames = List("StrandBias","AlleleBalance","QualByDepth","HomopolymerRun") - handFilter.filterExpressions = List("\"SB>=0.10\"","\"AB>=0.75\"","QD<5","\"HRun>=4\"") - handFilter.filteredVCF = swapExt(annotated.annotatedVCF,".vcf",".handfiltered.vcf") + val handFilter = new VariantFiltration with CommandLineGATKArgs + handFilter.rodBind :+= RodBind("variant", "VCF", annotated.vcfOutput) + handFilter.rodBind :+= RodBind("mask", "VCF", indels.variants_out) + handFilter.filterName ++= List("StrandBias","AlleleBalance","QualByDepth","HomopolymerRun") + handFilter.filterExpression ++= List("\"SB>=0.10\"","\"AB>=0.75\"","QD<5","\"HRun>=4\"") + handFilter.out = swapExt(annotated.vcfOutput,".vcf",".handfiltered.vcf") // 3.i generate gaussian clusters on the masked vcf - val clusters = new GenerateVariantClusters - clusters.initialFilteredVCF = masker.filteredVCF - clusters.clusterFile = swapExt(snps.rawVCF,".vcf",".cluster") + val clusters = new GenerateVariantClusters with CommandLineGATKArgs + clusters.rodBind :+= RodBind("input", "VCF", masker.out) + //clusters.clusterFile = swapExt(snps.variants_out,".vcf",".cluster") + val clusters_clusterFile = swapExt(snps.variants_out,".vcf",".cluster") + clusters.clusterFile = clusters_clusterFile.getAbsolutePath + clusters.memoryLimit = Some(8) + clusters.jobQueue = "hugemem" + // todo -- args for annotations? + // todo -- args for resources (properties file) + clusters.use_annotation ++= List("QD", "SB", "MyHaplotypeScore", "HRun") + clusters.path_to_resources = "/humgen/gsa-scr1/chartl/sting/R" // 3.ii apply gaussian clusters to the masked vcf - val recalibrate = new ApplyGaussianClusters + val recalibrate = new VariantRecalibrator with CommandLineGATKArgs recalibrate.clusterFile = clusters.clusterFile - recalibrate.inputVCF = masker.filteredVCF - recalibrate.recalibratedVCF = swapExt(masker.filteredVCF,".vcf",".optimized.vcf") + recalibrate.rodBind :+= RodBind("input", "VCF", masker.out) + recalibrate.out = swapExt(masker.out,".vcf",".optimized.vcf") + // todo -- inputs for Ti/Tv expectation and other things + recalibrate.target_titv = Some(2.1) // 3.iii apply variant cuts to the clusters - val cut = new ApplyVariantCuts - cut.recalibratedVCF = recalibrate.recalibratedVCF - cut.tranchedVCF = swapExt(recalibrate.recalibratedVCF,".vcf",".tranched.vcf") - cut.tranchFile = swapExt(recalibrate.recalibratedVCF,".vcf",".tranch") + val cut = new ApplyVariantCuts with CommandLineGATKArgs + cut.rodBind :+= RodBind("input", "VCF", recalibrate.out) + //cut.outputVCFFile = swapExt(recalibrate.out,".vcf",".tranched.vcf") + //cut.tranchesFile = swapExt(recalibrate.out,".vcf",".tranch") + val cut_outputVCFFile = swapExt(recalibrate.out,".vcf",".tranched.vcf") + val cut_tranchesFile = swapExt(recalibrate.out,".vcf",".tranch") + cut.outputVCFFile = cut_outputVCFFile.getAbsolutePath + cut.tranchesFile = cut_tranchesFile.getAbsolutePath + // todo -- fdr inputs, etc + cut.fdr_filter_level = Some(10) // 4. Variant eval the cut and the hand-filtered vcf files - val eval = new VariantEval - eval.optimizedVCF = cut.tranchedVCF - eval.handFilteredVCF = handFilter.filteredVCF - eval.evalOutput = new File(base+".eval") + val eval = new VariantEval with CommandLineGATKArgs + eval.rodBind :+= RodBind("evalOptimized", "VCF", cut_outputVCFFile) + eval.rodBind :+= RodBind("evalHandFiltered", "VCF", handFilter.out) + // todo -- make comp tracks command-line arguments or properties + eval.evalModule ++= List("CountFunctionalClasses", "CompOverlap", "CountVariants", "TiTvVariantEvaluator") + eval.out = new File(base+".eval") add(snps,indels,annotated,masker,handFilter,clusters,recalibrate,cut,eval) } -endToEnd(uncleanedBase,uncleanSNPCalls,uncleanIndelCalls) -endToEnd(cleanedBase,cleanSNPCalls,cleanIndelCalls) - -setParams -run +} diff --git a/scala/qscript/recalibrate.scala b/scala/qscript/recalibrate.scala index 539fc27ef..df4cb5f57 100755 --- a/scala/qscript/recalibrate.scala +++ b/scala/qscript/recalibrate.scala @@ -1,73 +1,77 @@ -import java.io.File -import org.broadinstitute.sting.queue.QScript._ +import org.broadinstitute.sting.queue.extensions.gatk._ +import org.broadinstitute.sting.queue.QScript import org.apache.commons.io.FilenameUtils; -// Other imports can be added here -val unusedArgs = setArgs(args) +class recalibrate extends QScript { + @Input(doc="bamIn", shortName="I") + var bamIns: List[File] = Nil + + @Argument(doc="scatter") + var scatter = false -def runPipeline(arg: String) = { - val scatter = arg == "scatter" + @Argument(doc="gatk jar file") + var gatkJarFile: File = _ - for (bamIn <- inputs(".bam")) { +def script = { + for (bamIn <- bamIns) { val root = bamIn.getPath() val bamRoot = FilenameUtils.removeExtension(root); val recalData = new File(bamRoot + ".recal_data.csv") val recalBam = new File(bamRoot + ".recal.bam") val recalRecalData = new File(bamRoot + ".recal.recal_data.csv") //add(new CountCovariates(root, recalData, "-OQ")) - val tableRecal = new TableRecalibrate(bamIn, recalData, recalBam, "-OQ") + val tableRecal = new TableRecalibrate(bamIn, recalData, recalBam) { useOriginalQualities = true } if ( scatter ) { tableRecal.intervals = new File("/humgen/gsa-hpprojects/GATK/data/chromosomes.hg18.interval_list") tableRecal.scatterCount = 25 } add(tableRecal) add(new Index(recalBam)) - add(new CountCovariates(recalBam, recalRecalData, "-nt 4")) + add(new CountCovariates(recalBam, recalRecalData) { num_threads = Some(4) }) add(new AnalyzeCovariates(recalData, new File(recalData.getPath() + ".analyzeCovariates"))) add(new AnalyzeCovariates(recalRecalData, new File(recalRecalData.getPath() + ".analyzeCovariates"))) } } -runPipeline(unusedArgs(0)) - -// Populate parameters passed in via -P -setParams - -// Run the pipeline -run - def bai(bam: File) = new File(bam + ".bai") -class Index(bamIn: File) extends GatkFunction { - @Input(doc="foo") var bam = bamIn - @Output(doc="foo") var bamIndex = bai(bamIn) - memoryLimit = Some(1) - override def dotString = "Index: %s".format(bamIn.getName) - def commandLine = "samtools index %s".format(bam) +class Index(bamIn: File) extends BamIndexFunction { + bamFile = bamIn } -class CountCovariates(bamIn: File, recalDataIn: File, args: String = "") extends GatkFunction { - @Input(doc="foo") var bam = bamIn - @Input(doc="foo") var bamIndex = bai(bamIn) - @Output(doc="foo") var recalData = recalDataIn - memoryLimit = Some(4) - override def dotString = "CountCovariates: %s [args %s]".format(bamIn.getName, args) - def commandLine = gatkCommandLine("CountCovariates") + args + " -l INFO -D /humgen/gsa-hpprojects/GATK/data/dbsnp_129_hg18.rod -I %s --max_reads_at_locus 20000 -cov ReadGroupCovariate -cov QualityScoreCovariate -cov CycleCovariate -cov DinucCovariate -recalFile %s".format(bam, recalData) +class CountCovariates(bamIn: File, recalDataIn: File) extends org.broadinstitute.sting.queue.extensions.gatk.CountCovariates { + this.jarFile = gatkJarFile + this.input_file :+= bamIn + this.recal_file = recalDataIn + this.DBSNP = new File("/humgen/gsa-hpprojects/GATK/data/dbsnp_129_hg18.rod") + this.logging_level = "INFO" + this.max_reads_at_locus = Some(20000) + this.covariate ++= List("ReadGroupCovariate", "QualityScoreCovariate", "CycleCovariate", "DinucCovariate") + this.memoryLimit = Some(4) + + override def dotString = "CountCovariates: %s [args %s]".format(bamIn.getName, if (this.num_threads.isDefined) "-nt " + this.num_threads else "") } -class TableRecalibrate(bamInArg: File, recalDataIn: File, bamOutArg: File, args: String = "") extends GatkFunction { - @Input(doc="foo") var bamIn = bamInArg - @Input(doc="foo") var recalData = recalDataIn - @Gather(classOf[BamGatherFunction]) - @Output(doc="foo") var bamOut = bamOutArg - override def dotString = "TableRecalibrate: %s => %s [args %s]".format(bamInArg.getName, bamOutArg.getName, args) - memoryLimit = Some(2) - def commandLine = gatkCommandLine("TableRecalibration") + args + " -l INFO -I %s -recalFile %s -outputBam %s".format(bamIn, recalData, bamOut) // bamOut.getPath()) +class TableRecalibrate(bamInArg: File, recalDataIn: File, bamOutArg: File) extends org.broadinstitute.sting.queue.extensions.gatk.TableRecalibration { + this.jarFile = gatkJarFile + this.input_file :+= bamInArg + this.recal_file = recalDataIn + this.output_bam = bamOutArg + this.logging_level = "INFO" + this.memoryLimit = Some(2) + + override def dotString = "TableRecalibrate: %s => %s".format(bamInArg.getName, bamOutArg.getName, if (this.useOriginalQualities) " -OQ" else "") } -class AnalyzeCovariates(recalDataIn: File, outputDir: File) extends GatkFunction { - @Input(doc="foo") var recalData = recalDataIn - memoryLimit = Some(4) +class AnalyzeCovariates(recalDataIn: File, outputDir: File) extends org.broadinstitute.sting.queue.extensions.gatk.AnalyzeCovariates { + this.jarFile = new File("/home/radon01/depristo/dev/GenomeAnalysisTK/trunk/dist/AnalyzeCovariates.jar") + this.recal_file = recalDataIn + this.output_dir = outputDir.toString + this.path_to_resources = "/home/radon01/depristo/dev/GenomeAnalysisTK/trunk/R/" + this.ignoreQ = Some(5) + this.path_to_Rscript = "/broad/tools/apps/R-2.6.0/bin/Rscript" + this.memoryLimit = Some(4) + override def dotString = "AnalyzeCovariates: %s".format(recalDataIn.getName) - def commandLine = "java -Xmx4g -jar /home/radon01/depristo/dev/GenomeAnalysisTK/trunk/dist/AnalyzeCovariates.jar -recalFile %s -outputDir %s -resources /home/radon01/depristo/dev/GenomeAnalysisTK/trunk/R/ -ignoreQ 5 -Rscript /broad/tools/apps/R-2.6.0/bin/Rscript".format(recalData, outputDir) +} } diff --git a/scala/qscript/rpoplin/variantRecalibrator.scala b/scala/qscript/rpoplin/variantRecalibrator.scala index 09e1e34f4..21a267465 100755 --- a/scala/qscript/rpoplin/variantRecalibrator.scala +++ b/scala/qscript/rpoplin/variantRecalibrator.scala @@ -1,7 +1,11 @@ -import org.broadinstitute.sting.queue.QScript._ -// Other imports can be added here +import org.broadinstitute.sting.queue.extensions.gatk._ +import org.broadinstitute.sting.queue.QScript -setArgs(args) +class variantRecalibrator extends QScript { + @Argument(doc="gatkJarFile") + var gatkJarFile: File = _ + + def script = { val gList = List(30) val sList = List(0.0001, 0.01) @@ -13,66 +17,40 @@ for (g: Int <- gList) { for (d: Double <- dList) { for(b: Double <- bList) { - // Using classes defined below + // Using classes defined by QueueGATKExtensions.jar val gvc = new GenerateVariantClusters val vr = new VariantRecalibrator - gvc.maxGaussians = g - gvc.shrinkage = s - gvc.dirichlet = d - gvc.clusterFile = new File("g%d_s%.6f_d%.6f_b%.2f.cluster".format(g,s,d,b)) - gvc.jobOutputFile = swapExt(gvc.clusterFile, ".cluster", ".gvc.out") + gvc.jarFile = gatkJarFile + gvc.rodBind :+= RodBind("input20", "VCF", new File("/broad/shptmp/rpoplin/CEUTSI.chr20.filtered.vcf")) + gvc.logging_level = "INFO" + gvc.intervalsString :+= "20" + gvc.use_annotation ++= List("QD", "SB", "HaplotypeScore", "HRun") + gvc.path_to_resources = "/humgen/gsa-scr1/rpoplin/sting_dev_vb/R/" + gvc.maxGaussians = Some(g) + gvc.shrinkage = Some(s) + gvc.shrinkageFormat = "%.6f" + gvc.dirichlet = Some(d) + gvc.dirichletFormat = "%.6f" + gvc.clusterFile = "g%d_s%.6f_d%.6f_b%.2f.cluster".format(g,s,d,b) + gvc.jobOutputFile = new File(gvc.clusterFile.stripSuffix(".cluster") + ".gvc.out") + vr.jarFile = gatkJarFile + vr.rodBind :+= RodBind("input20", "VCF", new File("/broad/shptmp/rpoplin/CEUTSI.chr20.filtered.vcf")) + vr.logging_level = "INFO" + vr.intervalsString :+= "20" + vr.target_titv = Some(2.1) + vr.ignore_filter :+= "HARD_TO_VALIDATE" + vr.path_to_resources = "/humgen/gsa-scr1/rpoplin/sting_dev_vb/R/" vr.clusterFile = gvc.clusterFile - vr.jobOutputFile = swapExt(vr.clusterFile, ".cluster", ".vr.out") - vr.backOff = b + vr.jobOutputFile = new File(vr.clusterFile.stripSuffix(".cluster") + ".vr.out") + vr.backOff = Some(b) + vr.backOffFormat = "%.2f" add(gvc, vr) } } } } - -// Populate parameters passed in via -P -setParams - -// Run the pipeline -run - - - -// A very basic GATK UnifiedGenotyper -class GenerateVariantClusters extends GatkFunction { - var maxGaussians: Int = _ - var shrinkage: Double = _ - var dirichlet: Double = _ - - @Output - var clusterFile: File = _ - - def commandLine = gatkCommandLine("GenerateVariantClusters") + - "-B input20,VCF,/broad/shptmp/rpoplin/CEUTSI.chr20.filtered.vcf " + - "-l INFO -L 20 -an QD -an SB -an HaplotypeScore -an HRun " + - "-resources /humgen/gsa-scr1/rpoplin/sting_dev_vb/R/ " + - "-mG %d ".format(maxGaussians) + - "-shrinkage %.6f ".format(shrinkage) + - "-dirichlet %.6f ".format(dirichlet) + - "-clusterFile %s".format(clusterFile) -} - -// A basic GATK VariantFiltration -class VariantRecalibrator extends GatkFunction { - var backOff: Double = _ - - @Input - var clusterFile: File = _ - - def commandLine = gatkCommandLine("VariantRecalibrator") + - "-B input20,VCF,/broad/shptmp/rpoplin/CEUTSI.chr20.filtered.vcf " + - "-l INFO -L 20 -titv 2.1 " + - "--ignore_filter HARD_TO_VALIDATE " + - "-resources /humgen/gsa-scr1/rpoplin/sting_dev_vb/R/ " + - "-backOff %.2f ".format(backOff) + - "-clusterFile %s ".format(clusterFile) + - "-output %s".format(clusterFile) + } } diff --git a/scala/qscript/unifiedgenotyper_example.properties b/scala/qscript/unifiedgenotyper_example.properties deleted file mode 100644 index 4c4668db1..000000000 --- a/scala/qscript/unifiedgenotyper_example.properties +++ /dev/null @@ -1,7 +0,0 @@ -gatkJar = /humgen/gsa-hpprojects/GATK/bin/current/GenomeAnalysisTK.jar -referenceFile = /path/to/reference.fasta -dbsnp = /path/to/dbsnp -intervals = /path/to/my.interval_list -jobNamePrefix = Q -memoryLimit = 2 -gatkLoggingLevel = INFO diff --git a/scala/qscript/unifiedgenotyper_example.scala b/scala/qscript/unifiedgenotyper_example.scala deleted file mode 100644 index d21a1ef6c..000000000 --- a/scala/qscript/unifiedgenotyper_example.scala +++ /dev/null @@ -1,54 +0,0 @@ -import org.broadinstitute.sting.queue.QScript._ - -setArgs(args) - -for (bam <- inputs("bam")) { - val ug = new UnifiedGenotyper - val vf = new VariantFiltration - val ve = new GatkFunction { - @Input(doc="vcf") var vcfFile: File = _ - @Output(doc="eval") var evalFile: File = _ - def commandLine = gatkCommandLine("VariantEval") + "-B eval,VCF,%s -o %s".format(vcfFile, evalFile) - } - - // Make sure the Sting/shell folder is in your path to use mergeText.sh and splitIntervals.sh. - ug.scatterCount = 3 - ug.bamFiles :+= bam - ug.vcfFile = swapExt(bam, "bam", "unfiltered.vcf") - - vf.vcfInput = ug.vcfFile - vf.vcfOutput = swapExt(bam, "bam", "filtered.vcf") - - ve.vcfFile = vf.vcfOutput - ve.evalFile = swapExt(bam, "bam", "eval") - - add(ug, vf, ve) -} - -setParams -run - - -class UnifiedGenotyper extends GatkFunction { - @Output(doc="vcf") - @Gather(classOf[SimpleTextGatherFunction]) - var vcfFile: File = _ - def commandLine = gatkCommandLine("UnifiedGenotyper") + "-varout %s".format(vcfFile) -} - -class VariantFiltration extends GatkFunction { - @Input(doc="input vcf") - var vcfInput: File = _ - - @Input(doc="filter names") - var filterNames: List[String] = Nil - - @Input(doc="filter expressions") - var filterExpressions: List[String] = Nil - - @Output(doc="output vcf") - var vcfOutput: File = _ - - def commandLine = gatkCommandLine("VariantFiltration") + "%s%s -B variant,VCF,%s -o %s" - .format(repeat(" -filterName ", filterNames), repeat(" -filterExpression ", filterExpressions), vcfInput, vcfOutput) -} diff --git a/scala/src/org/broadinstitute/sting/queue/QArguments.scala b/scala/src/org/broadinstitute/sting/queue/QArguments.scala deleted file mode 100755 index 5c921231b..000000000 --- a/scala/src/org/broadinstitute/sting/queue/QArguments.scala +++ /dev/null @@ -1,105 +0,0 @@ -package org.broadinstitute.sting.queue - -import collection.mutable.ListBuffer -import collection.JavaConversions._ -import org.broadinstitute.sting.queue.util.Logging -import org.broadinstitute.sting.utils.text.XReadLines -import java.io.{FileInputStream, File} -import java.util.Properties - -class QArguments(args: Array[String]) { - var bsubAllJobs = false - var bsubWaitJobs = false - var dryRun = false - val scripts = new ListBuffer[String] - var inputPaths = List.empty[File] - var properties = Map.empty[String, String] - - val userArgs = parseArgs(args) - - private def parseArgs(args: Array[String]) = { - var filtered = new ListBuffer[String] - filtered.appendAll(args) - - if (isFlagged(filtered, "-debug")) - Logging.setDebug - if (isFlagged(filtered, "-trace")) - Logging.setTrace - if (isFlagged(filtered, "-dry")) - dryRun = true - if (isFlagged(filtered, "-bsub")) - bsubAllJobs = true - if (isFlagged(filtered, "-bsubWait")) - bsubWaitJobs = true - for (arg <- getArgs(filtered, "-P")) - addProperties(arg) - for (arg <- getArgs(filtered, "-I")) - addFile(arg) - for (arg <- getArgs(filtered, "-S")) - scripts.append(arg) - - List(filtered:_*) - } - - private def isFlagged(filtered: ListBuffer[String], search: String) = { - var found = false - var index = 0 - while (0 <= index && index < filtered.size) { - index = filtered.indexOf(search) - if (index >= 0) { - found = true - filtered.remove(index) - } - } - found - } - - private def getArgs(filtered: ListBuffer[String], search: String) = { - var found = new ListBuffer[String] - var index = 0 - while (0 <= index && index < filtered.size) { - index = filtered.indexOf(search) - if (index >= 0) { - found.append(filtered(index+1)) - filtered.remove(index, 2) - } - } - found - } - - def addProperties(arg: String) = { - var file = new File(arg) - if (arg.contains("=") && !file.exists) { - val tokens = arg.split("=", 2) - properties += tokens(0) -> tokens(1) - } else if (arg.endsWith(".properties")) { - if (!file.exists) - throw new QException("File not found: " + file.getAbsolutePath) - var props = new Properties - props.load(new FileInputStream(file)) - for ((name, value) <- props) - properties += name -> value - } else { - throw new QException("Invalid property: " + arg) - } - } - - def addFile(arg: String): Unit = { - var file = new File(arg) - inputPaths :+= file - if (arg.endsWith(".list")) - new XReadLines(file).iterator.foreach(addFile(_)) - } -} - -object QArguments { - def strip(filtered: ListBuffer[String], search: String) = { - var index = 0 - while (0 <= index && index < filtered.size) { - index = filtered.indexOf(search) - if (index >= 0) { - filtered.remove(index, 2) - } - } - } -} diff --git a/scala/src/org/broadinstitute/sting/queue/QCommandLine.scala b/scala/src/org/broadinstitute/sting/queue/QCommandLine.scala index f59ea960b..1e4a05cad 100755 --- a/scala/src/org/broadinstitute/sting/queue/QCommandLine.scala +++ b/scala/src/org/broadinstitute/sting/queue/QCommandLine.scala @@ -1,47 +1,115 @@ package org.broadinstitute.sting.queue -import tools.nsc.MainGenericRunner -import org.broadinstitute.sting.queue.util.ClasspathUtils -import collection.mutable.ListBuffer -import org.broadinstitute.sting.queue.util.Logging +import java.io.File +import java.util.Arrays +import org.broadinstitute.sting.queue.engine.QGraph +import org.broadinstitute.sting.commandline.{ClassType, Input, Argument, CommandLineProgram} +import org.broadinstitute.sting.queue.util.{Logging, ScalaCompoundArgumentTypeDescriptor} -object QCommandLine extends Application with Logging { - var usage = """usage: java -jar Queue.jar [-P name=value] [-P file.properties] [-I input.file] [-I input_files.list] [-bsub] [-bsubWait] [-dry] [-debug] -S pipeline.scala""" +/** + * Entry point of Queue. Compiles and runs QScripts passed in to the command line. + */ +class QCommandLine extends CommandLineProgram with Logging { + @Input(fullName="script", shortName="S", doc="QScript scala file", required=true) + @ClassType(classOf[File]) + private var scripts = List.empty[File] - override def main(args: Array[String]) = { - val qArgs: QArguments = try { - new QArguments(args) - } catch { - case exception => { - println(exception) - println(usage) - System.exit(-1) - } - null + @Argument(fullName="bsub_all_jobs", shortName="bsub", doc="Use bsub to submit jobs", required=false) + private var bsubAllJobs = false + + @Argument(fullName="bsub_wait_jobs", shortName="bsubWait", doc="Wait for bsub submitted jobs before exiting", required=false) + private var bsubWaitJobs = false + + @Argument(fullName="run_scripts", shortName="run", doc="Run QScripts", required=false) + private var run = false + + @Argument(fullName="dot_graph", shortName="dot", doc="Outputs the queue graph to a .dot file. See: http://en.wikipedia.org/wiki/DOT_language", required=false) + private var queueDot: File = _ + + /** + * Takes the QScripts passed in, runs their script() methods, retrieves their generated + * functions, and then builds and runs a QGraph based on the dependencies. + */ + def execute = { + val qGraph = new QGraph + qGraph.dryRun = !run + qGraph.bsubAllJobs = bsubAllJobs + qGraph.bsubWaitJobs = bsubWaitJobs + + val scripts = qScriptManager.createScripts() + for (script <- scripts) { + logger.info("Scripting " + qScriptManager.getName(script.getClass.asSubclass(classOf[QScript]))) + loadArgumentsIntoObject(script) + script.script + script.functions.foreach(qGraph.add(_)) + logger.info("Added " + script.functions.size + " functions") } - logger.debug("starting") - - if (qArgs.scripts.size == 0) { - println("Error: Missing script") - println(usage) - System.exit(-1) + logger.info("Binding functions") + qGraph.fillIn + if (queueDot != null) { + logger.info("Generating " + queueDot) + qGraph.renderToDot(queueDot) } - // NOTE: Something in MainGenericRunner is exiting the VM. - if (qArgs.scripts.size != 1) { - println("Error: Only one script can be run at a time") - println(usage) - System.exit(-1) - } + logger.info("Running generated graph") + qGraph.run + logger.info("Done") + 0 + } - val newArgs = new ListBuffer[String] - newArgs.appendAll(args) - QArguments.strip(newArgs, "-S") - newArgs.prepend("-nocompdaemon", "-classpath", ClasspathUtils.manifestAwareClassPath, qArgs.scripts.head) - MainGenericRunner.main(newArgs.toArray) + /** + * Returns true as QScripts are located and compiled. + * @return true + */ + override def canAddArgumentsDynamically = true - // NOTE: This line is not reached because the MainGenericRunner exits the VM. - logger.debug("exiting") + /** + * Returns the list of QScripts passed in via -S so that their + * arguments can be inspected before QScript.script is called. + * @return Array of QScripts passed in. + */ + override def getArgumentSources = + qScriptManager.getValues.asInstanceOf[Array[Class[_]]] + + /** + * Returns the name of a QScript + * @return The name of a QScript + */ + override def getArgumentSourceName(source: Class[_]) = + qScriptManager.getName(source.asSubclass(classOf[QScript])) + + /** + * Returns a ScalaCompoundArgumentTypeDescriptor that can parse argument sources into scala collections. + * @return a ScalaCompoundArgumentTypeDescriptor + */ + override def getArgumentTypeDescriptors = + Arrays.asList(new ScalaCompoundArgumentTypeDescriptor) + + /** + * Loads the QScripts passed in and returns a new QScriptManager than can be used to create them. + */ + private lazy val qScriptManager = { + QScriptManager.loadScripts(scripts) + new QScriptManager + } +} + +/** + * Entry point of Queue. Compiles and runs QScripts passed in to the command line. + */ +object QCommandLine { + /** + * Main. + * @param argv Arguments. + */ + def main(argv: Array[String]) { + try { + CommandLineProgram.start(new QCommandLine, argv); + if (CommandLineProgram.result != 0) + System.exit(CommandLineProgram.result); + } catch { + case e: Exception => CommandLineProgram.exitSystemWithError(e) + } } } diff --git a/scala/src/org/broadinstitute/sting/queue/QScript.scala b/scala/src/org/broadinstitute/sting/queue/QScript.scala index 7fa24e9ee..a795f664c 100755 --- a/scala/src/org/broadinstitute/sting/queue/QScript.scala +++ b/scala/src/org/broadinstitute/sting/queue/QScript.scala @@ -1,109 +1,41 @@ package org.broadinstitute.sting.queue -import org.broadinstitute.sting.queue.function.CommandLineFunction -import org.broadinstitute.sting.queue.engine.QGraph +import org.broadinstitute.sting.queue.util.Logging /** - * Syntactic sugar for filling in a pipeline using a Scala script. + * Defines a Queue pipeline as a collection of CommandLineFunctions. */ -object QScript { +trait QScript extends Logging { // Type aliases so users don't have to import type File = java.io.File type Input = org.broadinstitute.sting.commandline.Input type Output = org.broadinstitute.sting.commandline.Output + type Argument = org.broadinstitute.sting.commandline.Argument + type ArgumentCollection = org.broadinstitute.sting.commandline.ArgumentCollection type CommandLineFunction = org.broadinstitute.sting.queue.function.CommandLineFunction - type GatkFunction = org.broadinstitute.sting.queue.function.gatk.GatkFunction type ScatterGatherableFunction = org.broadinstitute.sting.queue.function.scattergather.ScatterGatherableFunction type Scatter = org.broadinstitute.sting.queue.function.scattergather.Scatter type Gather = org.broadinstitute.sting.queue.function.scattergather.Gather - type BamGatherFunction = org.broadinstitute.sting.queue.function.scattergather.BamGatherFunction type SimpleTextGatherFunction = org.broadinstitute.sting.queue.function.scattergather.SimpleTextGatherFunction - // The arguments for executing pipelines - private var qArgs: QArguments = _ - - // A default pipeline. Can also use multiple 'new Pipeline()' - private val pipeline = new Pipeline + /** + * Builds the CommandLineFunctions that will be used to run this script and adds them to this.functions directly or using the add() utility method. + */ + def script: Unit /** - * Initializes the QArguments and returns a list of the rest of the user args. + * The command line functions that will be executed for this QScript. */ - def setArgs(params: Array[String]) = { - qArgs = new QArguments(params) - qArgs.userArgs - } - - /** - * Returns a list of files that were specified with "-I " on the command line - * or inside a .list file. - */ - def inputs(extension: String) = qArgs.inputPaths.filter(_.getName.endsWith(extension)) + var functions = List.empty[CommandLineFunction] /** * Exchanges the extension on a file. */ - def swapExt(file: File, oldExtension: String, newExtension: String) = + protected def swapExt(file: File, oldExtension: String, newExtension: String) = new File(file.getName.stripSuffix(oldExtension) + newExtension) /** - * Adds one or more command line functions for dispatch later during run() + * Adds one or more command line functions to be run. */ - def add(functions: CommandLineFunction*) = pipeline.add(functions:_*) - - /** - * Sets the @Input and @Output values for all the functions - */ - def setParams(): Unit = pipeline.setParams() - - /** - * Sets the @Input and @Output values for a single function - */ - def setParams(function: CommandLineFunction): Unit = pipeline.setParams(function) - - /** - * Executes functions that have been added to the pipeline. - */ - def run() = pipeline.run() - - - /** - * Encapsulates a set of functions to run together. - */ - protected class Pipeline { - private var functions = List.empty[CommandLineFunction] - - /** - * Adds one or more command line functions for dispatch later during run() - */ - def add(functions: CommandLineFunction*) = - this.functions :::= List(functions:_*) - - /** - * Sets the @Input and @Output values for all the functions - */ - def setParams(): Unit = - for (function <- functions) setParams(function) - - /** - * Sets the @Input and @Output values for a single function - */ - def setParams(function: CommandLineFunction): Unit = - function.properties = qArgs.properties - - /** - * Executes functions that have been added to the pipeline. - */ - def run() = { - val qGraph = new QGraph - qGraph.dryRun = qArgs.dryRun - qGraph.bsubAllJobs = qArgs.bsubAllJobs - qGraph.bsubWaitJobs = qArgs.bsubWaitJobs - qGraph.properties = qArgs.properties - for (function <- functions) - qGraph.add(function) - qGraph.fillIn - qGraph.run - qGraph.renderToDot(new File("queue.dot")) - } - } + def add(functions: CommandLineFunction*) = this.functions ++= List(functions:_*) } diff --git a/scala/src/org/broadinstitute/sting/queue/QScriptManager.scala b/scala/src/org/broadinstitute/sting/queue/QScriptManager.scala new file mode 100644 index 000000000..1b8a00d91 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/QScriptManager.scala @@ -0,0 +1,163 @@ +package org.broadinstitute.sting.queue + +import org.broadinstitute.sting.utils.classloader.PluginManager +import scala.tools.nsc.{Global, Settings} +import scala.tools.nsc.io.PlainFile +import org.broadinstitute.sting.queue.util.{Logging, ClasspathUtils, IOUtils} +import collection.JavaConversions +import java.io.File +import scala.tools.nsc.reporters.AbstractReporter +import java.lang.String +import org.apache.log4j.Level +import scala.tools.nsc.util.{FakePos, NoPosition, Position} + +/** + * Plugin manager for QScripts which loads QScripts into the current class loader. + */ +class QScriptManager extends PluginManager[QScript](classOf[QScript], "QScript", "Script") with Logging { + + /** + * Returns the list of QScripts classes found in the classpath. + * @return QScripts classes found in the classpath. + */ + def getValues = { + if (logger.isTraceEnabled) { + logger.trace(JavaConversions.asMap(this.pluginsByName) + .foreach{case (name, clazz) => "Found QScript %s: %s".format(name, clazz)}) + } + JavaConversions.asIterable(this.pluginsByName.values).toArray + } + + /** + * Creates the QScripts for all values found in the classpath. + * @return QScripts found in the classpath. + */ + def createScripts() = getValues.map(_.newInstance.asInstanceOf[QScript]) +} + +/** + * Plugin manager for QScripts which loads QScripts into the current classloader. + */ +object QScriptManager extends Logging { + /** + * Compiles and loads the scripts in the files into the current classloader. + * Heavily based on scala/src/compiler/scala/tools/ant/Scalac.scala + * @param scripts Scala classes to compile. + */ + def loadScripts(scripts: List[File]) { + if (scripts.size > 0) { + + val settings = new Settings((error: String) => logger.error(error)) + val outdir = IOUtils.tempDir("Q-classes").getAbsoluteFile + settings.outdir.value = outdir.getPath + + // Set the classpath to the current class path. + ClasspathUtils.manifestAwareClassPath.foreach(path => settings.classpath.append(path.getPath)) + + val reporter = new Log4JReporter(settings) + + val compiler = new Global(settings, reporter) + val run = new compiler.Run + + logger.debug("Compiling %s QScript%s".format(scripts.size, plural(scripts.size))) + logger.trace("Compilation directory: " + settings.outdir.value) + run.compileFiles(scripts.map(new PlainFile(_))) + + reporter.printSummary() + if (reporter.hasErrors) { + val msg = "Compile failed with %d error%s".format( + reporter.ERROR.count, plural(reporter.ERROR.count)) + throw new QException(msg) + } + else if (reporter.WARNING.count > 0) + logger.warn("Compile succeeded with %d warning%s".format( + reporter.WARNING.count, plural(reporter.WARNING.count))) + else + logger.debug("Compilation complete") + + // Add the new compilation output directory to the classpath. + ClasspathUtils.addClasspath(outdir) + } + } + + /** + * Returns the string "s" if x is greater than 1. + * @param x Value to test. + * @return "s" if x is greater than one else "". + */ + private def plural(x: Int) = if (x > 1) "s" else "" + + /** + * NSC (New Scala Compiler) reporter which logs to Log4J. + * Heavily based on scala/src/compiler/scala/tools/nsc/reporters/ConsoleReporter.scala + */ + private class Log4JReporter(val settings: Settings) extends AbstractReporter { + def displayPrompt = throw new UnsupportedOperationException("Unable to prompt the user. Prompting should be off.") + + /** + * Displays the message at position with severity. + * @param posIn Position of the event in the file that generated the message. + * @param msg Message to display. + * @param severity Severity of the event. + */ + def display(posIn: Position, msg: String, severity: Severity) = { + severity.count += 1 + val level = severity match { + case INFO => Level.INFO + case WARNING => Level.WARN + case ERROR => Level.ERROR + } + val pos = if (posIn eq null) NoPosition + else if (posIn.isDefined) posIn.inUltimateSource(posIn.source) + else posIn + pos match { + case FakePos(fmsg) => + printMessage(level, fmsg+" "+msg) + case NoPosition => + printMessage(level, msg) + case _ => + val buf = new StringBuilder(msg) + val file = pos.source.file + printMessage(level, file.name+":"+pos.line+": "+msg) + printSourceLine(level, pos) + } + } + + /** + * Prints a summary count of warnings and errors. + */ + def printSummary() = { + if (WARNING.count > 0) + printMessage(Level.WARN, countElementsAsString(WARNING.count, "warning") + " found") + if (ERROR.count > 0) + printMessage(Level.ERROR, countElementsAsString(ERROR.count, "error") + " found") + } + + /** + * Prints the source code line of an event followed by a pointer within the line to the error. + * @param level Severity level. + * @param pos Position in the file of the event. + */ + private def printSourceLine(level: Level, pos: Position) { + printMessage(level, pos.lineContent.stripLineEnd) + printColumnMarker(level, pos) + } + + /** + * Prints the column marker of the given position. + * @param level Severity level. + * @param pos Position in the file of the event. + */ + private def printColumnMarker(level: Level, pos: Position) = + if (pos.isDefined) { printMessage(level, " " * (pos.column - 1) + "^") } + + /** + * Prints the message at the severity level. + * @param level Severity level. + * @param message Message content. + */ + private def printMessage(level: Level, message: String) = { + logger.log(level, message) + } + } +} diff --git a/scala/src/org/broadinstitute/sting/queue/engine/CommandLineRunner.scala b/scala/src/org/broadinstitute/sting/queue/engine/CommandLineRunner.scala deleted file mode 100755 index da23d3766..000000000 --- a/scala/src/org/broadinstitute/sting/queue/engine/CommandLineRunner.scala +++ /dev/null @@ -1,20 +0,0 @@ -package org.broadinstitute.sting.queue.engine - -import org.broadinstitute.sting.queue.util.{Logging, ProcessUtils} -import org.broadinstitute.sting.queue.function.CommandLineFunction - -/** - * Runs jobs one at a time locally - */ -trait CommandLineRunner extends Logging { - def run(function: CommandLineFunction, qGraph: QGraph) = { - if (logger.isDebugEnabled) { - logger.debug(function.commandDirectory + " > " + function.commandLine) - } else { - logger.info(function.commandLine) - } - - if (!qGraph.dryRun) - ProcessUtils.runCommandAndWait(function.commandLine, function.commandDirectory) - } -} diff --git a/scala/src/org/broadinstitute/sting/queue/engine/DispatchJobRunner.scala b/scala/src/org/broadinstitute/sting/queue/engine/DispatchJobRunner.scala index 88f48c1e5..d1d80d99b 100755 --- a/scala/src/org/broadinstitute/sting/queue/engine/DispatchJobRunner.scala +++ b/scala/src/org/broadinstitute/sting/queue/engine/DispatchJobRunner.scala @@ -1,22 +1,38 @@ package org.broadinstitute.sting.queue.engine import collection.JavaConversions._ -import org.broadinstitute.sting.queue.function.{DispatchFunction, QFunction} +import org.broadinstitute.sting.queue.function.{CommandLineFunction, QFunction} import scala.collection.immutable.ListSet +/** + * Dispatches jobs to a compute cluster. + */ trait DispatchJobRunner { + /** Type of the job. */ type DispatchJobType - private var dispatchJobs = Map.empty[DispatchFunction, DispatchJobType] + /** An internal cache of all the jobs that have run by command line function. */ + private var dispatchJobs = Map.empty[CommandLineFunction, DispatchJobType] + /** An internal list of functions that have no other dependencies. */ private var waitJobsByGraph = Map.empty[QGraph, ListSet[DispatchJobType]] /** * Dispatches a function to the queue and returns immediately, unless the function is a DispatchWaitFunction * in which case it waits for all other terminal functions to complete. + * @param function Command to run. + * @param qGraph graph that holds the job, and if this is a dry run. */ - def dispatch(function: DispatchFunction, qGraph: QGraph) + def dispatch(function: CommandLineFunction, qGraph: QGraph) - protected def addJob(function: DispatchFunction, qGraph: QGraph, - dispatchJob: DispatchJobType, previousJobs: List[DispatchJobType]) = { + /** + * Adds the job to the internal cache of previous jobs and removes the previous jobs that + * the job was dependent on from the list of function that have no dependencies. + * @param function CommandLineFunction to add to the list. + * @param qGraph Current qGraph being iterated over. + * @param dispatchJob The job that is being added to the cache. + * @param previousJobs The previous jobs that the job was dependent one. + */ + protected def addJob(function: CommandLineFunction, qGraph: QGraph, + dispatchJob: DispatchJobType, previousJobs: Iterable[DispatchJobType]) = { dispatchJobs += function -> dispatchJob var waitJobs = getWaitJobs(qGraph) for (previousJob <- previousJobs) @@ -26,7 +42,10 @@ trait DispatchJobRunner { } /** - * Walks up the graph looking for the previous LsfJobs + * Walks up the graph looking for the previous LsfJobs. + * @param function Function to examine for a previous command line job. + * @param qGraph The graph that contains the jobs. + * @return A list of prior jobs. */ protected def previousJobs(function: QFunction, qGraph: QGraph) : List[DispatchJobType] = { var previous = List.empty[DispatchJobType] @@ -36,10 +55,10 @@ trait DispatchJobRunner { incomingEdge match { // Stop recursing when we find a job along the edge and return its job id - case dispatchFunction: DispatchFunction => previous :+= dispatchJobs(dispatchFunction) + case dispatchFunction: CommandLineFunction => previous :+= dispatchJobs(dispatchFunction) // For any other type of edge find the LSF jobs preceding the edge - case qFunction: QFunction => previous = previousJobs(qFunction, qGraph) ::: previous + case qFunction: QFunction => previous ++= previousJobs(qFunction, qGraph) } } previous @@ -47,10 +66,25 @@ trait DispatchJobRunner { /** * Returns a set of jobs that have no following jobs in the graph. + * @param qGraph The graph that contains the jobs. + * @return ListSet[DispatchJobType] of previous jobs that have no dependent jobs. */ protected def getWaitJobs(qGraph: QGraph) = { if (!waitJobsByGraph.contains(qGraph)) waitJobsByGraph += qGraph -> ListSet.empty[DispatchJobType] waitJobsByGraph(qGraph) } + + /** + * Builds a command line that can be run to force an automount of the directories. + * @param function Function to look jobDirectories. + * @return A "cd [&& cd ]" command. + */ + protected def mountCommand(function: CommandLineFunction) = { + val dirs = function.jobDirectories + if (dirs.size > 0) + Some("\'" + dirs.mkString("cd ", " && cd ", "") + "\'") + else + None + } } diff --git a/scala/src/org/broadinstitute/sting/queue/engine/LsfJobRunner.scala b/scala/src/org/broadinstitute/sting/queue/engine/LsfJobRunner.scala index dc0780527..d49534a24 100644 --- a/scala/src/org/broadinstitute/sting/queue/engine/LsfJobRunner.scala +++ b/scala/src/org/broadinstitute/sting/queue/engine/LsfJobRunner.scala @@ -1,55 +1,76 @@ package org.broadinstitute.sting.queue.engine -import collection.JavaConversions._ -import edu.mit.broad.core.lsf.LocalLsfJob -import java.util.ArrayList -import org.broadinstitute.sting.queue.util.Logging -import org.broadinstitute.sting.queue.function.{DispatchWaitFunction, DispatchFunction} +import org.broadinstitute.sting.queue.function.{CommandLineFunction, DispatchWaitFunction} +import org.broadinstitute.sting.queue.util.{IOUtils, LsfJob, Logging} +/** + * Runs jobs on an LSF compute cluster. + */ trait LsfJobRunner extends DispatchJobRunner with Logging { - type DispatchJobType = LocalLsfJob + type DispatchJobType = LsfJob - def dispatch(function: DispatchFunction, qGraph: QGraph) = { - val job = new LocalLsfJob - job.setName(function.jobName) - job.setOutputFile(function.jobOutputFile) - job.setErrFile(function.jobErrorFile) - job.setWorkingDir(function.commandDirectory) - job.setProject(function.jobProject) - job.setQueue(function.jobQueue) - job.setCommand(function.commandLine) + /** + * Dispatches the function on the LSF cluster. + * @param function Command to run. + * @param qGraph graph that holds the job, and if this is a dry run. + */ + def dispatch(function: CommandLineFunction, qGraph: QGraph) = { + val job = new LsfJob + job.name = function.jobName + job.outputFile = function.jobOutputFile + job.errorFile = function.jobErrorFile + job.project = function.jobProject + job.queue = function.jobQueue + job.command = function.commandLine - var extraArgs = List("-r") + if (!IOUtils.CURRENT_DIR.getCanonicalFile.equals(function.commandDirectory)) + job.workingDir = function.commandDirectory + + if (function.jobRestartable) + job.extraBsubArgs :+= "-r" if (function.memoryLimit.isDefined) - extraArgs :::= List("-R", "rusage[mem=" + function.memoryLimit.get + "]") + job.extraBsubArgs ++= List("-R", "rusage[mem=" + function.memoryLimit.get + "]") - val previous = + val previous: Iterable[LsfJob] = if (function.isInstanceOf[DispatchWaitFunction]) { - extraArgs :+= "-K" - getWaitJobs(qGraph).toList + job.waitForCompletion = true + getWaitJobs(qGraph) } else { previousJobs(function, qGraph) } - if (previous.size > 0) - extraArgs :::= List("-w", dependencyExpression(previous)) + mountCommand(function) match { + case Some(command) => job.preExecCommand = command + case None => /* ignore */ + } - job.setExtraBsubArgs(new ArrayList(extraArgs)) + if (previous.size > 0) + job.extraBsubArgs ++= List("-w", dependencyExpression(previous, function.jobRunOnlyIfPreviousSucceed)) addJob(function, qGraph, job, previous) if (logger.isDebugEnabled) { - logger.debug(function.commandDirectory + " > " + job.getBsubCommand.mkString(" ")) + logger.debug(function.commandDirectory + " > " + job.bsubCommand.mkString(" ")) } else { - logger.info(job.getBsubCommand.mkString(" ")) + logger.info(job.bsubCommand.mkString(" ")) } if (!qGraph.dryRun) - job.start + job.run } - private def dependencyExpression(jobs: List[LocalLsfJob]) = { - jobs.toSet[LocalLsfJob].map(_.getName).mkString("ended(\"", "\") && ended(\"", "\")") + /** + * Returns the dependency expression for the prior jobs. + * @param jobs Previous jobs this job is dependent on. + * @param runOnSuccess Run the job only if the previous jobs succeed. + * @return The dependency expression for the prior jobs. + */ + private def dependencyExpression(jobs: Iterable[LsfJob], runOnSuccess: Boolean) = { + val jobNames = jobs.toSet[LsfJob].map(_.name) + if (runOnSuccess) + jobNames.mkString("done(\"", "\") && done(\"", "\")") + else + jobNames.mkString("ended(\"", "\") && ended(\"", "\")") } } diff --git a/scala/src/org/broadinstitute/sting/queue/engine/QGraph.scala b/scala/src/org/broadinstitute/sting/queue/engine/QGraph.scala index 2670e82b3..9e1d68d86 100755 --- a/scala/src/org/broadinstitute/sting/queue/engine/QGraph.scala +++ b/scala/src/org/broadinstitute/sting/queue/engine/QGraph.scala @@ -6,22 +6,27 @@ import scala.collection.JavaConversions import scala.collection.JavaConversions._ import org.broadinstitute.sting.queue.function.{MappingFunction, CommandLineFunction, QFunction} import org.broadinstitute.sting.queue.function.scattergather.ScatterGatherableFunction -import org.broadinstitute.sting.queue.util.{CollectionUtils, Logging} +import org.broadinstitute.sting.queue.util.Logging import org.broadinstitute.sting.queue.QException import org.jgrapht.alg.CycleDetector import org.jgrapht.EdgeFactory import org.jgrapht.ext.DOTExporter -import org.broadinstitute.sting.queue.function.DispatchFunction -import org.broadinstitute.sting.queue.function.gatk.GatkFunction +import java.io.File +/** + * The internal dependency tracker between sets of function input and output files. + */ class QGraph extends Logging { var dryRun = true var bsubAllJobs = false var bsubWaitJobs = false - var properties = Map.empty[String, String] val jobGraph = newGraph def numJobs = JavaConversions.asSet(jobGraph.edgeSet).filter(_.isInstanceOf[CommandLineFunction]).size + /** + * Adds a QScript created CommandLineFunction to the graph. + * @param command Function to add to the graph. + */ def add(command: CommandLineFunction) { addFunction(command) } @@ -49,22 +54,30 @@ class QGraph extends Logging { jobGraph.removeAllVertices(jobGraph.vertexSet.filter(isOrphan(_))) } + /** + * Checks the functions for missing values and the graph for cyclic dependencies and then runs the functions in the graph. + */ def run = { var isReady = true + var totalMissingValues = 0 for (function <- JavaConversions.asSet(jobGraph.edgeSet)) { function match { case cmd: CommandLineFunction => - val missingValues = cmd.missingValues - if (missingValues.size > 0) { - isReady = false - logger.error("Missing values for function: %s".format(cmd.commandLine)) - for (missing <- missingValues) + val missingFieldValues = cmd.missingFields + if (missingFieldValues.size > 0) { + totalMissingValues += missingFieldValues.size + logger.error("Missing %s values for function: %s".format(missingFieldValues.size, cmd.commandLine)) + for (missing <- missingFieldValues) logger.error(" " + missing) } case _ => } } + if (totalMissingValues > 0) { + isReady = false + } + val detector = new CycleDetector(jobGraph) if (detector.detectCycles) { logger.error("Cycles were detected in the graph:") @@ -75,11 +88,29 @@ class QGraph extends Logging { if (isReady || this.dryRun) (new TopologicalJobScheduler(this) with LsfJobRunner).runJobs + + if (totalMissingValues > 0) { + logger.error("Total missing values: " + totalMissingValues) + } + + if (isReady && this.dryRun) { + logger.info("Dry run completed successfully!") + logger.info("Re-run with \"-run\" to execute the functions.") + } } + /** + * Creates a new graph where if new edges are needed (for cyclic dependency checking) they can be automatically created using a generic MappingFunction. + * @return A new graph + */ private def newGraph = new SimpleDirectedGraph[QNode, QFunction](new EdgeFactory[QNode, QFunction] { - def createEdge(input: QNode, output: QNode) = new MappingFunction(input.items, output.items)}) + def createEdge(input: QNode, output: QNode) = new MappingFunction(input.files, output.files)}) + /** + * Adds a generic QFunction to the graph. + * If the function is scatterable and the jobs request bsub, splits the job into parts and adds the parts instead. + * @param f Generic QFunction to add to the graph. + */ private def addFunction(f: QFunction): Unit = { try { f.freeze @@ -113,31 +144,53 @@ class QGraph extends Logging { } } - private def addCollectionInputs(value: Any): Unit = { - CollectionUtils.foreach(value, (item, collection) => - addMappingEdge(item, collection)) + /** + * Checks to see if the set of files has more than one file and if so adds input mappings between the set and the individual files. + * @param files Set to check. + */ + private def addCollectionInputs(files: Set[File]): Unit = { + if (files.size > 1) + for (file <- files) + addMappingEdge(Set(file), files) } - private def addCollectionOutputs(value: Any): Unit = { - CollectionUtils.foreach(value, (item, collection) => - addMappingEdge(collection, item)) + /** + * Checks to see if the set of files has more than one file and if so adds output mappings between the individual files and the set. + * @param files Set to check. + */ + private def addCollectionOutputs(files: Set[File]): Unit = { + if (files.size > 1) + for (file <- files) + addMappingEdge(files, Set(file)) } - private def addMappingEdge(input: Any, output: Any) = { - val inputSet = asSet(input) - val outputSet = asSet(output) - val hasEdge = inputSet == outputSet || - jobGraph.getEdge(QNode(inputSet), QNode(outputSet)) != null || - jobGraph.getEdge(QNode(outputSet), QNode(inputSet)) != null + /** + * Adds a directed graph edge between the input set and the output set if there isn't a direct relationship between the two nodes already. + * @param input Input set of files. + * @param output Output set of files. + */ + private def addMappingEdge(input: Set[File], output: Set[File]) = { + val hasEdge = input == output || + jobGraph.getEdge(QNode(input), QNode(output)) != null || + jobGraph.getEdge(QNode(output), QNode(input)) != null if (!hasEdge) - addFunction(new MappingFunction(inputSet, outputSet)) + addFunction(new MappingFunction(input, output)) } - private def asSet(value: Any): Set[Any] = if (value.isInstanceOf[Set[_]]) value.asInstanceOf[Set[Any]] else Set(value) - + /** + * Returns true if the edge is an internal mapping edge. + * @param edge Edge to check. + * @return true if the edge is an internal mapping edge. + */ private def isMappingEdge(edge: QFunction) = edge.isInstanceOf[MappingFunction] + /** + * Returns true if the edge is mapping edge that is not needed because it does + * not direct input or output from a user generated CommandLineFunction. + * @param edge Edge to check. + * @return true if the edge is not needed in the graph. + */ private def isFiller(edge: QFunction) = { if (isMappingEdge(edge)) { if (jobGraph.outgoingEdgesOf(jobGraph.getEdgeTarget(edge)).size == 0) @@ -148,9 +201,19 @@ class QGraph extends Logging { } else false } + /** + * Returns true if the node is not connected to any edges. + * @param node Node (set of files) to check + * @return true if this set of files is not needed in the graph. + */ private def isOrphan(node: QNode) = (jobGraph.incomingEdgesOf(node).size + jobGraph.outgoingEdgesOf(node).size) == 0 + /** + * Outputs the graph to a .dot file. + * http://en.wikipedia.org/wiki/DOT_language + * @param file Path to output the .dot file. + */ def renderToDot(file: java.io.File) = { val out = new java.io.FileWriter(file) diff --git a/scala/src/org/broadinstitute/sting/queue/engine/QNode.scala b/scala/src/org/broadinstitute/sting/queue/engine/QNode.scala index 01d3b814c..480c1c88f 100644 --- a/scala/src/org/broadinstitute/sting/queue/engine/QNode.scala +++ b/scala/src/org/broadinstitute/sting/queue/engine/QNode.scala @@ -1,6 +1,9 @@ package org.broadinstitute.sting.queue.engine +import java.io.File + /** * Represents a state between QFunctions the directed acyclic QGraph + * @param files The set of files that represent this node state. */ -case class QNode (val items: Set[Any]) +case class QNode (val files: Set[File]) diff --git a/scala/src/org/broadinstitute/sting/queue/engine/ShellJobRunner.scala b/scala/src/org/broadinstitute/sting/queue/engine/ShellJobRunner.scala new file mode 100755 index 000000000..abffa3c08 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/engine/ShellJobRunner.scala @@ -0,0 +1,31 @@ +package org.broadinstitute.sting.queue.engine + +import org.broadinstitute.sting.queue.util.{Logging, ShellJob} +import org.broadinstitute.sting.queue.function.CommandLineFunction + +/** + * Runs jobs one at a time locally + */ +trait ShellJobRunner extends Logging { + /** + * Runs the function on the local shell. + * @param function Command to run. + * @param qGraph graph that holds the job, and if this is a dry run. + */ + def run(function: CommandLineFunction, qGraph: QGraph) = { + val job = new ShellJob + job.command = function.commandLine + job.workingDir = function.commandDirectory + job.outputFile = function.jobOutputFile + job.errorFile = function.jobErrorFile + + if (logger.isDebugEnabled) { + logger.debug(function.commandDirectory + " > " + function.commandLine) + } else { + logger.info(function.commandLine) + } + + if (!qGraph.dryRun) + job.run + } +} diff --git a/scala/src/org/broadinstitute/sting/queue/engine/TopologicalJobScheduler.scala b/scala/src/org/broadinstitute/sting/queue/engine/TopologicalJobScheduler.scala index 0831e184f..23a69846f 100755 --- a/scala/src/org/broadinstitute/sting/queue/engine/TopologicalJobScheduler.scala +++ b/scala/src/org/broadinstitute/sting/queue/engine/TopologicalJobScheduler.scala @@ -7,21 +7,29 @@ import org.broadinstitute.sting.queue.util.Logging import org.broadinstitute.sting.queue.function._ /** - * Loops over the job graph running jobs as the edges are traversed + * Loops over the job graph running jobs as the edges are traversed. + * @param val The graph that contains the jobs to be run. */ abstract class TopologicalJobScheduler(private val qGraph: QGraph) - extends CommandLineRunner with DispatchJobRunner with Logging { + extends ShellJobRunner with DispatchJobRunner with Logging { protected val iterator = new TopologicalOrderIterator(qGraph.jobGraph) iterator.addTraversalListener(new TraversalListenerAdapter[QNode, QFunction] { + /** + * As each edge is traversed, either dispatch the job or run it locally. + * @param event Event holding the edge that was passed. + */ override def edgeTraversed(event: EdgeTraversalEvent[QNode, QFunction]) = event.getEdge match { - case f: DispatchFunction if (qGraph.bsubAllJobs) => dispatch(f, qGraph) + case f: CommandLineFunction if (qGraph.bsubAllJobs) => dispatch(f, qGraph) case f: CommandLineFunction => run(f, qGraph) case f: MappingFunction => /* do nothing for mapping functions */ } }) + /** + * Runs the jobs by traversing the graph. + */ def runJobs = { logger.info("Number of jobs: %s".format(qGraph.numJobs)) if (logger.isTraceEnabled) @@ -39,7 +47,6 @@ abstract class TopologicalJobScheduler(private val qGraph: QGraph) if (qGraph.bsubAllJobs && qGraph.bsubWaitJobs) { logger.info("Waiting for jobs to complete.") val wait = new DispatchWaitFunction - wait.properties = qGraph.properties wait.freeze dispatch(wait, qGraph) } diff --git a/scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamGatherFunction.scala b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamGatherFunction.scala new file mode 100644 index 000000000..13ce477c4 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamGatherFunction.scala @@ -0,0 +1,17 @@ +package org.broadinstitute.sting.queue.extensions.gatk + +import org.broadinstitute.sting.queue.function.JarCommandLineFunction +import org.broadinstitute.sting.commandline.Argument +import org.broadinstitute.sting.queue.function.scattergather.GatherFunction + +/** + * Merges BAM files using Picards MergeSampFiles.jar. + * At the Broad the jar can be found at /seq/software/picard/current/bin/MergeSamFiles.jar. Outside the broad see http://picard.sourceforge.net/") + */ +class BamGatherFunction extends GatherFunction with JarCommandLineFunction { + @Argument(doc="Compression level 1-9", required=false) + var compressionLevel: Option[Int] = None + + override def commandLine = super.commandLine + "%s%s%s".format( + optional(" COMPRESSION_LEVEL=", compressionLevel), " AS=true VALIDATION_STRINGENCY=SILENT SO=coordinate OUTPUT=" + originalOutput, repeat(" INPUT=", gatherParts)) +} diff --git a/scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamIndexFunction.scala b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamIndexFunction.scala new file mode 100644 index 000000000..82ef24b2d --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/BamIndexFunction.scala @@ -0,0 +1,35 @@ +package org.broadinstitute.sting.queue.extensions.gatk + +import org.broadinstitute.sting.queue.function.CommandLineFunction +import java.io.File +import org.broadinstitute.sting.commandline.{Argument, Output, Input} + +/** + * Indexes a BAM file. + * By default uses samtools index. + * The syntax of the script must be: + * + */ +class BamIndexFunction extends CommandLineFunction { + @Argument(doc="BAM file script") + var bamIndexScript: String = "samtools index" + + @Input(doc="BAM file to index") + var bamFile: File = _ + + @Output(doc="BAM file index to output", required=false) + var bamFileIndex: File = _ + + /** + * Sets the bam file index to the bam file name + ".bai". + */ + override def freezeFieldValues = { + super.freezeFieldValues + if (bamFileIndex == null && bamFile != null) + bamFileIndex = new File(bamFile.getPath + ".bai") + } + + def commandLine = "%s %s %s".format(bamIndexScript, bamFile, bamFileIndex) + + override def dotString = "Index: %s".format(bamFile.getName) +} diff --git a/scala/src/org/broadinstitute/sting/queue/extensions/gatk/ContigScatterFunction.scala b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/ContigScatterFunction.scala new file mode 100755 index 000000000..01de9c8f9 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/ContigScatterFunction.scala @@ -0,0 +1,8 @@ +package org.broadinstitute.sting.queue.extensions.gatk + +/** + * Splits intervals by contig instead of evenly. + */ +class ContigScatterFunction extends IntervalScatterFunction { + splitIntervalsScript = "splitIntervalsByContig.py" +} diff --git a/scala/src/org/broadinstitute/sting/queue/extensions/gatk/IntervalScatterFunction.scala b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/IntervalScatterFunction.scala new file mode 100644 index 000000000..dfb94d48f --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/IntervalScatterFunction.scala @@ -0,0 +1,16 @@ +package org.broadinstitute.sting.queue.extensions.gatk + +import org.broadinstitute.sting.commandline.Argument +import org.broadinstitute.sting.queue.function.scattergather.ScatterFunction + +/** + * An interval scatter function that allows the script to be swapped out. + * The syntax of the script must be: + * [.. ] + */ +class IntervalScatterFunction extends ScatterFunction { + @Argument(doc="Interval split script") + var splitIntervalsScript: String = "splitIntervals.sh" + + def commandLine = "%s %s%s".format(splitIntervalsScript, originalInput, repeat(" ", scatterParts)) +} diff --git a/scala/src/org/broadinstitute/sting/queue/extensions/gatk/RodBind.scala b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/RodBind.scala new file mode 100644 index 000000000..bce054ba0 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/extensions/gatk/RodBind.scala @@ -0,0 +1,14 @@ +package org.broadinstitute.sting.queue.extensions.gatk + +import java.io.File +import org.broadinstitute.sting.queue.function.FileProvider + +/** + * Used to provide -B rodBinding arguments to the GATK. + */ +case class RodBind(var trackName: String, var trackType: String, var file: File) extends FileProvider { + require(trackName != null, "RodBind trackName cannot be null") + require(trackType != null, "RodBind trackType cannot be null") + require(file != null, "RodBind file cannot be null") + override def toString = "%s,%s,%s".format(trackName, trackType, file) +} diff --git a/scala/src/org/broadinstitute/sting/queue/function/CommandLineFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/CommandLineFunction.scala index d94ac998b..847103f10 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/CommandLineFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/CommandLineFunction.scala @@ -1,74 +1,402 @@ package org.broadinstitute.sting.queue.function import org.broadinstitute.sting.queue.util._ -import java.lang.reflect.Field import java.lang.annotation.Annotation -import org.broadinstitute.sting.commandline.{Input, Output} +import org.broadinstitute.sting.commandline._ +import java.io.File +import collection.JavaConversions._ +import org.broadinstitute.sting.queue.function.scattergather.{SimpleTextGatherFunction, Gather} +import java.lang.management.ManagementFactory +import org.broadinstitute.sting.queue.QException -trait CommandLineFunction extends InputOutputFunction with DispatchFunction { - var properties = Map.empty[String, String] +/** + * A command line that will be run in a pipeline. + */ +trait CommandLineFunction extends QFunction with Logging { + def commandLine: String - def inputFieldsWithValues = inputFields.filter(hasFieldValue(_)) - def outputFieldsWithValues = outputFields.filter(hasFieldValue(_)) + /** Upper memory limit */ + var memoryLimit: Option[Int] = None + + /** Whether a job is restartable */ + var jobRestartable = true + + /** Directory to run the command in. */ + var commandDirectory: File = IOUtils.CURRENT_DIR + + /** Prefix for automatic job name creation */ + var jobNamePrefix: String = CommandLineFunction.processNamePrefix + + /** The name name of the job */ + var jobName: String = _ + + /** Job project to run the command */ + var jobProject = "Queue" + + /** Job queue to run the command */ + var jobQueue = "broad" + + /** Temporary directory to write any files */ + var jobTempDir: File = new File(System.getProperty("java.io.tmpdir")) + + /** If true this function will run only if the jobs it is dependent on succeed. */ + var jobRunOnlyIfPreviousSucceed = true + + /** File to redirect any output. Defaults to .out */ + @Output(doc="File to redirect any output", required=false) + @Gather(classOf[SimpleTextGatherFunction]) + var jobOutputFile: File = _ + + /** File to redirect any errors. Defaults to .out */ + @Output(doc="File to redirect any errors", required=false) + @Gather(classOf[SimpleTextGatherFunction]) + var jobErrorFile: File = _ + + /** The complete list of fields on this CommandLineFunction. */ + lazy val functionFields: List[ArgumentSource] = ParsingEngine.extractArgumentSources(this.getClass).toList + /** The @Input fields on this CommandLineFunction. */ + lazy val inputFields = functionFields.filter(source => ReflectionUtils.hasAnnotation(source.field, classOf[Input])) + /** The @Output fields on this CommandLineFunction. */ + lazy val outputFields = functionFields.filter(source => ReflectionUtils.hasAnnotation(source.field, classOf[Output])) + /** The @Argument fields on this CommandLineFunction. */ + lazy val argumentFields = functionFields.filter(source => ReflectionUtils.hasAnnotation(source.field, classOf[Argument])) /** - * Sets parameters from the arg map. + * Returns set of directories required to run the command. + * @return Set of directories required to run the command. */ - override def freeze = { - for ((name, value) <- properties) addOrUpdateWithStringValue(name, value) + def jobDirectories = { + var dirs = Set.empty[File] + dirs += commandDirectory + if (jobTempDir != null) + dirs += jobTempDir + dirs ++= inputs.map(_.getParentFile) + dirs ++= outputs.map(_.getParentFile) + dirs + } + + /** + * Returns the input files for this function. + * @return Set[File] inputs for this function. + */ + def inputs = getFieldFiles(inputFields) + + /** + * Returns the output files for this function. + * @return Set[File] outputs for this function. + */ + def outputs = getFieldFiles(outputFields) + + /** + * Gets the files from the fields. The fields must be a File, a FileProvider, or a List or Set of either. + * @param fields Fields to get files. + * @return Set[File] for the fields. + */ + private def getFieldFiles(fields: List[ArgumentSource]): Set[File] = { + var files = Set.empty[File] + for (field <- fields) + files ++= getFieldFiles(field) + files + } + + /** + * Gets the files from the field. The field must be a File, a FileProvider, or a List or Set of either. + * @param fields Field to get files. + * @return Set[File] for the field. + */ + def getFieldFiles(field: ArgumentSource): Set[File] = { + var files = Set.empty[File] + CollectionUtils.foreach(getFieldValue(field), (fieldValue) => { + val file = fieldValueToFile(field, fieldValue) + if (file != null) + files += file + }) + files + } + + /** + * Gets the file from the field. The field must be a File or a FileProvider and not a List or Set. + * @param field Field to get the file. + * @return File for the field. + */ + def getFieldFile(field: ArgumentSource): File = + fieldValueToFile(field, getFieldValue(field)) + + /** + * Converts the field value to a file. The field must be a File or a FileProvider. + * @param field Field to get the file. + * @param value Value of the File or FileProvider or null. + * @return Null if value is null, otherwise the File. + * @throws QException if the value is not a File or FileProvider. + */ + private def fieldValueToFile(field: ArgumentSource, value: Any): File = value match { + case file: File => file + case fileProvider: FileProvider => fileProvider.file + case null => null + case unknown => throw new QException("Non-file found. Try removing the annotation, change the annotation to @Argument, or implement FileProvider: %s: %s".format(field.field, unknown)) + } + + /** + * Resets the field to the temporary directory. + * @param field Field to get and set the file. + * @param tempDir new root for the file. + */ + def resetFieldFile(field: ArgumentSource, tempDir: File): File = { + getFieldValue(field) match { + case file: File => { + val newFile = IOUtils.resetParent(tempDir, file) + setFieldValue(field, newFile) + newFile + } + case fileProvider: FileProvider => { + fileProvider.file = IOUtils.resetParent(tempDir, fileProvider.file) + fileProvider.file + } + case null => null + case unknown => + throw new QException("Unable to set file from %s: %s".format(field, unknown)) + } + } + + /** + * The function description in .dot files + */ + override def dotString = jobName + " => " + commandLine + + /** + * Sets all field values and makes them canonical so that the graph can + * match the inputs of one function to the output of another using equals(). + */ + final override def freeze = { + freezeFieldValues + canonFieldValues super.freeze } + /** + * Sets all field values. + */ + def freezeFieldValues = { + if (jobName == null) + jobName = CommandLineFunction.nextJobName(jobNamePrefix) + + if (jobOutputFile == null) + jobOutputFile = new File(jobName + ".out") + + commandDirectory = IOUtils.subDir(IOUtils.CURRENT_DIR, commandDirectory) + } + + /** + * Makes all field values canonical so that the graph can match the + * inputs of one function to the output of another using equals(). + */ + def canonFieldValues = { + for (field <- this.functionFields) { + var fieldValue = this.getFieldValue(field) + fieldValue = CollectionUtils.updated(fieldValue, canon).asInstanceOf[AnyRef] + this.setFieldValue(field, fieldValue) + } + } + + /** + * Set value to a uniform value across functions. + * Base implementation changes any relative path to an absolute path. + * @param value to be updated + * @returns the modified value, or a copy if the value is immutable + */ + protected def canon(value: Any) = { + value match { + case file: File => absolute(file) + case fileProvider: FileProvider => fileProvider.file = absolute(fileProvider.file); fileProvider + case x => x + } + } + + /** + * Returns the absolute path to the file relative to the job command directory. + * @param file File to root relative to the command directory if it is not already absolute. + * @return The absolute path to file. + */ + private def absolute(file: File) = IOUtils.subDir(commandDirectory, file) + /** * Repeats parameters with a prefix/suffix if they are set otherwise returns "". * Skips null, Nil, None. Unwraps Some(x) to x. Everything else is called with x.toString. + * @param prefix Command line prefix per parameter. + * @param params Traversable parameters. + * @param suffix Optional suffix per parameter. + * @param separator Optional separator per parameter. + * @param format Format string if the value has a value + * @return The generated string */ - protected def repeat(prefix: String, params: Seq[_], suffix: String = "", separator: String = "") = - params.filter(param => hasValue(param)).map(param => prefix + toValue(param) + suffix).mkString(separator) + protected def repeat(prefix: String, params: Traversable[_], suffix: String = "", separator: String = "", format: String = "%s") = + params.filter(param => hasValue(param)).map(param => prefix + toValue(param, format) + suffix).mkString(separator) /** * Returns parameter with a prefix/suffix if it is set otherwise returns "". * Does not output null, Nil, None. Unwraps Some(x) to x. Everything else is called with x.toString. + * @param prefix Command line prefix per parameter. + * @param param Parameters to check for a value. + * @param suffix Optional suffix per parameter. + * @param format Format string if the value has a value + * @return The generated string */ - protected def optional(prefix: String, param: Any, suffix: String = "") = - if (hasValue(param)) prefix + toValue(param) + suffix else "" + protected def optional(prefix: String, param: Any, suffix: String = "", format: String = "%s") = + if (hasValue(param)) prefix + toValue(param, format) + suffix else "" - def missingValues = { + /** + * Returns fields that do not have values which are required. + * @return List[String] names of fields missing values. + */ + def missingFields: List[String] = { val missingInputs = missingFields(inputFields, classOf[Input]) val missingOutputs = missingFields(outputFields, classOf[Output]) - missingInputs | missingOutputs + val missingArguments = missingFields(argumentFields, classOf[Argument]) + (missingInputs | missingOutputs | missingArguments).toList.sorted } - private def missingFields(fields: List[Field], annotation: Class[_ <: Annotation]) = { + /** + * Returns fields that do not have values which are required. + * @param sources Fields to check. + * @param annotation Annotation. + * @return Set[String] names of fields missing values. + */ + private def missingFields(sources: List[ArgumentSource], annotation: Class[_ <: Annotation]): Set[String] = { var missing = Set.empty[String] - for (field <- fields) { - if (isRequired(field, annotation)) - if (!hasValue(ReflectionUtils.getValue(this, field))) - missing += field.getName + for (source <- sources) { + if (isRequired(source, annotation)) + if (!hasFieldValue(source)) + if (!exclusiveOf(source, annotation).exists(otherSource => hasFieldValue(otherSource))) + missing += "@%s: %s - %s".format(annotation.getSimpleName, source.field.getName, doc(source, annotation)) } missing } - private def isRequired(field: Field, annotationClass: Class[_ <: Annotation]) = - getAnnotationValue(field.getAnnotation(annotationClass), "required").asInstanceOf[Boolean] - - private def getAnnotationValue(annotation: Annotation, method: String) = - annotation.getClass.getMethod(method).invoke(annotation) - - protected def hasFieldValue(field: Field) = hasValue(this.getFieldValue(field)) - - private def hasValue(param: Any) = param match { - case null => false - case Nil => false - case None => false - case _ => true + /** + * Scala sugar type for checking annotation required and exclusiveOf. + */ + private type ArgumentAnnotation = { + /** + * Returns true if the field is required. + * @return true if the field is required. + */ + def required(): Boolean + /** + * Returns the comma separated list of fields that may be set instead of this field. + * @return the comma separated list of fields that may be set instead of this field. + */ + def exclusiveOf(): String + /** + * Returns the documentation for this field. + * @return the documentation for this field. + */ + def doc(): String } - private def toValue(param: Any): String = param match { - case null => "" - case Nil => "" - case None => "" - case Some(x) => x.toString - case x => x.toString + /** + * Returns the isRequired value from the field. + * @param field Field to check. + * @param annotation Annotation. + * @return the isRequired value from the field annotation. + */ + private def isRequired(field: ArgumentSource, annotation: Class[_ <: Annotation]) = + ReflectionUtils.getAnnotation(field.field, annotation).asInstanceOf[ArgumentAnnotation].required + + /** + * Returns an array of ArgumentSources from functionFields listed in the exclusiveOf of the original field + * @param field Field to check. + * @param annotation Annotation. + * @return the Array[ArgumentSource] that may be set instead of the field. + */ + private def exclusiveOf(field: ArgumentSource, annotation: Class[_ <: Annotation]) = + ReflectionUtils.getAnnotation(field.field, annotation).asInstanceOf[ArgumentAnnotation].exclusiveOf + .split(",").map(_.trim).filter(_.length > 0) + .map(fieldName => functionFields.find(fieldName == _.field.getName) match { + case Some(x) => x + case None => throw new QException("Unable to find exclusion field %s on %s".format(fieldName, this.getClass.getSimpleName)) + }) + + /** + * Returns the doc value from the field. + * @param field Field to check. + * @param annotation Annotation. + * @return the doc value from the field annotation. + */ + private def doc(field: ArgumentSource, annotation: Class[_ <: Annotation]) = + ReflectionUtils.getAnnotation(field.field, annotation).asInstanceOf[ArgumentAnnotation].doc + + /** + * Returns true if the field has a value. + * @param source Field to check for a value. + * @return true if the field has a value. + */ + protected def hasFieldValue(source: ArgumentSource) = this.hasValue(this.getFieldValue(source)) + + /** + * Returns false if the value is null or an empty collection. + * @param value Value to test for null, or a collection to test if it is empty. + * @return false if the value is null, or false if the collection is empty, otherwise true. + */ + private def hasValue(param: Any) = CollectionUtils.isNotNullOrNotEmpty(param) + + /** + * Returns "" if the value is null or an empty collection, otherwise return the value.toString. + * @param value Value to test for null, or a collection to test if it is empty. + * @param format Format string if the value has a value + * @return "" if the value is null, or "" if the collection is empty, otherwise the value.toString. + */ + private def toValue(param: Any, format: String): String = if (CollectionUtils.isNullOrEmpty(param)) "" else + param match { + case Some(x) => format.format(x) + case x => format.format(x) + } + + /** + * Gets the value of a field. + * @param source Field to get the value for. + * @return value of the field. + */ + def getFieldValue(source: ArgumentSource) = ReflectionUtils.getValue(invokeObj(source), source.field) + + /** + * Gets the value of a field. + * @param source Field to set the value for. + * @return value of the field. + */ + def setFieldValue(source: ArgumentSource, value: Any) = ReflectionUtils.setValue(invokeObj(source), source.field, value) + + /** + * Walks gets the fields in this object or any collections in that object + * recursively to find the object holding the field to be retrieved or set. + * @param source Field find the invoke object for. + * @return Object to invoke the field on. + */ + private def invokeObj(source: ArgumentSource) = source.parentFields.foldLeft[AnyRef](this)(ReflectionUtils.getValue(_, _)) +} + +/** + * A command line that will be run in a pipeline. + */ +object CommandLineFunction { + /** A semi-unique job prefix using the host name and the process id. */ + private val processNamePrefix = "Q-" + { + var prefix = ManagementFactory.getRuntimeMXBean.getName + val index = prefix.indexOf(".") + if (index >= 0) + prefix = prefix.substring(0, index) + prefix + } + + /** Job index counter for this run of Queue. */ + private var jobIndex = 0 + + /** + * Returns the next job name using the prefix. + * @param prefix Prefix of the job name. + * @return the next job name. + */ + private def nextJobName(prefix: String) = { + jobIndex += 1 + prefix + "-" + jobIndex } } diff --git a/scala/src/org/broadinstitute/sting/queue/function/DispatchFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/DispatchFunction.scala deleted file mode 100644 index eb70e31f7..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/DispatchFunction.scala +++ /dev/null @@ -1,93 +0,0 @@ -package org.broadinstitute.sting.queue.function - -import java.io.File -import java.lang.management.ManagementFactory -import org.broadinstitute.sting.queue.function.scattergather.{Gather, SimpleTextGatherFunction} -import org.broadinstitute.sting.queue.util.IOUtils -import org.broadinstitute.sting.commandline.{ClassType, Output, Input} - -trait DispatchFunction extends InputOutputFunction { - def commandLine: String - - @Input(doc="Upper memory limit", required=false) - @ClassType(classOf[Int]) - var memoryLimit: Option[Int] = None - - /** - * The directory where the command should run. - */ - @Input(doc="Directory to write any files", required=false) - var commandDirectory: File = IOUtils.CURRENT_DIR - - @Input(doc="Prefix for automatic job name creation", required=false) - var jobNamePrefix: String = _ - - @Input(doc="Job name to run on the farm", required=false) - var jobName: String = _ - - @Output(doc="File to redirect any output", required=false) - @Gather(classOf[SimpleTextGatherFunction]) - var jobOutputFile: File = _ - - @Output(doc="File to redirect any errors", required=false) - @Gather(classOf[SimpleTextGatherFunction]) - var jobErrorFile: File = _ - - @Input(doc="Job project to run the command", required=false) - var jobProject = "Queue" - - @Input(doc="Job queue to run the command", required=false) - var jobQueue = "broad" - - override def freeze = { - if (jobNamePrefix == null) - jobNamePrefix = DispatchFunction.processNamePrefix - - if (jobName == null) - jobName = DispatchFunction.nextJobName(jobNamePrefix) - - if (jobOutputFile == null) - jobOutputFile = new File(jobName + ".out") - - if (jobErrorFile == null) - jobErrorFile = new File(jobName + ".err") - - commandDirectory = IOUtils.absolute(IOUtils.CURRENT_DIR, commandDirectory) - - super.freeze - } - - override def dotString = jobName + " => " + commandLine - - /** - * Override the canon function to change any relative path to an absolute path. - */ - override protected def canon(value: Any) = { - value match { - case file: File => IOUtils.absolute(commandDirectory, file) - case x => super.canon(x) - } - } - - def absolute(file: File) = IOUtils.absolute(commandDirectory, file) - def temp(subDir: String) = IOUtils.sub(commandDirectory, jobName + "-" + subDir) - - override def toString = commandLine -} - -object DispatchFunction { - private val processNamePrefix = "Q-" + { - var prefix = ManagementFactory.getRuntimeMXBean.getName - val index = prefix.indexOf(".") - if (index >= 0) - prefix = prefix.substring(0, index) - prefix - } - - private var jobIndex = 0 - - private def nextJobName(prefix: String) = { - jobIndex += 1 - prefix + "-" + jobIndex - } -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/DispatchWaitFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/DispatchWaitFunction.scala index 6bcafa87a..83e1557ea 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/DispatchWaitFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/DispatchWaitFunction.scala @@ -2,10 +2,14 @@ package org.broadinstitute.sting.queue.function import java.io.File +/** An internal class that is used by bsub to wait on all other jobs before exiting. */ class DispatchWaitFunction extends CommandLineFunction { + /** + * Returns the command line "echo". + * @return echo + */ def commandLine = "echo" jobQueue = "short" jobOutputFile = File.createTempFile("Q-wait", ".out") - jobErrorFile = File.createTempFile("Q-wait", ".err") } diff --git a/scala/src/org/broadinstitute/sting/queue/function/FileProvider.scala b/scala/src/org/broadinstitute/sting/queue/function/FileProvider.scala new file mode 100644 index 000000000..b139cfff6 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/function/FileProvider.scala @@ -0,0 +1,11 @@ +package org.broadinstitute.sting.queue.function + +import java.io.File + +/** + * An trait for @Input or @Output CommandLineFunction fields that are not files, but have a File that can be get/set. + */ +trait FileProvider { + /** Gets/Sets the file. */ + var file: File +} diff --git a/scala/src/org/broadinstitute/sting/queue/function/InputOutputFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/InputOutputFunction.scala deleted file mode 100644 index 5d686437b..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/InputOutputFunction.scala +++ /dev/null @@ -1,67 +0,0 @@ -package org.broadinstitute.sting.queue.function - -import java.lang.reflect.Field -import org.broadinstitute.sting.queue.util._ -import org.broadinstitute.sting.commandline.{Input, Output} - -/** - * A function with @Inputs and @Outputs tagging fields that can be set by the user in a QScript - */ -trait InputOutputFunction extends QFunction with Cloneable { - def getFieldValue(field: Field) = ReflectionUtils.getValue(this, field) - def setFieldValue(field: Field, value: Any) = ReflectionUtils.setValue(this, field, value) - - def functionFields: List[Field] = inputFields ::: outputFields - def inputFields = ReflectionUtils.filterFields(fields, classOf[Input]) - def outputFields = ReflectionUtils.filterFields(fields, classOf[Output]) - - private lazy val fields = ReflectionUtils.getAllFields(this.getClass) - // TODO: Need to handle argument collections where field is not on THIS - def inputs = CollectionUtils.removeNullOrEmpty(ReflectionUtils.getFieldValues(this, inputFields)).toSet - def outputs = CollectionUtils.removeNullOrEmpty(ReflectionUtils.getFieldValues(this, outputFields)).toSet - - /** - * Sets a field value using the name of the field. - * Field must be annotated with @Input or @Output - * @return true if the value was found and set - */ - protected def addOrUpdateWithStringValue(name: String, value: String) = { - fields.find(_.getName == name) match { - case Some(field) => - val isInput = ReflectionUtils.hasAnnotation(field, classOf[Input]) - val isOutput = ReflectionUtils.hasAnnotation(field, classOf[Output]) - if (isInput || isOutput) { - ReflectionUtils.addOrUpdateWithStringValue(this, field, value) - } - true - // TODO: Need to handle argument collections where field is not on THIS - case None => false - } - } - - def cloneFunction() = clone.asInstanceOf[this.type] - // explicitly overriden so that trait function cloneFunction can use this.clone - override protected def clone = super.clone - - /** - * As the function is frozen, changes all fields to their canonical forms. - */ - override def freeze = { - for (field <- this.functionFields) - mapField(field, canon) - super.freeze - } - - def mapField(field: Field, f: Any => Any): Any = { - var fieldValue = this.getFieldValue(field) - fieldValue = CollectionUtils.updated(fieldValue, f).asInstanceOf[AnyRef] - this.setFieldValue(field, fieldValue) - fieldValue - } - - /** - * Set value to a uniform value across functions. - * The biggest example is file paths relative to the command directory in DispatchFunction - */ - protected def canon(value: Any): Any = value -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/IntervalFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/IntervalFunction.scala deleted file mode 100644 index e525f58b6..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/IntervalFunction.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.broadinstitute.sting.queue.function - -import java.io.File - -trait IntervalFunction extends InputOutputFunction { - var referenceFile: File - var intervals: File -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/JarCommandLineFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/JarCommandLineFunction.scala new file mode 100644 index 000000000..29d5d3ca7 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/function/JarCommandLineFunction.scala @@ -0,0 +1,15 @@ +package org.broadinstitute.sting.queue.function + +import org.broadinstitute.sting.commandline.Argument +import java.io.File + +/** + * Defines a command line function that runs from a jar file. + */ +trait JarCommandLineFunction extends CommandLineFunction { + @Argument(doc="jar") + var jarFile: File = _ + + def commandLine = "java%s -Djava.io.tmpdir=%s -jar %s" + .format(optional(" -Xmx", memoryLimit, "g"), jobTempDir, jarFile) +} diff --git a/scala/src/org/broadinstitute/sting/queue/function/MappingFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/MappingFunction.scala index cd4b14246..a1d28df21 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/MappingFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/MappingFunction.scala @@ -1,9 +1,15 @@ package org.broadinstitute.sting.queue.function +import java.io.File + /** * Utility class to map a set of inputs to set of outputs. * The QGraph uses this function internally to map between user defined functions. */ -class MappingFunction(val inputs: Set[Any], val outputs: Set[Any]) extends QFunction { - override def toString = "" // For debugging +class MappingFunction(val inputs: Set[File], val outputs: Set[File]) extends QFunction { + /** + * For debugging purposes returns . + * @returns + */ + override def toString = "" } diff --git a/scala/src/org/broadinstitute/sting/queue/function/QFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/QFunction.scala index 491e4c887..68a4bf4bc 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/QFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/QFunction.scala @@ -1,5 +1,7 @@ package org.broadinstitute.sting.queue.function +import java.io.File + /** * The base interface for all functions in Queue. * Inputs and outputs are specified as Sets of values. @@ -16,12 +18,15 @@ trait QFunction { /** * Set of inputs for this function. */ - def inputs: Set[Any] + def inputs: Set[File] /** * Set of outputs for this function. */ - def outputs: Set[Any] + def outputs: Set[File] + /** + * The function description in .dot files + */ def dotString = "" } diff --git a/scala/src/org/broadinstitute/sting/queue/function/gatk/GatkFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/gatk/GatkFunction.scala deleted file mode 100644 index b509fceb9..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/gatk/GatkFunction.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.broadinstitute.sting.queue.function.gatk - -import java.io.File -import org.broadinstitute.sting.queue.function.IntervalFunction -import org.broadinstitute.sting.queue.function.scattergather.{Scatter, ScatterGatherableFunction, IntervalScatterFunction} -import org.broadinstitute.sting.commandline.{ClassType, Input} -import org.apache.log4j.Level - -trait GatkFunction extends ScatterGatherableFunction with IntervalFunction { - @Input(doc="Temporary directory to write any files", required=false) - var javaTmpDir: String = _ - - @Input(doc="GATK jar") - var gatkJar: String = _ - - @Input(doc="Reference fasta") - var referenceFile: File = _ - - @Input(doc="Bam files", required=false) - @ClassType(classOf[File]) - var bamFiles: List[File] = Nil - - @Input(doc="Intervals", required=false) - @Scatter(classOf[IntervalScatterFunction]) - var intervals: File = _ - - @Input(doc="DBSNP", required=false) - var dbsnp: File = _ - - @Input(doc="Logging level", required=false) - var gatkLoggingLevel: String = _ - - protected def gatkCommandLine(walker: String) = - "java%s%s -jar %s -T %s -R %s%s%s%s%s " - .format(optional(" -Xmx", memoryLimit, "g"), optional(" -Djava.io.tmpdir=", javaTmpDir), - gatkJar, walker, referenceFile, repeat(" -I ", bamFiles), optional(" -l ", gatkLoggingLevel), - optional(" -D ", dbsnp), optional(" -L ", intervals)) -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/BamGatherFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/BamGatherFunction.scala deleted file mode 100644 index b433ee10e..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/BamGatherFunction.scala +++ /dev/null @@ -1,17 +0,0 @@ -package org.broadinstitute.sting.queue.function.scattergather - -import java.io.File -import org.broadinstitute.sting.commandline.Input - -class BamGatherFunction extends GatherFunction { - type GatherType = File - - @Input(doc="Picard MergeSamFiles.jar. At the Broad this can be found at /seq/software/picard/current/bin/MergeSamFiles.jar. Outside the broad see http://picard.sourceforge.net/") - var picardMergeSamFilesJar: String = _ - - @Input(doc="Compression level 1-9", required=false) - var picardMergeCompressionLevel: Option[Int] = None - - def commandLine = "java -jar %s%s%s%s".format(picardMergeSamFilesJar, - optional(" COMPRESSION_LEVEL=", picardMergeCompressionLevel), " AS=true VALIDATION_STRINGENCY=SILENT SO=coordinate OUTPUT=" + originalOutput, repeat(" INPUT=", gatherParts)) -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/CleanupTempDirsFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/CleanupTempDirsFunction.scala index a3ebc953b..cd6b9bf38 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/CleanupTempDirsFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/scattergather/CleanupTempDirsFunction.scala @@ -1,15 +1,24 @@ package org.broadinstitute.sting.queue.function.scattergather import org.broadinstitute.sting.queue.function.CommandLineFunction -import org.broadinstitute.sting.commandline.Input import java.io.File +import org.broadinstitute.sting.commandline.{Argument, Input} +/** + * Removes the temporary directories for scatter / gather. + * The script can be changed by setting rmdirScript. + * By default uses rm -rf. + * The format of the call is [.. ] + */ class CleanupTempDirsFunction extends CommandLineFunction { @Input(doc="Original outputs of the gather functions") - var originalOutputs: Set[Any] = Set.empty[Any] + var originalOutputs: Set[File] = Set.empty[File] @Input(doc="Temporary directories to be deleted") var tempDirectories: List[File] = Nil - def commandLine = "rm -rf%s".format(repeat(" '", tempDirectories, "'")) + @Argument(doc="rmdir script or command") + var rmdirScript = "rm -rf" + + def commandLine = "%s%s".format(rmdirScript, repeat(" '", tempDirectories, "'")) } diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/ContigScatterFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/ContigScatterFunction.scala deleted file mode 100755 index 613c17e35..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/ContigScatterFunction.scala +++ /dev/null @@ -1,21 +0,0 @@ -package org.broadinstitute.sting.queue.function.scattergather - -import java.io.File -import org.broadinstitute.sting.commandline.Input -import org.broadinstitute.sting.queue.function.IntervalFunction - -class ContigScatterFunction extends ScatterFunction { - type ScatterType = File - - @Input(doc="Reference file to scatter") - var referenceFile: File = _ - - override def setOriginalFunction(originalFunction: ScatterGatherableFunction) = { - val command = originalFunction.asInstanceOf[IntervalFunction] - referenceFile = command.referenceFile - super.setOriginalFunction(originalFunction) - } - - // TODO: Use the reference file for "all" - def commandLine = "splitIntervalsByContig.py %s%s".format(originalInput, repeat(" ", scatterParts)) -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/CreateTempDirsFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/CreateTempDirsFunction.scala index de1a16652..67161169b 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/CreateTempDirsFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/scattergather/CreateTempDirsFunction.scala @@ -2,25 +2,28 @@ package org.broadinstitute.sting.queue.function.scattergather import java.io.File import org.broadinstitute.sting.queue.function.CommandLineFunction -import org.broadinstitute.sting.commandline.{Output, Input} +import org.broadinstitute.sting.commandline.{Argument, Output, Input} +/** + * Creates the temporary directories for scatter / gather. + * The script can be changed by setting mkdirScript. + * By default uses mkdir -pv + * The format of the call is [.. ] + */ class CreateTempDirsFunction extends CommandLineFunction { @Input(doc="Original inputs to the scattered function") - var originalInputs: Set[Any] = Set.empty[Any] + var originalInputs: Set[File] = Set.empty[File] @Output(doc="Temporary directories to create") var tempDirectories: List[File] = Nil - @Input(doc="Sleep seconds", required=false) - var mkdirSleepSeconds: Option[Int] = None + @Argument(doc="mkdir script or command") + var mkdirScript = "mkdir -pv" - // TODO: After port of LSF submitter use -cwd

instead of trying to run from the directory - // For now, create the directory so that BroadCore can run bsub from it -kshakir July 27, 2010 on chartl's computer + def commandLine = "%s%s".format(mkdirScript, repeat(" '", tempDirectories, "'")) - override def freeze = { - super.freeze - tempDirectories.foreach(_.mkdirs) - } - - def commandLine = "mkdir -pv%s%s".format(repeat(" '", tempDirectories, "'"), optional(" && sleep ", mkdirSleepSeconds)) + /** + * This function is creating the directories, so returns just this command directory. + */ + override def jobDirectories = Set(commandDirectory) } diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/FixMatesGatherFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/FixMatesGatherFunction.scala deleted file mode 100644 index 6a36236ce..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/FixMatesGatherFunction.scala +++ /dev/null @@ -1,17 +0,0 @@ -package org.broadinstitute.sting.queue.function.scattergather - -import java.io.File -import org.broadinstitute.sting.commandline.Input - -class FixMatesGatherFunction extends GatherFunction { - type GatherType = File - - @Input(doc="Picard FixMateInformation.jar. At the Broad this can be found at /seq/software/picard/current/bin/FixMateInformation.jar. Outside the broad see http://picard.sourceforge.net/") - var picardFixMatesJar: String = _ - - @Input(doc="Compression level 1-9", required=false) - var picardMergeCompressionLevel: Option[Int] = None - - def commandLine = "java -Djava.io.tmpdir=/broad/shptmp/queue -jar %s%s%s%s".format(picardFixMatesJar, - optional(" COMPRESSION_LEVEL=", picardMergeCompressionLevel), " VALIDATION_STRINGENCY=SILENT SO=coordinate OUTPUT=" + originalOutput, repeat(" INPUT=", gatherParts)) -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/GatherFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/GatherFunction.scala index 3bced51ee..f5886865a 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/GatherFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/scattergather/GatherFunction.scala @@ -1,20 +1,31 @@ package org.broadinstitute.sting.queue.function.scattergather import org.broadinstitute.sting.queue.function.{CommandLineFunction} -import org.broadinstitute.sting.commandline.{Input, Output} +import java.io.File +import org.broadinstitute.sting.commandline.{ArgumentSource, Input, Output} /** * Base class for Gather command line functions. - * NOTE: Using an abstract class instead of a trait due to scala parameterized type erasure on traits. */ -abstract class GatherFunction extends CommandLineFunction { - type GatherType - +trait GatherFunction extends CommandLineFunction { @Input(doc="Parts to gather back into the original output") - var gatherParts: List[GatherType] = Nil + var gatherParts: List[File] = Nil @Output(doc="The original output of the scattered function") - var originalOutput: GatherType = _ + var originalOutput: File = _ - def setOriginalFunction(originalFunction: ScatterGatherableFunction) = {} + /** + * Sets the original function used to create this scatter function. + * @param originalFunction The ScatterGatherableFunction. + * @param gatherField The field being gathered. + */ + def setOriginalFunction(originalFunction: ScatterGatherableFunction, gatherField: ArgumentSource) = {} + + /** + * Sets the clone function creating one of the inputs for this gather function. + * @param cloneFunction The clone of the ScatterGatherableFunction. + * @param index The one based index (from 1..scatterCount inclusive) of the scatter piece. + * @param gatherField The field to be gathered. + */ + def setCloneFunction(cloneFunction: ScatterGatherableFunction, index: Int, gatherField: ArgumentSource) = {} } diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/IntervalScatterFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/IntervalScatterFunction.scala deleted file mode 100644 index 8408622c0..000000000 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/IntervalScatterFunction.scala +++ /dev/null @@ -1,21 +0,0 @@ -package org.broadinstitute.sting.queue.function.scattergather - -import java.io.File -import org.broadinstitute.sting.commandline.Input -import org.broadinstitute.sting.queue.function.IntervalFunction - -class IntervalScatterFunction extends ScatterFunction { - type ScatterType = File - - @Input(doc="Reference file to scatter") - var referenceFile: File = _ - - override def setOriginalFunction(originalFunction: ScatterGatherableFunction) = { - val command = originalFunction.asInstanceOf[IntervalFunction] - referenceFile = command.referenceFile - super.setOriginalFunction(originalFunction) - } - - // TODO: Use the reference file for "all" - def commandLine = "splitIntervals.sh %s%s".format(originalInput, repeat(" ", scatterParts)) -} diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterFunction.scala index 05320ccb8..b0a8ab794 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterFunction.scala @@ -2,23 +2,33 @@ package org.broadinstitute.sting.queue.function.scattergather import org.broadinstitute.sting.queue.function.CommandLineFunction import java.io.File -import org.broadinstitute.sting.commandline.{Input, Output} +import org.broadinstitute.sting.commandline.{ArgumentSource, Input, Output} /** * Base class for Scatter command line functions. - * NOTE: Using an abstract class instead of a trait due to scala parameterized type erasure on traits. */ -abstract class ScatterFunction extends CommandLineFunction { - type ScatterType - +trait ScatterFunction extends CommandLineFunction { @Input(doc="Original input to scatter") - var originalInput: ScatterType = _ + var originalInput: File = _ + + @Output(doc="Scattered parts of the original input, one per temp directory") + var scatterParts: List[File] = Nil @Input(doc="Temporary directories for each scatter part") var tempDirectories: List[File] = Nil - @Output(doc="Scattered parts of the original input, one per temp directory") - var scatterParts: List[ScatterType] = Nil + /** + * Sets the original function used to create this scatter function. + * @param originalFunction The ScatterGatherableFunction. + * @param scatterField The field being scattered. + */ + def setOriginalFunction(originalFunction: ScatterGatherableFunction, scatterField: ArgumentSource) = {} - def setOriginalFunction(originalFunction: ScatterGatherableFunction) = {} + /** + * Sets the clone function using one of the outputs of this scatter function. + * @param cloneFunction The clone of the ScatterGatherableFunction. + * @param index The one based index (from 1..scatterCount inclusive) of the scatter piece. + * @param scatterField The field being scattered. + */ + def setCloneFunction(cloneFunction: ScatterGatherableFunction, index: Int, scatterField: ArgumentSource) = {} } diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterGatherableFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterGatherableFunction.scala index a70263dd0..aa54f5672 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterGatherableFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/scattergather/ScatterGatherableFunction.scala @@ -1,141 +1,367 @@ package org.broadinstitute.sting.queue.function.scattergather -import org.broadinstitute.sting.queue.function.CommandLineFunction -import java.lang.reflect.Field import java.io.File import org.broadinstitute.sting.queue.util._ -import org.broadinstitute.sting.commandline.Input +import org.broadinstitute.sting.commandline.ArgumentSource +import org.broadinstitute.sting.queue.function.CommandLineFunction +import com.rits.cloning.Cloner +/** + * A function that can be run faster by splitting it up into pieces and then joining together the results. + */ trait ScatterGatherableFunction extends CommandLineFunction { - @Input(doc="Number of parts to scatter the function into") + /** Number of parts to scatter the function into" */ var scatterCount: Int = 1 - def scatterField = this.inputFields.find(field => ReflectionUtils.hasAnnotation(field, classOf[Scatter])).get + /** scatter gather directory */ + var scatterGatherDirectory: File = _ - def scatterGatherable = { - if (scatterCount < 2) - false - else if (!hasFieldValue(scatterField)) - false - else - true - } + /** cleanup temporary directories */ + var cleanupTempDirectories = false - def generateFunctions() = ScatterGatherableFunction.generateFunctions(this) -} + /** Class to use for creating temporary directories. Defaults to CreateTempDirsFunction. */ + var createTempDirsClass: Class[_ <: CreateTempDirsFunction] = _ -object ScatterGatherableFunction { - private def generateFunctions(originalFunction: ScatterGatherableFunction) = { + /** Class to use for scattering. Defaults to the annotation used in the @Scatter tag. */ + var scatterClass: Class[_ <: ScatterFunction] = _ + + /** + * Function that returns the class to use for gathering a directory. If it returns null then @Gather annotation will be used. + * @param gatherField Field that is to be gathered. + * @return The class of the GatherFunction to be used or null. + */ + var gatherClass: PartialFunction[ArgumentSource, Class[_ <: GatherFunction]] = _ + + /** Class to use for removing temporary directories. Defaults to CleanupTempDirsFunction. */ + var cleanupTempDirsClass: Class[_ <: CleanupTempDirsFunction] = _ + + /** + * Allows external modification of the CreateTempDirsFunction that will create the temporary directories. + * @param initializeFunction The function that will create the temporary directories. + * @param inputFields The input fields that the original function was dependent on. + */ + var setupInitializeFunction: PartialFunction[(CreateTempDirsFunction, List[ArgumentSource]), Unit] = _ + + /** + * Allows external modification of the ScatterFunction that will create the scatter pieces in the temporary directories. + * @param scatterFunction The function that will create the scatter pieces in the temporary directories. + * @param scatterField The input field being scattered. + */ + var setupScatterFunction: PartialFunction[(ScatterFunction, ArgumentSource), Unit] = _ + + /** + * Allows external modification of the GatherFunction that will collect the gather pieces in the temporary directories. + * @param gatherFunction The function that will merge the gather pieces from the temporary directories. + * @param gatherField The output field being gathered. + */ + var setupGatherFunction: PartialFunction[(GatherFunction, ArgumentSource), Unit] = _ + + /** + * Allows external modification of the cloned function. + * @param cloneFunction The clone of this ScatterGatherableFunction + * @param index The one based index (from 1..scatterCount inclusive) of the scatter piece. + */ + var setupCloneFunction: PartialFunction[(ScatterGatherableFunction, Int), Unit] = _ + + /** + * Allows external modification of the CleanupTempDirsFunction that will remove the temporary directories. + * @param cleanupFunction The function that will remove the temporary directories. + * @param gatherFunctions The functions that will gather up the original output fields. + * @param outputFields The output fields that the original function was dependent on. + */ + var setupCleanupFunction: PartialFunction[(CleanupTempDirsFunction, Map[ArgumentSource, GatherFunction], List[ArgumentSource]), Unit] = _ + + /** + * Returns true if the function is ready to be scatter / gathered. + * The base implementation checks if the scatter count is greater than one, + * and that the scatter field has a value. + * @return true if the function is ready to be scatter / gathered. + */ + def scatterGatherable = this.scatterCount > 1 && hasFieldValue(this.scatterField) + + /** + * Returns a list of scatter / gather and clones of this function + * that can be run in parallel to produce the same output as this + * command line function. + * @return List[CommandLineFunction] to run instead of this function. + */ + def generateFunctions() = { var functions = List.empty[CommandLineFunction] var tempDirectories = List.empty[File] - // Create a function that will remove any temporary items - var cleanupFunction = new CleanupTempDirsFunction - cleanupFunction.properties = originalFunction.properties - cleanupFunction.jobNamePrefix = originalFunction.jobNamePrefix - cleanupFunction.commandDirectory = originalFunction.commandDirectory - - // Find the field with @Scatter and its value - var scatterField = originalFunction.scatterField - val originalValue = originalFunction.getFieldValue(scatterField) + // Only depend on input fields that have a value + val inputFieldsWithValues = this.inputFields.filter(hasFieldValue(_)) + // Only gather up fields that will have a value + val outputFieldsWithValues = this.outputFields.filter(hasFieldValue(_)) // Create the scatter function based on @Scatter - val scatterFunction = getScatterFunction(scatterField) - scatterFunction.setOriginalFunction(originalFunction) - scatterFunction.properties = originalFunction.properties - scatterFunction.jobNamePrefix = originalFunction.jobNamePrefix - scatterFunction.commandDirectory = originalFunction.temp("scatter-" + scatterField.getName) - scatterFunction.originalInput = originalValue.asInstanceOf[scatterFunction.ScatterType] + val scatterFunction = this.newScatterFunction(this.scatterField) + initScatterFunction(scatterFunction, this.scatterField) tempDirectories :+= scatterFunction.commandDirectory functions :+= scatterFunction // Create the gather functions for each output field - var gatherFunctions = Map.empty[Field, GatherFunction] - for (outputField <- originalFunction.outputFieldsWithValues) { - - // Create the gather function based on @Gather - val gatherFunction = getGatherFunction(outputField) - gatherFunction.setOriginalFunction(originalFunction) - gatherFunction.properties = originalFunction.properties - gatherFunction.jobNamePrefix = originalFunction.jobNamePrefix - gatherFunction.commandDirectory = originalFunction.temp("gather-" + outputField.getName) - - val gatheredValue = originalFunction.getFieldValue(outputField).asInstanceOf[gatherFunction.GatherType] - gatherFunction.originalOutput = gatheredValue - + var gatherFunctions = Map.empty[ArgumentSource, GatherFunction] + for (gatherField <- outputFieldsWithValues) { + val gatherFunction = this.newGatherFunction(gatherField) + initGatherFunction(gatherFunction, gatherField) tempDirectories :+= gatherFunction.commandDirectory - cleanupFunction.originalOutputs += gatheredValue - functions :+= gatherFunction - - gatherFunctions += outputField -> gatherFunction + gatherFunctions += gatherField -> gatherFunction } // Create the clone functions for running the parallel jobs var cloneFunctions = List.empty[CommandLineFunction] - for (i <- 1 to originalFunction.scatterCount) { - val cloneFunction = newFunctionClone(originalFunction) + for (i <- 1 to this.scatterCount) { + val cloneFunction = this.newCloneFunction() + initCloneFunction(cloneFunction, i) cloneFunctions :+= cloneFunction + tempDirectories :+= cloneFunction.commandDirectory - val tempDir = originalFunction.temp("temp-"+i) - cloneFunction.commandDirectory = tempDir - tempDirectories :+= tempDir - - // Reset the input of the clone to the the temp dir and add it as an output of the scatter - var scatterPart = CollectionUtils.updated(originalValue, resetToTempDir(tempDir)) - scatterFunction.scatterParts :+= scatterPart.asInstanceOf[scatterFunction.ScatterType] - cloneFunction.setFieldValue(scatterField, scatterPart) - - // For each each output field, change value to the temp dir and feed it into the gatherer - for (outputField <- originalFunction.outputFields) { - val gatherFunction = gatherFunctions(outputField) - val gatherPart = cloneFunction.mapField(outputField, resetToTempDir(tempDir)) - gatherFunction.gatherParts :+= gatherPart.asInstanceOf[gatherFunction.GatherType] - } + bindCloneFunctionScatter(scatterFunction, this.scatterField, cloneFunction, i) + // For each each output field, change value to the scatterGatherTempDir dir and feed it into the gatherer + for (gatherField <- outputFieldsWithValues) + bindCloneFunctionGather(gatherFunctions(gatherField), gatherField, cloneFunction, i) } - functions = cloneFunctions ::: functions + functions ++= cloneFunctions - // Create a function to create all of the temp directories. + // Create a function to create all of the scatterGatherTempDir directories. // All of its inputs are the inputs of the original function. - val initializeFunction = new CreateTempDirsFunction - initializeFunction.properties = originalFunction.properties - initializeFunction.jobNamePrefix = originalFunction.jobNamePrefix - initializeFunction.commandDirectory = originalFunction.commandDirectory + val initializeFunction = this.newInitializeFunction() + initInitializeFunction(initializeFunction, inputFieldsWithValues) - for (inputField <- originalFunction.inputFieldsWithValues) - initializeFunction.originalInputs += originalFunction.getFieldValue(inputField) + // Create a function that will remove any temporary items + // All of its inputs are the outputs of the original function. + var cleanupFunction = newCleanupFunction() + initCleanupFunction(cleanupFunction, gatherFunctions, outputFieldsWithValues) + // Set the temporary directories, for the initialize function as outputs for scatter and cleanup as inputs. initializeFunction.tempDirectories = tempDirectories scatterFunction.tempDirectories = tempDirectories cleanupFunction.tempDirectories = tempDirectories functions +:= initializeFunction - functions :+= cleanupFunction + if (this.cleanupTempDirectories) + functions :+= cleanupFunction // Return all the various functions we created functions } - private def resetToTempDir(tempDir: File): Any => Any = { - (any: Any) => { - any match { - case file: File => IOUtils.reset(tempDir, file) - case x => x - } - } + /** + * Sets the scatter gather directory to the command directory if it is not already set. + */ + override def freezeFieldValues = { + super.freezeFieldValues + if (this.scatterGatherDirectory == null) + this.scatterGatherDirectory = this.commandDirectory } - private def getScatterFunction(inputField: Field) = - ReflectionUtils.getAnnotation(inputField, classOf[Scatter]).value.newInstance.asInstanceOf[ScatterFunction] + /** + * Retrieves the scatter field from the first field that has the annotation @Scatter. + */ + protected lazy val scatterField = + this.inputFields.find(field => ReflectionUtils.hasAnnotation(field.field, classOf[Scatter])).get - private def getGatherFunction(outputField: Field) = - ReflectionUtils.getAnnotation(outputField, classOf[Gather]).value.newInstance.asInstanceOf[GatherFunction] + /** + * Creates a new initialize CreateTempDirsFunction that will create the temporary directories. + * @return A CreateTempDirsFunction that will create the temporary directories. + */ + protected def newInitializeFunction(): CreateTempDirsFunction = { + if (createTempDirsClass != null) + this.createTempDirsClass.newInstance + else + new CreateTempDirsFunction + } - private def newFunctionClone(originalFunction: ScatterGatherableFunction) = { - val cloneFunction = originalFunction.cloneFunction.asInstanceOf[ScatterGatherableFunction] + /** + * Initializes the CreateTempDirsFunction that will create the temporary directories. + * The initializeFunction jobNamePrefix is set so that the CreateTempDirsFunction runs with the same prefix as this ScatterGatherableFunction. + * The initializeFunction commandDirectory is set so that the function runs in the directory as this ScatterGatherableFunction. + * The initializeFunction is modified to become dependent on the input files for this ScatterGatherableFunction. + * Calls setupInitializeFunction with initializeFunction. + * @param initializeFunction The function that will create the temporary directories. + * @param inputFields The input fields that the original function was dependent on. + */ + protected def initInitializeFunction(initializeFunction: CreateTempDirsFunction, inputFields: List[ArgumentSource]) = { + initializeFunction.jobNamePrefix = this.jobNamePrefix + initializeFunction.commandDirectory = this.commandDirectory + for (inputField <- inputFields) + initializeFunction.originalInputs ++= this.getFieldFiles(inputField) + if (this.setupInitializeFunction != null) + if (this.setupInitializeFunction.isDefinedAt(initializeFunction, inputFields)) + this.setupInitializeFunction(initializeFunction, inputFields) + } + + /** + * Creates a new ScatterFunction for the scatterField. + * @param scatterField Field that defined @Scatter. + * @return A ScatterFunction instantiated from @Scatter or scatterClass if scatterClass was set on this ScatterGatherableFunction. + */ + protected def newScatterFunction(scatterField: ArgumentSource): ScatterFunction = { + var scatterClass = this.scatterClass + if (scatterClass == null) + scatterClass = ReflectionUtils.getAnnotation(scatterField.field, classOf[Scatter]) + .value.asSubclass(classOf[ScatterFunction]) + scatterClass.newInstance.asInstanceOf[ScatterFunction] + } + + /** + * Initializes the ScatterFunction created by newScatterFunction() that will create the scatter pieces in the temporary directories. + * The scatterFunction jobNamePrefix is set so that the ScatterFunction runs with the same prefix as this ScatterGatherableFunction. + * The scatterFunction commandDirectory is set so that the function runs from a temporary directory under the scatterDirectory. + * The scatterFunction has it's originalInput set with the file to be scattered into scatterCount pieces. + * Calls scatterFunction.setOriginalFunction with this ScatterGatherableFunction. + * Calls setupScatterFunction with scatterFunction. + * @param scatterFunction The function that will create the scatter pieces in the temporary directories. + * @param scatterField The input field being scattered. + */ + protected def initScatterFunction(scatterFunction: ScatterFunction, scatterField: ArgumentSource) = { + scatterFunction.jobNamePrefix = this.jobNamePrefix + scatterFunction.commandDirectory = this.scatterGatherTempDir("scatter-" + scatterField.field.getName) + scatterFunction.originalInput = this.getFieldFile(scatterField) + scatterFunction.setOriginalFunction(this, scatterField) + if (this.setupScatterFunction != null) + if (this.setupScatterFunction.isDefinedAt(scatterFunction, scatterField)) + this.setupScatterFunction(scatterFunction, scatterField) + } + + /** + * Creates a new GatherFunction for the gatherField. + * @param gatherField Field that defined @Gather. + * @return A GatherFunction instantiated from @Gather. + */ + protected def newGatherFunction(gatherField: ArgumentSource) : GatherFunction = { + var gatherClass: Class[_ <: GatherFunction] = null + if (this.gatherClass != null) + if (this.gatherClass.isDefinedAt(gatherField)) + gatherClass = this.gatherClass(gatherField) + if (gatherClass == null) + gatherClass = ReflectionUtils.getAnnotation(gatherField.field, classOf[Gather]) + .value.asSubclass(classOf[GatherFunction]) + gatherClass.newInstance.asInstanceOf[GatherFunction] + } + + /** + * Initializes the GatherFunction created by newGatherFunction() that will collect the gather pieces in the temporary directories. + * The gatherFunction jobNamePrefix is set so that the GatherFunction runs with the same prefix as this ScatterGatherableFunction. + * The gatherFunction commandDirectory is set so that the function runs from a temporary directory under the scatterDirectory. + * The gatherFunction has it's originalOutput set with the file to be gathered from the scatterCount pieces. + * Calls the gatherFunction.setOriginalFunction with this ScatterGatherableFunction. + * Calls setupGatherFunction with gatherFunction. + * @param gatherFunction The function that will merge the gather pieces from the temporary directories. + * @param gatherField The output field being gathered. + */ + protected def initGatherFunction(gatherFunction: GatherFunction, gatherField: ArgumentSource) = { + gatherFunction.jobNamePrefix = this.jobNamePrefix + gatherFunction.commandDirectory = this.scatterGatherTempDir("gather-" + gatherField.field.getName) + gatherFunction.originalOutput = this.getFieldFile(gatherField) + gatherFunction.setOriginalFunction(this, gatherField) + if (this.setupGatherFunction != null) + if (this.setupGatherFunction.isDefinedAt(gatherFunction, gatherField)) + this.setupGatherFunction(gatherFunction, gatherField) + } + + /** + * Creates a new clone of this ScatterGatherableFunction, setting the scatterCount to 1 so it doesn't infinitely scatter. + * @return A clone of this ScatterGatherableFunction + */ + protected def newCloneFunction(): ScatterGatherableFunction = { + val cloneFunction = ScatterGatherableFunction.cloner.deepClone(this) // Make sure clone doesn't get scattered cloneFunction.scatterCount = 1 cloneFunction } + + /** + * Initializes the cloned function created by newCloneFunction() by setting it's commandDirectory to a temporary directory under scatterDirectory. + * Calls setupCloneFunction with cloneFunction. + * @param cloneFunction The clone of this ScatterGatherableFunction + * @param index The one based index (from 1..scatterCount inclusive) of the scatter piece. + */ + protected def initCloneFunction(cloneFunction: ScatterGatherableFunction, index: Int) = { + cloneFunction.commandDirectory = this.scatterGatherTempDir("temp-"+index) + if (this.setupCloneFunction != null) + if (this.setupCloneFunction.isDefinedAt(cloneFunction, index)) + this.setupCloneFunction(cloneFunction, index) + } + + /** + * Joins a piece of the ScatterFunction output to the cloned function's input. + * The input of the clone is changed to be in the output directory of the clone. + * The scatter function piece is added as an output of the scatterFunction. + * The clone function's original input is changed to use the piece from the output directory. + * Finally the scatterFunction.setCloneFunction is called with the clone of this ScatterGatherableFunction. + * @param scatterFunction Function that will create the pieces including the piece that will go to cloneFunction. + * @param scatterField The field to be scattered. + * @param cloneFunction Clone of this ScatterGatherableFunction. + * @param index The one based index (from 1..scatterCount inclusive) of the scatter piece. + */ + protected def bindCloneFunctionScatter(scatterFunction: ScatterFunction, scatterField: ArgumentSource, cloneFunction: ScatterGatherableFunction, index: Int) = { + // Reset the input of the clone to the the scatterGatherTempDir dir and add it as an output of the scatter + val scatterPart = IOUtils.resetParent(cloneFunction.commandDirectory, scatterFunction.originalInput) + scatterFunction.scatterParts :+= scatterPart + cloneFunction.setFieldValue(scatterField, scatterPart) + scatterFunction.setCloneFunction(cloneFunction, index, scatterField) + } + + /** + * Joins the cloned function's output as a piece of the GatherFunction's input. + * Finally the scatterFunction.setCloneFunction is called with the clone of this ScatterGatherableFunction. + * @param cloneFunction Clone of this ScatterGatherableFunction. + * @param gatherFunction Function that will create the pieces including the piece that will go to cloneFunction. + * @param gatherField The field to be gathered. + */ + protected def bindCloneFunctionGather(gatherFunction: GatherFunction, gatherField: ArgumentSource, cloneFunction: ScatterGatherableFunction, index: Int) = { + val gatherPart = cloneFunction.resetFieldFile(gatherField, cloneFunction.commandDirectory) + gatherFunction.gatherParts :+= gatherPart + gatherFunction.setCloneFunction(cloneFunction, index, gatherField) + } + + /** + * Creates a new function that will remove the temporary directories. + * @return A CleanupTempDirs function that will remove the temporary directories. + */ + protected def newCleanupFunction(): CleanupTempDirsFunction = { + if (cleanupTempDirsClass != null) + this.cleanupTempDirsClass.newInstance + else + new CleanupTempDirsFunction + } + + /** + * Initializes the CleanupTempDirsFunction created by newCleanupFunction() that will remove the temporary directories. + * The cleanupFunction jobNamePrefix is set so that the CleanupTempDirsFunction runs with the same prefix as this ScatterGatherableFunction. + * The cleanupFunction commandDirectory is set so that the function runs in the directory as this ScatterGatherableFunction. + * The initializeFunction is modified to become dependent on the output files for this ScatterGatherableFunction. + * Calls setupCleanupFunction with cleanupFunction. + * @param cleanupFunction The function that will remove the temporary directories. + * @param gatherFunctions The functions that will gather up the original output fields. + * @param outputFields The output fields that the original function was dependent on. + */ + protected def initCleanupFunction(cleanupFunction: CleanupTempDirsFunction, gatherFunctions: Map[ArgumentSource, GatherFunction], outputFields: List[ArgumentSource]) = { + cleanupFunction.jobNamePrefix = this.jobNamePrefix + cleanupFunction.commandDirectory = this.commandDirectory + for (gatherField <- outputFields) + cleanupFunction.originalOutputs += gatherFunctions(gatherField).originalOutput + if (this.setupCleanupFunction != null) + if (this.setupCleanupFunction.isDefinedAt(cleanupFunction, gatherFunctions, outputFields)) + this.setupCleanupFunction(cleanupFunction, gatherFunctions, outputFields) + } + + /** + * Returns a temporary directory under this scatter gather directory. + * @param Sub directory under the scatter gather directory. + * @return temporary directory under this scatter gather directory. + */ + private def scatterGatherTempDir(subDir: String) = IOUtils.subDir(this.scatterGatherDirectory, this.jobName + "-" + subDir) +} + +/** + * A function that can be run faster by splitting it up into pieces and then joining together the results. + */ +object ScatterGatherableFunction { + /** Used to deep clone a ScatterGatherableFunction. */ + private lazy val cloner = new Cloner } diff --git a/scala/src/org/broadinstitute/sting/queue/function/scattergather/SimpleTextGatherFunction.scala b/scala/src/org/broadinstitute/sting/queue/function/scattergather/SimpleTextGatherFunction.scala index 070c36115..9a5681e4d 100644 --- a/scala/src/org/broadinstitute/sting/queue/function/scattergather/SimpleTextGatherFunction.scala +++ b/scala/src/org/broadinstitute/sting/queue/function/scattergather/SimpleTextGatherFunction.scala @@ -1,10 +1,16 @@ package org.broadinstitute.sting.queue.function.scattergather -import java.io.File +import org.broadinstitute.sting.commandline.Argument +/** + * Merges a text file. + * The script can be changed by setting rmdirScript. + * By default uses mergeText.sh in Sting/shell. + * The format of the call is [.. ] + */ class SimpleTextGatherFunction extends GatherFunction { - type GatherType = File + @Argument(doc="merge text script") + var mergeTextScript = "mergeText.sh" - // TODO: Write a text merging utility that takes into account headers. - def commandLine = "mergeText.sh %s%s".format(originalOutput, repeat(" ", gatherParts)) + def commandLine = "%s %s%s".format(mergeTextScript, originalOutput, repeat(" ", gatherParts)) } diff --git a/scala/src/org/broadinstitute/sting/queue/util/ClasspathUtils.scala b/scala/src/org/broadinstitute/sting/queue/util/ClasspathUtils.scala index f3a0f43b5..36bc97a7c 100755 --- a/scala/src/org/broadinstitute/sting/queue/util/ClasspathUtils.scala +++ b/scala/src/org/broadinstitute/sting/queue/util/ClasspathUtils.scala @@ -4,14 +4,32 @@ import collection.JavaConversions._ import org.reflections.util.ManifestAwareClasspathHelper import java.io.File import javax.print.URIException +import java.net.{URL, URLClassLoader} /** * Builds the correct class path by examining the manifests */ object ClasspathUtils { + + /** + * Returns a list of files that build up the classpath, taking into account jar file manifests. + * @return List[File] that build up the current classpath. + */ def manifestAwareClassPath = { var urls = ManifestAwareClasspathHelper.getUrlsForManifestCurrentClasspath - var files = urls.map(url => try {new File(url.toURI)} catch {case urie: URIException => new File(url.getPath)}) - files.mkString(File.pathSeparator) + urls.map(url => try {new File(url.toURI)} catch {case urie: URIException => new File(url.getPath)}) + } + + /** + * Adds the directory to the system class loader classpath using reflection. + * HACK: Uses reflection to modify the class path, and assumes loader is a URLClassLoader + * @param path Directory to add to the system class loader classpath. + */ + def addClasspath(path: File): Unit = { + val url = path.toURI.toURL + val method = classOf[URLClassLoader].getDeclaredMethod("addURL", classOf[URL]); + if (!method.isAccessible) + method.setAccessible(true); + method.invoke(ClassLoader.getSystemClassLoader(), url); } } diff --git a/scala/src/org/broadinstitute/sting/queue/util/CollectionUtils.scala b/scala/src/org/broadinstitute/sting/queue/util/CollectionUtils.scala index b384c8dfa..6871d8f4b 100644 --- a/scala/src/org/broadinstitute/sting/queue/util/CollectionUtils.scala +++ b/scala/src/org/broadinstitute/sting/queue/util/CollectionUtils.scala @@ -1,18 +1,16 @@ package org.broadinstitute.sting.queue.util /** - * Utilities that try to deeply apply operations to collections + * Utilities that try to deeply apply operations to collections, specifically Traversable and Option. */ object CollectionUtils { - def test(value: Any, f: Any => Boolean): Boolean = { - var result = f(value) - foreach(value, (item, collection) => { - result |= f(item) - }) - result - } - + /** + * Loops though a collection running the function f on each value. + * @param value The value to run f on, or a collection of values for which f should be run on. + * @param f The function to run on value, or to run on the values within the collection. + * @return The updated value. + */ def updated(value: Any, f: Any => Any): Any = { value match { case traversable: Traversable[_] => traversable.map(updated(_, f)) @@ -21,6 +19,11 @@ object CollectionUtils { } } + /** + * Utility for recursively processing collections. + * @param value Initial the collection to be processed + * @param f a function that will be called for each (item, collection) in the initial collection + */ def foreach(value: Any, f: (Any, Any) => Unit): Unit = { value match { case traversable: Traversable[_] => @@ -37,11 +40,24 @@ object CollectionUtils { } } - // Because scala allows but throws NPE when trying to hash a collection with a null in it. - // http://thread.gmane.org/gmane.comp.lang.scala.internals/3267 - // https://lampsvn.epfl.ch/trac/scala/ticket/2935 - def removeNullOrEmpty[T](value: T): T = filterNotNullOrNotEmpty(value) + /** + * Utility for recursively processing collections. + * @param value Initial the collection to be processed + * @param f a function that will be called for each (item, collection) in the initial collection + */ + def foreach(value: Any, f: (Any) => Unit): Unit = { + value match { + case traversable: Traversable[_] => traversable.foreach(f(_)) + case option: Option[_] => option.foreach(f(_)) + case item => f(item) + } + } + /** + * Removes empty values from collections. + * @param value The collection to test. + * @return The value if it is not a collection, otherwise the collection with nulls and empties removed. + */ private def filterNotNullOrNotEmpty[T](value: T): T = { val newValue = value match { case traversable: Traversable[_] => traversable.map(filterNotNullOrNotEmpty(_)).filter(isNotNullOrNotEmpty(_)).asInstanceOf[T] @@ -51,7 +67,20 @@ object CollectionUtils { newValue } - private def isNotNullOrNotEmpty(value: Any): Boolean = { + + /** + * Returns true if the value is null or an empty collection. + * @param value Value to test for null, or a collection to test if it is empty. + * @return true if the value is null, or false if the collection is empty, otherwise true. + */ + def isNullOrEmpty(value: Any): Boolean = !isNotNullOrNotEmpty(value) + + /** + * Returns false if the value is null or an empty collection. + * @param value Value to test for null, or a collection to test if it is empty. + * @return false if the value is null, or false if the collection is empty, otherwise true. + */ + def isNotNullOrNotEmpty(value: Any): Boolean = { val result = value match { case traversable: Traversable[_] => !filterNotNullOrNotEmpty(traversable).isEmpty case option: Option[_] => !filterNotNullOrNotEmpty(option).isEmpty diff --git a/scala/src/org/broadinstitute/sting/queue/util/CommandLineJob.scala b/scala/src/org/broadinstitute/sting/queue/util/CommandLineJob.scala new file mode 100644 index 000000000..0b322301e --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/util/CommandLineJob.scala @@ -0,0 +1,51 @@ +package org.broadinstitute.sting.queue.util + +import java.io.File + +/** + * Base class for a command line job. + */ +abstract class CommandLineJob { + var command: String = _ + var workingDir: File = _ + var inputFile: File = _ + var outputFile: File = _ + var errorFile: File = _ + + /** + * Runs the command, either immediately or dispatching it to a compute farm. + * If it is dispatched to a compute farm it should not start until jobs it depends on are finished. + */ + def run() + + /** + * Returns the content of a command output. + * @param streamOutput The output of the command. + * @return The content of the command, along with a message if it was truncated. + */ + protected def content(streamOutput: ProcessController.StreamOutput) = { + var content = streamOutput.content + if (streamOutput.contentTruncated) + content += "%n%n".format() + content + } + + /** + * Returns the ProcessController for this thread. + * @return The ProcessController for this thread. + */ + protected def processController = CommandLineJob.threadProcessController.get + + /** A five mb limit of characters for display. */ + protected val FIVE_MB = 1024 * 512 * 5; +} + +/** + * Base class for a command line job. + */ +object CommandLineJob { + /** Thread local process controller container. */ + private val threadProcessController = new ThreadLocal[ProcessController] { + override def initialValue = new ProcessController + } +} diff --git a/scala/src/org/broadinstitute/sting/queue/util/IOUtils.scala b/scala/src/org/broadinstitute/sting/queue/util/IOUtils.scala index 5fa902391..4a9fa8f4a 100644 --- a/scala/src/org/broadinstitute/sting/queue/util/IOUtils.scala +++ b/scala/src/org/broadinstitute/sting/queue/util/IOUtils.scala @@ -2,30 +2,69 @@ package org.broadinstitute.sting.queue.util import java.io.{IOException, File} +/** + * A collection of utilities for modifying java.io. + */ object IOUtils { + /** The current directory "." */ val CURRENT_DIR = new File(".") - def sub(parent: File, subPath: String) = { - val file = new File(subPath) + + /** + * Returns the sub path rooted at the parent. + * If the sub path is already absolute, returns the sub path. + * If the parent is the current directory, returns the sub path. + * If the sub bath is the current directory, returns the parent. + * Else returns new File(parent, subPath) + * @param parent The parent directory + * @param path The sub path to append to the parent, if the path is not absolute. + * @return The absolute path to the file in the parent dir if the path was not absolute, otherwise the original path. + */ + def subDir(dir: File, path: String): File = + subDir(dir.getAbsoluteFile, new File(path)) + + /** + * Returns the sub path rooted at the parent. + * If the sub path is already absolute, returns the sub path. + * If the parent is the current directory, returns the sub path. + * If the sub bath is the current directory, returns the parent. + * Else returns new File(parent, subPath) + * @param parent The parent directory + * @param file The sub path to append to the parent, if the path is not absolute. + * @return The absolute path to the file in the parent dir if the path was not absolute, otherwise the original path. + */ + def subDir(parent: File, file: File): File = { if (parent == CURRENT_DIR && file == CURRENT_DIR) - CURRENT_DIR.getCanonicalFile + CURRENT_DIR.getCanonicalFile.getAbsoluteFile else if (parent == CURRENT_DIR || file.isAbsolute) - file + file.getAbsoluteFile else if (file == CURRENT_DIR) - parent + parent.getAbsoluteFile else - new File(parent, subPath) + new File(parent, file.getPath).getAbsoluteFile } - def temp(prefix: String, suffix: String = "") = { - val tempDir = File.createTempFile(prefix + "-", suffix) - if(!tempDir.delete) - throw new IOException("Could not delete sub file: " + tempDir.getAbsolutePath()) - if(!tempDir.mkdir) - throw new IOException("Could not create sub directory: " + tempDir.getAbsolutePath()) - tempDir - } + /** + * Resets the parent of the file to the directory. + * @param dir New parent directory. + * @param file Path to the file to be re-rooted. + * @return Absolute path to the new file. + */ + def resetParent(dir: File, file: File) = subDir(dir.getAbsoluteFile, file.getName).getAbsoluteFile - def reset(dir: File, file: File) = sub(dir, file.getName).getAbsoluteFile - def absolute(dir: File, file: File) = sub(dir, file.getPath).getAbsoluteFile + /** + * Creates a scatterGatherTempDir directory with the prefix and optional suffix. + * @param prefix Prefix for the directory name. + * @param suffix Optional suffix for the directory name. Defaults to "". + * @return The created temporary directory. + * @throws IOException if the directory could not be created. + */ + def tempDir(prefix: String, suffix: String = "") = { + val temp = File.createTempFile(prefix + "-", suffix) + if(!temp.delete) + throw new IOException("Could not delete sub file: " + temp.getAbsolutePath()) + if(!temp.mkdir) + throw new IOException("Could not create sub directory: " + temp.getAbsolutePath()) + temp + } } diff --git a/scala/src/org/broadinstitute/sting/queue/util/Logging.scala b/scala/src/org/broadinstitute/sting/queue/util/Logging.scala index c61a6267f..5a9fed204 100755 --- a/scala/src/org/broadinstitute/sting/queue/util/Logging.scala +++ b/scala/src/org/broadinstitute/sting/queue/util/Logging.scala @@ -7,25 +7,5 @@ import org.apache.log4j._ */ trait Logging { private val className = this.getClass.getName - protected lazy val logger = { - Logging.configureLogging - Logger.getLogger(className) - } -} - -object Logging { - private var configured = false - private var level = Level.INFO - def configureLogging = { - if (!configured) { - var root = Logger.getRootLogger - root.addAppender(new ConsoleAppender(new PatternLayout("%-5p %d{HH:mm:ss,SSS} - %m %n"))) - root.setLevel(level) - configured = true - } - } - - def setDebug = setLevel(Level.DEBUG) - def setTrace = setLevel(Level.TRACE) - private def setLevel(level: Level) = {this.level = level; Logger.getRootLogger.setLevel(level)} + protected lazy val logger = Logger.getLogger(className) } diff --git a/scala/src/org/broadinstitute/sting/queue/util/LsfJob.scala b/scala/src/org/broadinstitute/sting/queue/util/LsfJob.scala new file mode 100644 index 000000000..f18ad4304 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/util/LsfJob.scala @@ -0,0 +1,142 @@ +package org.broadinstitute.sting.queue.util + +import java.util.regex.Pattern +import collection.JavaConversions._ +import org.broadinstitute.sting.queue.QException + +/** + * An job submitted to LSF. This class is designed to work somewhat like + * java.lang.Process, but has some extensions. + * + * @author A subset of the original BroadCore ported to scala by Khalid Shakir + */ +class LsfJob extends CommandLineJob with Logging { + var name: String = _ + var project: String = _ + var queue: String = _ + var preExecCommand: String = _ + var postExecCommand: String = _ + var waitForCompletion = false + var extraBsubArgs: List[String] = Nil + var bsubJobId: String = _ + + /** + * Starts the job. Command must exist. The job will be submitted to LSF. + */ + def run() = { + assert(bsubJobId == null, "LSF job was already started") + assert(command != null, "Command was not set on LSF job") + assert(outputFile != null, "Output file must be set on LSF job") + + // capture the output for debugging + val stdinSettings = new ProcessController.InputStreamSettings(null, null) + val stdoutSettings = new ProcessController.OutputStreamSettings(FIVE_MB, null, false) + val stderrSettings = new ProcessController.OutputStreamSettings(FIVE_MB, null, false) + + // This is really nice for debugging, but spits out way too much stuff otherwise! + // log.info("About to execute LSF command: " + StringUtils.join(argArray, " ")); + + // Get environment vars and strip out LD_ASSUME_KERNEL + // This is necessary since GAP servers on linux 2.4.x kernel and can be removed when + // its no longer true. Only 'classic' LSF queue has 2.4 kernel-based machines. + + // launch the bsub job from the current directory + val processSettings = new ProcessController.ProcessSettings( + bsubCommand, environmentVariables, null, stdinSettings, stdoutSettings, stderrSettings, false) + val bsubOutput = processController.exec(processSettings) + + if (bsubOutput.exitValue != 0) { + logger.error("Failed to submit LSF job, got exit code %s. Standard error contained: %n%s" + .format(bsubOutput.exitValue, content(bsubOutput.stderr))) + throw new QException("Failed to submit LSF job, got exit code %s.".format(bsubOutput.exitValue)) + } + + // get the LSF job ID + val matcher = LsfJob.JOB_ID.matcher(bsubOutput.stdout.content) + matcher.find() + bsubJobId = matcher.group + + // set job name to LSF_ if not set already + if (name == null) + name = "lsf_job_" + bsubJobId + } + + /** + * Generates the bsub command line for this LsfJob. + * @return command line as a Array[String] + */ + def bsubCommand = { + var args = List.empty[String] + args :+= "bsub" + + if (name != null) { + args :+= "-J" + args :+= name + } + + if (inputFile != null) { + args :+= "-i" + args :+= inputFile.getAbsolutePath + } + + args :+= "-o" + args :+= outputFile.getAbsolutePath + + if (errorFile != null) { + args :+= "-e" + args :+= errorFile.getAbsolutePath + } + + if (queue != null) { + args :+= "-q" + args :+= queue + } + + if (project != null) { + args :+= "-P" + args :+= project + } + + if (preExecCommand != null) { + args :+= "-E" + args :+= preExecCommand + } + + if (postExecCommand != null) { + args :+= "-Ep" + args :+= postExecCommand + } + + if (workingDir != null) { + args :+= "-cwd" + args :+= workingDir.getPath + } + + if (waitForCompletion) { + args :+= "-K" + } + + args ++= extraBsubArgs + + args :+= command + + args.toArray + } + + /** + * Get the list of environment variables and pass into the exec job. We strip + * out LD_ASSUME_KERNEL because it behaves badly when running bsub jobs across + * different versions of the linux OS. + * + * @return array of environment vars in 'name=value' format. + */ + private def environmentVariables = + System.getenv() + .filterNot{case (name, value) => name.equalsIgnoreCase("LD_ASSUME_KERNEL") || value == null} + .toMap +} + +object LsfJob { + /** Used to search the stdout for the job id. */ + private val JOB_ID = Pattern.compile("\\d+") +} diff --git a/scala/src/org/broadinstitute/sting/queue/util/ProcessController.scala b/scala/src/org/broadinstitute/sting/queue/util/ProcessController.scala new file mode 100644 index 000000000..80162582e --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/util/ProcessController.scala @@ -0,0 +1,360 @@ +package org.broadinstitute.sting.queue.util + +import java.io._ +import scala.collection.mutable.{HashSet, ListMap} + +/** + * Facade to Runtime.exec() and java.lang.Process. Handles + * running a process to completion and returns stdout and stderr + * as strings. Creates separate threads for reading stdout and stderr, + * then reuses those threads for each process most efficient use is + * to create one of these and use it repeatedly. Instances are not + * thread-safe, however. + * + * @author originally by Michael Koehrsen ported to scala and enhanced by Khalid Shakir + */ +class ProcessController extends Logging { + + // Threads that capture stdout and stderr + private val stdoutCapture = new OutputCapture(ProcessController.STDOUT_KEY) + private val stderrCapture = new OutputCapture(ProcessController.STDERR_KEY) + + // Communication channels with output capture threads + /** Holds the stdout and stderr sent to the background capture threads */ + private val toCapture = new ListMap[String, ProcessController.CapturedStreamOutput] + + /** Holds the results of the capture from the background capture threads. + * May be the content via toCapture or an EmptyStreamOutput if the capture was interrupted. */ + private val fromCapture = new ListMap[String, ProcessController.StreamOutput] + + // Start the background threads for this controller. + stdoutCapture.start() + stderrCapture.start() + + /** + * Executes a command line program with the settings and waits for it to return, processing the output on a background thread. + * @param settings Settings to be run. + * @return The output of the command. + */ + def exec(settings: ProcessController.ProcessSettings): ProcessController.ProcessOutput = { + var builder = new ProcessBuilder(settings.cmdarray:_*) + builder.directory(settings.directory) + + if (settings.environment != null) { + val builderEnvironment = builder.environment + builderEnvironment.clear() + settings.environment.foreach{case (name, value) => builderEnvironment.put(name, value)} + } + + builder.redirectErrorStream(settings.redirectErrorStream) + + var stdout: ProcessController.StreamOutput = null + var stderr: ProcessController.StreamOutput = null + val process = builder.start + + ProcessController.running.add(process) + try { + val stdoutSettings = if (settings.stdoutSettings == null) ProcessController.EmptyStreamSettings else settings.stdoutSettings + val stderrSettings = if (settings.stderrSettings == null) ProcessController.EmptyStreamSettings else settings.stderrSettings + + toCapture.synchronized { + toCapture.put(ProcessController.STDOUT_KEY, new ProcessController.CapturedStreamOutput(process.getInputStream, stdoutSettings)) + toCapture.put(ProcessController.STDERR_KEY, new ProcessController.CapturedStreamOutput(process.getErrorStream, stderrSettings)) + toCapture.notifyAll() + } + + if (settings.stdinSettings.input != null) { + val writer = new OutputStreamWriter(process.getOutputStream) + writer.write(settings.stdinSettings.input) + writer.flush() + } + if (settings.stdinSettings.inputFile != null) { + val reader = new FileReader(settings.stdinSettings.inputFile) + val writer = new OutputStreamWriter(process.getOutputStream) + val buf = new Array[Char](4096) + var readCount = 0 + while ({readCount = reader.read(buf); readCount} >= 0) + writer.write(buf, 0, readCount) + writer.flush() + reader.close() + } + + try { + process.getOutputStream.close() + process.waitFor() + } finally { + while (stdout == null || stderr == null) { + fromCapture.synchronized { + fromCapture.remove(ProcessController.STDOUT_KEY) match { + case Some(stream) => stdout = stream + case None => /* ignore */ + } + fromCapture.remove(ProcessController.STDERR_KEY) match { + case Some(stream) => stderr = stream + case None => /* ignore */ + } + + try { + if (stdout == null || stderr == null) + fromCapture.wait() + } catch { + case e: InterruptedException => + logger.error(e) + } + } + } + } + } finally { + ProcessController.running.remove(process) + } + + new ProcessController.ProcessOutput(process.exitValue, stdout, stderr) + } + + /** Ensures that the threads used to manipulate the IO for the process are cleaned up properly. */ + def close() = { + try { + stdoutCapture.interrupt() + stderrCapture.interrupt() + } catch { + case e => + logger.error(e) + } + } + + /** calls close() */ + override def finalize = close() + + /** + * Reads in the output of a stream on a background thread to keep the output pipe from backing up and freezing the called process. + * @param key The stdout or stderr key for this output capture. + */ + private class OutputCapture(private val key: String) + extends Thread("OutputCapture-" + key + "-" + Thread.currentThread.getName) { + + setDaemon(true) + + /** Runs the capture. */ + override def run = { + var break = false + while (!break) { + var processStream: ProcessController.StreamOutput = ProcessController.EmptyStreamOutput + try { + // Wait for a new input stream to be passed from this process controller. + var capturedProcessStream: ProcessController.CapturedStreamOutput = null + while (capturedProcessStream == null) { + toCapture.synchronized { + toCapture.remove(key) match { + case Some(stream) => capturedProcessStream = stream + case None => toCapture.wait() + } + } + } + // Read in the input stream + processStream = capturedProcessStream + capturedProcessStream.read + } catch { + case e: InterruptedException => { + logger.info("OutputReader interrupted, exiting") + break = true + } + case e: IOException => { + logger.error("Error reading process output", e) + } + } finally { + // Send the string back to the process controller. + fromCapture.synchronized { + fromCapture.put(key, processStream) + fromCapture.notify() + } + } + } + } + } +} + +/** + * Facade to Runtime.exec() and java.lang.Process. Handles + * running a process to completion and returns stdout and stderr + * as strings. Creates separate threads for reading stdout and stderr, + * then reuses those threads for each process most efficient use is + * to create one of these and use it repeatedly. Instances are not + * thread-safe, however. + * + * @author originally by Michael Koehrsen ported to scala and enhanced by Khalid Shakir + */ +object ProcessController extends Logging { + + /** + * Settings that define how to run a process. + * @param cmdarray Command line to run. + * @param environment Environment settings to override System.getEnv, or null to use System.getEnv. + * @param directory The directory to run the command in, or null to run in the current directory. + * @param stdinSettings Settings for writing to the process stdin. + * @param stdoutSettings Settings for capturing the process stdout. + * @param stderrSettings Setting for capturing the process stderr. + * @param redirectErrorStream true if stderr should be sent to stdout. + */ + class ProcessSettings(val cmdarray: Array[String], val environment: Map[String, String], val directory: File, + val stdinSettings: InputStreamSettings, val stdoutSettings: OutputStreamSettings, + val stderrSettings: OutputStreamSettings, val redirectErrorStream: Boolean) + + /** + * Settings that define text to write to the process stdin. + * @param input String to write to stdin. + * @param inputFile File to write to stdin. + */ + class InputStreamSettings(val input: String, val inputFile: File) + + /** + * Settings that define text to capture from a process stream. + * @param stringSize The number of characters to capture, or -1 for unlimited. + * @param outputFile The file to write output to, or null to skip output. + * @param outputFileAppend true if the output file should be appended to. + */ + class OutputStreamSettings(val stringSize: Int, val outputFile: File, val outputFileAppend: Boolean) + + /** + * The output of a process. + * @param exitValue The exit value. + * @param stdout The capture of stdout as defined by the stdout OutputStreamSettings. + * @param stderr The capture of stderr as defined by the stderr OutputStreamSettings. + */ + class ProcessOutput(val exitValue: Int, val stdout: StreamOutput, val stderr: StreamOutput) + + /** + * The base class of stream output. + */ + abstract class StreamOutput { + /** + * Returns the content as a string. + * @return The content as a string. + */ + def content: String + + /** + * Returns true if the content was truncated. + * @return true if the content was truncated. + */ + def contentTruncated: Boolean + } + + private var currentCaptureId = 0 + /** + * Returns the next output capture id. + * @return The next output capture id. + */ + private def NEXT_OUTPUT_CAPTURE_ID = { + currentCaptureId += 1 + currentCaptureId + } + private val STDOUT_KEY = "stdout" + private val STDERR_KEY = "stderr" + + /** Tracks running processes so that they can be killed as the JVM shuts down. */ + private val running = new HashSet[Process]() + Runtime.getRuntime.addShutdownHook(new Thread { + /** Kills running processes as the JVM shuts down. */ + override def run = for (process <- running.clone) { + logger.warn("Killing: " + process) + process.destroy + } + }) + + /** Empty stream settings used when no output is requested. */ + private object EmptyStreamSettings extends OutputStreamSettings(0, null, false) + + /** Empty stream output when no output is captured due to an error. */ + private object EmptyStreamOutput extends StreamOutput { + def content = "" + def contentTruncated = false + } + + /** + * Stream output captured from a stream. + * @param stream Stream to capture output. + * @param settings Settings that define what to capture. + */ + private class CapturedStreamOutput(val stream: InputStream, val settings: OutputStreamSettings) extends StreamOutput { + /** + * Returns the captured content as a string. + * @return The captured content as a string. + */ + def content = stringWriter.toString() + + /** + * Returns true if the captured content was truncated. + * @return true if the captured content was truncated. + */ + def contentTruncated = stringTruncated + + /** + * Drain the input stream to keep the process from backing up until it's empty. + */ + def read() = { + val reader = new InputStreamReader(stream) + val buf = new Array[Char](4096) + var readCount = 0 + while ({readCount = reader.read(buf); readCount} >= 0) { + writeString(buf, readCount) + writeFile(buf, readCount) + } + closeFile() + stream.close() + } + + /** The string to write capture content. */ + private lazy val stringWriter = if (settings.stringSize < 0) new StringWriter else new StringWriter(settings.stringSize) + + /** True if the content is truncated. */ + private var stringTruncated = false + + /** The number of characters left until the buffer is full. */ + private var stringRemaining = settings.stringSize + + /** + * Writes the buffer to the stringWriter up to stringRemaining characters. + * @param chars Character buffer to write. + * @param len Number of characters in the buffer. + */ + private def writeString(chars: Array[Char], len: Int) = { + if (settings.stringSize < 0) { + stringWriter.write(chars, 0, len) + } else { + if (!stringTruncated) { + stringWriter.write(chars, 0, if (len > stringRemaining) stringRemaining else len) + stringRemaining -= len + if (stringRemaining < 0) + stringTruncated = true + } + } + } + + /** The file writer to capture content or null if no output file was requested. */ + private lazy val fileWriter = { + if (settings.outputFile == null) { + null + } else { + new FileWriter(settings.outputFile, settings.outputFileAppend) + } + } + + /** + * Writes the buffer to the fileWriter if it is not null. + * @param chars Character buffer to write. + * @param len Number of characters in the buffer. + */ + private def writeFile(chars: Array[Char], len: Int) = { + if (fileWriter != null) + fileWriter.write(chars, 0, len) + } + + /** Closes the fileWriter if it is not null. */ + private def closeFile() = { + if (fileWriter != null) { + fileWriter.flush + fileWriter.close + } + } + } +} diff --git a/scala/src/org/broadinstitute/sting/queue/util/ProcessUtils.scala b/scala/src/org/broadinstitute/sting/queue/util/ProcessUtils.scala deleted file mode 100755 index f79a4f33d..000000000 --- a/scala/src/org/broadinstitute/sting/queue/util/ProcessUtils.scala +++ /dev/null @@ -1,43 +0,0 @@ -package org.broadinstitute.sting.queue.util - -import org.broadinstitute.sting.utils.text.XReadLines -import collection.mutable.ListBuffer -import collection.JavaConversions._ -import java.io.File - -object ProcessUtils extends Logging { - - Runtime.getRuntime.addShutdownHook(new Thread { - override def run = for (process <- running.clone) { - logger.warn("Killing: " + process) - process.destroy - } - }) - - val running = new ListBuffer[Process]() - - def runCommandAndWait(command: String, directory: File) = { - logger.debug("Running command: " + command) - - var builder = new ProcessBuilder("sh", "-c", command).directory(directory) - - var process = builder.start - running += process - var result = process.waitFor - running -= process - - if (logger.isDebugEnabled) { - for (line <- new XReadLines(process.getInputStream).iterator) { - logger.debug("command: " + line) - } - - for (line <- new XReadLines(process.getErrorStream).iterator) { - logger.error("command: " + line) - } - } - - logger.debug("Command exited with result: " + result) - - result - } -} diff --git a/scala/src/org/broadinstitute/sting/queue/util/ReflectionUtils.scala b/scala/src/org/broadinstitute/sting/queue/util/ReflectionUtils.scala index 566e3cc02..6f6ffdcc7 100644 --- a/scala/src/org/broadinstitute/sting/queue/util/ReflectionUtils.scala +++ b/scala/src/org/broadinstitute/sting/queue/util/ReflectionUtils.scala @@ -2,67 +2,90 @@ package org.broadinstitute.sting.queue.util import org.broadinstitute.sting.queue.QException import java.lang.annotation.Annotation -import scala.concurrent.JavaConversions._ import java.lang.reflect.{ParameterizedType, Field} import org.broadinstitute.sting.commandline.ClassType +import org.broadinstitute.sting.utils.classloader.JVMUtils +/** + * A collection of scala extensions to the Sting JVMUtils. + */ object ReflectionUtils { + + /** + * Returns true if field has the annotation. + * @param field Field to check. + * @param annotation Class of the annotation to look for. + * @return true if field has the annotation. + */ def hasAnnotation(field: Field, annotation: Class[_ <: Annotation]) = field.getAnnotation(annotation) != null + /** + * Gets the annotation or throws an exception if the annotation is not found. + * @param field Field to check. + * @param annotation Class of the annotation to look for. + * @return The annotation. + */ def getAnnotation[T <: Annotation](field: Field, annotation: Class[T]): T = { if (!hasAnnotation(field, annotation)) throw new QException("Field %s is missing annotation %s".format(field, annotation)) field.getAnnotation(annotation).asInstanceOf[T] } - + + /** + * Returns all the declared fields on a class in order of sub type to super type. + * @param clazz Base class to start looking for fields. + * @return List[Field] found on the class and all super classes. + */ def getAllFields(clazz: Class[_]) = getAllTypes(clazz).map(_.getDeclaredFields).flatMap(_.toList) - def filterFields(fields: List[Field], annotation: Class[_ <: Annotation]) = fields.filter(field => hasAnnotation(field, annotation)) - - def getFieldValues(obj: AnyRef, fields: List[Field]) = fields.map(field => fieldGetter(field).invoke(obj)) - + /** + * Gets all the types on a class in order of sub type to super type. + * @param clazz Base class. + * @return List[Class] including the class and all super classes. + */ def getAllTypes(clazz: Class[_]) = { var types = List.empty[Class[_]] - var c = clazz - while (c != null) { - types :+= c - c = c.getSuperclass - } + var c = clazz + while (c != null) { + types :+= c + c = c.getSuperclass + } types } - def getValue(obj: AnyRef, field: Field) = fieldGetter(field).invoke(obj) - def setValue(obj: AnyRef, field: Field, value: Any) = fieldSetter(field).invoke(obj, value.asInstanceOf[AnyRef]) - - def addOrUpdateWithStringValue(obj: AnyRef, field: Field, value: String) = { - val getter = fieldGetter(field) - val setter = fieldSetter(field) - - if (classOf[Seq[_]].isAssignableFrom(field.getType)) { - - val fieldType = getCollectionType(field) - val typeValue = coerce(fieldType, value) - - var list = getter.invoke(obj).asInstanceOf[Seq[_]] - list :+= typeValue - setter.invoke(obj, list) - - } else if (classOf[Option[_]].isAssignableFrom(field.getType)) { - - val fieldType = getCollectionType(field) - val typeValue = coerce(fieldType, value) - - setter.invoke(obj, Some(typeValue)) - - } else { - - val fieldType = field.getType - val typeValue = coerce(fieldType, value) - - setter.invoke(obj, typeValue.asInstanceOf[AnyRef]) + /** + * Gets a field value using reflection. + * Attempts to use the scala getter then falls back to directly accessing the field. + * @param obj Object to inspect. + * @param field Field to retrieve. + * @return The field value. + */ + def getValue(obj: AnyRef, field: Field): AnyRef = + try { + field.getDeclaringClass.getMethod(field.getName).invoke(obj) + } catch { + case e: NoSuchMethodException => JVMUtils.getFieldValue(field, obj) } - } + /** + * Sets a field value using reflection. + * Attempts to use the scala setter then falls back to directly accessing the field. + * @param obj Object to inspect. + * @param field Field to set. + * @param value The new field value. + */ + def setValue(obj: AnyRef, field: Field, value: Any) = + try { + field.getDeclaringClass.getMethod(field.getName+"_$eq", field.getType).invoke(obj, value.asInstanceOf[AnyRef]) + } catch { + case e: NoSuchMethodException => JVMUtils.setFieldValue(field, obj, value) + } + + /** + * Returns the collection type of a field or throws an exception if the field contains more than one parameterized type, or the collection type cannot be found. + * @param field Field to retrieve the collection type. + * @return The collection type for the field. + */ def getCollectionType(field: Field) = { getGenericTypes(field) match { case Some(classes) => @@ -70,10 +93,15 @@ object ReflectionUtils { throw new IllegalArgumentException("Field contains more than one generic type: " + field) classes(0) case None => - throw new QException("Generic type not set for collection: " + field) + throw new QException("Generic type not set for collection. Did it declare an @ClassType?: " + field) } } + /** + * Returns the generic types for a field or None. + * @param field Field to retrieve the collection type. + * @return The array of classes that are in the collection type, or None if the type cannot be found. + */ private def getGenericTypes(field: Field): Option[Array[Class[_]]] = { // TODO: Refactor: based on java code in org.broadinstitute.sting.commandline.ArgumentTypeDescriptor // If this is a parameterized collection, find the contained type. If blow up if only one type exists. @@ -85,39 +113,4 @@ object ReflectionUtils { } else None } - - private def fieldGetter(field: Field) = - try { - field.getDeclaringClass.getMethod(field.getName) - } catch { - case e: NoSuchMethodException => throw new QException("Field may be private? Unable to find getter for field: " + field) - } - - private def fieldSetter(field: Field) = - try { - field.getDeclaringClass.getMethod(field.getName+"_$eq", field.getType) - } catch { - case e: NoSuchMethodException => throw new QException("Field may be a val instead of var? Unable to find setter for field: " + field) - } - - private def coerce(clazz: Class[_], value: String) = { - if (classOf[String] == clazz) value - else if (classOf[Boolean] == clazz) value.toBoolean - else if (classOf[Byte] == clazz) value.toByte - else if (classOf[Short] == clazz) value.toShort - else if (classOf[Int] == clazz) value.toInt - else if (classOf[Long] == clazz) value.toLong - else if (classOf[Float] == clazz) value.toFloat - else if (classOf[Double] == clazz) value.toDouble - else if (hasStringConstructor(clazz)) - clazz.getConstructor(classOf[String]).newInstance(value) - else throw new QException("Unable to coerce value '%s' to type '%s'.".format(value, clazz)) - } - - private def hasStringConstructor(clazz: Class[_]) = { - clazz.getConstructors.exists(constructor => { - val parameters = constructor.getParameterTypes - parameters.size == 1 && parameters.head == classOf[String] - }) - } } diff --git a/scala/src/org/broadinstitute/sting/queue/util/ScalaCompoundArgumentTypeDescriptor.scala b/scala/src/org/broadinstitute/sting/queue/util/ScalaCompoundArgumentTypeDescriptor.scala new file mode 100644 index 000000000..f2c84649c --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/util/ScalaCompoundArgumentTypeDescriptor.scala @@ -0,0 +1,71 @@ +package org.broadinstitute.sting.queue.util + +import collection.JavaConversions._ +import org.broadinstitute.sting.queue.QException +import java.lang.Class +import org.broadinstitute.sting.commandline.{ArgumentMatches, ArgumentSource, ArgumentTypeDescriptor} + +/** + * An ArgumentTypeDescriptor that can parse the scala collections. + */ +class ScalaCompoundArgumentTypeDescriptor extends ArgumentTypeDescriptor { + + /** + * Checks if the class type is a scala collection. + * @param classType Class type to check. + * @return true if the class is a List, Set, or an Option. + */ + def supports(classType: Class[_]) = isCompound(classType) + + /** + * Checks if the class type is a scala collection. + * @param source Argument source to check. + * @return true if the source is a List, Set, or an Option. + */ + override def isMultiValued(source: ArgumentSource) = isCompound(source.field.getType) + + /** + * Checks if the class type is a scala collection. + * @param classType Class type to check. + * @return true if the class is a List, Set, or an Option. + */ + private def isCompound(classType: Class[_]) = { + classOf[List[_]].isAssignableFrom(classType) || + classOf[Set[_]].isAssignableFrom(classType) || + classOf[Option[_]].isAssignableFrom(classType) + } + + /** + * Parses the argument matches based on the class type of the argument source's field. + * @param source Argument source that contains the field being populated. + * @param classType Class type being parsed. + * @param argumentMatches The argument match strings that were found for this argument source. + * @return The parsed object. + */ + def parse(source: ArgumentSource, classType: Class[_], argumentMatches: ArgumentMatches) = { + val componentType = ReflectionUtils.getCollectionType(source.field) + val componentArgumentParser = ArgumentTypeDescriptor.create(componentType) + + if (classOf[List[_]].isAssignableFrom(classType)) { + var list = List.empty[Any] + for (argumentMatch <- argumentMatches) + for (value <- argumentMatch) + list :+= componentArgumentParser.parse(source, componentType, new ArgumentMatches(value)) + list + } else if (classOf[Set[_]].isAssignableFrom(classType)) { + var set = Set.empty[Any] + for (argumentMatch <- argumentMatches) + for (value <- argumentMatch) + set += componentArgumentParser.parse(source, componentType, new ArgumentMatches(value)) + set + } else if (classOf[Option[_]].isAssignableFrom(classType)) { + if (argumentMatches.size > 1) + throw new QException("Unable to set Option to multiple values: " + argumentMatches.mkString(" ")) + else if (argumentMatches.size == 1) + Some(componentArgumentParser.parse(source, componentType, argumentMatches)) + else + None + } else + throw new QException("Unsupported compound argument type: " + classType) + } +} diff --git a/scala/src/org/broadinstitute/sting/queue/util/ShellJob.scala b/scala/src/org/broadinstitute/sting/queue/util/ShellJob.scala new file mode 100755 index 000000000..e4f8f2899 --- /dev/null +++ b/scala/src/org/broadinstitute/sting/queue/util/ShellJob.scala @@ -0,0 +1,37 @@ +package org.broadinstitute.sting.queue.util + +import org.broadinstitute.sting.queue.QException + +/** + * Runs a job on the command line by invoking "sh -c " + */ +class ShellJob extends CommandLineJob with Logging { + /** + * Runs the command and waits for the output. + */ + def run() = { + assert(command != null, "Command was not set on job") + + val (redirectError, errorFile) = if (this.errorFile == null) (true, null) else (false, this.errorFile) + val bufferSize = if (logger.isDebugEnabled) FIVE_MB else 0 + val stdinSettings = new ProcessController.InputStreamSettings(null, this.inputFile) + val stdoutSettings = new ProcessController.OutputStreamSettings(bufferSize, this.outputFile, true) + val stderrSettings = new ProcessController.OutputStreamSettings(FIVE_MB, errorFile, true) + val processSettings = new ProcessController.ProcessSettings( + Array("sh", "-c", command), null, this.workingDir, stdinSettings, stdoutSettings, stderrSettings, redirectError) + + val output = processController.exec(processSettings) + + if (logger.isDebugEnabled) { + logger.debug("output: " + content(output.stdout)) + logger.debug("error: " + content(output.stderr)) + logger.debug("Command exited with result: " + output.exitValue) + } + + if (output.exitValue != 0) { + logger.error("Failed to run job, got exit code %s. Standard error contained: %n%s" + .format(output.exitValue, content(output.stderr))) + throw new QException("Failed to run job, got exit code %s.".format(output.exitValue)) + } + } +} diff --git a/settings/ivysettings.xml b/settings/ivysettings.xml index 9a2acdd28..e5f39d0f2 100644 --- a/settings/ivysettings.xml +++ b/settings/ivysettings.xml @@ -6,7 +6,9 @@ - + + + @@ -15,5 +17,8 @@ + + + diff --git a/settings/repository/edu.mit.broad/broad-core-all-2.8.jar b/settings/repository/edu.mit.broad/broad-core-all-2.8.jar deleted file mode 100644 index 715288886e4d6ec5986b65849a1d4573fd3e0e81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172746 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

pE#o7p|Vn1N6k4zj#|Rf2Sypy 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 diff --git a/settings/repository/edu.mit.broad/broad-core-all-2.8.xml b/settings/repository/edu.mit.broad/broad-core-all-2.8.xml deleted file mode 100644 index 7e7b31e80..000000000 --- a/settings/repository/edu.mit.broad/broad-core-all-2.8.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/settings/repository/org.reflections/reflections-0.9.5-svnversion79M_mod2.xml b/settings/repository/org.reflections/reflections-0.9.5-svnversion79M_mod2.xml index 65899298f..75fd688fb 100644 --- a/settings/repository/org.reflections/reflections-0.9.5-svnversion79M_mod2.xml +++ b/settings/repository/org.reflections/reflections-0.9.5-svnversion79M_mod2.xml @@ -1,3 +1,12 @@ + + + + + + + + +