| // Copyright 2017 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 static java.util.Comparator.comparing; | 
 |  | 
 | import com.google.common.collect.ImmutableList; | 
 | import com.google.devtools.common.options.OptionsParser.ConstructionException; | 
 | import java.lang.reflect.Constructor; | 
 | import java.lang.reflect.Field; | 
 | import java.lang.reflect.ParameterizedType; | 
 | import java.lang.reflect.Type; | 
 | import java.util.Arrays; | 
 | import java.util.Comparator; | 
 | import java.util.List; | 
 |  | 
 | /** | 
 |  * Everything the {@link OptionsParser} needs to know about how an option is defined. | 
 |  * | 
 |  * <p>An {@code OptionDefinition} is effectively a wrapper around the {@link Option} annotation and | 
 |  * the {@link Field} that is annotated, and should contain all logic about default settings and | 
 |  * behavior. | 
 |  */ | 
 | public class OptionDefinition implements Comparable<OptionDefinition> { | 
 |  | 
 |   /** | 
 |    * A special value used to specify an absence of default value. | 
 |    * | 
 |    * @see Option#defaultValue | 
 |    */ | 
 |   public static final String SPECIAL_NULL_DEFAULT_VALUE = "null"; | 
 |  | 
 |   // TODO(b/65049598) make ConstructionException checked, which will make this checked as well. | 
 |   static class NotAnOptionException extends ConstructionException { | 
 |     NotAnOptionException(Field field) { | 
 |       super( | 
 |           "The field " | 
 |               + field.getName() | 
 |               + " does not have the right annotation to be considered an option."); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * If the {@code field} is annotated with the appropriate @{@link Option} annotation, returns the | 
 |    * {@code OptionDefinition} for that option. Otherwise, throws a {@link NotAnOptionException}. | 
 |    * | 
 |    * <p>These values are cached in the {@link OptionsData} layer and should be accessed through | 
 |    * {@link OptionsParser#getOptionDefinitions(Class)}. | 
 |    */ | 
 |   static OptionDefinition extractOptionDefinition(Field field) { | 
 |     Option annotation = field == null ? null : field.getAnnotation(Option.class); | 
 |     if (annotation == null) { | 
 |       throw new NotAnOptionException(field); | 
 |     } | 
 |     return new OptionDefinition(field, annotation); | 
 |   } | 
 |  | 
 |   private final Field field; | 
 |   private final Option optionAnnotation; | 
 |   private Converter<?> converter = null; | 
 |   private Object defaultValue = null; | 
 |  | 
 |   private OptionDefinition(Field field, Option optionAnnotation) { | 
 |     this.field = field; | 
 |     this.optionAnnotation = optionAnnotation; | 
 |   } | 
 |  | 
 |   /** Returns the underlying {@code field} for this {@code OptionDefinition}. */ | 
 |   public Field getField() { | 
 |     return field; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Returns the name of the option ("--name"). | 
 |    * | 
 |    * <p>Labelled "Option" name to distinguish it from the field's name. | 
 |    */ | 
 |   public String getOptionName() { | 
 |     return optionAnnotation.name(); | 
 |   } | 
 |  | 
 |   /** The single-character abbreviation of the option ("-a"). */ | 
 |   public char getAbbreviation() { | 
 |     return optionAnnotation.abbrev(); | 
 |   } | 
 |  | 
 |   /** {@link Option#help()} */ | 
 |   public String getHelpText() { | 
 |     return optionAnnotation.help(); | 
 |   } | 
 |  | 
 |   /** {@link Option#valueHelp()} */ | 
 |   public String getValueTypeHelpText() { | 
 |     return optionAnnotation.valueHelp(); | 
 |   } | 
 |  | 
 |   /** {@link Option#defaultValue()} */ | 
 |   public String getUnparsedDefaultValue() { | 
 |     return optionAnnotation.defaultValue(); | 
 |   } | 
 |  | 
 |   /** {@link Option#category()} */ | 
 |   public String getOptionCategory() { | 
 |     return optionAnnotation.category(); | 
 |   } | 
 |  | 
 |   /** {@link Option#documentationCategory()} */ | 
 |   public OptionDocumentationCategory getDocumentationCategory() { | 
 |     return optionAnnotation.documentationCategory(); | 
 |   } | 
 |  | 
 |   /** {@link Option#effectTags()} */ | 
 |   public OptionEffectTag[] getOptionEffectTags() { | 
 |     return optionAnnotation.effectTags(); | 
 |   } | 
 |  | 
 |   /** {@link Option#metadataTags()} */ | 
 |   public OptionMetadataTag[] getOptionMetadataTags() { | 
 |     return optionAnnotation.metadataTags(); | 
 |   } | 
 |  | 
 |   /** {@link Option#converter()} ()} */ | 
 |   @SuppressWarnings({"rawtypes"}) | 
 |   public Class<? extends Converter> getProvidedConverter() { | 
 |     return optionAnnotation.converter(); | 
 |   } | 
 |  | 
 |   /** {@link Option#allowMultiple()} */ | 
 |   public boolean allowsMultiple() { | 
 |     return optionAnnotation.allowMultiple(); | 
 |   } | 
 |  | 
 |   /** {@link Option#expansion()} */ | 
 |   public String[] getOptionExpansion() { | 
 |     return optionAnnotation.expansion(); | 
 |   } | 
 |  | 
 |   /** {@link Option#expansionFunction()} ()} */ | 
 |   public Class<? extends ExpansionFunction> getExpansionFunction() { | 
 |     return optionAnnotation.expansionFunction(); | 
 |   } | 
 |  | 
 |   /** {@link Option#implicitRequirements()} ()} */ | 
 |   public String[] getImplicitRequirements() { | 
 |     return optionAnnotation.implicitRequirements(); | 
 |   } | 
 |  | 
 |   /** {@link Option#deprecationWarning()} ()} */ | 
 |   public String getDeprecationWarning() { | 
 |     return optionAnnotation.deprecationWarning(); | 
 |   } | 
 |  | 
 |   /** {@link Option#oldName()} ()} ()} */ | 
 |   public String getOldOptionName() { | 
 |     return optionAnnotation.oldName(); | 
 |   } | 
 |  | 
 |   /** Returns whether an option --foo has a negative equivalent --nofoo. */ | 
 |   public boolean hasNegativeOption() { | 
 |     return getType().equals(boolean.class) || getType().equals(TriState.class); | 
 |   } | 
 |  | 
 |   /** The type of the optionDefinition. */ | 
 |   public Class<?> getType() { | 
 |     return field.getType(); | 
 |   } | 
 |  | 
 |   /** Whether this field has type Void. */ | 
 |   boolean isVoidField() { | 
 |     return getType().equals(Void.class); | 
 |   } | 
 |  | 
 |   public boolean isSpecialNullDefault() { | 
 |     return SPECIAL_NULL_DEFAULT_VALUE.equals(getUnparsedDefaultValue()) && !getType().isPrimitive(); | 
 |   } | 
 |  | 
 |   /** Returns whether the arg is an expansion option. */ | 
 |   public boolean isExpansionOption() { | 
 |     return (getOptionExpansion().length > 0 || usesExpansionFunction()); | 
 |   } | 
 |  | 
 |   /** Returns whether the arg is an expansion option. */ | 
 |   public boolean hasImplicitRequirements() { | 
 |     return (getImplicitRequirements().length > 0); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Returns whether the arg is an expansion option defined by an expansion function (and not a | 
 |    * constant expansion value). | 
 |    */ | 
 |   public boolean usesExpansionFunction() { | 
 |     return getExpansionFunction() != ExpansionFunction.class; | 
 |   } | 
 |  | 
 |   /** | 
 |    * For an option that does not use {@link Option#allowMultiple}, returns its type. For an option | 
 |    * that does use it, asserts that the type is a {@code List<T>} and returns its element type | 
 |    * {@code T}. | 
 |    */ | 
 |   Type getFieldSingularType() { | 
 |     Type fieldType = getField().getGenericType(); | 
 |     if (allowsMultiple()) { | 
 |       // The validity of the converter is checked at compile time. We know the type to be | 
 |       // List<singularType>. | 
 |       ParameterizedType pfieldType = (ParameterizedType) fieldType; | 
 |       fieldType = pfieldType.getActualTypeArguments()[0]; | 
 |     } | 
 |     return fieldType; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Retrieves the {@link Converter} that will be used for this option, taking into account the | 
 |    * default converters if an explicit one is not specified. | 
 |    * | 
 |    * <p>Memoizes the converter-finding logic to avoid repeating the computation. | 
 |    */ | 
 |   public Converter<?> getConverter() { | 
 |     if (converter != null) { | 
 |       return converter; | 
 |     } | 
 |     Class<? extends Converter> converterClass = getProvidedConverter(); | 
 |     if (converterClass == Converter.class) { | 
 |       // No converter provided, use the default one. | 
 |       Type type = getFieldSingularType(); | 
 |       converter = Converters.DEFAULT_CONVERTERS.get(type); | 
 |     } else { | 
 |       try { | 
 |         // Instantiate the given Converter class. | 
 |         Constructor<?> constructor = converterClass.getConstructor(); | 
 |         converter = (Converter<?>) constructor.newInstance(); | 
 |       } catch (SecurityException | IllegalArgumentException | ReflectiveOperationException e) { | 
 |         // This indicates an error in the Converter, and should be discovered the first time it is | 
 |         // used. | 
 |         throw new ConstructionException( | 
 |             String.format("Error in the provided converter for option %s", getField().getName()), | 
 |             e); | 
 |       } | 
 |     } | 
 |     return converter; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Returns whether a field should be considered as boolean. | 
 |    * | 
 |    * <p>Can be used for usage help and controlling whether the "no" prefix is allowed. | 
 |    */ | 
 |   public boolean usesBooleanValueSyntax() { | 
 |     return getType().equals(boolean.class) | 
 |         || getType().equals(TriState.class) | 
 |         || getConverter() instanceof BoolOrEnumConverter; | 
 |   } | 
 |  | 
 |   /** Returns the evaluated default value for this option & memoizes the result. */ | 
 |   public Object getDefaultValue() { | 
 |     if (defaultValue != null) { | 
 |       return defaultValue; | 
 |     } | 
 |  | 
 |     if (isSpecialNullDefault()) { | 
 |       return allowsMultiple() ? ImmutableList.of() : null; | 
 |     } | 
 |  | 
 |     Converter<?> converter = getConverter(); | 
 |     String defaultValueAsString = getUnparsedDefaultValue(); | 
 |     try { | 
 |       Object convertedDefaultValue = converter.convert(defaultValueAsString); | 
 |       defaultValue = | 
 |           allowsMultiple() | 
 |               ? maybeWrapMultipleDefaultValue(convertedDefaultValue) | 
 |               : convertedDefaultValue; | 
 |     } catch (OptionsParsingException e) { | 
 |       throw new ConstructionException( | 
 |           String.format( | 
 |               "OptionsParsingException while retrieving the default value for %s: %s", | 
 |               getField().getName(), e.getMessage()), | 
 |           e); | 
 |     } | 
 |  | 
 |     return defaultValue; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Wraps a converted default value into a {@link List} if the converter doesn't do it on its own. | 
 |    * | 
 |    * <p>This is to make sure multiple ({@link Option#allowMultiple()}) options' default values are | 
 |    * always converted to a list representation. | 
 |    * | 
 |    * <p>In general it mimics the {@link RepeatableOptionValueDescription# addOptionInstance} | 
 |    * behavior: multiple option default value is treated as if it appeared on the command line only | 
 |    * once with the specified value. | 
 |    * | 
 |    * <p>Note that on a command line multiple options can appear multiple times while each can | 
 |    * support multiple values (e.g. comma-separated - depending on a converter). Thus default value | 
 |    * for multiple option is (depending on the converter) a strict subset of the set of potential | 
 |    * values for the option. | 
 |    */ | 
 |   @SuppressWarnings("unchecked") // Not an unchecked cast - there's an explicit type check before it | 
 |   private static List<Object> maybeWrapMultipleDefaultValue(Object convertedDefaultValue) { | 
 |     if (convertedDefaultValue instanceof List) { | 
 |       return (List<Object>) convertedDefaultValue; | 
 |     } else { | 
 |       return Arrays.asList(convertedDefaultValue); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * {@link OptionDefinition} is really a wrapper around a {@link Field} that caches information | 
 |    * obtained through reflection. Checking that the fields they represent are equal is sufficient to | 
 |    * check that two {@link OptionDefinition} objects are equal. | 
 |    */ | 
 |   @Override | 
 |   public boolean equals(Object object) { | 
 |     if (!(object instanceof OptionDefinition)) { | 
 |       return false; | 
 |     } | 
 |     OptionDefinition otherOption = (OptionDefinition) object; | 
 |     return field.equals(otherOption.field); | 
 |   } | 
 |  | 
 |   @Override | 
 |   public int hashCode() { | 
 |     return field.hashCode(); | 
 |   } | 
 |  | 
 |   @Override | 
 |   public int compareTo(OptionDefinition o) { | 
 |     return getOptionName().compareTo(o.getOptionName()); | 
 |   } | 
 |  | 
 |   @Override | 
 |   public String toString() { | 
 |     return String.format("option '--%s'", getOptionName()); | 
 |   } | 
 |  | 
 |   static final Comparator<OptionDefinition> BY_OPTION_NAME = | 
 |       Comparator.comparing(OptionDefinition::getOptionName); | 
 |  | 
 |   /** | 
 |    * An ordering relation for option-field fields that first groups together options of the same | 
 |    * category, then sorts by name within the category. | 
 |    */ | 
 |   static final Comparator<OptionDefinition> BY_CATEGORY = | 
 |       comparing(OptionDefinition::getOptionCategory).thenComparing(BY_OPTION_NAME); | 
 | } |