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 // Explicitly disallow --config in invocation policy. 252 if (policy.getFlagName().equals("config")) { 253 throw new OptionsParsingException( 254 "Invocation policy is applied after --config expansion, changing config values now " 255 + "would have no effect and is disallowed to prevent confusion. Please remove the " 256 + "following policy : " 257 + policy); 258 } 259 260 // These policies are high-level, before expansion, and so are not the implicitDependents or 261 // expansions of any other flag, other than in an obtuse sense from --invocation_policy. 262 OptionPriority currentPriority = nextPriority; 263 OptionInstanceOrigin origin = 264 new OptionInstanceOrigin(currentPriority, INVOCATION_POLICY_SOURCE, null, null); 265 nextPriority = OptionPriority.nextOptionPriority(currentPriority); 266 if (!policyApplies(policy, commandAndParentCommands)) { 267 // Only keep and expand policies that are applicable to the current command. 268 continue; 269 } 270 271 OptionDescription optionDescription = parser.getOptionDescription(policy.getFlagName()); 272 if (optionDescription == null) { 273 // InvocationPolicy ignores policy on non-existing flags by design, for version 274 // compatibility. 275 logger.log( 276 loglevel, 277 String.format( 278 "Flag '%s' specified by invocation policy does not exist, and will be ignored", 279 policy.getFlagName())); 280 continue; 281 } 282 FlagPolicyWithContext policyWithContext = 283 new FlagPolicyWithContext(policy, optionDescription, origin); 284 List<FlagPolicyWithContext> policies = expandPolicy(policyWithContext, parser, loglevel); 285 expandedPolicies.addAll(policies); 286 } 287 288 // Only keep that last policy for each flag. 289 Map<String, FlagPolicyWithContext> effectivePolicy = new HashMap<>(); 290 for (FlagPolicyWithContext expandedPolicy : expandedPolicies) { 291 String flagName = expandedPolicy.policy.getFlagName(); 292 effectivePolicy.put(flagName, expandedPolicy); 293 } 294 295 return ImmutableList.copyOf(effectivePolicy.values()); 296 } 297 298 private static void throwAllowValuesOnExpansionFlagException(String flagName) 299 throws OptionsParsingException { 300 throw new OptionsParsingException( 301 String.format("Allow_Values on expansion flags like %s is not allowed.", flagName)); 302 } 303 304 private static void throwDisallowValuesOnExpansionFlagException(String flagName) 305 throws OptionsParsingException { 306 throw new OptionsParsingException( 307 String.format("Disallow_Values on expansion flags like %s is not allowed.", flagName)); 308 } 309 310 /** 311 * Expand a single policy. If the policy is not about an expansion flag, this will simply return a 312 * list with a single element, oneself. If the policy is for an expansion flag, the policy will 313 * get split into multiple policies applying to each flag the original flag expands to. 314 * 315 * <p>None of the flagPolicies returned should be on expansion flags. 316 */ 317 private static List<FlagPolicyWithContext> expandPolicy( 318 FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel) 319 throws OptionsParsingException { 320 List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>(); 321 322 boolean isExpansion = originalPolicy.description.isExpansion(); 323 ImmutableList<ParsedOptionDescription> subflags = 324 parser.getExpansionValueDescriptions( 325 originalPolicy.description.getOptionDefinition(), originalPolicy.origin); 326 327 // If we have nothing to expand to, no need to do any further work. 328 if (subflags.isEmpty()) { 329 return ImmutableList.of(originalPolicy); 330 } 331 332 if (logger.isLoggable(loglevel)) { 333 // Log the expansion. This is only really useful for understanding the invocation policy 334 // itself. 335 List<String> subflagNames = new ArrayList<>(subflags.size()); 336 for (ParsedOptionDescription subflag : subflags) { 337 subflagNames.add("--" + subflag.getOptionDefinition().getOptionName()); 338 } 339 340 logger.logp( 341 loglevel, 342 "InvocationPolicyEnforcer", 343 "expandPolicy", 344 String.format( 345 "Expanding %s on option %s to its %s: %s.", 346 originalPolicy.policy.getOperationCase(), 347 originalPolicy.policy.getFlagName(), 348 isExpansion ? "expansions" : "implied flags", 349 Joiner.on("; ").join(subflagNames))); 350 } 351 352 // Repeated flags are special, and could set multiple times in an expansion, with the user 353 // expecting both values to be valid. Collect these separately. 354 Multimap<OptionDescription, ParsedOptionDescription> repeatableSubflagsInSetValues = 355 ArrayListMultimap.create(); 356 357 // Create a flag policy for the child that looks like the parent's policy "transferred" to its 358 // child. Note that this only makes sense for SetValue, when setting an expansion flag, or 359 // UseDefault, when preventing it from being set. 360 for (ParsedOptionDescription currentSubflag : subflags) { 361 OptionDescription subflagOptionDescription = 362 parser.getOptionDescription(currentSubflag.getOptionDefinition().getOptionName()); 363 364 if (currentSubflag.getOptionDefinition().allowsMultiple() 365 && originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)) { 366 repeatableSubflagsInSetValues.put(subflagOptionDescription, currentSubflag); 367 } else { 368 FlagPolicyWithContext subflagAsPolicy = 369 getSingleValueSubflagAsPolicy( 370 subflagOptionDescription, currentSubflag, originalPolicy, isExpansion); 371 // In case any of the expanded flags are themselves expansions, recurse. 372 expandedPolicies.addAll(expandPolicy(subflagAsPolicy, parser, loglevel)); 373 } 374 } 375 376 // If there are any repeatable flag SetValues, deal with them together now. 377 // Note that expansion flags have no value, and so cannot have multiple values either. 378 // Skipping the recursion above is fine. 379 for (OptionDescription repeatableFlag : repeatableSubflagsInSetValues.keySet()) { 380 int numValues = repeatableSubflagsInSetValues.get(repeatableFlag).size(); 381 ArrayList<String> newValues = new ArrayList<>(numValues); 382 ArrayList<OptionInstanceOrigin> origins = new ArrayList<>(numValues); 383 for (ParsedOptionDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) { 384 newValues.add(setValue.getUnconvertedValue()); 385 origins.add(setValue.getOrigin()); 386 } 387 // These options come from expanding a single policy, so they have effectively the same 388 // priority. They could have come from different expansions or implicit requirements in the 389 // recursive resolving of the option list, so just pick the first one. Do collapse the source 390 // strings though, in case there are different sources. 391 OptionInstanceOrigin arbitraryFirstOptionOrigin = origins.get(0); 392 OptionInstanceOrigin originOfSubflags = 393 new OptionInstanceOrigin( 394 arbitraryFirstOptionOrigin.getPriority(), 395 origins 396 .stream() 397 .map(OptionInstanceOrigin::getSource) 398 .distinct() 399 .collect(Collectors.joining(", ")), 400 arbitraryFirstOptionOrigin.getImplicitDependent(), 401 arbitraryFirstOptionOrigin.getExpandedFrom()); 402 expandedPolicies.add( 403 getSetValueSubflagAsPolicy(repeatableFlag, newValues, originOfSubflags, originalPolicy)); 404 } 405 406 // Don't add the original policy if it was an expansion flag, which have no value, but do add 407 // it if there was either no expansion or if it was a valued flag with implicit requirements. 408 if (!isExpansion) { 409 expandedPolicies.add(originalPolicy); 410 } 411 412 return expandedPolicies; 413 } 414 415 /** 416 * Expand a SetValue flag policy on a repeatable flag. SetValue operations are the only flag 417 * policies that set the flag, and so interact with repeatable flags, flags that can be set 418 * multiple times, in subtle ways. 419 * 420 * @param subflagDesc, the description of the flag the SetValue'd expansion flag expands to. 421 * @param subflagValue, the values that the SetValue'd expansion flag expands to for this flag. 422 * @param originalPolicy, the original policy on the expansion flag. 423 * @return the flag policy for the subflag given, this will be part of the expanded form of the 424 * SetValue policy on the original flag. 425 */ 426 private static FlagPolicyWithContext getSetValueSubflagAsPolicy( 427 OptionDescription subflagDesc, 428 List<String> subflagValue, 429 OptionInstanceOrigin subflagOrigin, 430 FlagPolicyWithContext originalPolicy) { 431 // Some sanity checks. 432 OptionDefinition subflag = subflagDesc.getOptionDefinition(); 433 Verify.verify(originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)); 434 if (!subflag.allowsMultiple()) { 435 Verify.verify(subflagValue.size() <= 1); 436 } 437 438 // Flag value from the expansion, overridability from the original policy, unless the flag is 439 // repeatable, in which case we care about appendability, not overridability. 440 SetValue.Builder setValueExpansion = SetValue.newBuilder(); 441 for (String value : subflagValue) { 442 setValueExpansion.addFlagValue(value); 443 } 444 if (subflag.allowsMultiple()) { 445 setValueExpansion.setAppend(originalPolicy.policy.getSetValue().getOverridable()); 446 } else { 447 setValueExpansion.setOverridable(originalPolicy.policy.getSetValue().getOverridable()); 448 } 449 450 // Commands from the original policy, flag name of the expansion 451 return new FlagPolicyWithContext( 452 FlagPolicy.newBuilder() 453 .addAllCommands(originalPolicy.policy.getCommandsList()) 454 .setFlagName(subflag.getOptionName()) 455 .setSetValue(setValueExpansion) 456 .build(), 457 subflagDesc, 458 subflagOrigin); 459 } 460 461 /** 462 * For an expansion flag in an invocation policy, each flag it expands to must be given a 463 * corresponding policy. 464 */ 465 private static FlagPolicyWithContext getSingleValueSubflagAsPolicy( 466 OptionDescription subflagContext, 467 ParsedOptionDescription currentSubflag, 468 FlagPolicyWithContext originalPolicy, 469 boolean isExpansion) 470 throws OptionsParsingException { 471 FlagPolicyWithContext subflagAsPolicy = null; 472 switch (originalPolicy.policy.getOperationCase()) { 473 case SET_VALUE: 474 if (currentSubflag.getOptionDefinition().allowsMultiple()) { 475 throw new AssertionError( 476 "SetValue subflags with allowMultiple should have been dealt with separately and " 477 + "accumulated into a single FlagPolicy."); 478 } 479 // Accept null originalValueStrings, they are expected when the subflag is also an expansion 480 // flag. 481 List<String> subflagValue; 482 if (currentSubflag.getUnconvertedValue() == null) { 483 subflagValue = ImmutableList.of(); 484 } else { 485 subflagValue = ImmutableList.of(currentSubflag.getUnconvertedValue()); 486 } 487 subflagAsPolicy = 488 getSetValueSubflagAsPolicy( 489 subflagContext, subflagValue, currentSubflag.getOrigin(), originalPolicy); 490 break; 491 492 case USE_DEFAULT: 493 // Commands from the original policy, flag name of the expansion 494 subflagAsPolicy = 495 new FlagPolicyWithContext( 496 FlagPolicy.newBuilder() 497 .addAllCommands(originalPolicy.policy.getCommandsList()) 498 .setFlagName(currentSubflag.getOptionDefinition().getOptionName()) 499 .setUseDefault(UseDefault.getDefaultInstance()) 500 .build(), 501 subflagContext, 502 currentSubflag.getOrigin()); 503 break; 504 505 case ALLOW_VALUES: 506 if (isExpansion) { 507 throwAllowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName()); 508 } 509 // If this flag is an implicitRequirement, and some values for the parent flag are 510 // allowed, nothing needs to happen on the implicitRequirement that is set for all 511 // values of the flag. 512 break; 513 514 case DISALLOW_VALUES: 515 if (isExpansion) { 516 throwDisallowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName()); 517 } 518 // If this flag is an implicitRequirement, and some values for the parent flag are 519 // disallowed, that implies that all others are allowed, so nothing needs to happen 520 // on the implicitRequirement that is set for all values of the parent flag. 521 break; 522 523 case OPERATION_NOT_SET: 524 throw new PolicyOperationNotSetException(originalPolicy.policy.getFlagName()); 525 526 default: 527 return null; 528 } 529 return subflagAsPolicy; 530 } 531 532 private static void logInApplySetValueOperation( 533 Level loglevel, String formattingString, Object... objects) { 534 // Finding the caller here is relatively expensive and shows up in profiling, so provide it 535 // manually. 536 logger.logp( 537 loglevel, 538 "InvocationPolicyEnforcer", 539 "applySetValueOperation", 540 String.format(formattingString, objects)); 541 } 542 543 private static void applySetValueOperation( 544 OptionsParser parser, 545 FlagPolicyWithContext flagPolicy, 546 OptionValueDescription valueDescription, 547 Level loglevel) 548 throws OptionsParsingException { 549 SetValue setValue = flagPolicy.policy.getSetValue(); 550 OptionDefinition optionDefinition = flagPolicy.description.getOptionDefinition(); 551 552 // SetValue.flag_value must have at least 1 value. 553 if (setValue.getFlagValueCount() == 0) { 554 throw new OptionsParsingException( 555 String.format( 556 "SetValue operation from invocation policy for %s does not have a value", 557 optionDefinition)); 558 } 559 560 // Flag must allow multiple values if multiple values are specified by the policy. 561 if (setValue.getFlagValueCount() > 1 562 && !flagPolicy.description.getOptionDefinition().allowsMultiple()) { 563 throw new OptionsParsingException( 564 String.format( 565 "SetValue operation from invocation policy sets multiple values for %s which " 566 + "does not allow multiple values", 567 optionDefinition)); 568 } 569 570 if (setValue.getOverridable() && valueDescription != null) { 571 // The user set the value for the flag but the flag policy is overridable, so keep the user's 572 // value. 573 logInApplySetValueOperation( 574 loglevel, 575 "Keeping value '%s' from source '%s' for %s because the invocation policy specifying " 576 + "the value(s) '%s' is overridable", 577 valueDescription.getValue(), 578 valueDescription.getSourceString(), 579 optionDefinition, 580 setValue.getFlagValueList()); 581 } else { 582 583 if (!setValue.getAppend()) { 584 // Clear the value in case the flag is a repeated flag so that values don't accumulate. 585 parser.clearValue(flagPolicy.description.getOptionDefinition()); 586 } 587 588 // Set all the flag values from the policy. 589 for (String flagValue : setValue.getFlagValueList()) { 590 if (valueDescription == null) { 591 logInApplySetValueOperation( 592 loglevel, 593 "Setting value for %s from invocation policy to '%s', overriding the default value " 594 + "'%s'", 595 optionDefinition, 596 flagValue, 597 optionDefinition.getDefaultValue()); 598 } else { 599 logInApplySetValueOperation( 600 loglevel, 601 "Setting value for %s from invocation policy to '%s', overriding value '%s' from " 602 + "'%s'", 603 optionDefinition, 604 flagValue, 605 valueDescription.getValue(), 606 valueDescription.getSourceString()); 607 } 608 609 parser.addOptionValueAtSpecificPriority(flagPolicy.origin, optionDefinition, flagValue); 610 } 611 } 612 } 613 614 private static void applyUseDefaultOperation( 615 OptionsParser parser, String policyType, OptionDefinition option, Level loglevel) 616 throws OptionsParsingException { 617 OptionValueDescription clearedValueDescription = parser.clearValue(option); 618 if (clearedValueDescription != null) { 619 // Log the removed value. 620 String clearedFlagName = clearedValueDescription.getOptionDefinition().getOptionName(); 621 Object clearedFlagDefaultValue = 622 clearedValueDescription.getOptionDefinition().getDefaultValue(); 623 logger.log( 624 loglevel, 625 String.format( 626 "Using default value '%s' for flag '%s' as specified by %s invocation policy, " 627 + "overriding original value '%s' from '%s'", 628 clearedFlagDefaultValue, 629 clearedFlagName, 630 policyType, 631 clearedValueDescription.getValue(), 632 clearedValueDescription.getSourceString())); 633 } 634 } 635 636 /** Checks the user's flag values against a filtering function. */ 637 private abstract static class FilterValueOperation { 638 639 private static final class AllowValueOperation extends FilterValueOperation { 640 AllowValueOperation(Level loglevel) { 641 super("Allow", loglevel); 642 } 643 644 @Override 645 boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) { 646 return convertedPolicyValues.contains(value); 647 } 648 } 649 650 private static final class DisallowValueOperation extends FilterValueOperation { 651 DisallowValueOperation(Level loglevel) { 652 super("Disalllow", loglevel); 653 } 654 655 @Override 656 boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) { 657 // In a disallow operation, the values that the flag policy specifies are not allowed, 658 // so the value is allowed if the set of policy values does not contain the current 659 // flag value. 660 return !convertedPolicyValues.contains(value); 661 } 662 } 663 664 private final String policyType; 665 private final Level loglevel; 666 667 FilterValueOperation(String policyType, Level loglevel) { 668 this.policyType = policyType; 669 this.loglevel = loglevel; 670 } 671 672 /** 673 * Determines if the given value is allowed. 674 * 675 * @param convertedPolicyValues The values given from the FlagPolicy, converted to real objects. 676 * @param value The user value of the flag. 677 * @return True if the value should be allowed, false if it should not. 678 */ 679 abstract boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value); 680 681 void apply( 682 OptionsParser parser, 683 OptionInstanceOrigin origin, 684 List<String> policyValues, 685 String newValue, 686 boolean useDefault, 687 OptionValueDescription valueDescription, 688 OptionDescription optionDescription) 689 throws OptionsParsingException { 690 OptionDefinition optionDefinition = optionDescription.getOptionDefinition(); 691 // Convert all the allowed values from strings to real objects using the options' 692 // converters so that they can be checked for equality using real .equals() instead 693 // of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-" 694 // (if the option has an abbreviation) are all equal for boolean flags. Plus converters 695 // can be arbitrarily complex. 696 Set<Object> convertedPolicyValues = new HashSet<>(); 697 for (String value : policyValues) { 698 Object convertedValue = optionDefinition.getConverter().convert(value); 699 // Some converters return lists, and if the flag is a repeatable flag, the items in the 700 // list from the converter should be added, and not the list itself. Otherwise the items 701 // from invocation policy will be compared to lists, which will never work. 702 // See OptionsParserImpl.ParsedOptionEntry.addValue. 703 if (optionDefinition.allowsMultiple() && convertedValue instanceof List<?>) { 704 convertedPolicyValues.addAll((List<?>) convertedValue); 705 } else { 706 convertedPolicyValues.add(optionDefinition.getConverter().convert(value)); 707 } 708 } 709 710 // Check that if the default value of the flag is disallowed by the policy, that the policy 711 // does not also set use_default. Otherwise the default value would still be set if the 712 // user uses a disallowed value. This doesn't apply to repeatable flags since the default 713 // value for repeatable flags is always the empty list. It also doesn't apply to flags that 714 // are null by default, since these flags' default value is not parsed by the converter, so 715 // there is no guarantee that there exists an accepted user-input value that would also set 716 // the value to NULL. In these cases, we assume that "unset" is a distinct value that is 717 // always allowed. 718 if (!optionDescription.getOptionDefinition().allowsMultiple() 719 && !optionDescription.getOptionDefinition().isSpecialNullDefault()) { 720 boolean defaultValueAllowed = 721 isFlagValueAllowed( 722 convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue()); 723 if (!defaultValueAllowed && useDefault) { 724 throw new OptionsParsingException( 725 String.format( 726 "%sValues policy disallows the default value '%s' for %s but also specifies to " 727 + "use the default value", 728 policyType, optionDefinition.getDefaultValue(), optionDefinition)); 729 } 730 } 731 732 if (valueDescription == null) { 733 // Nothing has set the value yet, so check that the default value from the flag's 734 // definition is allowed. The else case below (i.e. valueDescription is not null) checks for 735 // the flag allowing multiple values, however, flags that allow multiple values cannot have 736 // default values, and their value is always the empty list if they haven't been specified, 737 // which is why new_default_value is not a repeated field. 738 checkDefaultValue( 739 parser, origin, optionDescription, policyValues, newValue, convertedPolicyValues); 740 } else { 741 checkUserValue( 742 parser, 743 origin, 744 optionDescription, 745 valueDescription, 746 policyValues, 747 newValue, 748 useDefault, 749 convertedPolicyValues); 750 } 751 } 752 753 void checkDefaultValue( 754 OptionsParser parser, 755 OptionInstanceOrigin origin, 756 OptionDescription optionDescription, 757 List<String> policyValues, 758 String newValue, 759 Set<Object> convertedPolicyValues) 760 throws OptionsParsingException { 761 762 OptionDefinition optionDefinition = optionDescription.getOptionDefinition(); 763 if (optionDefinition.isSpecialNullDefault()) { 764 // Do nothing, the unset value by definition cannot be set. In option filtering operations, 765 // the value is being filtered, but the value that is `no value` passes any filter. 766 // Otherwise, there is no way to "usedefault" on one of these options that has no value by 767 // default. 768 } else if (!isFlagValueAllowed(convertedPolicyValues, optionDefinition.getDefaultValue())) { 769 if (newValue != null) { 770 // Use the default value from the policy, since the original default is not allowed 771 logger.log( 772 loglevel, 773 String.format( 774 "Overriding default value '%s' for %s with value '%s' specified by invocation " 775 + "policy. %sed values are: %s", 776 optionDefinition.getDefaultValue(), 777 optionDefinition, 778 newValue, 779 policyType, 780 policyValues)); 781 parser.clearValue(optionDefinition); 782 parser.addOptionValueAtSpecificPriority(origin, optionDefinition, newValue); 783 } else { 784 // The operation disallows the default value, but doesn't supply a new value. 785 throw new OptionsParsingException( 786 String.format( 787 "Default flag value '%s' for %s is not allowed by invocation policy, but " 788 + "the policy does not provide a new value. %sed values are: %s", 789 optionDescription.getOptionDefinition().getDefaultValue(), 790 optionDefinition, 791 policyType, 792 policyValues)); 793 } 794 } 795 } 796 797 void checkUserValue( 798 OptionsParser parser, 799 OptionInstanceOrigin origin, 800 OptionDescription optionDescription, 801 OptionValueDescription valueDescription, 802 List<String> policyValues, 803 String newValue, 804 boolean useDefault, 805 Set<Object> convertedPolicyValues) 806 throws OptionsParsingException { 807 OptionDefinition option = optionDescription.getOptionDefinition(); 808 if (optionDescription.getOptionDefinition().allowsMultiple()) { 809 // allowMultiple requires that the type of the option be List<T>, so cast from Object 810 // to List<?>. 811 List<?> optionValues = (List<?>) valueDescription.getValue(); 812 for (Object value : optionValues) { 813 if (!isFlagValueAllowed(convertedPolicyValues, value)) { 814 if (useDefault) { 815 applyUseDefaultOperation(parser, policyType + "Values", option, loglevel); 816 } else { 817 throw new OptionsParsingException( 818 String.format( 819 "Flag value '%s' for %s is not allowed by invocation policy. %sed values " 820 + "are: %s", 821 value, option, policyType, policyValues)); 822 } 823 } 824 } 825 826 } else { 827 828 if (!isFlagValueAllowed(convertedPolicyValues, valueDescription.getValue())) { 829 if (newValue != null) { 830 logger.log( 831 loglevel, 832 String.format( 833 "Overriding disallowed value '%s' for %s with value '%s' " 834 + "specified by invocation policy. %sed values are: %s", 835 valueDescription.getValue(), option, newValue, policyType, policyValues)); 836 parser.clearValue(option); 837 parser.addOptionValueAtSpecificPriority(origin, option, newValue); 838 } else if (useDefault) { 839 applyUseDefaultOperation(parser, policyType + "Values", option, loglevel); 840 } else { 841 throw new OptionsParsingException( 842 String.format( 843 "Flag value '%s' for %s is not allowed by invocation policy and the " 844 + "policy does not specify a new value. %sed values are: %s", 845 valueDescription.getValue(), option, policyType, policyValues)); 846 } 847 } 848 } 849 } 850 } 851 } 852