diff --git a/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentDefinitions.java b/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentDefinitions.java index 4df99dd68..b40f08324 100755 --- a/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentDefinitions.java +++ b/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentDefinitions.java @@ -2,7 +2,6 @@ package org.broadinstitute.sting.utils.cmdLine; import org.broadinstitute.sting.utils.StingException; -import java.lang.reflect.Field; import java.util.Set; import java.util.HashSet; import java.util.Collection; @@ -262,42 +261,24 @@ class ArgumentDefinition { */ public final String validation; - public final Class sourceClass; - public final Field sourceField; + /** + * The target into which to inject arguments meeting this definition. + */ + public final ArgumentSource source; /** * Creates a new argument definition. - * @param argument Attributes of the argument, read from the source field. - * @param sourceClass Source class for the argument, provided to the ParsingEngine. - * @param sourceField Source field for the argument, extracted from the sourceClass. + * @param source Source information for defining the argument. */ - public ArgumentDefinition( Argument argument, Class sourceClass, Field sourceField ) { - this.sourceClass = sourceClass; - this.sourceField = sourceField; + public ArgumentDefinition( ArgumentSource source ) { + this.source = source; - fullName = argument.fullName().trim().length() > 0 ? argument.fullName().trim() : sourceField.getName().toLowerCase(); - shortName = argument.shortName().trim().length() > 0 ? argument.shortName().trim() : null; - doc = argument.doc(); - required = argument.required() && !isFlag(); - exclusiveOf = argument.exclusiveOf().trim().length() > 0 ? argument.exclusiveOf().trim() : null; - validation = argument.validation().trim().length() > 0 ? argument.validation().trim() : null; - } - - /** - * Can this argument support multiple values, or just one? - * @return True if the argument supports multiple values. - */ - public boolean isMultiValued() { - Class argumentType = sourceField.getType(); - return Collection.class.isAssignableFrom(argumentType) || sourceField.getType().isArray(); - } - - /** - * Is this argument a flag (meaning a boolean value whose presence indicates 'true'). - * @return True if this argument is a flag. - */ - public boolean isFlag() { - return (sourceField.getType() == Boolean.class) || (sourceField.getType() == Boolean.TYPE); + fullName = source.getFullName(); + shortName = source.getShortName(); + doc = source.getDoc(); + required = source.isRequired(); + exclusiveOf = source.getExclusiveOf(); + validation = source.getValidationRegex(); } } diff --git a/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentMatches.java b/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentMatches.java index c3c3892b0..3ba08f9ca 100755 --- a/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentMatches.java +++ b/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentMatches.java @@ -41,25 +41,6 @@ public class ArgumentMatches implements Iterable { */ public ArgumentMatch MissingArgument = new ArgumentMatch(); - void mergeInto( ArgumentMatch match ) { - boolean definitionExists = false; - - // Clone the list of argument matches to avoid ConcurrentModificationExceptions. - for( ArgumentMatch argumentMatch: getUniqueMatches() ) { - if( argumentMatch.definition == match.definition ) { - argumentMatch.mergeInto( match ); - for( int index: match.indices.keySet() ) - argumentMatches.put( index, argumentMatch ); - definitionExists = true; - } - } - - if( !definitionExists ) { - for( int index: match.indices.keySet() ) - argumentMatches.put( index, match ); - } - } - /** * Get an iterator cycling through *unique* command-line argument <-> definition matches. * @return Iterator over all argument matches. @@ -89,14 +70,6 @@ public class ArgumentMatches implements Iterable { return argumentMatches.get(site); } - /** - * Determines, of the argument matches by position, which are unique and returns that list. - * @return A unique set of matches. - */ - private Set getUniqueMatches() { - return new HashSet( argumentMatches.values() ); - } - /** * Does the match collection have a match for this argument definition. * @param definition Definition to match. @@ -106,6 +79,21 @@ public class ArgumentMatches implements Iterable { return findMatches( definition ).size() > 0; } + /** + * Return all argument matches of this source. + * @param argumentSource Argument source to match. + * @return List of all matches. + */ + + public Collection findMatches( ArgumentSource argumentSource ) { + Collection matches = new HashSet(); + for( ArgumentMatch argumentMatch: getUniqueMatches() ) { + if( argumentMatch.definition != null && argumentMatch.definition.source.equals( argumentSource ) ) + matches.add( argumentMatch ); + } + return matches; + } + /** * Return all argument matches of this definition. * @param definition Argument definition to match. @@ -131,6 +119,52 @@ public class ArgumentMatches implements Iterable { } return matches; } + + /** + * Find arguments that are unmatched to any definition. + * @return Set of matches that have no associated definition. + */ + public Collection findUnmatched() { + Collection matches = new HashSet(); + for( ArgumentMatch argumentMatch: getUniqueMatches() ) { + if( argumentMatch.definition == null ) + matches.add( argumentMatch ); + } + return matches; + } + + /** + * Merges the given argument match into the set of existing argument matches. + * If multiple arguments are present, those arguments will end up grouped. + * @param match The match to merge into. + * @return The merged match. + */ + void mergeInto( ArgumentMatch match ) { + boolean definitionExists = false; + + // Clone the list of argument matches to avoid ConcurrentModificationExceptions. + for( ArgumentMatch argumentMatch: getUniqueMatches() ) { + if( argumentMatch.definition == match.definition ) { + argumentMatch.mergeInto( match ); + for( int index: match.indices.keySet() ) + argumentMatches.put( index, argumentMatch ); + definitionExists = true; + } + } + + if( !definitionExists ) { + for( int index: match.indices.keySet() ) + argumentMatches.put( index, match ); + } + } + + /** + * Determines, of the argument matches by position, which are unique and returns that list. + * @return A unique set of matches. + */ + private Set getUniqueMatches() { + return new HashSet( argumentMatches.values() ); + } } /** @@ -215,6 +249,6 @@ class ArgumentMatch { * @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(); + return definition != null && definition.source.isFlag(); } } \ No newline at end of file diff --git a/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentSource.java b/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentSource.java new file mode 100644 index 000000000..c8b3adeb8 --- /dev/null +++ b/java/src/org/broadinstitute/sting/utils/cmdLine/ArgumentSource.java @@ -0,0 +1,181 @@ +/* + * 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.utils.cmdLine; + +import org.broadinstitute.sting.utils.StingException; + +import java.lang.reflect.Field; +import java.util.Collection; + +/** + * Describes the source field which defines a command-line argument. + * A parsed-object version of the command-line argument will be + * injected into an object containing this field. + * + * @author mhanna + * @version 0.1 + */ +public class ArgumentSource { + /** + * Class to which the field belongs. + */ + public final Class clazz; + + /** + * Field into which to inject command-line arguments. + */ + public final Field field; + + /** + * Descriptor for the argument. Contains name, validation info, etc. + */ + public final Argument descriptor; + + /** + * 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'. + */ + public ArgumentSource( Class clazz, Field field ) { + this.clazz = clazz; + this.field = field; + this.descriptor = field.getAnnotation(Argument.class); + if( descriptor == null ) + throw new StingException("Cannot build out a command-line argument without a descriptor."); + } + + /** + * True if this argument source equals other. + * @param other Another object, possibly an argument source, to test for equality. Any object can + * be tested, but only instances of ArgumentSource will result in equals returning true. + * @return True if this argument source matches other. False otherwise. + */ + @Override + public boolean equals( Object other ) { + if( other == null ) + return false; + if( !(other instanceof ArgumentSource) ) + return false; + + ArgumentSource otherArgumentSource = (ArgumentSource)other; + return this.clazz.equals(otherArgumentSource.clazz) && this.field.equals(otherArgumentSource.field); + } + + /** + * Returns an appropriate hash code for this argument source. + * @return A uniformly distributed hashcode representing this argument source. + */ + @Override + public int hashCode() { + return clazz.hashCode() ^ field.hashCode(); + } + + /** + * Retrieves the full name of the argument, specifiable with the '--' prefix. The full name can be + * either specified explicitly with the fullName annotation parameter or implied by the field name. + * @return full name of the argument. Never null. + */ + public String getFullName() { + return descriptor.fullName().trim().length() > 0 ? descriptor.fullName().trim() : field.getName().toLowerCase(); + } + + /** + * Retrieves the short name of the argument, specifiable with the '-' prefix. The short name can + * be specified or not; if left unspecified, no short name will be present. + * @return short name of the argument. Null if no short name exists. + */ + public String getShortName() { + return descriptor.shortName().trim().length() > 0 ? descriptor.shortName().trim() : null; + } + + /** + * Documentation for this argument. Mandatory field. + * @return Documentation for this argument. + */ + public String getDoc() { + return descriptor.doc(); + } + + /** + * Returns whether this field is required. Note that flag fields are always forced to 'not required'. + * @return True if the field is mandatory and not a boolean flag. False otherwise. + */ + public boolean isRequired() { + return descriptor.required() && !isFlag(); + } + + /** + * Specifies other arguments which cannot be used in conjunction with tihs argument. Comma-separated list. + * @return A comma-separated list of exclusive arguments, or null if none are present. + */ + public String getExclusiveOf() { + return descriptor.exclusiveOf().trim().length() > 0 ? descriptor.exclusiveOf().trim() : null; + } + + /** + * A regular expression which can be used for validation. + * @return a JVM regex-compatible regular expression, or null to permit any possible value. + */ + public String getValidationRegex() { + return descriptor.validation().trim().length() > 0 ? descriptor.validation().trim() : null; + } + + /** + * Set the value of the field in the passed object to value + * @param targetInstance Instance in which to find the field. + * @param value Value to which to set the field. + */ + public void setValue( Object targetInstance, Object value ) { + try { + field.setAccessible(true); + field.set(targetInstance, value); + } + catch( IllegalAccessException ex ) { + //logger.fatal("processArgs: cannot convert field " + field.toString()); + throw new StingException("processArgs: Failed conversion " + ex.getMessage(), ex); + } + + } + + /** + * Returns true if the argument is a flag (a 0-valued argument). + * @return True if argument is a flag; false otherwise. + */ + public boolean isFlag() { + return (field.getType() == Boolean.class) || (field.getType() == Boolean.TYPE); + } + + /** + * Can this argument support multiple values, or just one? + * @return True if the argument supports multiple values. + */ + public boolean isMultiValued() { + Class argumentType = field.getType(); + return Collection.class.isAssignableFrom(argumentType) || field.getType().isArray(); + } + + +} diff --git a/java/src/org/broadinstitute/sting/utils/cmdLine/FieldParser.java b/java/src/org/broadinstitute/sting/utils/cmdLine/FieldParser.java new file mode 100644 index 000000000..617bcca4c --- /dev/null +++ b/java/src/org/broadinstitute/sting/utils/cmdLine/FieldParser.java @@ -0,0 +1,238 @@ +/* + * 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.utils.cmdLine; + +import org.broadinstitute.sting.utils.StingException; +import org.apache.log4j.Logger; + +import java.lang.reflect.*; +import java.util.*; + +/** + * An factory capable of providing parsers that can parse any type + * of supported command-line argument. + * + * @author mhanna + * @version 0.1 + */ +public abstract class FieldParser { + + /** + * our log, which we want to capture anything from org.broadinstitute.sting + */ + protected static Logger logger = Logger.getLogger(FieldParser.class); + + /** + * Name of the field which should be parsed by this argument. + */ + protected final String fieldName; + + public static FieldParser create( Field field ) { + Class type = field.getType(); + + if( Collection.class.isAssignableFrom(type) || type.isArray() ) + return new JRECompoundFieldParser( field ); + else + return new JRESimpleFieldParser( field ); + } + + static FieldParser create( String fieldName, Class type ) { + if( Collection.class.isAssignableFrom(type) || type.isArray() ) + return new JRECompoundFieldParser( fieldName, type ); + else + return new JRESimpleFieldParser( fieldName, type ); + } + + protected FieldParser( Field field ) { + fieldName = field.toString(); + } + + protected FieldParser( String fieldName ) { + this.fieldName = fieldName; + } + + public abstract Object parse( String... values ); +} + +class JRESimpleFieldParser extends FieldParser { + private final Class type; + + public JRESimpleFieldParser( Field field ) { + super( field ); + this.type = field.getType(); + } + + public JRESimpleFieldParser( String fieldName, Class type ) { + super( fieldName ); + this.type = type; + } + + public Object parse( String... values ) { + if( values.length > 1 ) + throw new StingException("Simple argument parser is unable to parse multiple arguments."); + + String value = values[0]; + + // lets go through the types we support + try { + if (type.isPrimitive()) { + Method valueOf = primitiveToWrapperMap.get(type).getMethod("valueOf",String.class); + return valueOf.invoke(null,value.trim()); + } else if (type.isEnum()) { + return Enum.valueOf(type,value.toUpperCase().trim()); + } else { + Constructor ctor = type.getConstructor(String.class); + return ctor.newInstance(value); + } + } + catch (NoSuchMethodException e) { + logger.fatal("ArgumentParser: NoSuchMethodException: cannot convert field " + fieldName); + throw new StingException("constructFromString:NoSuchMethodException: Failed conversion " + e.getMessage()); + } catch (IllegalAccessException e) { + logger.fatal("ArgumentParser: IllegalAccessException: cannot convert field " + fieldName); + throw new StingException("constructFromString:IllegalAccessException: Failed conversion " + e.getMessage()); + } catch (InvocationTargetException e) { + logger.fatal("ArgumentParser: InvocationTargetException: cannot convert field " + fieldName); + throw new StingException("constructFromString:InvocationTargetException: Failed conversion " + e.getMessage()); + } catch (InstantiationException e) { + logger.fatal("ArgumentParser: InstantiationException: cannot convert field " + fieldName); + throw new StingException("constructFromString:InstantiationException: Failed conversion " + e.getMessage()); + } + + } + + /** + * A mapping of the primitive types to their associated wrapper classes. Is there really no way to infer + * this association available in the JRE? + */ + private static Map primitiveToWrapperMap = new HashMap() { + { + put( Boolean.TYPE, Boolean.class ); + put( Character.TYPE, Character.class ); + put( Byte.TYPE, Byte.class ); + put( Short.TYPE, Short.class ); + put( Integer.TYPE, Integer.class ); + put( Long.TYPE, Long.class ); + put( Float.TYPE, Float.class ); + put( Double.TYPE, Double.class ); + } + }; +} + +class JRECompoundFieldParser extends FieldParser { + private final Class type; + private final Class componentType; + private final FieldParser componentArgumentParser; + + public JRECompoundFieldParser( Field field ) { + super( field ); + Class candidateType = field.getType(); + + if( Collection.class.isAssignableFrom(candidateType) ) { + + // If this is a generic interface, pick a concrete implementation to create and pass back. + // Because of type erasure, don't worry about creating one of exactly the correct type. + if( Modifier.isInterface(candidateType.getModifiers()) || Modifier.isAbstract(candidateType.getModifiers()) ) + { + if( java.util.List.class.isAssignableFrom(candidateType) ) candidateType = ArrayList.class; + else if( java.util.Queue.class.isAssignableFrom(candidateType) ) candidateType = java.util.ArrayDeque.class; + else if( java.util.Set.class.isAssignableFrom(candidateType) ) candidateType = java.util.TreeSet.class; + } + + this.type = candidateType; + + // If this is a parameterized collection, find the contained type. If blow up if only 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()); + componentType = (Class)parameterizedType.getActualTypeArguments()[0]; + } + else + componentType = String.class; + } + else if( candidateType.isArray() ) { + this.type = candidateType; + this.componentType = candidateType.getComponentType(); + } + else + throw new StingException("Unsupported compound argument type: " + candidateType); + + componentArgumentParser = FieldParser.create( fieldName, componentType ); + } + + public JRECompoundFieldParser( String fieldName, Class type ) { + super(fieldName); + + this.type = type; + + if( Collection.class.isAssignableFrom(type) ) { + this.componentType = String.class; + } + else if( type.isArray() ) { + this.componentType = type.getComponentType(); + } + else + throw new StingException("Unsupported compound argument type: " + type); + + componentArgumentParser = FieldParser.create( fieldName, componentType ); + } + + @Override + public Object parse( String... values ) + { + if( Collection.class.isAssignableFrom(type) ) { + Collection collection = null; + try { + collection = (Collection)type.newInstance(); + } + catch (InstantiationException e) { + logger.fatal("ArgumentParser: InstantiationException: cannot convert field " + fieldName); + throw new StingException("constructFromString:InstantiationException: Failed conversion " + e.getMessage()); + } + catch (IllegalAccessException e) { + logger.fatal("ArgumentParser: IllegalAccessException: cannot convert field " + fieldName); + throw new StingException("constructFromString:IllegalAccessException: Failed conversion " + e.getMessage()); + } + + for( String value: values ) + collection.add( componentArgumentParser.parse(value) ); + + return collection; + } + else if( type.isArray() ) { + Object arr = Array.newInstance(componentType,values.length); + + for( int i = 0; i < values.length; i++ ) + Array.set( arr,i,componentArgumentParser.parse(values[i])); + + return arr; + } + else + throw new StingException("Unsupported compound argument type: " + type); + } +} + diff --git a/java/src/org/broadinstitute/sting/utils/cmdLine/HelpFormatter.java b/java/src/org/broadinstitute/sting/utils/cmdLine/HelpFormatter.java index 9458208f0..964d31f6a 100755 --- a/java/src/org/broadinstitute/sting/utils/cmdLine/HelpFormatter.java +++ b/java/src/org/broadinstitute/sting/utils/cmdLine/HelpFormatter.java @@ -64,7 +64,7 @@ public class HelpFormatter { lineFormatter.format("-%s", argumentDefinition.shortName); else lineFormatter.format("--%s", argumentDefinition.fullName); - if( !argumentDefinition.isFlag() ) + if( !argumentDefinition.source.isFlag() ) lineFormatter.format(" <%s>", argumentDefinition.fullName); if( !argumentDefinition.required ) lineFormatter.format("]"); } @@ -146,7 +146,7 @@ public class HelpFormatter { if( argumentDefinition.shortName != null ) formatter.format("-%s,", argumentDefinition.shortName); formatter.format("--%s", argumentDefinition.fullName); - if( !argumentDefinition.isFlag() ) + if( !argumentDefinition.source.isFlag() ) formatter.format(" <%s>", argumentDefinition.fullName); return builder.toString(); diff --git a/java/src/org/broadinstitute/sting/utils/cmdLine/ParsingEngine.java b/java/src/org/broadinstitute/sting/utils/cmdLine/ParsingEngine.java index 8f4d6d680..90e2773ef 100755 --- a/java/src/org/broadinstitute/sting/utils/cmdLine/ParsingEngine.java +++ b/java/src/org/broadinstitute/sting/utils/cmdLine/ParsingEngine.java @@ -4,23 +4,8 @@ import org.broadinstitute.sting.utils.StingException; import org.broadinstitute.sting.utils.Pair; import org.apache.log4j.Logger; -import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Modifier; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; -import java.util.Collection; -import java.util.Arrays; -import java.util.EnumSet; -import java.io.File; - -import net.sf.samtools.SAMFileReader; -import net.sf.samtools.SAMFileWriter; -import net.sf.samtools.SAMFileWriterFactory; -import net.sf.samtools.SAMFileHeader; +import java.lang.reflect.*; +import java.util.*; /** * Created by IntelliJ IDEA. @@ -40,6 +25,11 @@ import net.sf.samtools.SAMFileHeader; * A parser for Sting command-line arguments. */ public class ParsingEngine { + /** + * A collection of all the source fields which define command-line arguments. + */ + List argumentSources = new ArrayList(); + /** * A list of defined arguments against which command lines are matched. * Package protected for testing access. @@ -90,22 +80,12 @@ public class ParsingEngine { * any number of fields with an @Argument annotation attached. * @param sourceName name for this argument source. 'Null' indicates that this source should be treated * as the main module. - * @param source An argument source from which to extract command-line arguments. + * @param sourceClass A class containing argument sources from which to extract command-line arguments. */ - public void addArgumentSource( String sourceName, Class source ) { + public void addArgumentSource( String sourceName, Class sourceClass ) { List argumentsFromSource = new ArrayList(); - while( source != null ) { - Field[] fields = source.getDeclaredFields(); - for( Field field: fields ) { - Argument argument = field.getAnnotation(Argument.class); - if(argument != null) - argumentsFromSource.add( new ArgumentDefinition(argument,source,field) ); - ArgumentCollection argumentCollection = field.getAnnotation(ArgumentCollection.class); - if(argumentCollection != null) - addArgumentSource(sourceName, field.getType()); - } - source = source.getSuperclass(); - } + for( ArgumentSource argumentSource: extractArgumentSources(sourceClass,true) ) + argumentsFromSource.add( new ArgumentDefinition(argumentSource) ); argumentDefinitions.add( new ArgumentDefinitionGroup(sourceName, argumentsFromSource) ); } @@ -131,8 +111,30 @@ public class ParsingEngine { * an empty object, but will never return null. */ public void parse( String[] tokens ) { - argumentMatches = parseArguments( tokens ); - fitValuesToArguments( argumentMatches, tokens ); + argumentMatches = new ArgumentMatches(); + + int lastArgumentMatchSite = -1; + + for( int i = 0; i < tokens.length; i++ ) { + String token = tokens[i]; + // If the token is of argument form, parse it into its own argument match. + // Otherwise, pair it with the most recently used argument discovered. + if( isArgumentForm(token) ) { + ArgumentMatch argumentMatch = parseArgument( token, i ); + if( argumentMatch != null ) { + argumentMatches.mergeInto( argumentMatch ); + lastArgumentMatchSite = i; + } + } + else { + if( argumentMatches.hasMatch(lastArgumentMatchSite) && + !argumentMatches.getMatch(lastArgumentMatchSite).hasValueAtSite(lastArgumentMatchSite)) + argumentMatches.getMatch(lastArgumentMatchSite).addValue( lastArgumentMatchSite, token ); + else + argumentMatches.MissingArgument.addValue( i, token ); + + } + } } public enum ValidationType { MissingRequiredArgument, @@ -140,7 +142,7 @@ public class ParsingEngine { InvalidArgumentValue, ValueMissingArgument, TooManyValuesForArgument, - MutuallyExclusive }; + MutuallyExclusive } /** * Validates the list of command-line argument matches. @@ -171,7 +173,7 @@ public class ParsingEngine { // Find invalid arguments. Invalid arguments will have a null argument definition. if( !skipValidationOf.contains(ValidationType.InvalidArgument) ) { - Collection invalidArguments = argumentMatches.findMatches(null); + Collection invalidArguments = argumentMatches.findUnmatched(); if( invalidArguments.size() > 0 ) throw new InvalidArgumentException( invalidArguments ); } @@ -206,7 +208,7 @@ public class ParsingEngine { Collection overvaluedArguments = new ArrayList(); for( ArgumentMatch argumentMatch: argumentMatches.findSuccessfulMatches() ) { // Warning: assumes that definition is not null (asserted by checks above). - if( !argumentMatch.definition.isMultiValued() && argumentMatch.values().size() > 1 ) + if( !argumentMatch.definition.source.isMultiValued() && argumentMatch.values().size() > 1 ) overvaluedArguments.add(argumentMatch); } @@ -240,35 +242,41 @@ public class ParsingEngine { * @param object Object into which to add arguments. */ public void loadArgumentsIntoObject( Object object ) { - for( ArgumentMatch match: argumentMatches ) - loadArgumentIntoObject( match, object ); + // Get a list of argument sources, not including the children of this argument. For now, skip loading + // arguments into the object recursively. + List argumentSources = extractArgumentSources( object.getClass(), false ); + for( ArgumentSource argumentSource: argumentSources ) { + Collection argumentsMatchingSource = argumentMatches.findMatches( argumentSource ); + if( argumentsMatchingSource.size() != 0 ) + loadMatchesIntoObject( argumentsMatchingSource, object ); + } } /** * Loads a single argument into the object. - * @param argument Argument to load into the object. + * @param argumentMatches Argument matches to load into the object. * @param object Target for the argument. */ - public void loadArgumentIntoObject( ArgumentMatch argument, Object object ) { - ArgumentDefinition definition = argument.definition; + private void loadMatchesIntoObject( Collection argumentMatches, Object object ) { + if( argumentMatches.size() > 1 ) + throw new StingException("Too many matches"); + + ArgumentMatch match = argumentMatches.iterator().next(); + ArgumentDefinition definition = match.definition; // A null definition might be in the list if some invalid arguments were passed in but we // want to load in a subset of data for better error reporting. Ignore null definitions. if( definition == null ) return; - if( definition.sourceClass.isAssignableFrom(object.getClass()) ) { - try { - definition.sourceField.setAccessible(true); - if( !isArgumentFlag(definition) ) - definition.sourceField.set( object, constructFromString( definition.sourceField, argument.values() ) ); - else - definition.sourceField.set( object, true ); - } - catch( IllegalAccessException ex ) { - //logger.fatal("processArgs: cannot convert field " + field.toString()); - throw new StingException("processArgs: Failed conversion " + ex.getMessage(), ex); + if( definition.source.clazz.isAssignableFrom(object.getClass()) ) { + if( !definition.source.isFlag() ) { + String[] tokens = match.values().toArray(new String[0]); + FieldParser fieldParser = FieldParser.create(definition.source.field); + definition.source.setValue( object, fieldParser.parse(tokens) ); } + else + definition.source.setValue( object, true ); } } @@ -280,13 +288,25 @@ public class ParsingEngine { } /** - * Returns true if the argument is a flag (a 0-valued argument). - * @param definition Argument definition. - * @return True if argument is a flag; false otherwise. + * Extract all the argument sources from a given object. + * @param sourceClass class to act as sources for other arguments. + * @param recursive Whether to recursively look for argument collections and add their contents. + * @return A list of sources associated with this object and its aggregated objects. */ - private boolean isArgumentFlag( ArgumentDefinition definition ) { - return (definition.sourceField.getType() == Boolean.class) || (definition.sourceField.getType() == Boolean.TYPE); - } + private List extractArgumentSources( Class sourceClass, boolean recursive ) { + List argumentSources = new ArrayList(); + while( sourceClass != null ) { + Field[] fields = sourceClass.getDeclaredFields(); + for( Field field: fields ) { + if( field.isAnnotationPresent(Argument.class) ) + argumentSources.add( new ArgumentSource(sourceClass,field) ); + if( field.isAnnotationPresent(ArgumentCollection.class) && recursive ) + argumentSources.addAll( extractArgumentSources(field.getType(),recursive) ); + } + sourceClass = sourceClass.getSuperclass(); + } + return argumentSources; + } /** * Determines whether a token looks like the name of an argument. @@ -320,49 +340,6 @@ public class ParsingEngine { return null; } - /** - * Extracts the argument portions of the string and assemble them into a data structure. - * @param tokens List of tokens from which to find arguments. - * @return Set of argument matches. - */ - private ArgumentMatches parseArguments( String[] tokens ) { - ArgumentMatches argumentMatches = new ArgumentMatches(); - - for( int i = 0; i < tokens.length; i++ ) { - String token = tokens[i]; - if( isArgumentForm(token) ) { - ArgumentMatch argumentMatch = parseArgument( token, i ); - if( argumentMatch != null ) - argumentMatches.mergeInto( argumentMatch ); - } - } - - return argumentMatches; - } - - /** - * Fit the options presented on the command line to the given arguments. - * @param argumentMatches List of arguments already matched to data. - * @param tokens The command-line input. - */ - private void fitValuesToArguments( ArgumentMatches argumentMatches, String[] tokens ) { - for( int i = 0; i < tokens.length; i++ ) { - // If this is the site of a successfully matched argument, pass it over. - if( argumentMatches.hasMatch(i) ) - continue; - - // tokens[i] must be an argument value. Match it with the previous argument. - String value = tokens[i]; - int argumentSite = i - 1; - - // If the argument is present and doesn't already have a value associated with the given site, add the value. - if( argumentMatches.hasMatch(argumentSite) && !argumentMatches.getMatch(argumentSite).hasValueAtSite(argumentSite)) - argumentMatches.getMatch(argumentSite).addValue( argumentSite, value ); - else - argumentMatches.MissingArgument.addValue( i, value ); - } - } - /** * Constructs a command-line argument given a string and field. * @param f Field type from which to infer the type. @@ -370,125 +347,8 @@ public class ParsingEngine { * @return Parsed object of the inferred type. */ private Object constructFromString(Field f, List strs) { - Class type = f.getType(); - - if( Collection.class.isAssignableFrom(type) ) { - Collection collection = null; - Class containedType = null; - - // If this is a parameterized collection, find the contained type. If blow up if only one type exists. - if( f.getGenericType() instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType)f.getGenericType(); - if( parameterizedType.getActualTypeArguments().length > 1 ) - throw new IllegalArgumentException("Unable to determine collection type of field: " + f.toString()); - containedType = (Class)parameterizedType.getActualTypeArguments()[0]; - } - else - containedType = String.class; - - // If this is a generic interface, pick a concrete implementation to create and pass back. - // Because of type erasure, don't worry about creating one of exactly the correct type. - if( Modifier.isInterface(type.getModifiers()) || Modifier.isAbstract(type.getModifiers()) ) - { - if( java.util.List.class.isAssignableFrom(type) ) type = ArrayList.class; - else if( java.util.Queue.class.isAssignableFrom(type) ) type = java.util.ArrayDeque.class; - else if( java.util.Set.class.isAssignableFrom(type) ) type = java.util.TreeSet.class; - } - - try - { - collection = (Collection)type.newInstance(); - } - catch( Exception ex ) { - // Runtime exceptions are definitely unexpected parsing simple collection classes. - throw new IllegalArgumentException(ex); - } - - for( String str: strs ) - collection.add( constructSingleElement(f,containedType,str) ); - - return collection; - } - else if( type.isArray() ) { - Class containedType = type.getComponentType(); - - Object arr = Array.newInstance(containedType,strs.size()); - for( int i = 0; i < strs.size(); i++ ) - Array.set( arr,i,constructSingleElement(f,containedType,strs.get(i)) ); - return arr; - } - else { - if( strs.size() != 1 ) - throw new IllegalArgumentException("Passed multiple arguments to an object expecting a single value."); - return constructSingleElement(f,type,strs.get(0)); - } - } - - /** - * Builds a single element of the given type. - * @param f Implies type of data to construct. - * @param str String representation of data. - * @return parsed form of String. - */ - private Object constructSingleElement(Field f, Class type, String str) { - if( customArgumentFactory != null ) { - Object instance = customArgumentFactory.createArgument(type, str); - if( instance != null ) - return instance; - } - - // lets go through the types we support - if (type == Boolean.TYPE) { - boolean b = false; - if (str.toLowerCase().equals("true")) { - b = true; - } - Boolean bool = new Boolean(b); - return bool; - } else if (type == Byte.TYPE) { - Byte b = Byte.valueOf(str); - return b; - } else if (type == Short.TYPE) { - Short s = Short.valueOf(str); - return s; - } else if (type == Integer.TYPE) { - Integer in = Integer.valueOf(str); - return in; - } else if (type == Long.TYPE) { - Long l = Long.valueOf(str); - return l; - } else if (type == Float.TYPE) { - Float fl = Float.valueOf(str); - return fl; - } else if (type == Double.TYPE) { - Double db = Double.valueOf(str); - return db; - } else if (type == Character.TYPE) { - if( str.trim().length() != 1 ) - throw new StingException("Unable to parse argument '" + str + "' into a character."); - Character c = str.trim().charAt(0); - return c; - } else if (type.isEnum()) { - return Enum.valueOf(type,str.toUpperCase().trim()); - } else { - Constructor ctor = null; - try { - ctor = type.getConstructor(String.class); - return ctor.newInstance(str); - } catch (NoSuchMethodException e) { - logger.fatal("constructFromString:NoSuchMethodException: cannot convert field " + f.toString()); - throw new RuntimeException("constructFromString:NoSuchMethodException: Failed conversion " + e.getMessage()); - } catch (IllegalAccessException e) { - logger.fatal("constructFromString:IllegalAccessException: cannot convert field " + f.toString()); - throw new RuntimeException("constructFromString:IllegalAccessException: Failed conversion " + e.getMessage()); - } catch (InvocationTargetException e) { - logger.fatal("constructFromString:InvocationTargetException: cannot convert field " + f.toString()); - throw new RuntimeException("constructFromString:InvocationTargetException: Failed conversion " + e.getMessage()); - } catch (InstantiationException e) { - logger.fatal("constructFromString:InstantiationException: cannot convert field " + f.toString()); - throw new RuntimeException("constructFromString:InstantiationException: Failed conversion " + e.getMessage()); - } - } + FieldParser fieldParser = FieldParser.create(f); + return fieldParser.parse( strs.toArray(new String[0]) ); } } diff --git a/java/test/org/broadinstitute/sting/utils/cmdLine/ParsingEngineTest.java b/java/test/org/broadinstitute/sting/utils/cmdLine/ParsingEngineTest.java index c2eaa1e7e..ce0e0cf3c 100755 --- a/java/test/org/broadinstitute/sting/utils/cmdLine/ParsingEngineTest.java +++ b/java/test/org/broadinstitute/sting/utils/cmdLine/ParsingEngineTest.java @@ -100,6 +100,25 @@ public class ParsingEngineTest extends BaseTest { Assert.assertEquals("Argument is not correctly initialized", "na12878.bam", argProvider.inputFile ); } + @Test + public void primitiveArgumentTest() { + final String[] commandLine = new String[] {"--foo", "5"}; + + parsingEngine.addArgumentSource( PrimitiveArgProvider.class ); + parsingEngine.parse( commandLine ); + parsingEngine.validate(); + + PrimitiveArgProvider argProvider = new PrimitiveArgProvider(); + parsingEngine.loadArgumentsIntoObject( argProvider ); + + Assert.assertEquals("Argument is not correctly initialized", 5, argProvider.foo ); + } + + private class PrimitiveArgProvider { + @Argument(doc="simple integer") + int foo; + } + @Test public void flagTest() { final String[] commandLine = new String[] {"--all_loci"};