// Copyright 2014 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.common.options; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.escape.Escaper; import com.google.devtools.common.options.OptionDefinition.NotAnOptionException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; /** * A parser for options. Typical use case in a main method: * * <pre> * OptionsParser parser = OptionsParser.newOptionsParser(FooOptions.class, BarOptions.class); * parser.parseAndExitUponError(args); * FooOptions foo = parser.getOptions(FooOptions.class); * BarOptions bar = parser.getOptions(BarOptions.class); * List<String> otherArguments = parser.getResidue(); * </pre> * * <p>FooOptions and BarOptions would be options specification classes, derived from OptionsBase, * that contain fields annotated with @Option(...). * * <p>Alternatively, rather than calling {@link * #parseAndExitUponError(OptionPriority.PriorityCategory, String, String[])}, client code may call * {@link #parse(OptionPriority.PriorityCategory,String,List)}, and handle parser exceptions usage * messages themselves. * * <p>This options parsing implementation has (at least) one design flaw. It allows both '--foo=baz' * and '--foo baz' for all options except void, boolean and tristate options. For these, the 'baz' * in '--foo baz' is not treated as a parameter to the option, making it is impossible to switch * options between void/boolean/tristate and everything else without breaking backwards * compatibility. * * @see Options a simpler class which you can use if you only have one options specification class */ public class OptionsParser implements OptionsProvider { // TODO(b/65049598) make ConstructionException checked. /** * An unchecked exception thrown when there is a problem constructing a parser, e.g. an error * while validating an {@link OptionDefinition} in one of its {@link OptionsBase} subclasses. * * <p>This exception is unchecked because it generally indicates an internal error affecting all * invocations of the program. I.e., any such error should be immediately obvious to the * developer. Although unchecked, we explicitly mark some methods as throwing it as a reminder in * the API. */ public static class ConstructionException extends RuntimeException { public ConstructionException(String message) { super(message); } public ConstructionException(Throwable cause) { super(cause); } public ConstructionException(String message, Throwable cause) { super(message, cause); } } /** * A cache for the parsed options data. Both keys and values are immutable, so * this is always safe. Only access this field through the {@link * #getOptionsData} method for thread-safety! The cache is very unlikely to * grow to a significant amount of memory, because there's only a fixed set of * options classes on the classpath. */ private static final Map<ImmutableList<Class<? extends OptionsBase>>, OptionsData> optionsData = new HashMap<>(); /** * Returns {@link OpaqueOptionsData} suitable for passing along to {@link * #newOptionsParser(OpaqueOptionsData optionsData)}. * * <p>This is useful when you want to do the work of analyzing the given {@code optionsClasses} * exactly once, but you want to parse lots of different lists of strings (and thus need to * construct lots of different {@link OptionsParser} instances). */ public static OpaqueOptionsData getOptionsData( List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException { return getOptionsDataInternal(optionsClasses); } /** * Returns the {@link OptionsData} associated with the given list of options classes. */ static synchronized OptionsData getOptionsDataInternal( List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException { ImmutableList<Class<? extends OptionsBase>> immutableOptionsClasses = ImmutableList.copyOf(optionsClasses); OptionsData result = optionsData.get(immutableOptionsClasses); if (result == null) { try { result = OptionsData.from(immutableOptionsClasses); } catch (Exception e) { Throwables.throwIfInstanceOf(e, ConstructionException.class); throw new ConstructionException(e.getMessage(), e); } optionsData.put(immutableOptionsClasses, result); } return result; } /** * Returns the {@link OptionsData} associated with the given options class. */ static OptionsData getOptionsDataInternal(Class<? extends OptionsBase> optionsClass) throws ConstructionException { return getOptionsDataInternal(ImmutableList.of(optionsClass)); } /** * @see #newOptionsParser(Iterable) */ public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1) throws ConstructionException { return newOptionsParser(ImmutableList.<Class<? extends OptionsBase>>of(class1)); } /** @see #newOptionsParser(Iterable) */ public static OptionsParser newOptionsParser( Class<? extends OptionsBase> class1, Class<? extends OptionsBase> class2) throws ConstructionException { return newOptionsParser(ImmutableList.of(class1, class2)); } /** Create a new {@link OptionsParser}. */ public static OptionsParser newOptionsParser( Iterable<? extends Class<? extends OptionsBase>> optionsClasses) throws ConstructionException { return newOptionsParser(getOptionsDataInternal(ImmutableList.copyOf(optionsClasses))); } /** * Create a new {@link OptionsParser}, using {@link OpaqueOptionsData} previously returned from * {@link #getOptionsData}. */ public static OptionsParser newOptionsParser(OpaqueOptionsData optionsData) { return new OptionsParser((OptionsData) optionsData); } private final OptionsParserImpl impl; private final List<String> residue = new ArrayList<String>(); private boolean allowResidue = true; OptionsParser(OptionsData optionsData) { impl = new OptionsParserImpl(optionsData); } /** * Indicates whether or not the parser will allow a non-empty residue; that * is, iff this value is true then a call to one of the {@code parse} * methods will throw {@link OptionsParsingException} unless * {@link #getResidue()} is empty after parsing. */ public void setAllowResidue(boolean allowResidue) { this.allowResidue = allowResidue; } /** * Indicates whether or not the parser will allow long options with a * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example. */ public void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) { this.impl.setAllowSingleDashLongOptions(allowSingleDashLongOptions); } /** * Enables the Parser to handle params files using the provided {@link ParamsFilePreProcessor}. */ public void enableParamsFileSupport(ParamsFilePreProcessor preProcessor) { this.impl.setArgsPreProcessor(preProcessor); } public void parseAndExitUponError(String[] args) { parseAndExitUponError(OptionPriority.PriorityCategory.COMMAND_LINE, "unknown", args); } /** * A convenience function for use in main methods. Parses the command line parameters, and exits * upon error. Also, prints out the usage message if "--help" appears anywhere within {@code * args}. */ public void parseAndExitUponError( OptionPriority.PriorityCategory priority, String source, String[] args) { for (String arg : args) { if (arg.equals("--help")) { System.out.println( describeOptionsWithDeprecatedCategories(ImmutableMap.of(), HelpVerbosity.LONG)); System.exit(0); } } try { parse(priority, source, Arrays.asList(args)); } catch (OptionsParsingException e) { System.err.println("Error parsing command line: " + e.getMessage()); System.err.println("Try --help."); System.exit(2); } } /** The metadata about an option, in the context of this options parser. */ public static final class OptionDescription { private final OptionDefinition optionDefinition; private final ImmutableList<String> evaluatedExpansion; OptionDescription(OptionDefinition definition, OptionsData optionsData) { this.optionDefinition = definition; this.evaluatedExpansion = optionsData.getEvaluatedExpansion(optionDefinition); } public OptionDefinition getOptionDefinition() { return optionDefinition; } public boolean isExpansion() { return optionDefinition.isExpansionOption(); } /** Return a list of flags that this option expands to. */ public ImmutableList<String> getExpansion() throws OptionsParsingException { return evaluatedExpansion; } @Override public boolean equals(Object obj) { if (obj instanceof OptionDescription) { OptionDescription other = (OptionDescription) obj; // Check that the option is the same, with the same expansion. return other.optionDefinition.equals(optionDefinition) && other.evaluatedExpansion.equals(evaluatedExpansion); } return false; } @Override public int hashCode() { return optionDefinition.hashCode() + evaluatedExpansion.hashCode(); } } /** * The verbosity with which option help messages are displayed: short (just * the name), medium (name, type, default, abbreviation), and long (full * description). */ public enum HelpVerbosity { LONG, MEDIUM, SHORT } /** * Returns a description of all the options this parser can digest. In addition to {@link Option} * annotations, this method also interprets {@link OptionsUsage} annotations which give an * intuitive short description for the options. Options of the same category (see {@link * OptionDocumentationCategory}) will be grouped together. * * @param productName the name of this product (blaze, bazel) * @param helpVerbosity if {@code long}, the options will be described verbosely, including their * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if * {@code short}, the options are just enumerated. */ public String describeOptions(String productName, HelpVerbosity helpVerbosity) { StringBuilder desc = new StringBuilder(); LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory = getOptionsSortedByCategory(); ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions = OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e : optionsByCategory.entrySet()) { String categoryDescription = optionCategoryDescriptions.get(e.getKey()); List<OptionDefinition> categorizedOptionList = e.getValue(); // Describe the category if we're going to end up using it at all. if (!categorizedOptionList.isEmpty()) { desc.append("\n").append(categoryDescription).append(":\n"); } // Describe the options in this category. for (OptionDefinition optionDef : categorizedOptionList) { OptionsUsage.getUsage(optionDef, desc, helpVerbosity, impl.getOptionsData(), true); } } return desc.toString().trim(); } /** * @return all documented options loaded in this parser, grouped by categories in display order. */ private LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> getOptionsSortedByCategory() { OptionsData data = impl.getOptionsData(); if (data.getOptionsClasses().isEmpty()) { return new LinkedHashMap<>(); } // Get the documented options grouped by category. ListMultimap<OptionDocumentationCategory, OptionDefinition> optionsByCategories = ArrayListMultimap.create(); for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { for (OptionDefinition optionDefinition : OptionsData.getAllOptionDefinitionsForClass(optionsClass)) { // Only track documented options. if (optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED) { optionsByCategories.put(optionDefinition.getDocumentationCategory(), optionDefinition); } } } // Put the categories into display order and sort the options in each category. LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> sortedCategoriesToOptions = new LinkedHashMap<>(OptionFilterDescriptions.documentationOrder.length, 1); for (OptionDocumentationCategory category : OptionFilterDescriptions.documentationOrder) { List<OptionDefinition> optionList = optionsByCategories.get(category); if (optionList != null) { optionList.sort(OptionDefinition.BY_OPTION_NAME); sortedCategoriesToOptions.put(category, optionList); } } return sortedCategoriesToOptions; } /** * Returns a description of all the options this parser can digest. In addition to {@link Option} * annotations, this method also interprets {@link OptionsUsage} annotations which give an * intuitive short description for the options. Options of the same category (see {@link * Option#category}) will be grouped together. * * @param categoryDescriptions a mapping from category names to category descriptions. * Descriptions are optional; if omitted, a string based on the category name will be used. * @param helpVerbosity if {@code long}, the options will be described verbosely, including their * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if * {@code short}, the options are just enumerated. */ @Deprecated public String describeOptionsWithDeprecatedCategories( Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity) { OptionsData data = impl.getOptionsData(); StringBuilder desc = new StringBuilder(); if (!data.getOptionsClasses().isEmpty()) { List<OptionDefinition> allFields = new ArrayList<>(); for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass)); } Collections.sort(allFields, OptionDefinition.BY_CATEGORY); String prevCategory = null; for (OptionDefinition optionDefinition : allFields) { String category = optionDefinition.getOptionCategory(); if (!category.equals(prevCategory) && optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED) { String description = categoryDescriptions.get(category); if (description == null) { description = "Options category '" + category + "'"; } desc.append("\n").append(description).append(":\n"); prevCategory = category; } if (optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED) { OptionsUsage.getUsage( optionDefinition, desc, helpVerbosity, impl.getOptionsData(), false); } } } return desc.toString().trim(); } /** * Returns a description of all the options this parser can digest. In addition to {@link Option} * annotations, this method also interprets {@link OptionsUsage} annotations which give an * intuitive short description for the options. * * @param categoryDescriptions a mapping from category names to category descriptions. Options of * the same category (see {@link Option#category}) will be grouped together, preceded by the * description of the category. */ @Deprecated public String describeOptionsHtmlWithDeprecatedCategories( Map<String, String> categoryDescriptions, Escaper escaper) { OptionsData data = impl.getOptionsData(); StringBuilder desc = new StringBuilder(); if (!data.getOptionsClasses().isEmpty()) { List<OptionDefinition> allFields = new ArrayList<>(); for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass)); } Collections.sort(allFields, OptionDefinition.BY_CATEGORY); String prevCategory = null; for (OptionDefinition optionDefinition : allFields) { String category = optionDefinition.getOptionCategory(); if (!category.equals(prevCategory) && optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED) { String description = categoryDescriptions.get(category); if (description == null) { description = "Options category '" + category + "'"; } if (prevCategory != null) { desc.append("</dl>\n\n"); } desc.append(escaper.escape(description)).append(":\n"); desc.append("<dl>"); prevCategory = category; } if (optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED) { OptionsUsage.getUsageHtml(optionDefinition, desc, escaper, impl.getOptionsData(), false); } } desc.append("</dl>\n"); } return desc.toString(); } /** * Returns a description of all the options this parser can digest. In addition to {@link Option} * annotations, this method also interprets {@link OptionsUsage} annotations which give an * intuitive short description for the options. */ public String describeOptionsHtml(Escaper escaper, String productName) { StringBuilder desc = new StringBuilder(); LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory = getOptionsSortedByCategory(); ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions = OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e : optionsByCategory.entrySet()) { desc.append("<dl>"); String categoryDescription = optionCategoryDescriptions.get(e.getKey()); List<OptionDefinition> categorizedOptionsList = e.getValue(); // Describe the category if we're going to end up using it at all. if (!categorizedOptionsList.isEmpty()) { desc.append(escaper.escape(categoryDescription)).append(":\n"); } // Describe the options in this category. for (OptionDefinition optionDef : categorizedOptionsList) { OptionsUsage.getUsageHtml(optionDef, desc, escaper, impl.getOptionsData(), true); } desc.append("</dl>\n"); } return desc.toString(); } /** * Returns a string listing the possible flag completion for this command along with the command * completion if any. See {@link OptionsUsage#getCompletion(OptionDefinition, StringBuilder)} for * more details on the format for the flag completion. */ public String getOptionsCompletion() { StringBuilder desc = new StringBuilder(); visitOptions( optionDefinition -> optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED, optionDefinition -> OptionsUsage.getCompletion(optionDefinition, desc)); return desc.toString(); } public void visitOptions( Predicate<OptionDefinition> predicate, Consumer<OptionDefinition> visitor) { Preconditions.checkNotNull(predicate, "Missing predicate."); Preconditions.checkNotNull(visitor, "Missing visitor."); OptionsData data = impl.getOptionsData(); data.getOptionsClasses() // List all options .stream() .flatMap(optionsClass -> OptionsData.getAllOptionDefinitionsForClass(optionsClass).stream()) // Sort field for deterministic ordering .sorted(OptionDefinition.BY_OPTION_NAME) .filter(predicate) .forEach(visitor); } /** * Returns a description of the option. * * @return The {@link OptionDescription} for the option, or null if there is no option by the * given name. */ OptionDescription getOptionDescription(String name) throws OptionsParsingException { return impl.getOptionDescription(name); } /** * Returns the parsed options that get expanded from this option, whether it expands due to an * implicit requirement or expansion. * * @param expansionOption the option that might need to be expanded. If this option does not * expand to other options, the empty list will be returned. * @param originOfExpansionOption the origin of the option that's being expanded. This function * will take care of adjusting the source messages as necessary. */ ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions( OptionDefinition expansionOption, OptionInstanceOrigin originOfExpansionOption) throws OptionsParsingException { return impl.getExpansionValueDescriptions(expansionOption, originOfExpansionOption); } /** * Returns a description of the option value set by the last previous call to {@link * #parse(OptionPriority.PriorityCategory, String, List)} that successfully set the given option. * If the option is of type {@link List}, the description will correspond to any one of the calls, * but not necessarily the last. * * @return The {@link com.google.devtools.common.options.OptionValueDescription} for the option, * or null if the value has not been set. * @throws IllegalArgumentException if there is no option by the given name. */ public OptionValueDescription getOptionValueDescription(String name) { return impl.getOptionValueDescription(name); } /** * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null, * Arrays.asList(args))}. */ public void parse(String... args) throws OptionsParsingException { parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList(args)); } /** * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null, args)}. */ public void parse(List<String> args) throws OptionsParsingException { parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, args); } /** * Parses {@code args}, using the classes registered with this parser, at the given priority. * * <p>May be called multiple times; later options override existing ones if they have equal or * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this * parser allows it. * * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results. * * @param priority the priority at which to parse these options. Within this priority category, * each option will be given an index to track its position. If parse() has already been * called at this priority, the indexing will continue where it left off, to keep ordering. * @param source the source to track for each option parsed. * @param args the arg list to parse. Each element might be an option, a value linked to an * option, or residue. */ public void parse(OptionPriority.PriorityCategory priority, String source, List<String> args) throws OptionsParsingException { parseWithSourceFunction(priority, o -> source, args); } /** * Parses {@code args}, using the classes registered with this parser, at the given priority. * * <p>May be called multiple times; later options override existing ones if they have equal or * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this * parser allows it. * * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results. * * @param priority the priority at which to parse these options. Within this priority category, * each option will be given an index to track its position. If parse() has already been * called at this priority, the indexing will continue where it left off, to keep ordering. * @param sourceFunction a function that maps option names to the source of the option. * @param args the arg list to parse. Each element might be an option, a value linked to an * option, or residue. */ public void parseWithSourceFunction( OptionPriority.PriorityCategory priority, Function<OptionDefinition, String> sourceFunction, List<String> args) throws OptionsParsingException { Preconditions.checkNotNull(priority); Preconditions.checkArgument(priority != OptionPriority.PriorityCategory.DEFAULT); residue.addAll(impl.parse(priority, sourceFunction, args)); if (!allowResidue && !residue.isEmpty()) { String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); throw new OptionsParsingException(errorMsg); } } public void parseOptionsFixedAtSpecificPriority( OptionPriority priority, String source, List<String> args) throws OptionsParsingException { Preconditions.checkNotNull(priority, "Priority not specified for arglist " + args); Preconditions.checkArgument( priority.getPriorityCategory() != OptionPriority.PriorityCategory.DEFAULT, "Priority cannot be default, which was specified for arglist " + args); residue.addAll(impl.parseOptionsFixedAtSpecificPriority(priority, o -> source, args)); if (!allowResidue && !residue.isEmpty()) { String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); throw new OptionsParsingException(errorMsg); } } /** * @param origin the origin of this option instance, it includes the priority of the value. If * other values have already been or will be parsed at a higher priority, they might override * the provided value. If this option already has a value at this priority, this value will * have precedence, but this should be avoided, as it breaks order tracking. * @param option the option to add the value for. * @param value the value to add at the given priority. */ void addOptionValueAtSpecificPriority( OptionInstanceOrigin origin, OptionDefinition option, String value) throws OptionsParsingException { impl.addOptionValueAtSpecificPriority(origin, option, value); } /** * Clears the given option. * * <p>This will not affect options objects that have already been retrieved from this parser * through {@link #getOptions(Class)}. * * @param option The option to clear. * @return The old value of the option that was cleared. * @throws IllegalArgumentException If the flag does not exist. */ public OptionValueDescription clearValue(OptionDefinition option) throws OptionsParsingException { return impl.clearValue(option); } @Override public List<String> getResidue() { return ImmutableList.copyOf(residue); } /** Returns a list of warnings about problems encountered by previous parse calls. */ public List<String> getWarnings() { return impl.getWarnings(); } @Override public <O extends OptionsBase> O getOptions(Class<O> optionsClass) { return impl.getParsedOptions(optionsClass); } @Override public boolean containsExplicitOption(String name) { return impl.containsExplicitOption(name); } @Override public List<ParsedOptionDescription> asCompleteListOfParsedOptions() { return impl.asCompleteListOfParsedOptions(); } @Override public List<ParsedOptionDescription> asListOfExplicitOptions() { return impl.asListOfExplicitOptions(); } @Override public List<ParsedOptionDescription> asListOfCanonicalOptions() { return impl.asCanonicalizedListOfParsedOptions(); } @Override public List<OptionValueDescription> asListOfOptionValues() { return impl.asListOfEffectiveOptions(); } @Override public List<String> canonicalize() { return impl.asCanonicalizedList(); } /** Returns all options fields of the given options class, in alphabetic order. */ public static ImmutableList<OptionDefinition> getOptionDefinitions( Class<? extends OptionsBase> optionsClass) { return OptionsData.getAllOptionDefinitionsForClass(optionsClass); } /** * Returns whether the given options class uses only the core types listed in {@link * UsesOnlyCoreTypes#CORE_TYPES}. These are guaranteed to be deeply immutable and serializable. */ public static boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) { OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass); return data.getUsesOnlyCoreTypes(optionsClass); } /** * Returns a mapping from each option {@link Field} in {@code optionsClass} (including inherited * ones) to its value in {@code options}. * * <p>To save space, the map directly stores {@code Fields} instead of the {@code * OptionDefinitions}. * * <p>The map is a mutable copy; changing the map won't affect {@code options} and vice versa. The * map entries appear sorted alphabetically by option name. * * <p>If {@code options} is an instance of a subclass of {@link OptionsBase}, any options defined * by the subclass are not included in the map, only the options declared in the provided class * are included. * * @throws IllegalArgumentException if {@code options} is not an instance of {@link OptionsBase} */ public static <O extends OptionsBase> Map<Field, Object> toMap(Class<O> optionsClass, O options) { // Alphabetized due to getAllOptionDefinitionsForClass()'s order. Map<Field, Object> map = new LinkedHashMap<>(); for (OptionDefinition optionDefinition : OptionsData.getAllOptionDefinitionsForClass(optionsClass)) { try { // Get the object value of the optionDefinition and place in map. map.put(optionDefinition.getField(), optionDefinition.getField().get(options)); } catch (IllegalAccessException e) { // All options fields of options classes should be public. throw new IllegalStateException(e); } catch (IllegalArgumentException e) { // This would indicate an inconsistency in the cached OptionsData. throw new IllegalStateException(e); } } return map; } /** * Given a mapping as returned by {@link #toMap}, and the options class it that its entries * correspond to, this constructs the corresponding instance of the options class. * * @param map Field to Object, expecting an entry for each field in the optionsClass. This * directly refers to the Field, without wrapping it in an OptionDefinition, see {@link * #toMap}. * @throws IllegalArgumentException if {@code map} does not contain exactly the fields of {@code * optionsClass}, with values of the appropriate type */ public static <O extends OptionsBase> O fromMap(Class<O> optionsClass, Map<Field, Object> map) { // Instantiate the options class. OptionsData data = getOptionsDataInternal(optionsClass); O optionsInstance; try { Constructor<O> constructor = data.getConstructor(optionsClass); Preconditions.checkNotNull(constructor, "No options class constructor available"); optionsInstance = constructor.newInstance(); } catch (ReflectiveOperationException e) { throw new IllegalStateException("Error while instantiating options class", e); } List<OptionDefinition> optionDefinitions = OptionsData.getAllOptionDefinitionsForClass(optionsClass); // Ensure all fields are covered, no extraneous fields. validateFieldsSets(optionsClass, new LinkedHashSet<Field>(map.keySet())); // Populate the instance. for (OptionDefinition optionDefinition : optionDefinitions) { // Non-null as per above check. Object value = map.get(optionDefinition.getField()); try { optionDefinition.getField().set(optionsInstance, value); } catch (IllegalAccessException e) { throw new IllegalStateException(e); } // May also throw IllegalArgumentException if map value is ill typed. } return optionsInstance; } /** * Raises a pretty {@link IllegalArgumentException} if the provided set of fields is a complete * set for the optionsClass. * * <p>The entries in {@code fieldsFromMap} may be ill formed by being null or lacking an {@link * Option} annotation. */ private static void validateFieldsSets( Class<? extends OptionsBase> optionsClass, LinkedHashSet<Field> fieldsFromMap) { ImmutableList<OptionDefinition> optionDefsFromClasses = OptionsData.getAllOptionDefinitionsForClass(optionsClass); Set<Field> fieldsFromClass = optionDefsFromClasses.stream().map(OptionDefinition::getField).collect(Collectors.toSet()); if (fieldsFromClass.equals(fieldsFromMap)) { // They are already equal, avoid additional checks. return; } List<String> extraNamesFromClass = new ArrayList<>(); List<String> extraNamesFromMap = new ArrayList<>(); for (OptionDefinition optionDefinition : optionDefsFromClasses) { if (!fieldsFromMap.contains(optionDefinition.getField())) { extraNamesFromClass.add("'" + optionDefinition.getOptionName() + "'"); } } for (Field field : fieldsFromMap) { // Extra validation on the map keys since they don't come from OptionsData. if (!fieldsFromClass.contains(field)) { if (field == null) { extraNamesFromMap.add("<null field>"); } else { OptionDefinition optionDefinition = null; try { // TODO(ccalvarin) This shouldn't be necessary, no option definitions should be found in // this optionsClass that weren't in the cache. optionDefinition = OptionDefinition.extractOptionDefinition(field); extraNamesFromMap.add("'" + optionDefinition.getOptionName() + "'"); } catch (NotAnOptionException e) { extraNamesFromMap.add("<non-Option field>"); } } } } throw new IllegalArgumentException( "Map keys do not match fields of options class; extra map keys: {" + Joiner.on(", ").join(extraNamesFromMap) + "}; extra options class options: {" + Joiner.on(", ").join(extraNamesFromClass) + "}"); } }