| // 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)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |