| // 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 static com.google.devtools.common.options.OptionsParser.STARLARK_SKIPPED_PREFIXES; |
| |
| import com.github.benmanes.caffeine.cache.CaffeineSpec; |
| import com.google.common.base.Ascii; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Maps; |
| import java.time.Duration; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.logging.Level; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| import javax.annotation.Nullable; |
| |
| /** Some convenient converters used by blaze. Note: These are specific to blaze. */ |
| public final class Converters { |
| /** |
| * The name of the flag used for shorthand aliasing in blaze. {@see |
| * com.google.devtools.build.lib.analysis.config.CoreOptions#commandLineFlagAliases} for the |
| * option definition. |
| */ |
| public static final String BLAZE_ALIASING_FLAG = "flag_alias"; |
| |
| private static final ImmutableSet<String> ENABLED_REPS = |
| ImmutableSet.of("true", "1", "yes", "t", "y"); |
| |
| private static final ImmutableSet<String> DISABLED_REPS = |
| ImmutableSet.of("false", "0", "no", "f", "n"); |
| |
| /** Standard converter for booleans. Accepts common shorthands/synonyms. */ |
| public static class BooleanConverter extends Converter.Contextless<Boolean> { |
| @Override |
| public Boolean convert(String input) throws OptionsParsingException { |
| if (input == null) { |
| return false; |
| } |
| input = Ascii.toLowerCase(input); |
| if (ENABLED_REPS.contains(input)) { |
| return true; |
| } |
| if (DISABLED_REPS.contains(input)) { |
| return false; |
| } |
| throw new OptionsParsingException("'" + input + "' is not a boolean"); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a boolean"; |
| } |
| } |
| |
| /** Standard converter for Strings. */ |
| public static class StringConverter extends Converter.Contextless<String> { |
| @Override |
| public String convert(String input) { |
| return input; |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a string"; |
| } |
| } |
| |
| /** Standard converter for integers. */ |
| public static class IntegerConverter extends Converter.Contextless<Integer> { |
| @Override |
| public Integer convert(String input) throws OptionsParsingException { |
| try { |
| return Integer.decode(input); |
| } catch (NumberFormatException e) { |
| throw new OptionsParsingException("'" + input + "' is not an int", e); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "an integer"; |
| } |
| } |
| |
| /** Standard converter for longs. */ |
| public static class LongConverter extends Converter.Contextless<Long> { |
| @Override |
| public Long convert(String input) throws OptionsParsingException { |
| try { |
| return Long.decode(input); |
| } catch (NumberFormatException e) { |
| throw new OptionsParsingException("'" + input + "' is not a long", e); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a long integer"; |
| } |
| } |
| |
| /** Standard converter for doubles. */ |
| public static class DoubleConverter extends Converter.Contextless<Double> { |
| @Override |
| public Double convert(String input) throws OptionsParsingException { |
| try { |
| return Double.parseDouble(input); |
| } catch (NumberFormatException e) { |
| throw new OptionsParsingException("'" + input + "' is not a double", e); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a double"; |
| } |
| } |
| |
| /** Standard converter for TriState values. */ |
| public static class TriStateConverter extends Converter.Contextless<TriState> { |
| @Override |
| public TriState convert(String input) throws OptionsParsingException { |
| if (input == null) { |
| return TriState.AUTO; |
| } |
| input = Ascii.toLowerCase(input); |
| if (input.equals("auto")) { |
| return TriState.AUTO; |
| } |
| if (ENABLED_REPS.contains(input)) { |
| return TriState.YES; |
| } |
| if (DISABLED_REPS.contains(input)) { |
| return TriState.NO; |
| } |
| throw new OptionsParsingException("'" + input + "' is not a boolean"); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a tri-state (auto, yes, no)"; |
| } |
| } |
| |
| /** |
| * Standard "converter" for Void. Should not actually be invoked. For instance, expansion flags |
| * are usually Void-typed and do not invoke the converter. |
| */ |
| public static class VoidConverter extends Converter.Contextless<Void> { |
| @Override |
| public Void convert(String input) throws OptionsParsingException { |
| if (input == null || input.equals("null")) { |
| return null; // expected input, return is unused so null is fine. |
| } |
| throw new OptionsParsingException("'" + input + "' unexpected"); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return ""; |
| } |
| } |
| |
| /** Standard converter for the {@link java.time.Duration} type. */ |
| public static class DurationConverter extends Converter.Contextless<Duration> { |
| private static final Pattern DURATION_REGEX = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$"); |
| |
| @Override |
| public Duration convert(String input) throws OptionsParsingException { |
| // To be compatible with the previous parser, '0' doesn't need a unit. |
| if ("0".equals(input)) { |
| return Duration.ZERO; |
| } |
| Matcher m = DURATION_REGEX.matcher(input); |
| if (!m.matches()) { |
| throw new OptionsParsingException("Illegal duration '" + input + "'."); |
| } |
| long duration = Long.parseLong(m.group(1)); |
| String unit = m.group(2); |
| switch (unit) { |
| case "d": |
| return Duration.ofDays(duration); |
| case "h": |
| return Duration.ofHours(duration); |
| case "m": |
| return Duration.ofMinutes(duration); |
| case "s": |
| return Duration.ofSeconds(duration); |
| case "ms": |
| return Duration.ofMillis(duration); |
| default: |
| throw new IllegalStateException( |
| "This must not happen. Did you update the regex without the switch case?"); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "An immutable length of time."; |
| } |
| } |
| |
| // 1:1 correspondence with UsesOnlyCoreTypes.CORE_TYPES. |
| /** |
| * The converters that are available to the options parser by default. These are used if the |
| * {@code @Option} annotation does not specify its own {@code converter}, and its type is one of |
| * the following. |
| */ |
| public static final ImmutableMap<Class<?>, Converter<?>> DEFAULT_CONVERTERS = |
| new ImmutableMap.Builder<Class<?>, Converter<?>>() |
| .put(String.class, new Converters.StringConverter()) |
| .put(int.class, new Converters.IntegerConverter()) |
| .put(long.class, new Converters.LongConverter()) |
| .put(double.class, new Converters.DoubleConverter()) |
| .put(boolean.class, new Converters.BooleanConverter()) |
| .put(TriState.class, new Converters.TriStateConverter()) |
| .put(Duration.class, new Converters.DurationConverter()) |
| .put(Void.class, new Converters.VoidConverter()) |
| .build(); |
| |
| /** |
| * Join a list of words as in English. Examples: "nothing" "one" "one or two" "one and two" "one, |
| * two or three". "one, two and three". The toString method of each element is used. |
| */ |
| static String joinEnglishList(Iterable<?> choices) { |
| StringBuilder buf = new StringBuilder(); |
| for (Iterator<?> ii = choices.iterator(); ii.hasNext(); ) { |
| Object choice = ii.next(); |
| if (buf.length() > 0) { |
| buf.append(ii.hasNext() ? ", " : " or "); |
| } |
| buf.append(choice); |
| } |
| return buf.length() == 0 ? "nothing" : buf.toString(); |
| } |
| |
| /** Converter for a list of options, separated by some separator character. */ |
| public static class SeparatedOptionListConverter |
| extends Converter.Contextless<ImmutableList<String>> { |
| private final String separatorDescription; |
| private final Splitter splitter; |
| private final boolean allowEmptyValues; |
| |
| protected SeparatedOptionListConverter( |
| char separator, String separatorDescription, boolean allowEmptyValues) { |
| this.separatorDescription = separatorDescription; |
| this.splitter = Splitter.on(separator); |
| this.allowEmptyValues = allowEmptyValues; |
| } |
| |
| @Override |
| public ImmutableList<String> convert(String input) throws OptionsParsingException { |
| ImmutableList<String> result = |
| input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input)); |
| if (!allowEmptyValues && result.contains("")) { |
| // If the list contains exactly the empty string, it means an empty value was passed and we |
| // should instead return an empty list. |
| if (result.size() == 1) { |
| return ImmutableList.of(); |
| } |
| |
| throw new OptionsParsingException( |
| "Empty values are not allowed as part of this " + getTypeDescription()); |
| } |
| return result; |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return separatorDescription + "-separated list of options"; |
| } |
| } |
| |
| /** |
| * Converter for options separated by some separator character, where order and count do not |
| * matter, i.e. semantically it is a set, not a list. |
| */ |
| public static class SeparatedOptionSetConverter extends SeparatedOptionListConverter { |
| private final String separatorDescription; |
| |
| protected SeparatedOptionSetConverter( |
| char separator, String separatorDescription, boolean allowEmptyValues) { |
| super(separator, separatorDescription, allowEmptyValues); |
| this.separatorDescription = separatorDescription; |
| } |
| |
| @Override |
| public ImmutableList<String> convert(String input) throws OptionsParsingException { |
| ImmutableList<String> result = super.convert(input); |
| return result.stream().distinct().sorted().collect(ImmutableList.toImmutableList()); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return separatorDescription + "-separated set of options"; |
| } |
| } |
| |
| /** |
| * Converter for comma separated values, where |
| * <li>order and multiplicity preserved |
| * <li>empty values are preserved |
| */ |
| public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter { |
| public CommaSeparatedOptionListConverter() { |
| super(',', "comma", true); |
| } |
| } |
| |
| /** |
| * Converter for comma separated values, where |
| * <li>order and multiplicity preserved |
| * <li>empty values are filtered out |
| */ |
| public static class CommaSeparatedNonEmptyOptionListConverter |
| extends SeparatedOptionListConverter { |
| public CommaSeparatedNonEmptyOptionListConverter() { |
| super(',', "comma", false); |
| } |
| } |
| |
| /** |
| * Converter for colon separated values, where |
| * <li>order and multiplicity preserved |
| * <li>empty values are preserved |
| */ |
| public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter { |
| public ColonSeparatedOptionListConverter() { |
| super(':', "colon", true); |
| } |
| } |
| |
| /** |
| * Converter for colon separated values, where |
| * <li>order and multiplicity are assumed to not matter |
| * <li>empty values are preserved |
| */ |
| public static class CommaSeparatedOptionSetConverter extends SeparatedOptionSetConverter { |
| public CommaSeparatedOptionSetConverter() { |
| super(',', "comma", true); |
| } |
| } |
| |
| /** Converter for {@link Level}. */ |
| public static class LogLevelConverter extends Converter.Contextless<Level> { |
| |
| static final ImmutableList<Level> LEVELS = |
| ImmutableList.of( |
| Level.OFF, |
| Level.SEVERE, |
| Level.WARNING, |
| Level.INFO, |
| Level.FINE, |
| Level.FINER, |
| Level.FINEST); |
| |
| @Override |
| public Level convert(String input) throws OptionsParsingException { |
| try { |
| int level = Integer.parseInt(input); |
| return LEVELS.get(level); |
| } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { |
| throw new OptionsParsingException("Not a log level: " + input, e); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "0 <= an integer <= " + (LEVELS.size() - 1); |
| } |
| } |
| |
| /** Checks whether a string is part of a set of strings. */ |
| public static class StringSetConverter extends Converter.Contextless<String> { |
| |
| // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/ |
| // here. |
| private final ImmutableList<String> values; |
| |
| public StringSetConverter(String... values) { |
| this.values = ImmutableList.copyOf(values); |
| } |
| |
| @Override |
| public String convert(String input) throws OptionsParsingException { |
| if (values.contains(input)) { |
| return input; |
| } |
| |
| throw new OptionsParsingException("Not one of " + values); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return joinEnglishList(values); |
| } |
| } |
| |
| /** Checks whether a string is a valid regex pattern and compiles it. */ |
| public static class RegexPatternConverter extends Converter.Contextless<RegexPatternOption> { |
| |
| @Override |
| public RegexPatternOption convert(String input) throws OptionsParsingException { |
| try { |
| return RegexPatternOption.create(Pattern.compile(input)); |
| } catch (PatternSyntaxException e) { |
| throw new OptionsParsingException("Not a valid regular expression: " + e.getMessage()); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a valid Java regular expression"; |
| } |
| } |
| |
| /** Limits the length of a string argument. */ |
| public static class LengthLimitingConverter extends Converter.Contextless<String> { |
| private final int maxSize; |
| |
| public LengthLimitingConverter(int maxSize) { |
| this.maxSize = maxSize; |
| } |
| |
| @Override |
| public String convert(String input) throws OptionsParsingException { |
| if (input.length() > maxSize) { |
| throw new OptionsParsingException("Input must be " + getTypeDescription()); |
| } |
| return input; |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a string <= " + maxSize + " characters"; |
| } |
| } |
| |
| /** Checks whether an integer is in the given range. */ |
| public static class RangeConverter extends Converter.Contextless<Integer> { |
| final int minValue; |
| final int maxValue; |
| |
| public RangeConverter(int minValue, int maxValue) { |
| this.minValue = minValue; |
| this.maxValue = maxValue; |
| } |
| |
| @Override |
| public Integer convert(String input) throws OptionsParsingException { |
| try { |
| int value = Integer.parseInt(input); |
| if (value < minValue) { |
| throw new OptionsParsingException("'" + input + "' should be >= " + minValue); |
| } else if (value < minValue || value > maxValue) { |
| throw new OptionsParsingException("'" + input + "' should be <= " + maxValue); |
| } |
| return value; |
| } catch (NumberFormatException e) { |
| throw new OptionsParsingException("'" + input + "' is not an int", e); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| if (minValue == Integer.MIN_VALUE) { |
| if (maxValue == Integer.MAX_VALUE) { |
| return "an integer"; |
| } else { |
| return "an integer, <= " + maxValue; |
| } |
| } else if (maxValue == Integer.MAX_VALUE) { |
| return "an integer, >= " + minValue; |
| } else { |
| return "an integer in " |
| + (minValue < 0 ? "(" + minValue + ")" : minValue) |
| + "-" |
| + maxValue |
| + " range"; |
| } |
| } |
| } |
| |
| /** |
| * A converter for variable assignments from the parameter list of a blaze command invocation. |
| * Assignments are expected to have the form "name=value", where names and values are defined to |
| * be as permissive as possible. |
| */ |
| public static class AssignmentConverter extends Converter.Contextless<Map.Entry<String, String>> { |
| |
| @Override |
| public Map.Entry<String, String> convert(String input) throws OptionsParsingException { |
| int pos = input.indexOf("="); |
| if (pos <= 0) { |
| throw new OptionsParsingException( |
| "Variable definitions must be in the form of a 'name=value' assignment"); |
| } |
| String name = input.substring(0, pos); |
| String value = input.substring(pos + 1); |
| return Maps.immutableEntry(name, value); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a 'name=value' assignment"; |
| } |
| } |
| |
| /** A converter for for assignments from a string value to a float value. */ |
| public static class StringToFloatAssignmentConverter |
| extends Converter.Contextless<Map.Entry<String, Float>> { |
| private static final AssignmentConverter baseConverter = new AssignmentConverter(); |
| |
| @Override |
| public Map.Entry<String, Float> convert(String input) |
| throws OptionsParsingException, NumberFormatException { |
| Map.Entry<String, String> stringEntry = baseConverter.convert(input); |
| return Maps.immutableEntry(stringEntry.getKey(), Float.parseFloat(stringEntry.getValue())); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a named float, 'name=value'"; |
| } |
| } |
| |
| /** |
| * A converter for command line flag aliases. It does additional validation on the name and value |
| * of the assignment to ensure they conform to the naming limitations. |
| */ |
| public static class FlagAliasConverter extends AssignmentConverter { |
| |
| @Override |
| public Map.Entry<String, String> convert(String input) throws OptionsParsingException { |
| Map.Entry<String, String> entry = super.convert(input); |
| String shortForm = entry.getKey(); |
| String longForm = entry.getValue(); |
| |
| String cmdLineAlias = "--" + BLAZE_ALIASING_FLAG + "=" + input; |
| |
| if (!Pattern.matches("([\\w])*", shortForm)) { |
| throw new OptionsParsingException( |
| shortForm + " should only consist of word characters to be a valid alias name.", |
| cmdLineAlias); |
| } |
| if (longForm.contains("=")) { |
| throw new OptionsParsingException( |
| "--" + BLAZE_ALIASING_FLAG + " does not support flag value assignment.", cmdLineAlias); |
| } |
| |
| // Remove this check if native options are permitted to be aliased |
| longForm = "--" + longForm; |
| if (STARLARK_SKIPPED_PREFIXES.stream().noneMatch(longForm::startsWith)) { |
| throw new OptionsParsingException( |
| "--" + BLAZE_ALIASING_FLAG + " only supports Starlark build settings.", cmdLineAlias); |
| } |
| |
| return entry; |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a 'name=value' flag alias"; |
| } |
| } |
| |
| /** |
| * Base converter for assignments from a value to a list of values. Both the key type as well as |
| * the type for all instances in the list of values are processed via passed converters. |
| */ |
| public abstract static class AssignmentToListOfValuesConverter<K, V> |
| implements Converter<Map.Entry<K, List<V>>> { |
| |
| /** Whether to allow keys in the assignment to be empty (i.e. just a list of values) */ |
| public enum AllowEmptyKeys { |
| YES, |
| NO |
| } |
| |
| private static final Splitter SPLITTER = Splitter.on(','); |
| |
| private final Converter<K> keyConverter; |
| private final Converter<V> valueConverter; |
| private final AllowEmptyKeys allowEmptyKeys; |
| |
| public AssignmentToListOfValuesConverter( |
| Converter<K> keyConverter, Converter<V> valueConverter, AllowEmptyKeys allowEmptyKeys) { |
| this.keyConverter = keyConverter; |
| this.valueConverter = valueConverter; |
| this.allowEmptyKeys = allowEmptyKeys; |
| } |
| |
| @Override |
| public Map.Entry<K, List<V>> convert(String input, @Nullable Object conversionContext) |
| throws OptionsParsingException { |
| int pos = input.indexOf("="); |
| if (allowEmptyKeys == AllowEmptyKeys.NO && pos <= 0) { |
| throw new OptionsParsingException( |
| "Must be in the form of a 'key=value[,value]' assignment"); |
| } |
| |
| String key = pos <= 0 ? "" : input.substring(0, pos); |
| List<String> values = SPLITTER.splitToList(input.substring(pos + 1)); |
| if (values.contains("")) { |
| // If the list contains exactly the empty string, it means an empty value was passed and we |
| // should instead return an empty list. |
| if (values.size() == 1) { |
| values = ImmutableList.of(); |
| } else { |
| throw new OptionsParsingException( |
| "Variable definitions must not contain empty strings or leading / trailing commas"); |
| } |
| } |
| ImmutableList.Builder<V> convertedValues = ImmutableList.builder(); |
| for (String value : values) { |
| convertedValues.add(valueConverter.convert(value, conversionContext)); |
| } |
| return Maps.immutableEntry( |
| keyConverter.convert(key, conversionContext), convertedValues.build()); |
| } |
| } |
| |
| /** |
| * A converter for variable assignments from the parameter list of a blaze command invocation. |
| * Assignments are expected to have the form {@code [name=]value1[,..,valueN]}, where names and |
| * values are defined to be as permissive as possible. If no name is provided, "" is used. |
| */ |
| public static class StringToStringListConverter |
| extends AssignmentToListOfValuesConverter<String, String> { |
| |
| public StringToStringListConverter() { |
| super(new StringConverter(), new StringConverter(), AllowEmptyKeys.YES); |
| } |
| |
| public Map.Entry<String, List<String>> convert(String input) throws OptionsParsingException { |
| return convert(input, /* conversionContext= */ null); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a '[name=]value1[,..,valueN]' assignment"; |
| } |
| } |
| |
| /** |
| * A converter for variable assignments from the parameter list of a blaze command invocation. |
| * Assignments are expected to have the form "name[=value]", where names and values are defined to |
| * be as permissive as possible and value part can be optional (in which case it is considered to |
| * be null). |
| */ |
| public static class OptionalAssignmentConverter |
| extends Converter.Contextless<Map.Entry<String, String>> { |
| |
| @Override |
| public Map.Entry<String, String> convert(String input) throws OptionsParsingException { |
| int pos = input.indexOf('='); |
| if (pos == 0 || input.length() == 0) { |
| throw new OptionsParsingException( |
| "Variable definitions must be in the form of a 'name=value' or 'name' assignment"); |
| } else if (pos < 0) { |
| return Maps.immutableEntry(input, null); |
| } |
| String name = input.substring(0, pos); |
| String value = input.substring(pos + 1); |
| return Maps.immutableEntry(name, value); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a 'name=value' assignment with an optional value part"; |
| } |
| } |
| |
| /** |
| * A converter for named integers of the form "[name=]value". When no name is specified, an empty |
| * string is used for the key. |
| */ |
| public static class NamedIntegersConverter |
| extends Converter.Contextless<Map.Entry<String, Integer>> { |
| |
| @Override |
| public Map.Entry<String, Integer> convert(String input) throws OptionsParsingException { |
| int pos = input.indexOf('='); |
| if (pos == 0 || input.length() == 0) { |
| throw new OptionsParsingException( |
| "Specify either 'value' or 'name=value', where 'value' is an integer"); |
| } else if (pos < 0) { |
| try { |
| return Maps.immutableEntry("", Integer.parseInt(input)); |
| } catch (NumberFormatException e) { |
| throw new OptionsParsingException("'" + input + "' is not an int", e); |
| } |
| } |
| String name = input.substring(0, pos); |
| String value = input.substring(pos + 1); |
| try { |
| return Maps.immutableEntry(name, Integer.parseInt(value)); |
| } catch (NumberFormatException e) { |
| throw new OptionsParsingException("'" + value + "' is not an int", e); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "an integer or a named integer, 'name=value'"; |
| } |
| } |
| |
| public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> { |
| public HelpVerbosityConverter() { |
| super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting"); |
| } |
| } |
| |
| /** |
| * A converter to check whether an integer denoting a percentage is in a valid range: [0, 100]. |
| */ |
| public static class PercentageConverter extends RangeConverter { |
| public PercentageConverter() { |
| super(0, 100); |
| } |
| } |
| |
| /** |
| * A {@link Converter} for {@link com.github.benmanes.caffeine.cache.CaffeineSpec}. The spec may |
| * be empty, in which case this converter returns null. |
| */ |
| public static final class CaffeineSpecConverter extends Converter.Contextless<CaffeineSpec> { |
| @Override |
| public CaffeineSpec convert(String spec) throws OptionsParsingException { |
| try { |
| return CaffeineSpec.parse(spec); |
| } catch (IllegalArgumentException e) { |
| throw new OptionsParsingException("Failed to parse CaffeineSpec: " + e.getMessage(), e); |
| } |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "Converts to a CaffeineSpec, or null if the input is empty"; |
| } |
| } |
| } |