Java tutorial
/* * Copyright 2012-2016 Broad Institute, Inc. * * 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.gatk.utils.commandline; import com.google.java.contract.Requires; import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.broadinstitute.gatk.utils.Utils; import org.broadinstitute.gatk.utils.classloader.JVMUtils; import org.broadinstitute.gatk.utils.classloader.PluginManager; import org.broadinstitute.gatk.utils.collections.Pair; import org.broadinstitute.gatk.utils.exceptions.ReviewedGATKException; import org.broadinstitute.gatk.utils.exceptions.UserException; import org.broadinstitute.gatk.utils.help.ApplicationDetails; import org.broadinstitute.gatk.utils.help.HelpFormatter; import java.io.File; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.*; /** * A parser for command-line arguments. */ public class ParsingEngine { /** * The loaded argument sources along with their back definitions. */ private Map<ArgumentDefinition, ArgumentSource> argumentSourcesByDefinition = new HashMap<ArgumentDefinition, ArgumentSource>(); /** * A list of defined arguments against which command lines are matched. * Package protected for testing access. */ public ArgumentDefinitions argumentDefinitions = new ArgumentDefinitions(); /** * A list of matches from defined arguments to command-line text. * Indicates as best as possible where command-line text remains unmatched * to existing arguments. */ private ArgumentMatches argumentMatches = null; /** * Techniques for parsing and for argument lookup. */ private List<ParsingMethod> parsingMethods = new ArrayList<ParsingMethod>(); /** * All of the RodBinding objects we've seen while parsing */ private List<RodBinding> rodBindings = new ArrayList<RodBinding>(); /** * Class reference to the different types of descriptors that the create method can create. * The type of set used must be ordered (but not necessarily sorted). */ private static final Set<ArgumentTypeDescriptor> STANDARD_ARGUMENT_TYPE_DESCRIPTORS = new LinkedHashSet<ArgumentTypeDescriptor>( Arrays.asList(new SimpleArgumentTypeDescriptor(), new IntervalBindingArgumentTypeDescriptor(), new RodBindingArgumentTypeDescriptor(), new RodBindingCollectionArgumentTypeDescriptor(), new CompoundArgumentTypeDescriptor(), new MultiplexArgumentTypeDescriptor())); private Set<ArgumentTypeDescriptor> argumentTypeDescriptors = new LinkedHashSet<ArgumentTypeDescriptor>(); /** * List of tags associated with the given instantiation of the command-line argument. */ private final Map<Object, Tags> tags = new IdentityHashMap<Object, Tags>(); private PluginManager<ParsingEngineArgumentProvider> argumentProviderPluginManager = new PluginManager<ParsingEngineArgumentProvider>( ParsingEngineArgumentProvider.class); /** * our log, which we want to capture anything from org.broadinstitute.gatk */ protected static Logger logger = Logger.getLogger(ParsingEngine.class); public ParsingEngine(CommandLineProgram clp) { RodBinding.resetNameCounter(); parsingMethods.add(ParsingMethod.FullNameParsingMethod); parsingMethods.add(ParsingMethod.ShortNameParsingMethod); // Order matters here! Make sure the clp's new type descriptors go in before the original type descriptors. if (clp != null) argumentTypeDescriptors.addAll(clp.getArgumentTypeDescriptors()); argumentTypeDescriptors.addAll(STANDARD_ARGUMENT_TYPE_DESCRIPTORS); List<Class<? extends ParsingEngineArgumentProvider>> providers = argumentProviderPluginManager.getPlugins(); for (Class<? extends ParsingEngineArgumentProvider> provider : providers) { addArgumentSource(provider); } } /** * Add a main argument source. Argument sources are expected to have * any number of fields with an @Argument annotation attached. * @param source An argument source from which to extract command-line arguments. */ public void addArgumentSource(Class source) { addArgumentSource(null, source); } public ArgumentMatches getArgumentMatches() { return argumentMatches; } /** * Add an argument source. Argument sources are expected to have * 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 sourceClass A class containing argument sources from which to extract command-line arguments. */ public void addArgumentSource(String sourceName, Class sourceClass) { List<ArgumentDefinition> argumentsFromSource = new ArrayList<ArgumentDefinition>(); for (ArgumentSource argumentSource : extractArgumentSources(sourceClass)) { List<ArgumentDefinition> argumentDefinitions = argumentSource.createArgumentDefinitions(); for (ArgumentDefinition argumentDefinition : argumentDefinitions) { argumentSourcesByDefinition.put(argumentDefinition, argumentSource); argumentsFromSource.add(argumentDefinition); } } argumentDefinitions.add(new ArgumentDefinitionGroup(sourceName, argumentsFromSource)); } /** * Do a cursory search to see if an argument with the given name is present. * @param argumentFullName full name of the argument. * @return True if the argument is present. False otherwise. */ public boolean isArgumentPresent(String argumentFullName) { ArgumentDefinition definition = argumentDefinitions.findArgumentDefinition(argumentFullName, ArgumentDefinitions.FullNameDefinitionMatcher); return argumentMatches.hasMatch(definition); } /** * Parse the given set of command-line arguments, returning * an ArgumentMatches object describing the best fit of these * command-line arguments to the arguments that are actually * required. * @param tokens Tokens passed on the command line. * @return The parsed arguments by file. */ public SortedMap<ArgumentMatchSource, ParsedArgs> parse(String[] tokens) { argumentMatches = new ArgumentMatches(); SortedMap<ArgumentMatchSource, ParsedArgs> parsedArgs = new TreeMap<ArgumentMatchSource, ParsedArgs>(); List<String> cmdLineTokens = Arrays.asList(tokens); parse(ArgumentMatchSource.COMMAND_LINE, cmdLineTokens, argumentMatches, parsedArgs); List<ParsingEngineArgumentProvider> providers = argumentProviderPluginManager.createAllTypes(); for (ParsingEngineArgumentProvider provider : providers) { // Load the arguments ONLY into the provider. // Validation may optionally run on the rest of the arguments. loadArgumentsIntoObject(provider); } for (ParsingEngineArgumentProvider provider : providers) { provider.parse(this, parsedArgs); } return parsedArgs; } public void parse(ArgumentMatchSource matchSource, List<String> tokens, ArgumentMatches argumentMatches, SortedMap<ArgumentMatchSource, ParsedArgs> parsedArgs) { ArgumentMatchSite lastArgumentMatchSite = new ArgumentMatchSite(matchSource, -1); int i = 0; for (String token : tokens) { // If the token is of argument form, parse it into its own argument match. // Otherwise, pair it with the most recently used argument discovered. ArgumentMatchSite site = new ArgumentMatchSite(matchSource, i); if (isArgumentForm(token)) { ArgumentMatch argumentMatch = parseArgument(token, site); if (argumentMatch != null) { argumentMatches.mergeInto(argumentMatch); lastArgumentMatchSite = site; } } else { if (argumentMatches.hasMatch(lastArgumentMatchSite) && !argumentMatches.getMatch(lastArgumentMatchSite).hasValueAtSite(lastArgumentMatchSite)) argumentMatches.getMatch(lastArgumentMatchSite).addValue(lastArgumentMatchSite, new ArgumentMatchStringValue(token)); else argumentMatches.MissingArgument.addValue(site, new ArgumentMatchStringValue(token)); } i++; } parsedArgs.put(matchSource, new ParsedListArgs(tokens)); } public void parsePairs(ArgumentMatchSource matchSource, List<Pair<String, ArgumentMatchValue>> tokens, ArgumentMatches argumentMatches, ParsedArgs matchSourceArgs, SortedMap<ArgumentMatchSource, ParsedArgs> parsedArgs) { int i = 0; for (Pair<String, ArgumentMatchValue> pair : tokens) { ArgumentMatchSite site = new ArgumentMatchSite(matchSource, i); List<DefinitionMatcher> matchers = Arrays.asList(ArgumentDefinitions.FullNameDefinitionMatcher, ArgumentDefinitions.ShortNameDefinitionMatcher); ArgumentDefinition definition = null; for (DefinitionMatcher matcher : matchers) { definition = argumentDefinitions.findArgumentDefinition(pair.getFirst(), matcher); if (definition != null) break; } if (definition == null) continue; ArgumentMatch argumentMatch = new ArgumentMatch(pair.getFirst(), definition, site, new Tags()); argumentMatches.mergeInto(argumentMatch); argumentMatch.addValue(site, pair.getSecond()); i++; } parsedArgs.put(matchSource, matchSourceArgs); } protected List<String> getArguments(File file) { try { if (file.getAbsolutePath().endsWith(".list")) { return getListArguments(file); } } catch (IOException e) { throw new UserException.CouldNotReadInputFile(file, e); } throw new UserException.CouldNotReadInputFile(file, "file extension is not .list"); } private List<String> getListArguments(File file) throws IOException { ArrayList<String> argsList = new ArrayList<String>(); for (String line : FileUtils.readLines(file)) argsList.addAll(Arrays.asList(Utils.escapeExpressions(line))); return argsList; } public enum ValidationType { MissingRequiredArgument, InvalidArgument, InvalidArgumentValue, ValueMissingArgument, TooManyValuesForArgument, MutuallyExclusive, OtherArgumentRequired } /** * Validates the list of command-line argument matches. */ public void validate() { validate(EnumSet.noneOf(ValidationType.class)); } /** * Validates the list of command-line argument matches. On failure throws an exception with detailed info about the * particular failures. Takes an EnumSet indicating which validation checks to skip. * @param skipValidationOf List of validation checks to skip. */ public void validate(EnumSet<ValidationType> skipValidationOf) { // Find missing required arguments. if (!skipValidationOf.contains(ValidationType.MissingRequiredArgument)) { Collection<ArgumentDefinition> requiredArguments = argumentDefinitions.findArgumentDefinitions(true, ArgumentDefinitions.RequiredDefinitionMatcher); Collection<ArgumentDefinition> missingArguments = new ArrayList<ArgumentDefinition>(); for (ArgumentDefinition requiredArgument : requiredArguments) { if (!argumentMatches.hasMatch(requiredArgument)) missingArguments.add(requiredArgument); } if (missingArguments.size() > 0) throw new MissingArgumentException(missingArguments); } // Find invalid arguments. Invalid arguments will have a null argument definition. if (!skipValidationOf.contains(ValidationType.InvalidArgument)) { ArgumentMatches invalidArguments = argumentMatches.findUnmatched(); if (invalidArguments.size() > 0) throw new InvalidArgumentException(invalidArguments); } // Find invalid argument values -- invalid arguments are either completely missing or fail the specified 'validation' regular expression. if (!skipValidationOf.contains(ValidationType.InvalidArgumentValue)) { Collection<ArgumentDefinition> verifiableArguments = argumentDefinitions.findArgumentDefinitions(null, ArgumentDefinitions.VerifiableDefinitionMatcher); Collection<Pair<ArgumentDefinition, String>> invalidValues = new ArrayList<Pair<ArgumentDefinition, String>>(); for (ArgumentDefinition verifiableArgument : verifiableArguments) { ArgumentMatches verifiableMatches = argumentMatches.findMatches(verifiableArgument); // Check to see whether an argument value was specified. Argument values must be provided // when the argument name is specified and the argument is not a flag type. for (ArgumentMatch verifiableMatch : verifiableMatches) { ArgumentSource argumentSource = argumentSourcesByDefinition.get(verifiableArgument); if (verifiableMatch.values().size() == 0 && !verifiableArgument.isFlag && !argumentSource.createsTypeDefault()) invalidValues.add(new Pair<ArgumentDefinition, String>(verifiableArgument, null)); } // Ensure that the field contents meet the validation criteria specified by the regular expression. for (ArgumentMatch verifiableMatch : verifiableMatches) { for (ArgumentMatchValue value : verifiableMatch.values()) { if (verifiableArgument.validation != null && !value.asString().matches(verifiableArgument.validation)) invalidValues.add( new Pair<ArgumentDefinition, String>(verifiableArgument, value.asString())); } } } if (invalidValues.size() > 0) throw new InvalidArgumentValueException(invalidValues); } // Find values without an associated mate. if (!skipValidationOf.contains(ValidationType.ValueMissingArgument)) { if (argumentMatches.MissingArgument.values().size() > 0) throw new UnmatchedArgumentException(argumentMatches.MissingArgument); } // Find arguments with too many values. if (!skipValidationOf.contains(ValidationType.TooManyValuesForArgument)) { Collection<ArgumentMatch> overvaluedArguments = new ArrayList<ArgumentMatch>(); for (ArgumentMatch argumentMatch : argumentMatches.findSuccessfulMatches()) { // Warning: assumes that definition is not null (asserted by checks above). if (!argumentMatch.definition.isMultiValued && argumentMatch.values().size() > 1) overvaluedArguments.add(argumentMatch); } if (!overvaluedArguments.isEmpty()) throw new TooManyValuesForArgumentException(overvaluedArguments); } // Find sets of options that are supposed to be mutually exclusive. if (!skipValidationOf.contains(ValidationType.MutuallyExclusive)) { Collection<Pair<ArgumentMatch, ArgumentMatch>> invalidPairs = new ArrayList<Pair<ArgumentMatch, ArgumentMatch>>(); for (ArgumentMatch argumentMatch : argumentMatches.findSuccessfulMatches()) { if (argumentMatch.definition.exclusiveOf != null) { for (ArgumentMatch conflictingMatch : argumentMatches.findSuccessfulMatches()) { // Skip over the current element. if (argumentMatch == conflictingMatch) continue; if (argumentMatch.definition.exclusiveOf.equals(conflictingMatch.definition.fullName) || argumentMatch.definition.exclusiveOf .equals(conflictingMatch.definition.shortName)) invalidPairs .add(new Pair<ArgumentMatch, ArgumentMatch>(argumentMatch, conflictingMatch)); } } } if (!invalidPairs.isEmpty()) throw new ArgumentsAreMutuallyExclusiveException(invalidPairs); } // Find sets of options that have other required arguments if (!skipValidationOf.contains(ValidationType.OtherArgumentRequired)) { Collection<ArgumentMatch> missingRequiredPairs = new ArrayList<ArgumentMatch>(); for (ArgumentMatch argumentMatch : argumentMatches.findSuccessfulMatches()) { if (argumentMatch.definition.otherArgumentRequired != null) { boolean otherRequiredArgumentSeen = false; for (ArgumentMatch otherRequiredMatch : argumentMatches.findSuccessfulMatches()) { if (argumentMatch.definition.otherArgumentRequired.equals(otherRequiredMatch.label)) { otherRequiredArgumentSeen = true; } } if (!otherRequiredArgumentSeen) { missingRequiredPairs.add(argumentMatch); throw new OtherRequiredArgumentMissingException(missingRequiredPairs); } } } } } /** * Loads a set of matched command-line arguments into the given object. * @param object Object into which to add arguments. */ public void loadArgumentsIntoObject(Object object) { loadArgumentsIntoObject(object, true); } /** * Loads a set of matched command-line arguments into the given object. * @param object Object into which to add arguments. * @param enforceArgumentRanges If true, check that the argument value is within the range specified * in the corresponding Argument annotation by min/max value attributes. This * check is only performed for numeric types, and only when a min and/or * max value is actually defined in the annotation. It is also only performed * for values actually specified on the command line, and not for default values. */ public void loadArgumentsIntoObject(Object object, boolean enforceArgumentRanges) { List<ArgumentSource> argumentSources = extractArgumentSources(object.getClass()); List<ArgumentSource> dependentArguments = new ArrayList<ArgumentSource>(); for (ArgumentSource argumentSource : argumentSources) { if (argumentSource.isDeprecated() && argumentMatches.findMatches(this, argumentSource).size() > 0) notifyDeprecatedCommandLineArgument(argumentSource); // If this argument source depends on other command-line arguments, skip it and make a note to process it later. if (argumentSource.isDependent()) { dependentArguments.add(argumentSource); continue; } loadValueIntoObject(argumentSource, object, argumentMatches.findMatches(this, argumentSource), enforceArgumentRanges); } for (ArgumentSource dependentArgument : dependentArguments) { MultiplexArgumentTypeDescriptor dependentDescriptor = dependentArgument .createDependentTypeDescriptor(this, object); ArgumentSource dependentSource = dependentArgument.copyWithCustomTypeDescriptor(dependentDescriptor); loadValueIntoObject(dependentSource, object, argumentMatches.findMatches(this, dependentSource), enforceArgumentRanges); } } /** * Notify the user that tags have been created. * @param key The key created. * @param tags List of tags, or empty list if no tags are present. */ public void addTags(Object key, final Tags tags) { this.tags.put(key, tags); } /** * Gets the tags associated with a given object. * @param key Key for which to find a tag. * @return List of tags associated with this key. */ public Tags getTags(Object key) { if (!tags.containsKey(key)) return new Tags(); return tags.get(key); } /** * Add a RodBinding type argument to this parser. Called during parsing to allow * us to track all of the RodBindings discovered in the command line. * @param rodBinding the rodbinding to add. Must not be added twice */ @Requires("rodBinding != null") public void addRodBinding(final RodBinding rodBinding) { rodBindings.add(rodBinding); } /** * Notify the user that a deprecated command-line argument has been used. * @param argumentSource Deprecated argument source specified by user. */ private void notifyDeprecatedCommandLineArgument(ArgumentSource argumentSource) { // Grab the first argument definition and report that one as the failure. Theoretically, we should notify of all failures. List<ArgumentDefinition> definitions = argumentSource.createArgumentDefinitions(); if (definitions.size() < 1) throw new ReviewedGATKException("Internal error. Argument source creates no definitions."); ArgumentDefinition definition = definitions.get(0); throw new UserException.DeprecatedArgument(definition.fullName, definition.doc); } /** * Loads a single argument into the object and that objects children. * @param argumentMatches Argument matches to load into the object. * @param source Argument source to load into the object. * @param instance Object into which to inject the value. The target might be in a container within the instance. * @param enforceArgumentRanges If true, check that the argument value is within the range specified * in the corresponding Argument annotation by min/max value attributes. This * check is only performed for numeric types, and only when a min and/or * max value is actually defined in the annotation. It is also only performed * for values actually specified on the command line, and not for default values. */ private void loadValueIntoObject(ArgumentSource source, Object instance, ArgumentMatches argumentMatches, boolean enforceArgumentRanges) { // Nothing to load if (argumentMatches.size() == 0 && !source.createsTypeDefault()) return; // Target instance into which to inject the value. Collection<Object> targets = findTargets(source, instance); // Abort if no home is found for the object. if (targets.size() == 0) throw new ReviewedGATKException( "Internal command-line parser error: unable to find a home for argument matches " + argumentMatches); for (Object target : targets) { Object value; boolean usedTypeDefault = false; if (argumentMatches.size() != 0) { value = source.parse(this, argumentMatches); } else { value = source.createTypeDefault(this); usedTypeDefault = true; } // Only check argument ranges if a check was requested AND we used a value from the command line rather // than the type default if (enforceArgumentRanges && !usedTypeDefault) { checkArgumentRange(source, value); } JVMUtils.setFieldValue(source.field, target, value); } } /** * Check the provided value against any range constraints specified in the Argument annotation * for the corresponding field. Throw an exception if hard limits are violated, or emit a warning * if soft limits are violated. * * Only checks numeric types (int, double, etc.) * Only checks fields with an actual @Argument annotation * Only checks manually-specified constraints (there are no default constraints). * * @param argumentSource The source field for the command-line argument * @param argumentValue The value we're considering putting in that source field */ private void checkArgumentRange(final ArgumentSource argumentSource, final Object argumentValue) { // Only validate numeric types if (!(argumentValue instanceof Number)) { return; } final double argumentDoubleValue = ((Number) argumentValue).doubleValue(); // Only validate fields with an @Argument annotation final Annotation argumentAnnotation = argumentSource.field.getAnnotation(Argument.class); if (argumentAnnotation == null) { return; } final double minValue = (Double) CommandLineUtils.getValue(argumentAnnotation, "minValue"); final double maxValue = (Double) CommandLineUtils.getValue(argumentAnnotation, "maxValue"); final double minRecommendedValue = (Double) CommandLineUtils.getValue(argumentAnnotation, "minRecommendedValue"); final double maxRecommendedValue = (Double) CommandLineUtils.getValue(argumentAnnotation, "maxRecommendedValue"); final String argumentName = (String) CommandLineUtils.getValue(argumentAnnotation, "fullName"); // Check hard limits first, if specified if (minValue != Double.NEGATIVE_INFINITY && argumentDoubleValue < minValue) { throw new ArgumentValueOutOfRangeException(argumentName, argumentDoubleValue, minValue, "minimum"); } if (maxValue != Double.POSITIVE_INFINITY && argumentDoubleValue > maxValue) { throw new ArgumentValueOutOfRangeException(argumentName, argumentDoubleValue, maxValue, "maximum"); } // Then check soft limits, if specified if (minRecommendedValue != Double.NEGATIVE_INFINITY && argumentDoubleValue < minRecommendedValue) { logger.warn( String.format("WARNING: argument --%s has value %.2f, but minimum recommended value is %.2f", argumentName, argumentDoubleValue, minRecommendedValue)); } if (maxRecommendedValue != Double.POSITIVE_INFINITY && argumentDoubleValue > maxRecommendedValue) { logger.warn( String.format("WARNING: argument --%s has value %.2f, but maximum recommended value is %.2f", argumentName, argumentDoubleValue, maxRecommendedValue)); } } public Collection<RodBinding> getRodBindings() { return Collections.unmodifiableCollection(rodBindings); } /** * 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<Object> findTargets(ArgumentSource source, Object instance) { LinkedHashSet<Object> targets = new LinkedHashSet<Object>(); 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. */ public void printHelp(ApplicationDetails applicationDetails) { new HelpFormatter().printHelp(applicationDetails, argumentDefinitions); } /** * Extract all the argument sources from a given object. * @param sourceClass class to act as sources for other arguments. * @return A list of sources associated with this object and its aggregated objects. */ public List<ArgumentSource> extractArgumentSources(Class sourceClass) { return extractArgumentSources(sourceClass, new Field[0]); } /** * Fetch the best command-line argument descriptor for the given class. * @param type Class for which to specify a descriptor. * @return descriptor for the given type. */ public ArgumentTypeDescriptor selectBestTypeDescriptor(Class type) { return ArgumentTypeDescriptor.selectBest(argumentTypeDescriptors, type); } private List<ArgumentSource> extractArgumentSources(Class sourceClass, Field[] parentFields) { // now simply call into the truly general routine extract argument bindings but with a null // object so bindings aren't computed Map<ArgumentSource, Object> bindings = extractArgumentBindings(null, sourceClass, parentFields); return new ArrayList<ArgumentSource>(bindings.keySet()); } public Map<ArgumentSource, Object> extractArgumentBindings(Object obj) { if (obj == null) throw new IllegalArgumentException("Incoming object cannot be null"); return extractArgumentBindings(obj, obj.getClass(), new Field[0]); } /** * Extract all the argument sources from a given object, along with their bindings if obj != null . * @param obj the object corresponding to the sourceClass * @param sourceClass class to act as sources for other arguments. * @param parentFields Parent Fields * @return A map of sources associated with this object and its aggregated objects and bindings to their bindings values */ private Map<ArgumentSource, Object> extractArgumentBindings(Object obj, Class sourceClass, Field[] parentFields) { Map<ArgumentSource, Object> bindings = new LinkedHashMap<ArgumentSource, Object>(); while (sourceClass != null) { Field[] fields = sourceClass.getDeclaredFields(); for (Field field : fields) { if (ArgumentTypeDescriptor.isArgumentAnnotationPresent(field)) { Object val = obj != null ? JVMUtils.getFieldValue(field, obj) : null; bindings.put(new ArgumentSource(parentFields, field, selectBestTypeDescriptor(field.getType())), val); } if (field.isAnnotationPresent(ArgumentCollection.class)) { Object val = obj != null ? JVMUtils.getFieldValue(field, obj) : null; Field[] newParentFields = Arrays.copyOf(parentFields, parentFields.length + 1); newParentFields[parentFields.length] = field; bindings.putAll(extractArgumentBindings(val, field.getType(), newParentFields)); } } sourceClass = sourceClass.getSuperclass(); } return bindings; } /** * Determines whether a token looks like the name of an argument. * @param token Token to inspect. Can be surrounded by whitespace. * @return True if token is of short name form. */ private boolean isArgumentForm(String token) { for (ParsingMethod parsingMethod : parsingMethods) { if (parsingMethod.matches(token)) return true; } return false; } /** * Parse a short name into an ArgumentMatch. * @param token The token to parse. The token should pass the isLongArgumentForm test. * @param position The position of the token in question. * @return ArgumentMatch associated with this token, or null if no match exists. */ private ArgumentMatch parseArgument(String token, ArgumentMatchSite position) { if (!isArgumentForm(token)) throw new IllegalArgumentException("Token is not recognizable as an argument: " + token); for (ParsingMethod parsingMethod : parsingMethods) { if (parsingMethod.matches(token)) return parsingMethod.match(argumentDefinitions, token, position); } // No parse results found. return null; } } /** * An exception indicating that some required arguments are missing. */ class MissingArgumentException extends ArgumentException { public MissingArgumentException(Collection<ArgumentDefinition> missingArguments) { super(formatArguments(missingArguments)); } private static String formatArguments(Collection<ArgumentDefinition> missingArguments) { StringBuilder sb = new StringBuilder(); for (ArgumentDefinition missingArgument : missingArguments) { if (missingArgument.shortName != null) sb.append(String.format("%nArgument with name '--%s' (-%s) is missing.", missingArgument.fullName, missingArgument.shortName)); else sb.append(String.format("%nArgument with name '--%s' is missing.", missingArgument.fullName)); } return sb.toString(); } } /** * An exception for undefined arguments. */ class InvalidArgumentException extends ArgumentException { public InvalidArgumentException(ArgumentMatches invalidArguments) { super(formatArguments(invalidArguments)); } private static String formatArguments(ArgumentMatches invalidArguments) { StringBuilder sb = new StringBuilder(); for (ArgumentMatch invalidArgument : invalidArguments) sb.append(String.format("%nArgument with name '%s' isn't defined.", invalidArgument.label)); return sb.toString(); } } /** * An exception for values whose format is invalid. */ class InvalidArgumentValueException extends ArgumentException { public InvalidArgumentValueException(Collection<Pair<ArgumentDefinition, String>> invalidArgumentValues) { super(formatArguments(invalidArgumentValues)); } private static String formatArguments(Collection<Pair<ArgumentDefinition, String>> invalidArgumentValues) { StringBuilder sb = new StringBuilder(); for (Pair<ArgumentDefinition, String> invalidValue : invalidArgumentValues) { if (invalidValue.getSecond() == null) sb.append(String.format("%nArgument '--%s' requires a value but none was provided", invalidValue.first.fullName)); else sb.append(String.format("%nArgument '--%s' has value of incorrect format: %s (should match %s)", invalidValue.first.fullName, invalidValue.second, invalidValue.first.validation)); } return sb.toString(); } } class ArgumentValueOutOfRangeException extends ArgumentException { public ArgumentValueOutOfRangeException(final String argumentName, final double argumentActualValue, final double argumentBoundaryValue, final String argumentBoundaryType) { super(String.format("Argument --%s has value %.2f, but %s allowed value is %.2f", argumentName, argumentActualValue, argumentBoundaryType, argumentBoundaryValue)); } } /** * An exception for values that can't be mated with any argument. */ class UnmatchedArgumentException extends ArgumentException { public UnmatchedArgumentException(ArgumentMatch invalidValues) { super(formatArguments(invalidValues)); } private static String formatArguments(ArgumentMatch invalidValues) { StringBuilder sb = new StringBuilder(); for (ArgumentMatchSite site : invalidValues.sites.keySet()) for (ArgumentMatchValue value : invalidValues.sites.get(site)) { switch (site.getSource().getType()) { case CommandLine: sb.append(String.format("%nInvalid argument value '%s' at position %d.", value.asString(), site.getIndex())); break; case Provider: sb.append(String.format("%nInvalid argument value '%s' in %s at position %d.", value.asString(), site.getSource().getDescription(), site.getIndex())); break; default: throw new RuntimeException( String.format("Unexpected argument match source type: %s", site.getSource().getType())); } if (value.asString() != null && Utils.dupString(' ', value.asString().length()).equals(value.asString())) sb.append( " Please make sure any line continuation backslashes on your command line are not followed by whitespace."); } return sb.toString(); } } /** * An exception indicating that too many values have been provided for the given argument. */ class TooManyValuesForArgumentException extends ArgumentException { public TooManyValuesForArgumentException(Collection<ArgumentMatch> arguments) { super(formatArguments(arguments)); } private static String formatArguments(Collection<ArgumentMatch> arguments) { StringBuilder sb = new StringBuilder(); for (ArgumentMatch argument : arguments) sb.append(String.format("%nArgument '%s' has too many values: %s.", argument.label, Arrays.deepToString(argument.values().toArray()))); return sb.toString(); } } /** * An exception indicating that mutually exclusive options have been passed in the same command line. */ class ArgumentsAreMutuallyExclusiveException extends ArgumentException { public ArgumentsAreMutuallyExclusiveException(Collection<Pair<ArgumentMatch, ArgumentMatch>> arguments) { super(formatArguments(arguments)); } private static String formatArguments(Collection<Pair<ArgumentMatch, ArgumentMatch>> arguments) { StringBuilder sb = new StringBuilder(); for (Pair<ArgumentMatch, ArgumentMatch> argument : arguments) sb.append(String.format("%nArguments '%s' and '%s' are mutually exclusive.", argument.first.definition.fullName, argument.second.definition.fullName)); return sb.toString(); } } class OtherRequiredArgumentMissingException extends ArgumentException { public OtherRequiredArgumentMissingException(Collection<ArgumentMatch> arguments) { super(formatArguments(arguments)); } private static String formatArguments(Collection<ArgumentMatch> arguments) { StringBuilder sb = new StringBuilder(); for (ArgumentMatch argument : arguments) sb.append(String.format("%nArguments '%s' and '%s' are required to go together.", argument.definition.fullName, argument.definition.otherArgumentRequired)); return sb.toString(); } } /** * An exception for when an argument doesn't match an of the enumerated options for that var type */ class UnknownEnumeratedValueException extends ArgumentException { public UnknownEnumeratedValueException(ArgumentDefinition definition, String argumentPassed) { super(formatArguments(definition, argumentPassed)); } private static String formatArguments(ArgumentDefinition definition, String argumentPassed) { return String.format("Invalid value %s specified for argument %s; valid options are (%s).", argumentPassed, definition.fullName, Utils.join(",", definition.validOptions)); } }