blob: 3b42a29d856544c7144d3e35ab3991ae1bf04692 [file] [log] [blame]
// Copyright 2015 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.Verify;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.AllowValues;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.DisallowValues;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy.OperationCase;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.UseDefault;
import com.google.devtools.common.options.OptionPriority.PriorityCategory;
import com.google.devtools.common.options.OptionsParser.OptionDescription;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* Enforces the {@link FlagPolicy}s (from an {@link InvocationPolicy} proto) on an {@link
* OptionsParser} by validating and changing the flag values in the given {@link OptionsParser}.
*
* <p>"Flag" and "Option" are used interchangeably in this file.
*/
public final class InvocationPolicyEnforcer {
private static final Logger logger = Logger.getLogger(InvocationPolicyEnforcer.class.getName());
private static final String INVOCATION_POLICY_SOURCE = "Invocation policy";
@Nullable private final InvocationPolicy invocationPolicy;
private final Level loglevel;
/**
* Creates an InvocationPolicyEnforcer that enforces the given policy.
*
* @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
* nothing in calls to enforce().
*/
public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy) {
this(invocationPolicy, Level.FINE);
}
/**
* Creates an InvocationPolicyEnforcer that enforces the given policy.
*
* @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
* nothing in calls to enforce().
* @param loglevel the level at which to log informational statements. Warnings and errors will
* still be logged at the appropriate level.
*/
public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy, Level loglevel) {
this.invocationPolicy = invocationPolicy;
this.loglevel = loglevel;
}
private static final class FlagPolicyWithContext {
private final FlagPolicy policy;
private final OptionDescription description;
private final OptionInstanceOrigin origin;
public FlagPolicyWithContext(
FlagPolicy policy, OptionDescription description, OptionInstanceOrigin origin) {
this.policy = policy;
this.description = description;
this.origin = origin;
}
}
public InvocationPolicy getInvocationPolicy() {
return invocationPolicy;
}
/**
* Applies this OptionsPolicyEnforcer's policy to the given OptionsParser for all blaze commands.
*
* @param parser The OptionsParser to enforce policy on.
* @throws OptionsParsingException if any flag policy is invalid.
*/
public void enforce(OptionsParser parser) throws OptionsParsingException {
enforce(parser, null);
}
/**
* Applies this OptionsPolicyEnforcer's policy to the given OptionsParser.
*
* @param parser The OptionsParser to enforce policy on.
* @param command The current blaze command, for flag policies that apply to only specific
* commands. Such policies will be enforced only if they contain this command or a command
* they inherit from
* @throws OptionsParsingException if any flag policy is invalid.
*/
public void enforce(OptionsParser parser, @Nullable String command)
throws OptionsParsingException {
if (invocationPolicy == null || invocationPolicy.getFlagPoliciesCount() == 0) {
return;
}
// The effective policy returned is expanded, filtered for applicable commands, and cleaned of
// redundancies and conflicts.
List<FlagPolicyWithContext> effectivePolicies =
getEffectivePolicies(invocationPolicy, parser, command, loglevel);
for (FlagPolicyWithContext flagPolicy : effectivePolicies) {
String flagName = flagPolicy.policy.getFlagName();
OptionValueDescription valueDescription;
try {
valueDescription = parser.getOptionValueDescription(flagName);
} catch (IllegalArgumentException e) {
// This flag doesn't exist. We are deliberately lenient if the flag policy has a flag
// we don't know about. This is for better future proofing so that as new flags are added,
// new policies can use the new flags without worrying about older versions of Bazel.
logger.log(
loglevel,
String.format("Flag '%s' specified by invocation policy does not exist", flagName));
continue;
}
// getOptionDescription() will return null if the option does not exist, however
// getOptionValueDescription() above would have thrown an IllegalArgumentException if that
// were the case.
Verify.verifyNotNull(flagPolicy.description);
switch (flagPolicy.policy.getOperationCase()) {
case SET_VALUE:
applySetValueOperation(parser, flagPolicy, valueDescription, loglevel);
break;
case USE_DEFAULT:
applyUseDefaultOperation(
parser, "UseDefault", flagPolicy.description.getOptionDefinition(), loglevel);
break;
case ALLOW_VALUES:
AllowValues allowValues = flagPolicy.policy.getAllowValues();
FilterValueOperation.AllowValueOperation allowValueOperation =
new FilterValueOperation.AllowValueOperation(loglevel);
allowValueOperation.apply(
parser,
flagPolicy.origin,
allowValues.getAllowedValuesList(),
allowValues.hasNewValue() ? allowValues.getNewValue() : null,
allowValues.hasUseDefault(),
valueDescription,
flagPolicy.description);
break;
case DISALLOW_VALUES:
DisallowValues disallowValues = flagPolicy.policy.getDisallowValues();
FilterValueOperation.DisallowValueOperation disallowValueOperation =
new FilterValueOperation.DisallowValueOperation(loglevel);
disallowValueOperation.apply(
parser,
flagPolicy.origin,
disallowValues.getDisallowedValuesList(),
disallowValues.hasNewValue() ? disallowValues.getNewValue() : null,
disallowValues.hasUseDefault(),
valueDescription,
flagPolicy.description);
break;
case OPERATION_NOT_SET:
throw new PolicyOperationNotSetException(flagName);
default:
logger.warning(
String.format(
"Unknown operation '%s' from invocation policy for flag '%s'",
flagPolicy.policy.getOperationCase(), flagName));
break;
}
}
}
private static class PolicyOperationNotSetException extends OptionsParsingException {
PolicyOperationNotSetException(String flagName) {
super(String.format("Flag policy for flag '%s' does not " + "have an operation", flagName));
}
}
private static boolean policyApplies(FlagPolicy policy, ImmutableSet<String> applicableCommands) {
// Skip the flag policy if it doesn't apply to this command. If the commands list is empty,
// then the policy applies to all commands.
if (policy.getCommandsList().isEmpty() || applicableCommands.isEmpty()) {
return true;
}
return !Collections.disjoint(policy.getCommandsList(), applicableCommands);
}
/** Returns the expanded and filtered policy that would be enforced for the given command. */
public static InvocationPolicy getEffectiveInvocationPolicy(
InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
throws OptionsParsingException {
ImmutableList<FlagPolicyWithContext> effectivePolicies =
getEffectivePolicies(invocationPolicy, parser, command, loglevel);
InvocationPolicy.Builder builder = InvocationPolicy.newBuilder();
for (FlagPolicyWithContext policyWithContext : effectivePolicies) {
builder.addFlagPolicies(policyWithContext.policy);
}
return builder.build();
}
/**
* Takes the provided policy and processes it to the form that can be used on the user options.
*
* <p>Expands any policies on expansion flags.
*/
private static ImmutableList<FlagPolicyWithContext> getEffectivePolicies(
InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
throws OptionsParsingException {
if (invocationPolicy == null) {
return ImmutableList.of();
}
ImmutableSet<String> commandAndParentCommands =
command == null
? ImmutableSet.of()
: CommandNameCache.CommandNameCacheInstance.INSTANCE.get(command);
// Expand all policies to transfer policies on expansion flags to policies on the child flags.
List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
OptionPriority nextPriority =
OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY);
for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) {
// Explicitly disallow --config in invocation policy.
if (policy.getFlagName().equals("config")) {
throw new OptionsParsingException(
"Invocation policy is applied after --config expansion, changing config values now "
+ "would have no effect and is disallowed to prevent confusion. Please remove the "
+ "following policy : "
+ policy);
}
// These policies are high-level, before expansion, and so are not the implicitDependents or
// expansions of any other flag, other than in an obtuse sense from --invocation_policy.
OptionPriority currentPriority = nextPriority;
OptionInstanceOrigin origin =
new OptionInstanceOrigin(currentPriority, INVOCATION_POLICY_SOURCE, null, null);
nextPriority = OptionPriority.nextOptionPriority(currentPriority);
if (!policyApplies(policy, commandAndParentCommands)) {
// Only keep and expand policies that are applicable to the current command.
continue;
}
OptionDescription optionDescription = parser.getOptionDescription(policy.getFlagName());
if (optionDescription == null) {
// InvocationPolicy ignores policy on non-existing flags by design, for version
// compatibility.
logger.log(
loglevel,
String.format(
"Flag '%s' specified by invocation policy does not exist, and will be ignored",
policy.getFlagName()));
continue;
}
FlagPolicyWithContext policyWithContext =
new FlagPolicyWithContext(policy, optionDescription, origin);
List<FlagPolicyWithContext> policies = expandPolicy(policyWithContext, parser, loglevel);
expandedPolicies.addAll(policies);
}
// Only keep that last policy for each flag.
Map<String, FlagPolicyWithContext> effectivePolicy = new HashMap<>();
for (FlagPolicyWithContext expandedPolicy : expandedPolicies) {
String flagName = expandedPolicy.policy.getFlagName();
effectivePolicy.put(flagName, expandedPolicy);
}
return ImmutableList.copyOf(effectivePolicy.values());
}
private static void throwAllowValuesOnExpansionFlagException(String flagName)
throws OptionsParsingException {
throw new OptionsParsingException(
String.format("Allow_Values on expansion flags like %s is not allowed.", flagName));
}
private static void throwDisallowValuesOnExpansionFlagException(String flagName)
throws OptionsParsingException {
throw new OptionsParsingException(
String.format("Disallow_Values on expansion flags like %s is not allowed.", flagName));
}
/**
* Expand a single policy. If the policy is not about an expansion flag, this will simply return a
* list with a single element, oneself. If the policy is for an expansion flag, the policy will
* get split into multiple policies applying to each flag the original flag expands to.
*
* <p>None of the flagPolicies returned should be on expansion flags.
*/
private static List<FlagPolicyWithContext> expandPolicy(
FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel)
throws OptionsParsingException {
List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
boolean isExpansion = originalPolicy.description.isExpansion();
ImmutableList<ParsedOptionDescription> subflags =
parser.getExpansionValueDescriptions(
originalPolicy.description.getOptionDefinition(), originalPolicy.origin);
// If we have nothing to expand to, no need to do any further work.
if (subflags.isEmpty()) {
return ImmutableList.of(originalPolicy);
}
if (logger.isLoggable(loglevel)) {
// Log the expansion. This is only really useful for understanding the invocation policy
// itself.
List<String> subflagNames = new ArrayList<>(subflags.size());
for (ParsedOptionDescription subflag : subflags) {
subflagNames.add("--" + subflag.getOptionDefinition().getOptionName());
}
logger.logp(
loglevel,
"InvocationPolicyEnforcer",
"expandPolicy",
String.format(
"Expanding %s on option %s to its %s: %s.",
originalPolicy.policy.getOperationCase(),
originalPolicy.policy.getFlagName(),
isExpansion ? "expansions" : "implied flags",
Joiner.on("; ").join(subflagNames)));
}
// Repeated flags are special, and could set multiple times in an expansion, with the user
// expecting both values to be valid. Collect these separately.
Multimap<OptionDescription, ParsedOptionDescription> repeatableSubflagsInSetValues =
ArrayListMultimap.create();
// Create a flag policy for the child that looks like the parent's policy "transferred" to its
// child. Note that this only makes sense for SetValue, when setting an expansion flag, or
// UseDefault, when preventing it from being set.
for (ParsedOptionDescription currentSubflag : subflags) {
OptionDescription subflagOptionDescription =
parser.getOptionDescription(currentSubflag.getOptionDefinition().getOptionName());
if (currentSubflag.getOptionDefinition().allowsMultiple()
&& originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)) {
repeatableSubflagsInSetValues.put(subflagOptionDescription, currentSubflag);
} else {
FlagPolicyWithContext subflagAsPolicy =
getSingleValueSubflagAsPolicy(
subflagOptionDescription, currentSubflag, originalPolicy, isExpansion);
// In case any of the expanded flags are themselves expansions, recurse.
expandedPolicies.addAll(expandPolicy(subflagAsPolicy, parser, loglevel));
}
}
// If there are any repeatable flag SetValues, deal with them together now.
// Note that expansion flags have no value, and so cannot have multiple values either.
// Skipping the recursion above is fine.
for (OptionDescription repeatableFlag : repeatableSubflagsInSetValues.keySet()) {
int numValues = repeatableSubflagsInSetValues.get(repeatableFlag).size();
ArrayList<String> newValues = new ArrayList<>(numValues);
ArrayList<OptionInstanceOrigin> origins = new ArrayList<>(numValues);
for (ParsedOptionDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) {
newValues.add(setValue.getUnconvertedValue());
origins.add(setValue.getOrigin());
}
// These options come from expanding a single policy, so they have effectively the same
// priority. They could have come from different expansions or implicit requirements in the
// recursive resolving of the option list, so just pick the first one. Do collapse the source
// strings though, in case there are different sources.
OptionInstanceOrigin arbitraryFirstOptionOrigin = origins.get(0);
OptionInstanceOrigin originOfSubflags =
new OptionInstanceOrigin(
arbitraryFirstOptionOrigin.getPriority(),
origins
.stream()
.map(OptionInstanceOrigin::getSource)
.distinct()
.collect(Collectors.joining(", ")),
arbitraryFirstOptionOrigin.getImplicitDependent(),
arbitraryFirstOptionOrigin.getExpandedFrom());
expandedPolicies.add(
getSetValueSubflagAsPolicy(repeatableFlag, newValues, originOfSubflags, originalPolicy));
}
// Don't add the original policy if it was an expansion flag, which have no value, but do add
// it if there was either no expansion or if it was a valued flag with implicit requirements.
if (!isExpansion) {
expandedPolicies.add(originalPolicy);
}
return expandedPolicies;
}
/**
* Expand a SetValue flag policy on a repeatable flag. SetValue operations are the only flag
* policies that set the flag, and so interact with repeatable flags, flags that can be set
* multiple times, in subtle ways.
*
* @param subflagDesc, the description of the flag the SetValue'd expansion flag expands to.
* @param subflagValue, the values that the SetValue'd expansion flag expands to for this flag.
* @param originalPolicy, the original policy on the expansion flag.
* @return the flag policy for the subflag given, this will be part of the expanded form of the
* SetValue policy on the original flag.
*/
private static FlagPolicyWithContext getSetValueSubflagAsPolicy(
OptionDescription subflagDesc,
List<String> subflagValue,
OptionInstanceOrigin subflagOrigin,
FlagPolicyWithContext originalPolicy) {
// Some sanity checks.
OptionDefinition subflag = subflagDesc.getOptionDefinition();
Verify.verify(originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE));
if (!subflag.allowsMultiple()) {
Verify.verify(subflagValue.size() <= 1);
}
// Flag value from the expansion, overridability from the original policy, unless the flag is
// repeatable, in which case we care about appendability, not overridability.
SetValue.Builder setValueExpansion = SetValue.newBuilder();
for (String value : subflagValue) {
setValueExpansion.addFlagValue(value);
}
if (subflag.allowsMultiple()) {
setValueExpansion.setAppend(originalPolicy.policy.getSetValue().getOverridable());
} else {
setValueExpansion.setOverridable(originalPolicy.policy.getSetValue().getOverridable());
}
// Commands from the original policy, flag name of the expansion
return new FlagPolicyWithContext(
FlagPolicy.newBuilder()
.addAllCommands(originalPolicy.policy.getCommandsList())
.setFlagName(subflag.getOptionName())
.setSetValue(setValueExpansion)
.build(),
subflagDesc,
subflagOrigin);
}
/**
* For an expansion flag in an invocation policy, each flag it expands to must be given a
* corresponding policy.
*/
private static FlagPolicyWithContext getSingleValueSubflagAsPolicy(
OptionDescription subflagContext,
ParsedOptionDescription currentSubflag,
FlagPolicyWithContext originalPolicy,
boolean isExpansion)
throws OptionsParsingException {
FlagPolicyWithContext subflagAsPolicy = null;
switch (originalPolicy.policy.getOperationCase()) {
case SET_VALUE:
if (currentSubflag.getOptionDefinition().allowsMultiple()) {
throw new AssertionError(
"SetValue subflags with allowMultiple should have been dealt with separately and "
+ "accumulated into a single FlagPolicy.");
}
// Accept null originalValueStrings, they are expected when the subflag is also an expansion
// flag.
List<String> subflagValue;
if (currentSubflag.getUnconvertedValue() == null) {
subflagValue = ImmutableList.of();
} else {
subflagValue = ImmutableList.of(currentSubflag.getUnconvertedValue());
}
subflagAsPolicy =
getSetValueSubflagAsPolicy(
subflagContext, subflagValue, currentSubflag.getOrigin(), originalPolicy);
break;
case USE_DEFAULT:
// Commands from the original policy, flag name of the expansion
subflagAsPolicy =
new FlagPolicyWithContext(
FlagPolicy.newBuilder()
.addAllCommands(originalPolicy.policy.getCommandsList())
.setFlagName(currentSubflag.getOptionDefinition().getOptionName())
.setUseDefault(UseDefault.getDefaultInstance())
.build(),
subflagContext,
currentSubflag.getOrigin());
break;
case ALLOW_VALUES:
if (isExpansion) {
throwAllowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
}
// If this flag is an implicitRequirement, and some values for the parent flag are
// allowed, nothing needs to happen on the implicitRequirement that is set for all
// values of the flag.
break;
case DISALLOW_VALUES:
if (isExpansion) {
throwDisallowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
}
// If this flag is an implicitRequirement, and some values for the parent flag are
// disallowed, that implies that all others are allowed, so nothing needs to happen
// on the implicitRequirement that is set for all values of the parent flag.
break;
case OPERATION_NOT_SET:
throw new PolicyOperationNotSetException(originalPolicy.policy.getFlagName());
default:
return null;
}
return subflagAsPolicy;
}
private static void logInApplySetValueOperation(
Level loglevel, String formattingString, Object... objects) {
// Finding the caller here is relatively expensive and shows up in profiling, so provide it
// manually.
logger.logp(
loglevel,
"InvocationPolicyEnforcer",
"applySetValueOperation",
String.format(formattingString, objects));
}
private static void applySetValueOperation(
OptionsParser parser,
FlagPolicyWithContext flagPolicy,
OptionValueDescription valueDescription,
Level loglevel)
throws OptionsParsingException {
SetValue setValue = flagPolicy.policy.getSetValue();
OptionDefinition optionDefinition = flagPolicy.description.getOptionDefinition();
// SetValue.flag_value must have at least 1 value.
if (setValue.getFlagValueCount() == 0) {
throw new OptionsParsingException(
String.format(
"SetValue operation from invocation policy for %s does not have a value",
optionDefinition));
}
// Flag must allow multiple values if multiple values are specified by the policy.
if (setValue.getFlagValueCount() > 1
&& !flagPolicy.description.getOptionDefinition().allowsMultiple()) {
throw new OptionsParsingException(
String.format(
"SetValue operation from invocation policy sets multiple values for %s which "
+ "does not allow multiple values",
optionDefinition));
}
if (setValue.getOverridable() && valueDescription != null) {
// The user set the value for the flag but the flag policy is overridable, so keep the user's
// value.
logInApplySetValueOperation(
loglevel,
"Keeping value '%s' from source '%s' for %s because the invocation policy specifying "
+ "the value(s) '%s' is overridable",
valueDescription.getValue(),
valueDescription.getSourceString(),
optionDefinition,
setValue.getFlagValueList());
} else {
if (!setValue.getAppend()) {
// Clear the value in case the flag is a repeated flag so that values don't accumulate.
parser.clearValue(flagPolicy.description.getOptionDefinition());
}
// Set all the flag values from the policy.
for (String flagValue : setValue.getFlagValueList()) {
if (valueDescription == null) {
logInApplySetValueOperation(
loglevel,
"Setting value for %s from invocation policy to '%s', overriding the default value "
+ "'%s'",
optionDefinition,
flagValue,
optionDefinition.getDefaultValue());
} else {
logInApplySetValueOperation(
loglevel,
"Setting value for %s from invocation policy to '%s', overriding value '%s' from "
+ "'%s'",
optionDefinition,
flagValue,
valueDescription.getValue(),
valueDescription.getSourceString());
}
parser.addOptionValueAtSpecificPriority(flagPolicy.origin, optionDefinition, flagValue);
}
}
}
private static void applyUseDefaultOperation(
OptionsParser parser, String policyType, OptionDefinition option, Level loglevel)
throws OptionsParsingException {
OptionValueDescription clearedValueDescription = parser.clearValue(option);
if (clearedValueDescription != null) {
// Log the removed value.
String clearedFlagName = clearedValueDescription.getOptionDefinition().getOptionName();
Object clearedFlagDefaultValue =
clearedValueDescription.getOptionDefinition().getDefaultValue();
logger.log(
loglevel,
String.format(
"Using default value '%s' for flag '%s' as specified by %s invocation policy, "
+ "overriding original value '%s' from '%s'",
clearedFlagDefaultValue,
clearedFlagName,
policyType,
clearedValueDescription.getValue(),
clearedValueDescription.getSourceString()));
}
}
/** Checks the user's flag values against a filtering function. */
private abstract static class FilterValueOperation {
private static final class AllowValueOperation extends FilterValueOperation {
AllowValueOperation(Level loglevel) {
super("Allow", loglevel);
}
@Override
boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
return convertedPolicyValues.contains(value);
}
}
private static final class DisallowValueOperation extends FilterValueOperation {
DisallowValueOperation(Level loglevel) {
super("Disalllow", loglevel);
}
@Override
boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
// In a disallow operation, the values that the flag policy specifies are not allowed,
// so the value is allowed if the set of policy values does not contain the current
// flag value.
return !convertedPolicyValues.contains(value);
}
}
private final String policyType;
private final Level loglevel;
FilterValueOperation(String policyType, Level loglevel) {
this.policyType = policyType;
this.loglevel = loglevel;
}
/**
* Determines if the given value is allowed.
*
* @param convertedPolicyValues The values given from the FlagPolicy, converted to real objects.
* @param value The user value of the flag.
* @return True if the value should be allowed, false if it should not.
*/
abstract boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value);
void apply(
OptionsParser parser,
OptionInstanceOrigin origin,
List<String> policyValues,
String newValue,
boolean useDefault,
OptionValueDescription valueDescription,
OptionDescription optionDescription)
throws OptionsParsingException {
OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
// Convert all the allowed values from strings to real objects using the options'
// converters so that they can be checked for equality using real .equals() instead
// of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-"
// (if the option has an abbreviation) are all equal for boolean flags. Plus converters
// can be arbitrarily complex.
Set<Object> convertedPolicyValues = new HashSet<>();
for (String value : policyValues) {
Object convertedValue = optionDefinition.getConverter().convert(value);
// Some converters return lists, and if the flag is a repeatable flag, the items in the
// list from the converter should be added, and not the list itself. Otherwise the items
// from invocation policy will be compared to lists, which will never work.
// See OptionsParserImpl.ParsedOptionEntry.addValue.
if (optionDefinition.allowsMultiple() && convertedValue instanceof List<?>) {
convertedPolicyValues.addAll((List<?>) convertedValue);
} else {
convertedPolicyValues.add(optionDefinition.getConverter().convert(value));
}
}
// Check that if the default value of the flag is disallowed by the policy, that the policy
// does not also set use_default. Otherwise the default value would still be set if the
// user uses a disallowed value. This doesn't apply to repeatable flags since the default
// value for repeatable flags is always the empty list. It also doesn't apply to flags that
// are null by default, since these flags' default value is not parsed by the converter, so
// there is no guarantee that there exists an accepted user-input value that would also set
// the value to NULL. In these cases, we assume that "unset" is a distinct value that is
// always allowed.
if (!optionDescription.getOptionDefinition().allowsMultiple()
&& !optionDescription.getOptionDefinition().isSpecialNullDefault()) {
boolean defaultValueAllowed =
isFlagValueAllowed(
convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue());
if (!defaultValueAllowed && useDefault) {
throw new OptionsParsingException(
String.format(
"%sValues policy disallows the default value '%s' for %s but also specifies to "
+ "use the default value",
policyType, optionDefinition.getDefaultValue(), optionDefinition));
}
}
if (valueDescription == null) {
// Nothing has set the value yet, so check that the default value from the flag's
// definition is allowed. The else case below (i.e. valueDescription is not null) checks for
// the flag allowing multiple values, however, flags that allow multiple values cannot have
// default values, and their value is always the empty list if they haven't been specified,
// which is why new_default_value is not a repeated field.
checkDefaultValue(
parser, origin, optionDescription, policyValues, newValue, convertedPolicyValues);
} else {
checkUserValue(
parser,
origin,
optionDescription,
valueDescription,
policyValues,
newValue,
useDefault,
convertedPolicyValues);
}
}
void checkDefaultValue(
OptionsParser parser,
OptionInstanceOrigin origin,
OptionDescription optionDescription,
List<String> policyValues,
String newValue,
Set<Object> convertedPolicyValues)
throws OptionsParsingException {
OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
if (optionDefinition.isSpecialNullDefault()) {
// Do nothing, the unset value by definition cannot be set. In option filtering operations,
// the value is being filtered, but the value that is `no value` passes any filter.
// Otherwise, there is no way to "usedefault" on one of these options that has no value by
// default.
} else if (!isFlagValueAllowed(convertedPolicyValues, optionDefinition.getDefaultValue())) {
if (newValue != null) {
// Use the default value from the policy, since the original default is not allowed
logger.log(
loglevel,
String.format(
"Overriding default value '%s' for %s with value '%s' specified by invocation "
+ "policy. %sed values are: %s",
optionDefinition.getDefaultValue(),
optionDefinition,
newValue,
policyType,
policyValues));
parser.clearValue(optionDefinition);
parser.addOptionValueAtSpecificPriority(origin, optionDefinition, newValue);
} else {
// The operation disallows the default value, but doesn't supply a new value.
throw new OptionsParsingException(
String.format(
"Default flag value '%s' for %s is not allowed by invocation policy, but "
+ "the policy does not provide a new value. %sed values are: %s",
optionDescription.getOptionDefinition().getDefaultValue(),
optionDefinition,
policyType,
policyValues));
}
}
}
void checkUserValue(
OptionsParser parser,
OptionInstanceOrigin origin,
OptionDescription optionDescription,
OptionValueDescription valueDescription,
List<String> policyValues,
String newValue,
boolean useDefault,
Set<Object> convertedPolicyValues)
throws OptionsParsingException {
OptionDefinition option = optionDescription.getOptionDefinition();
if (optionDescription.getOptionDefinition().allowsMultiple()) {
// allowMultiple requires that the type of the option be List<T>, so cast from Object
// to List<?>.
List<?> optionValues = (List<?>) valueDescription.getValue();
for (Object value : optionValues) {
if (!isFlagValueAllowed(convertedPolicyValues, value)) {
if (useDefault) {
applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
} else {
throw new OptionsParsingException(
String.format(
"Flag value '%s' for %s is not allowed by invocation policy. %sed values "
+ "are: %s",
value, option, policyType, policyValues));
}
}
}
} else {
if (!isFlagValueAllowed(convertedPolicyValues, valueDescription.getValue())) {
if (newValue != null) {
logger.log(
loglevel,
String.format(
"Overriding disallowed value '%s' for %s with value '%s' "
+ "specified by invocation policy. %sed values are: %s",
valueDescription.getValue(), option, newValue, policyType, policyValues));
parser.clearValue(option);
parser.addOptionValueAtSpecificPriority(origin, option, newValue);
} else if (useDefault) {
applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
} else {
throw new OptionsParsingException(
String.format(
"Flag value '%s' for %s is not allowed by invocation policy and the "
+ "policy does not specify a new value. %sed values are: %s",
valueDescription.getValue(), option, policyType, policyValues));
}
}
}
}
}
}