1 // Copyright 2015 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package com.google.devtools.common.options; 15 16 import com.google.common.base.Joiner; 17 import com.google.common.base.Verify; 18 import com.google.common.collect.ArrayListMultimap; 19 import com.google.common.collect.ImmutableList; 20 import com.google.common.collect.ImmutableSet; 21 import com.google.common.collect.Multimap; 22 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.AllowValues; 23 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.DisallowValues; 24 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy; 25 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy.OperationCase; 26 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; 27 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue; 28 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.UseDefault; 29 import com.google.devtools.common.options.OptionPriority.PriorityCategory; 30 import com.google.devtools.common.options.OptionsParser.OptionDescription; 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Set; 38 import java.util.logging.Level; 39 import java.util.logging.Logger; 40 import java.util.stream.Collectors; 41 import javax.annotation.Nullable; 42 43 /** 44 * Enforces the {@link FlagPolicy}s (from an {@link InvocationPolicy} proto) on an {@link 45 * OptionsParser} by validating and changing the flag values in the given {@link OptionsParser}. 46 * 47 * <p>"Flag" and "Option" are used interchangeably in this file. 48 */ 49 public final class InvocationPolicyEnforcer { 50 51 private static final Logger logger = Logger.getLogger(InvocationPolicyEnforcer.class.getName()); 52 53 private static final String INVOCATION_POLICY_SOURCE = "Invocation policy"; 54 @Nullable private final InvocationPolicy invocationPolicy; 55 private final Level loglevel; 56 57 /** 58 * Creates an InvocationPolicyEnforcer that enforces the given policy. 59 * 60 * @param invocationPolicy the policy to enforce. A null policy means this enforcer will do 61 * nothing in calls to enforce(). 62 */ 63 public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy) { 64 this(invocationPolicy, Level.FINE); 65 } 66 67 /** 68 * Creates an InvocationPolicyEnforcer that enforces the given policy. 69 * 70 * @param invocationPolicy the policy to enforce. A null policy means this enforcer will do 71 * nothing in calls to enforce(). 72 * @param loglevel the level at which to log informational statements. Warnings and errors will 73 * still be logged at the appropriate level. 74 */ 75 public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy, Level loglevel) { 76 this.invocationPolicy = invocationPolicy; 77 this.loglevel = loglevel; 78 } 79 80 private static final class FlagPolicyWithContext { 81 private final FlagPolicy policy; 82 private final OptionDescription description; 83 private final OptionInstanceOrigin origin; 84 85 public FlagPolicyWithContext( 86 FlagPolicy policy, OptionDescription description, OptionInstanceOrigin origin) { 87 this.policy = policy; 88 this.description = description; 89 this.origin = origin; 90 } 91 } 92 93 public InvocationPolicy getInvocationPolicy() { 94 return invocationPolicy; 95 } 96 97 /** 98 * Applies this OptionsPolicyEnforcer's policy to the given OptionsParser for all blaze commands. 99 * 100 * @param parser The OptionsParser to enforce policy on. 101 * @throws OptionsParsingException if any flag policy is invalid. 102 */ 103 public void enforce(OptionsParser parser) throws OptionsParsingException { 104 enforce(parser, null); 105 } 106 107 /** 108 * Applies this OptionsPolicyEnforcer's policy to the given OptionsParser. 109 * 110 * @param parser The OptionsParser to enforce policy on. 111 * @param command The current blaze command, for flag policies that apply to only specific 112 * commands. Such policies will be enforced only if they contain this command or a command 113 * they inherit from 114 * @throws OptionsParsingException if any flag policy is invalid. 115 */ 116 public void enforce(OptionsParser parser, @Nullable String command) 117 throws OptionsParsingException { 118 if (invocationPolicy == null || invocationPolicy.getFlagPoliciesCount() == 0) { 119 return; 120 } 121 122 // The effective policy returned is expanded, filtered for applicable commands, and cleaned of 123 // redundancies and conflicts. 124 List<FlagPolicyWithContext> effectivePolicies = 125 getEffectivePolicies(invocationPolicy, parser, command, loglevel); 126 127 for (FlagPolicyWithContext flagPolicy : effectivePolicies) { 128 String flagName = flagPolicy.policy.getFlagName(); 129 130 OptionValueDescription valueDescription; 131 try { 132 valueDescription = parser.getOptionValueDescription(flagName); 133 } catch (IllegalArgumentException e) { 134 // This flag doesn't exist. We are deliberately lenient if the flag policy has a flag 135 // we don't know about. This is for better future proofing so that as new flags are added, 136 // new policies can use the new flags without worrying about older versions of Bazel. 137 logger.log( 138 loglevel, 139 String.format("Flag '%s' specified by invocation policy does not exist", flagName)); 140 continue; 141 } 142 143 // getOptionDescription() will return null if the option does not exist, however 144 // getOptionValueDescription() above would have thrown an IllegalArgumentException if that 145 // were the case. 146 Verify.verifyNotNull(flagPolicy.description); 147 148 switch (flagPolicy.policy.getOperationCase()) { 149 case SET_VALUE: 150 applySetValueOperation(parser, flagPolicy, valueDescription, loglevel); 151 break; 152 153 case USE_DEFAULT: 154 applyUseDefaultOperation( 155 parser, "UseDefault", flagPolicy.description.getOptionDefinition(), loglevel); 156 break; 157 158 case ALLOW_VALUES: 159 AllowValues allowValues = flagPolicy.policy.getAllowValues(); 160 FilterValueOperation.AllowValueOperation allowValueOperation = 161 new FilterValueOperation.AllowValueOperation(loglevel); 162 allowValueOperation.apply( 163 parser, 164 flagPolicy.origin, 165 allowValues.getAllowedValuesList(), 166 allowValues.hasNewValue() ? allowValues.getNewValue() : null, 167 allowValues.hasUseDefault(), 168 valueDescription, 169 flagPolicy.description); 170 break; 171 172 case DISALLOW_VALUES: 173 DisallowValues disallowValues = flagPolicy.policy.getDisallowValues(); 174 FilterValueOperation.DisallowValueOperation disallowValueOperation = 175 new FilterValueOperation.DisallowValueOperation(loglevel); 176 disallowValueOperation.apply( 177 parser, 178 flagPolicy.origin, 179 disallowValues.getDisallowedValuesList(), 180 disallowValues.hasNewValue() ? disallowValues.getNewValue() : null, 181 disallowValues.hasUseDefault(), 182 valueDescription, 183 flagPolicy.description); 184 break; 185 186 case OPERATION_NOT_SET: 187 throw new PolicyOperationNotSetException(flagName); 188 189 default: 190 logger.warning( 191 String.format( 192 "Unknown operation '%s' from invocation policy for flag '%s'", 193 flagPolicy.policy.getOperationCase(), flagName)); 194 break; 195 } 196 } 197 } 198 199 private static class PolicyOperationNotSetException extends OptionsParsingException { 200 PolicyOperationNotSetException(String flagName) { 201 super(String.format("Flag policy for flag '%s' does not " + "have an operation", flagName)); 202 } 203 } 204 205 private static boolean policyApplies(FlagPolicy policy, ImmutableSet<String> applicableCommands) { 206 // Skip the flag policy if it doesn't apply to this command. If the commands list is empty, 207 // then the policy applies to all commands. 208 if (policy.getCommandsList().isEmpty() || applicableCommands.isEmpty()) { 209 return true; 210 } 211 212 return !Collections.disjoint(policy.getCommandsList(), applicableCommands); 213 } 214 215 /** Returns the expanded and filtered policy that would be enforced for the given command. */ 216 public static InvocationPolicy getEffectiveInvocationPolicy( 217 InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel) 218 throws OptionsParsingException { 219 ImmutableList<FlagPolicyWithContext> effectivePolicies = 220 getEffectivePolicies(invocationPolicy, parser, command, loglevel); 221 222 InvocationPolicy.Builder builder = InvocationPolicy.newBuilder(); 223 for (FlagPolicyWithContext policyWithContext : effectivePolicies) { 224 builder.addFlagPolicies(policyWithContext.policy); 225 } 226 return builder.build(); 227 } 228 229 /** 230 * Takes the provided policy and processes it to the form that can be used on the user options. 231 * 232 * <p>Expands any policies on expansion flags. 233 */ 234 private static ImmutableList<FlagPolicyWithContext> getEffectivePolicies( 235 InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel) 236 throws OptionsParsingException { 237 if (invocationPolicy == null) { 238 return ImmutableList.of(); 239 } 240 241 ImmutableSet<String> commandAndParentCommands = 242 command == null 243 ? ImmutableSet.of() 244 : CommandNameCache.CommandNameCacheInstance.INSTANCE.get(command); 245 246 // Expand all policies to transfer policies on expansion flags to policies on the child flags. 247 List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>(); 248 OptionPriority nextPriority = 249 OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY); 250 for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) { 251 // These policies are high-level, before expansion, and so are not the implicitDependents or 252 // expansions of any other flag, other than in an obtuse sense from --invocation_policy. 253 OptionPriority currentPriority = nextPriority; 254 OptionInstanceOrigin origin = 255 new OptionInstanceOrigin(currentPriority, INVOCATION_POLICY_SOURCE, null, null); 256 nextPriority = OptionPriority.nextOptionPriority(currentPriority); 257 if (!policyApplies(policy, commandAndParentCommands)) { 258 // Only keep and expand policies that are applicable to the current command. 259 continue; 260 } 261 262 OptionDescription optionDescription = parser.getOptionDescription(policy.getFlagName()); 263 if (optionDescription == null) { 264 // InvocationPolicy ignores policy on non-existing flags by design, for version 265 // compatibility. 266 logger.log( 267 loglevel, 268 String.format( 269 "Flag '%s' specified by invocation policy does not exist, and will be ignored", 270 policy.getFlagName())); 271 continue; 272 } 273 FlagPolicyWithContext policyWithContext = 274 new FlagPolicyWithContext(policy, optionDescription, origin); 275 List<FlagPolicyWithContext> policies = expandPolicy(policyWithContext, parser, loglevel); 276 expandedPolicies.addAll(policies); 277 } 278 279 // Only keep that last policy for each flag. 280 Map<String, FlagPolicyWithContext> effectivePolicy = new HashMap<>(); 281 for (FlagPolicyWithContext expandedPolicy : expandedPolicies) { 282 String flagName = expandedPolicy.policy.getFlagName(); 283 effectivePolicy.put(flagName, expandedPolicy); 284 } 285 286 return ImmutableList.copyOf(effectivePolicy.values()); 287 } 288 289 private static void throwAllowValuesOnExpansionFlagException(String flagName) 290 throws OptionsParsingException { 291 throw new OptionsParsingException( 292 String.format("Allow_Values on expansion flags like %s is not allowed.", flagName)); 293 } 294 295 private static void throwDisallowValuesOnExpansionFlagException(String flagName) 296 throws OptionsParsingException { 297 throw new OptionsParsingException( 298 String.format("Disallow_Values on expansion flags like %s is not allowed.", flagName)); 299 } 300 301 /** 302 * Expand a single policy. If the policy is not about an expansion flag, this will simply return a 303 * list with a single element, oneself. If the policy is for an expansion flag, the policy will 304 * get split into multiple policies applying to each flag the original flag expands to. 305 * 306 * <p>None of the flagPolicies returned should be on expansion flags. 307 */ 308 private static List<FlagPolicyWithContext> expandPolicy( 309 FlagPolicyWithContext originalPolicy, 310 OptionsParser parser, 311 Level loglevel) 312 throws OptionsParsingException { 313 List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>(); 314 315 boolean isExpansion = originalPolicy.description.isExpansion(); 316 ImmutableList<ParsedOptionDescription> subflags = 317 parser.getExpansionValueDescriptions( 318 originalPolicy.description.getOptionDefinition(), originalPolicy.origin); 319 320 // If we have nothing to expand to, no need to do any further work. 321 if (subflags.isEmpty()) { 322 return ImmutableList.of(originalPolicy); 323 } 324 325 if (logger.isLoggable(loglevel)) { 326 // Log the expansion. This is only really useful for understanding the invocation policy 327 // itself. 328 List<String> subflagNames = new ArrayList<>(subflags.size()); 329 for (ParsedOptionDescription subflag : subflags) { 330 subflagNames.add("--" + subflag.getOptionDefinition().getOptionName()); 331 } 332 333 logger.logp( 334 loglevel, 335 "InvocationPolicyEnforcer", 336 "expandPolicy", 337 String.format( 338 "Expanding %s on option %s to its %s: %s.", 339 originalPolicy.policy.getOperationCase(), 340 originalPolicy.policy.getFlagName(), 341 isExpansion ? "expansions" : "implied flags", 342 Joiner.on("; ").join(subflagNames))); 343 } 344 345 // Repeated flags are special, and could set multiple times in an expansion, with the user 346 // expecting both values to be valid. Collect these separately. 347 Multimap<OptionDescription, ParsedOptionDescription> repeatableSubflagsInSetValues = 348 ArrayListMultimap.create(); 349 350 // Create a flag policy for the child that looks like the parent's policy "transferred" to its 351 // child. Note that this only makes sense for SetValue, when setting an expansion flag, or 352 // UseDefault, when preventing it from being set. 353 for (ParsedOptionDescription currentSubflag : subflags) { 354 OptionDescription subflagOptionDescription = 355 parser.getOptionDescription(currentSubflag.getOptionDefinition().getOptionName()); 356 357 if (currentSubflag.getOptionDefinition().allowsMultiple() 358 && originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)) { 359 repeatableSubflagsInSetValues.put(subflagOptionDescription, currentSubflag); 360 } else { 361 FlagPolicyWithContext subflagAsPolicy = 362 getSingleValueSubflagAsPolicy( 363 subflagOptionDescription, currentSubflag, originalPolicy, isExpansion); 364 // In case any of the expanded flags are themselves expansions, recurse. 365 expandedPolicies.addAll(expandPolicy(subflagAsPolicy, parser, loglevel)); 366 } 367 } 368 369 // If there are any repeatable flag SetValues, deal with them together now. 370 // Note that expansion flags have no value, and so cannot have multiple values either. 371 // Skipping the recursion above is fine. 372 for (OptionDescription repeatableFlag : repeatableSubflagsInSetValues.keySet()) { 373 int numValues = repeatableSubflagsInSetValues.get(repeatableFlag).size(); 374 ArrayList<String> newValues = new ArrayList<>(numValues); 375 ArrayList<OptionInstanceOrigin> origins = new ArrayList<>(numValues); 376 for (ParsedOptionDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) { 377 newValues.add(setValue.getUnconvertedValue()); 378 origins.add(setValue.getOrigin()); 379 } 380 // These options come from expanding a single policy, so they have effectively the same 381 // priority. They could have come from different expansions or implicit requirements in the 382 // recursive resolving of the option list, so just pick the first one. Do collapse the source 383 // strings though, in case there are different sources. 384 OptionInstanceOrigin arbitraryFirstOptionOrigin = origins.get(0); 385 OptionInstanceOrigin originOfSubflags = 386 new OptionInstanceOrigin( 387 arbitraryFirstOptionOrigin.getPriority(), 388 origins 389 .stream() 390 .map(OptionInstanceOrigin::getSource) 391 .distinct() 392 .collect(Collectors.joining(", ")), 393 arbitraryFirstOptionOrigin.getImplicitDependent(), 394 arbitraryFirstOptionOrigin.getExpandedFrom()); 395 expandedPolicies.add( 396 getSetValueSubflagAsPolicy(repeatableFlag, newValues, originOfSubflags, originalPolicy)); 397 } 398 399 // Don't add the original policy if it was an expansion flag, which have no value, but do add 400 // it if there was either no expansion or if it was a valued flag with implicit requirements. 401 if (!isExpansion) { 402 expandedPolicies.add(originalPolicy); 403 } 404 405 return expandedPolicies; 406 } 407 408 /** 409 * Expand a SetValue flag policy on a repeatable flag. SetValue operations are the only flag 410 * policies that set the flag, and so interact with repeatable flags, flags that can be set 411 * multiple times, in subtle ways. 412 * 413 * @param subflagDesc, the description of the flag the SetValue'd expansion flag expands to. 414 * @param subflagValue, the values that the SetValue'd expansion flag expands to for this flag. 415 * @param originalPolicy, the original policy on the expansion flag. 416 * @return the flag policy for the subflag given, this will be part of the expanded form of the 417 * SetValue policy on the original flag. 418 */ 419 private static FlagPolicyWithContext getSetValueSubflagAsPolicy( 420 OptionDescription subflagDesc, 421 List<String> subflagValue, 422 OptionInstanceOrigin subflagOrigin, 423 FlagPolicyWithContext originalPolicy) { 424 // Some sanity checks. 425 OptionDefinition subflag = subflagDesc.getOptionDefinition(); 426 Verify.verify(originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)); 427 if (!subflag.allowsMultiple()) { 428 Verify.verify(subflagValue.size() <= 1); 429 } 430 431 // Flag value from the expansion, overridability from the original policy, unless the flag is 432 // repeatable, in which case we care about appendability, not overridability. 433 SetValue.Builder setValueExpansion = SetValue.newBuilder(); 434 for (String value : subflagValue) { 435 setValueExpansion.addFlagValue(value); 436 } 437 if (subflag.allowsMultiple()) { 438 setValueExpansion.setAppend(originalPolicy.policy.getSetValue().getOverridable()); 439 } else { 440 setValueExpansion.setOverridable(originalPolicy.policy.getSetValue().getOverridable()); 441 } 442 443 // Commands from the original policy, flag name of the expansion 444 return new FlagPolicyWithContext( 445 FlagPolicy.newBuilder() 446 .addAllCommands(originalPolicy.policy.getCommandsList()) 447 .setFlagName(subflag.getOptionName()) 448 .setSetValue(setValueExpansion) 449 .build(), 450 subflagDesc, 451 subflagOrigin); 452 } 453 454 /** 455 * For an expansion flag in an invocation policy, each flag it expands to must be given a 456 * corresponding policy. 457 */ 458 private static FlagPolicyWithContext getSingleValueSubflagAsPolicy( 459 OptionDescription subflagContext, 460 ParsedOptionDescription currentSubflag, 461 FlagPolicyWithContext originalPolicy, 462 boolean isExpansion) 463 throws OptionsParsingException { 464 FlagPolicyWithContext subflagAsPolicy = null; 465 switch (originalPolicy.policy.getOperationCase()) { 466 case SET_VALUE: 467 if (currentSubflag.getOptionDefinition().allowsMultiple()) { 468 throw new AssertionError( 469 "SetValue subflags with allowMultiple should have been dealt with separately and " 470 + "accumulated into a single FlagPolicy."); 471 } 472 // Accept null originalValueStrings, they are expected when the subflag is also an expansion 473 // flag. 474 List<String> subflagValue; 475 if (currentSubflag.getUnconvertedValue() == null) { 476 subflagValue = ImmutableList.of(); 477 } else { 478 subflagValue = ImmutableList.of(currentSubflag.getUnconvertedValue()); 479 } 480 subflagAsPolicy = 481 getSetValueSubflagAsPolicy( 482 subflagContext, subflagValue, currentSubflag.getOrigin(), originalPolicy); 483 break; 484 485 case USE_DEFAULT: 486 // Commands from the original policy, flag name of the expansion 487 subflagAsPolicy = 488 new FlagPolicyWithContext( 489 FlagPolicy.newBuilder() 490 .addAllCommands(originalPolicy.policy.getCommandsList()) 491 .setFlagName(currentSubflag.getOptionDefinition().getOptionName()) 492 .setUseDefault(UseDefault.getDefaultInstance()) 493 .build(), 494 subflagContext, 495 currentSubflag.getOrigin()); 496 break; 497 498 case ALLOW_VALUES: 499 if (isExpansion) { 500 throwAllowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName()); 501 } 502 // If this flag is an implicitRequirement, and some values for the parent flag are 503 // allowed, nothing needs to happen on the implicitRequirement that is set for all 504 // values of the flag. 505 break; 506 507 case DISALLOW_VALUES: 508 if (isExpansion) { 509 throwDisallowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName()); 510 } 511 // If this flag is an implicitRequirement, and some values for the parent flag are 512 // disallowed, that implies that all others are allowed, so nothing needs to happen 513 // on the implicitRequirement that is set for all values of the parent flag. 514 break; 515 516 case OPERATION_NOT_SET: 517 throw new PolicyOperationNotSetException(originalPolicy.policy.getFlagName()); 518 519 default: 520 return null; 521 } 522 return subflagAsPolicy; 523 } 524 525 private static void logInApplySetValueOperation( 526 Level loglevel, String formattingString, Object... objects) { 527 // Finding the caller here is relatively expensive and shows up in profiling, so provide it 528 // manually. 529 logger.logp( 530 loglevel, 531 "InvocationPolicyEnforcer", 532 "applySetValueOperation", 533 String.format(formattingString, objects)); 534 } 535 536 private static void applySetValueOperation( 537 OptionsParser parser, 538 FlagPolicyWithContext flagPolicy, 539 OptionValueDescription valueDescription, 540 Level loglevel) 541 throws OptionsParsingException { 542 SetValue setValue = flagPolicy.policy.getSetValue(); 543 OptionDefinition optionDefinition = flagPolicy.description.getOptionDefinition(); 544 545 // SetValue.flag_value must have at least 1 value. 546 if (setValue.getFlagValueCount() == 0) { 547 throw new OptionsParsingException( 548 String.format( 549 "SetValue operation from invocation policy for %s does not have a value", 550 optionDefinition)); 551 } 552 553 // Flag must allow multiple values if multiple values are specified by the policy. 554 if (setValue.getFlagValueCount() > 1 555 && !flagPolicy.description.getOptionDefinition().allowsMultiple()) { 556 throw new OptionsParsingException( 557 String.format( 558 "SetValue operation from invocation policy sets multiple values for %s which " 559 + "does not allow multiple values", 560 optionDefinition)); 561 } 562 563 if (setValue.getOverridable() && valueDescription != null) { 564 // The user set the value for the flag but the flag policy is overridable, so keep the user's 565 // value. 566 logInApplySetValueOperation( 567 loglevel, 568 "Keeping value '%s' from source '%s' for %s because the invocation policy specifying " 569 + "the value(s) '%s' is overridable", 570 valueDescription.getValue(), 571 valueDescription.getSourceString(), 572 optionDefinition, 573 setValue.getFlagValueList()); 574 } else { 575 576 if (!setValue.getAppend()) { 577 // Clear the value in case the flag is a repeated flag so that values don't accumulate. 578 parser.clearValue(flagPolicy.description.getOptionDefinition()); 579 } 580 581 // Set all the flag values from the policy. 582 for (String flagValue : setValue.getFlagValueList()) { 583 if (valueDescription == null) { 584 logInApplySetValueOperation( 585 loglevel, 586 "Setting value for %s from invocation policy to '%s', overriding the default value " 587 + "'%s'", 588 optionDefinition, 589 flagValue, 590 optionDefinition.getDefaultValue()); 591 } else { 592 logInApplySetValueOperation( 593 loglevel, 594 "Setting value for %s from invocation policy to '%s', overriding value '%s' from " 595 + "'%s'", 596 optionDefinition, 597 flagValue, 598 valueDescription.getValue(), 599 valueDescription.getSourceString()); 600 } 601 602 parser.addOptionValueAtSpecificPriority(flagPolicy.origin, optionDefinition, flagValue); 603 } 604 } 605 } 606 607 private static void applyUseDefaultOperation( 608 OptionsParser parser, String policyType, OptionDefinition option, Level loglevel) 609 throws OptionsParsingException { 610 OptionValueDescription clearedValueDescription = parser.clearValue(option); 611 if (clearedValueDescription != null) { 612 // Log the removed value. 613 String clearedFlagName = clearedValueDescription.getOptionDefinition().getOptionName(); 614 Object clearedFlagDefaultValue = 615 clearedValueDescription.getOptionDefinition().getDefaultValue(); 616 logger.log( 617 loglevel, 618 String.format( 619 "Using default value '%s' for flag '%s' as specified by %s invocation policy, " 620 + "overriding original value '%s' from '%s'", 621 clearedFlagDefaultValue, 622 clearedFlagName, 623 policyType, 624 clearedValueDescription.getValue(), 625 clearedValueDescription.getSourceString())); 626 } 627 } 628 629 /** Checks the user's flag values against a filtering function. */ 630 private abstract static class FilterValueOperation { 631 632 private static final class AllowValueOperation extends FilterValueOperation { 633 AllowValueOperation(Level loglevel) { 634 super("Allow", loglevel); 635 } 636 637 @Override 638 boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) { 639 return convertedPolicyValues.contains(value); 640 } 641 } 642 643 private static final class DisallowValueOperation extends FilterValueOperation { 644 DisallowValueOperation(Level loglevel) { 645 super("Disalllow", loglevel); 646 } 647 648 @Override 649 boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) { 650 // In a disallow operation, the values that the flag policy specifies are not allowed, 651 // so the value is allowed if the set of policy values does not contain the current 652 // flag value. 653 return !convertedPolicyValues.contains(value); 654 } 655 } 656 657 private final String policyType; 658 private final Level loglevel; 659 660 FilterValueOperation(String policyType, Level loglevel) { 661 this.policyType = policyType; 662 this.loglevel = loglevel; 663 } 664 665 /** 666 * Determines if the given value is allowed. 667 * 668 * @param convertedPolicyValues The values given from the FlagPolicy, converted to real objects. 669 * @param value The user value of the flag. 670 * @return True if the value should be allowed, false if it should not. 671 */ 672 abstract boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value); 673 674 void apply( 675 OptionsParser parser, 676 OptionInstanceOrigin origin, 677 List<String> policyValues, 678 String newValue, 679 boolean useDefault, 680 OptionValueDescription valueDescription, 681 OptionDescription optionDescription) 682 throws OptionsParsingException { 683 OptionDefinition optionDefinition = optionDescription.getOptionDefinition(); 684 // Convert all the allowed values from strings to real objects using the options' 685 // converters so that they can be checked for equality using real .equals() instead 686 // of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-" 687 // (if the option has an abbreviation) are all equal for boolean flags. Plus converters 688 // can be arbitrarily complex. 689 Set<Object> convertedPolicyValues = new HashSet<>(); 690 for (String value : policyValues) { 691 Object convertedValue = optionDefinition.getConverter().convert(value); 692 // Some converters return lists, and if the flag is a repeatable flag, the items in the 693 // list from the converter should be added, and not the list itself. Otherwise the items 694 // from invocation policy will be compared to lists, which will never work. 695 // See OptionsParserImpl.ParsedOptionEntry.addValue. 696 if (optionDefinition.allowsMultiple() && convertedValue instanceof List<?>) { 697 convertedPolicyValues.addAll((List<?>) convertedValue); 698 } else { 699 convertedPolicyValues.add(optionDefinition.getConverter().convert(value)); 700 } 701 } 702 703 // Check that if the default value of the flag is disallowed by the policy, that the policy 704 // does not also set use_default. Otherwise the default value would will still be set if the 705 // user uses a disallowed value. This doesn't apply to repeatable flags since the default 706 // value for repeatable flags is always the empty list. 707 if (!optionDescription.getOptionDefinition().allowsMultiple()) { 708 709 boolean defaultValueAllowed = 710 isFlagValueAllowed( 711 convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue()); 712 if (!defaultValueAllowed && useDefault) { 713 throw new OptionsParsingException( 714 String.format( 715 "%sValues policy disallows the default value '%s' for %s but also specifies to " 716 + "use the default value", 717 policyType, optionDefinition.getDefaultValue(), optionDefinition)); 718 } 719 } 720 721 if (valueDescription == null) { 722 // Nothing has set the value yet, so check that the default value from the flag's 723 // definition is allowed. The else case below (i.e. valueDescription is not null) checks for 724 // the flag allowing multiple values, however, flags that allow multiple values cannot have 725 // default values, and their value is always the empty list if they haven't been specified, 726 // which is why new_default_value is not a repeated field. 727 checkDefaultValue( 728 parser, origin, optionDescription, policyValues, newValue, convertedPolicyValues); 729 } else { 730 checkUserValue( 731 parser, 732 origin, 733 optionDescription, 734 valueDescription, 735 policyValues, 736 newValue, 737 useDefault, 738 convertedPolicyValues); 739 } 740 } 741 742 void checkDefaultValue( 743 OptionsParser parser, 744 OptionInstanceOrigin origin, 745 OptionDescription optionDescription, 746 List<String> policyValues, 747 String newValue, 748 Set<Object> convertedPolicyValues) 749 throws OptionsParsingException { 750 751 OptionDefinition optionDefinition = optionDescription.getOptionDefinition(); 752 if (!isFlagValueAllowed( 753 convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue())) { 754 if (newValue != null) { 755 // Use the default value from the policy, since the original default is not allowed 756 logger.log( 757 loglevel, 758 String.format( 759 "Overriding default value '%s' for %s with value '%s' specified by invocation " 760 + "policy. %sed values are: %s", 761 optionDefinition.getDefaultValue(), 762 optionDefinition, 763 newValue, 764 policyType, 765 policyValues)); 766 parser.clearValue(optionDefinition); 767 parser.addOptionValueAtSpecificPriority(origin, optionDefinition, newValue); 768 } else { 769 // The operation disallows the default value, but doesn't supply a new value. 770 throw new OptionsParsingException( 771 String.format( 772 "Default flag value '%s' for %s is not allowed by invocation policy, but " 773 + "the policy does not provide a new value. %sed values are: %s", 774 optionDescription.getOptionDefinition().getDefaultValue(), 775 optionDefinition, 776 policyType, 777 policyValues)); 778 } 779 } 780 } 781 782 void checkUserValue( 783 OptionsParser parser, 784 OptionInstanceOrigin origin, 785 OptionDescription optionDescription, 786 OptionValueDescription valueDescription, 787 List<String> policyValues, 788 String newValue, 789 boolean useDefault, 790 Set<Object> convertedPolicyValues) 791 throws OptionsParsingException { 792 OptionDefinition option = optionDescription.getOptionDefinition(); 793 if (optionDescription.getOptionDefinition().allowsMultiple()) { 794 // allowMultiple requires that the type of the option be List<T>, so cast from Object 795 // to List<?>. 796 List<?> optionValues = (List<?>) valueDescription.getValue(); 797 for (Object value : optionValues) { 798 if (!isFlagValueAllowed(convertedPolicyValues, value)) { 799 if (useDefault) { 800 applyUseDefaultOperation(parser, policyType + "Values", option, loglevel); 801 } else { 802 throw new OptionsParsingException( 803 String.format( 804 "Flag value '%s' for %s is not allowed by invocation policy. %sed values " 805 + "are: %s", 806 value, option, policyType, policyValues)); 807 } 808 } 809 } 810 811 } else { 812 813 if (!isFlagValueAllowed(convertedPolicyValues, valueDescription.getValue())) { 814 if (newValue != null) { 815 logger.log( 816 loglevel, 817 String.format( 818 "Overriding disallowed value '%s' for %s with value '%s' " 819 + "specified by invocation policy. %sed values are: %s", 820 valueDescription.getValue(), option, newValue, policyType, policyValues)); 821 parser.clearValue(option); 822 parser.addOptionValueAtSpecificPriority(origin, option, newValue); 823 } else if (useDefault) { 824 applyUseDefaultOperation(parser, policyType + "Values", option, loglevel); 825 } else { 826 throw new OptionsParsingException( 827 String.format( 828 "Flag value '%s' for %s is not allowed by invocation policy and the " 829 + "policy does not specify a new value. %sed values are: %s", 830 valueDescription.getValue(), option, policyType, policyValues)); 831 } 832 } 833 } 834 } 835 } 836 } 837