001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.oscal.lib.profile.resolver.alter; 007 008import java.util.Collections; 009import java.util.EnumMap; 010import java.util.EnumSet; 011import java.util.LinkedList; 012import java.util.List; 013import java.util.ListIterator; 014import java.util.Locale; 015import java.util.Map; 016import java.util.Set; 017import java.util.concurrent.ConcurrentHashMap; 018import java.util.function.Consumer; 019import java.util.function.Function; 020import java.util.function.Supplier; 021 022import dev.metaschema.core.datatype.markup.MarkupLine; 023import dev.metaschema.core.util.CollectionUtil; 024import dev.metaschema.core.util.CustomCollectors; 025import dev.metaschema.core.util.ObjectUtils; 026import dev.metaschema.oscal.lib.model.Catalog; 027import dev.metaschema.oscal.lib.model.CatalogGroup; 028import dev.metaschema.oscal.lib.model.Control; 029import dev.metaschema.oscal.lib.model.ControlPart; 030import dev.metaschema.oscal.lib.model.Link; 031import dev.metaschema.oscal.lib.model.Parameter; 032import dev.metaschema.oscal.lib.model.Property; 033import dev.metaschema.oscal.lib.model.control.catalog.ICatalogVisitor; 034import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionEvaluationException; 035import edu.umd.cs.findbugs.annotations.NonNull; 036import edu.umd.cs.findbugs.annotations.Nullable; 037 038@SuppressWarnings("PMD.CouplingBetweenObjects") 039public class AddVisitor implements ICatalogVisitor<Boolean, AddVisitor.Context> { 040 public enum TargetType { 041 CONTROL("control", Control.class), 042 PARAM("param", Parameter.class), 043 PART("part", ControlPart.class); 044 045 @NonNull 046 private static final Map<Class<?>, TargetType> CLASS_TO_TYPE; 047 @NonNull 048 private static final Map<String, TargetType> NAME_TO_TYPE; 049 @NonNull 050 private final String fieldName; 051 @NonNull 052 private final Class<?> clazz; 053 054 static { 055 { 056 Map<Class<?>, TargetType> map = new ConcurrentHashMap<>(); 057 for (TargetType type : values()) { 058 map.put(type.getClazz(), type); 059 } 060 CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map); 061 } 062 063 { 064 Map<String, TargetType> map = new ConcurrentHashMap<>(); 065 for (TargetType type : values()) { 066 map.put(type.fieldName(), type); 067 } 068 NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map); 069 } 070 } 071 072 /** 073 * Get the target type associated with the provided {@code clazz}. 074 * 075 * @param clazz 076 * the class to identify the target type for 077 * @return the associated target type or {@code null} if the class is not 078 * associated with a target type 079 */ 080 @Nullable 081 public static TargetType forClass(@NonNull Class<?> clazz) { 082 Class<?> target = clazz; 083 TargetType retval; 084 // recurse over parent classes to find a match 085 do { 086 retval = CLASS_TO_TYPE.get(target); 087 } while (retval == null && (target = target.getSuperclass()) != null); 088 return retval; 089 } 090 091 /** 092 * Get the target type associated with the provided field {@code name}. 093 * 094 * @param name 095 * the field name to identify the target type for 096 * @return the associated target type or {@code null} if the name is not 097 * associated with a target type 098 */ 099 @Nullable 100 public static TargetType forFieldName(@Nullable String name) { 101 return name == null ? null : NAME_TO_TYPE.get(name); 102 } 103 104 TargetType(@NonNull String fieldName, @NonNull Class<?> clazz) { 105 this.fieldName = fieldName; 106 this.clazz = clazz; 107 } 108 109 /** 110 * Get the field name associated with the target type. 111 * 112 * @return the name 113 */ 114 public String fieldName() { 115 return fieldName; 116 } 117 118 /** 119 * Get the bound class associated with the target type. 120 * 121 * @return the class 122 */ 123 public Class<?> getClazz() { 124 return clazz; 125 } 126 } 127 128 public enum Position { 129 BEFORE, 130 AFTER, 131 STARTING, 132 ENDING; 133 134 @NonNull 135 private static final Map<String, Position> NAME_TO_POSITION; 136 137 static { 138 Map<String, Position> map = new ConcurrentHashMap<>(); 139 for (Position position : values()) { 140 map.put(position.name().toLowerCase(Locale.ROOT), position); 141 } 142 NAME_TO_POSITION = CollectionUtil.unmodifiableMap(map); 143 } 144 145 /** 146 * Get the position associated with the provided {@code name}. 147 * 148 * @param name 149 * the name to identify the position for 150 * @return the associated position or {@code null} if the name is not associated 151 * with a position 152 */ 153 @Nullable 154 public static Position forName(@Nullable String name) { 155 return name == null ? null : NAME_TO_POSITION.get(name); 156 } 157 } 158 159 @NonNull 160 private static final AddVisitor INSTANCE = new AddVisitor(); 161 private static final Map<TargetType, Set<TargetType>> APPLICABLE_TARGETS; 162 163 static { 164 APPLICABLE_TARGETS = new EnumMap<>(TargetType.class); 165 APPLICABLE_TARGETS.put(TargetType.CONTROL, Set.of(TargetType.CONTROL, TargetType.PARAM, TargetType.PART)); 166 APPLICABLE_TARGETS.put(TargetType.PARAM, Set.of(TargetType.PARAM)); 167 APPLICABLE_TARGETS.put(TargetType.PART, Set.of(TargetType.PART)); 168 } 169 170 private static Set<TargetType> getApplicableTypes(@NonNull TargetType type) { 171 return APPLICABLE_TARGETS.getOrDefault(type, CollectionUtil.emptySet()); 172 } 173 174 /** 175 * Apply the add directive. 176 * 177 * @param control 178 * the control target 179 * @param position 180 * the position to apply the content or {@code null} 181 * @param byId 182 * the identifier of the target or {@code null} 183 * @param title 184 * a title to set 185 * @param params 186 * parameters to add 187 * @param props 188 * properties to add 189 * @param links 190 * links to add 191 * @param parts 192 * parts to add 193 * @return {@code true} if the modification was made or {@code false} otherwise 194 * @throws ProfileResolutionEvaluationException 195 * if a processing error occurred during profile resolution 196 */ 197 public static boolean add( 198 @NonNull Control control, 199 @Nullable Position position, 200 @Nullable String byId, 201 @Nullable MarkupLine title, 202 @NonNull List<Parameter> params, 203 @NonNull List<Property> props, 204 @NonNull List<Link> links, 205 @NonNull List<ControlPart> parts) { 206 return INSTANCE.visitControl( 207 control, 208 Context.newContext( 209 control, 210 position == null ? Position.ENDING : position, 211 byId, 212 title, 213 params, 214 props, 215 links, 216 parts)); 217 } 218 219 @Override 220 public Boolean visitCatalog(Catalog catalog, Context context) { 221 // not required 222 throw new UnsupportedOperationException("not needed"); 223 } 224 225 @Override 226 public Boolean visitGroup(CatalogGroup group, Context context) { 227 // not required 228 throw new UnsupportedOperationException("not needed"); 229 } 230 231 /** 232 * If the add applies to the current object, then apply the child objects. 233 * <p> 234 * An add applies if: 235 * <ol> 236 * <li>the {@code targetItem} supports all of the children</li> 237 * <li>the context matches if: 238 * <ul> 239 * <li>the target item's id matches the "by-id"; or</li> 240 * <li>the "by-id" is not defined and the target item is the control matching 241 * the target context</li> 242 * </ul> 243 * </li> 244 * </ol> 245 * 246 * @param <T> 247 * the type of the {@code targetItem} 248 * @param targetItem 249 * the current target to process 250 * @param titleConsumer 251 * a consumer to apply a title to or {@code null} if the object has no 252 * title field 253 * @param paramsSupplier 254 * a supplier for the child {@link Parameter} collection 255 * @param propsSupplier 256 * a supplier for the child {@link Property} collection 257 * @param linksSupplier 258 * a supplier for the child {@link Link} collection 259 * @param partsSupplier 260 * a supplier for the child {@link ControlPart} collection 261 * @param context 262 * the add context 263 * @return {@code true} if a modification was made or {@code false} otherwise 264 */ 265 private static <T> boolean handleCurrent( 266 @NonNull T targetItem, 267 @Nullable Consumer<MarkupLine> titleConsumer, 268 @Nullable Supplier<? extends List<Parameter>> paramsSupplier, 269 @Nullable Supplier<? extends List<Property>> propsSupplier, 270 @Nullable Supplier<? extends List<Link>> linksSupplier, 271 @Nullable Supplier<? extends List<ControlPart>> partsSupplier, 272 @NonNull Context context) { 273 boolean retval = false; 274 Position position = context.getPosition(); 275 if (context.appliesTo(targetItem) && !context.isSequenceTargeted(targetItem)) { 276 // the target item is the target of the add 277 MarkupLine newTitle = context.getTitle(); 278 if (newTitle != null) { 279 assert titleConsumer != null; 280 titleConsumer.accept(newTitle); 281 } 282 283 handleCollection(position, context.getParams(), paramsSupplier); 284 handleCollection(position, context.getProps(), propsSupplier); 285 handleCollection(position, context.getLinks(), linksSupplier); 286 handleCollection(position, context.getParts(), partsSupplier); 287 retval = true; 288 } 289 return retval; 290 } 291 292 private static <T> void handleCollection( 293 @NonNull Position position, 294 @NonNull List<T> newItems, 295 @Nullable Supplier<? extends List<T>> originalCollectionSupplier) { 296 if (originalCollectionSupplier != null) { 297 List<T> oldItems = originalCollectionSupplier.get(); 298 if (!newItems.isEmpty()) { 299 if (Position.STARTING.equals(position)) { 300 oldItems.addAll(0, newItems); 301 } else { // ENDING 302 oldItems.addAll(newItems); 303 } 304 } 305 } 306 } 307 308 // private static <T> void handleChild( 309 // @NonNull TargetType itemType, 310 // @NonNull Supplier<? extends List<T>> collectionSupplier, 311 // @Nullable Consumer<T> handler, 312 // @NonNull Context context) { 313 // boolean handleChildren = !Collections.disjoint(context.getTargetItemTypes(), 314 // getApplicableTypes(itemType)); 315 // if (handleChildren && handler != null) { 316 // // if the child item type is applicable and there is a handler, iterate over 317 // children 318 // Iterator<T> iter = collectionSupplier.get().iterator(); 319 // while (iter.hasNext()) { 320 // T item = iter.next(); 321 // if (item != null) { 322 // handler.accept(item); 323 // } 324 // } 325 // } 326 // } 327 328 @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" }) 329 private static <T> boolean handleChild( 330 @NonNull TargetType itemType, 331 @NonNull Supplier<? extends List<T>> originalCollectionSupplier, 332 @NonNull Supplier<? extends List<T>> newItemsSupplier, 333 @Nullable Function<T, Boolean> handler, 334 @NonNull Context context) { 335 336 // determine if this child type can match 337 boolean isItemTypeMatch = context.isMatchingType(itemType); 338 339 Set<TargetType> applicableTypes = getApplicableTypes(itemType); 340 boolean descendChild = handler != null && !Collections.disjoint(context.getTargetItemTypes(), applicableTypes); 341 342 boolean retval = false; 343 if (isItemTypeMatch || descendChild) { 344 // if the item type is applicable, attempt to match by id 345 List<T> collection = originalCollectionSupplier.get(); 346 ListIterator<T> iter = collection.listIterator(); 347 boolean deferred = false; 348 while (iter.hasNext()) { 349 T item = ObjectUtils.requireNonNull(iter.next()); 350 351 if (isItemTypeMatch && context.appliesTo(item) && context.isSequenceTargeted(item)) { 352 // if id match, inject the new items into the collection 353 switch (context.getPosition()) { 354 case AFTER: { 355 newItemsSupplier.get().forEach(iter::add); 356 retval = true; 357 break; 358 } 359 case BEFORE: { 360 iter.previous(); 361 List<T> adds = newItemsSupplier.get(); 362 adds.forEach(iter::add); 363 item = iter.next(); 364 retval = true; 365 break; 366 } 367 case STARTING: 368 case ENDING: 369 deferred = true; 370 break; 371 default: 372 throw new UnsupportedOperationException(context.getPosition().name().toLowerCase(Locale.ROOT)); 373 } 374 } 375 376 if (descendChild) { 377 assert handler != null; 378 379 // handle child items since they are applicable to the search criteria 380 retval = retval || handler.apply(item); 381 } 382 } 383 384 if (deferred) { 385 List<T> newItems = newItemsSupplier.get(); 386 if (Position.ENDING.equals(context.getPosition())) { 387 collection.addAll(newItems); 388 retval = true; 389 } else if (Position.STARTING.equals(context.getPosition())) { 390 collection.addAll(0, newItems); 391 retval = true; 392 } 393 } 394 } 395 return retval; 396 } 397 398 @Override 399 public Boolean visitControl(Control control, Context context) { 400 assert context != null; 401 402 boolean retval = handleCurrent( 403 control, 404 control::setTitle, 405 control::getParams, 406 control::getProps, 407 control::getLinks, 408 control::getParts, 409 context); 410 411 // visit params 412 retval = retval || handleChild( 413 TargetType.PARAM, 414 control::getParams, 415 context::getParams, 416 child -> visitParameter(ObjectUtils.notNull(child), context), 417 context); 418 419 // visit parts 420 retval = retval || handleChild( 421 TargetType.PART, 422 control::getParts, 423 context::getParts, 424 child -> visitPart(child, context), 425 context); 426 427 // visit control children 428 for (Control childControl : CollectionUtil.listOrEmpty(control.getControls())) { 429 Set<TargetType> applicableTypes = getApplicableTypes(TargetType.CONTROL); 430 if (!Collections.disjoint(context.getTargetItemTypes(), applicableTypes)) { 431 retval = retval || visitControl(ObjectUtils.requireNonNull(childControl), context); 432 } 433 } 434 return retval; 435 } 436 437 @Override 438 public Boolean visitParameter(Parameter parameter, Context context) { 439 assert context != null; 440 441 return handleCurrent( 442 parameter, 443 null, 444 null, 445 parameter::getProps, 446 parameter::getLinks, 447 null, 448 context); 449 } 450 451 /** 452 * Visit the control part. 453 * 454 * @param part 455 * the bound part object 456 * @param context 457 * the visitor context 458 * @return {@code true} if the removal was applied or {@code false} otherwise 459 */ 460 public boolean visitPart(ControlPart part, Context context) { 461 assert context != null; 462 463 boolean retval = handleCurrent( 464 part, 465 null, 466 null, 467 part::getProps, 468 part::getLinks, 469 part::getParts, 470 context); 471 472 return retval || handleChild( 473 TargetType.PART, 474 part::getParts, 475 context::getParts, 476 child -> visitPart(child, context), 477 context); 478 } 479 480 static final class Context { 481 @NonNull 482 private static final Set<TargetType> TITLE_TYPES = ObjectUtils.notNull( 483 Set.of(TargetType.CONTROL, TargetType.PART)); 484 @NonNull 485 private static final Set<TargetType> PARAM_TYPES = ObjectUtils.notNull( 486 Set.of(TargetType.CONTROL, TargetType.PARAM)); 487 @NonNull 488 private static final Set<TargetType> PROP_TYPES = ObjectUtils.notNull( 489 Set.of(TargetType.CONTROL, TargetType.PARAM, TargetType.PART)); 490 @NonNull 491 private static final Set<TargetType> LINK_TYPES = ObjectUtils.notNull( 492 Set.of(TargetType.CONTROL, TargetType.PARAM, TargetType.PART)); 493 @NonNull 494 private static final Set<TargetType> PART_TYPES = ObjectUtils.notNull( 495 Set.of(TargetType.CONTROL, TargetType.PART)); 496 497 @NonNull 498 private final Control control; 499 @NonNull 500 private final Position position; 501 @Nullable 502 private final String byId; 503 @Nullable 504 private final MarkupLine title; 505 @NonNull 506 private final List<Parameter> params; 507 @NonNull 508 private final List<Property> props; 509 @NonNull 510 private final List<Link> links; 511 @NonNull 512 private final List<ControlPart> parts; 513 @NonNull 514 private final Set<TargetType> targetItemTypes; 515 516 @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", "PMD.NPathComplexity" }) 517 public static Context newContext( 518 @NonNull Control control, 519 @NonNull Position position, 520 @Nullable String byId, 521 @Nullable MarkupLine title, 522 @NonNull List<Parameter> params, 523 @NonNull List<Property> props, 524 @NonNull List<Link> links, 525 @NonNull List<ControlPart> parts) { 526 Set<TargetType> targetItemTypes = ObjectUtils.notNull(EnumSet.allOf(TargetType.class)); 527 List<String> additionObjects = new LinkedList<>(); 528 529 boolean sequenceTarget = true; 530 if (title != null) { 531 targetItemTypes.retainAll(TITLE_TYPES); 532 additionObjects.add("title"); 533 sequenceTarget = false; 534 } 535 536 if (!params.isEmpty()) { 537 targetItemTypes.retainAll(PARAM_TYPES); 538 additionObjects.add("param"); 539 } 540 541 if (!props.isEmpty()) { 542 targetItemTypes.retainAll(PROP_TYPES); 543 additionObjects.add("prop"); 544 sequenceTarget = false; 545 } 546 547 if (!links.isEmpty()) { 548 targetItemTypes.retainAll(LINK_TYPES); 549 additionObjects.add("link"); 550 sequenceTarget = false; 551 } 552 553 if (!parts.isEmpty()) { 554 targetItemTypes.retainAll(PART_TYPES); 555 additionObjects.add("part"); 556 } 557 558 if (Position.BEFORE.equals(position) || Position.AFTER.equals(position)) { 559 if (!sequenceTarget) { 560 throw new ProfileResolutionEvaluationException( 561 "When using position before or after, one collection of parameters or parts can be specified." 562 + " Other additions must not be used."); 563 } 564 if (!params.isEmpty() && parts.isEmpty()) { 565 targetItemTypes.retainAll(Set.of(TargetType.PARAM)); 566 } else if (!parts.isEmpty() && params.isEmpty()) { 567 targetItemTypes.retainAll(Set.of(TargetType.PART)); 568 } else { 569 throw new ProfileResolutionEvaluationException( 570 "When using position before or after, only one collection of parameters or parts can be specified."); 571 } 572 } 573 574 if (targetItemTypes.isEmpty()) { 575 throw new ProfileResolutionEvaluationException("No parent object supports the requested objects to add: " + 576 additionObjects.stream().collect(CustomCollectors.joiningWithOxfordComma("or"))); 577 } 578 579 return new Context( 580 control, 581 position, 582 byId, 583 title, 584 params, 585 props, 586 links, 587 parts, 588 targetItemTypes); 589 } 590 591 private Context( 592 @NonNull Control control, 593 @NonNull Position position, 594 @Nullable String byId, 595 @Nullable MarkupLine title, 596 @NonNull List<Parameter> params, 597 @NonNull List<Property> props, 598 @NonNull List<Link> links, 599 @NonNull List<ControlPart> parts, 600 @NonNull Set<TargetType> targetItemTypes) { 601 this.control = control; 602 this.position = position; 603 this.byId = byId; 604 this.title = title; 605 this.params = params; 606 this.props = props; 607 this.links = links; 608 this.parts = parts; 609 this.targetItemTypes = CollectionUtil.unmodifiableSet(targetItemTypes); 610 } 611 612 @NonNull 613 private Control getControl() { 614 return control; 615 } 616 617 @NonNull 618 private Position getPosition() { 619 return position; 620 } 621 622 @Nullable 623 private String getById() { 624 return byId; 625 } 626 627 @Nullable 628 private MarkupLine getTitle() { 629 return title; 630 } 631 632 @NonNull 633 private List<Parameter> getParams() { 634 return params; 635 } 636 637 @NonNull 638 private List<Property> getProps() { 639 return props; 640 } 641 642 @NonNull 643 private List<Link> getLinks() { 644 return links; 645 } 646 647 @NonNull 648 private List<ControlPart> getParts() { 649 return parts; 650 } 651 652 @NonNull 653 private Set<TargetType> getTargetItemTypes() { 654 return targetItemTypes; 655 } 656 657 private boolean isMatchingType(@NonNull TargetType type) { 658 return getTargetItemTypes().contains(type); 659 } 660 661 private <T> boolean isSequenceTargeted(T targetItem) { 662 TargetType objectType = TargetType.forClass(targetItem.getClass()); 663 return (Position.BEFORE.equals(position) || Position.AFTER.equals(position)) 664 && (TargetType.PARAM.equals(objectType) && isMatchingType(TargetType.PARAM) 665 || TargetType.PART.equals(objectType) && isMatchingType(TargetType.PART)); 666 } 667 668 /** 669 * Determine if the provided {@code obj} is the target of the add. 670 * 671 * @param obj 672 * the current object 673 * @return {@code true} if the current object applies or {@code false} otherwise 674 */ 675 private boolean appliesTo(@NonNull Object obj) { 676 TargetType objectType = TargetType.forClass(obj.getClass()); 677 678 boolean retval = objectType != null && isMatchingType(objectType); 679 if (retval) { 680 assert objectType != null; 681 682 // check other criteria 683 String actualId = null; 684 switch (objectType) { 685 case CONTROL: { 686 Control control = (Control) obj; 687 actualId = control.getId(); 688 break; 689 } 690 case PARAM: { 691 Parameter param = (Parameter) obj; 692 actualId = param.getId(); 693 break; 694 } 695 case PART: { 696 ControlPart part = (ControlPart) obj; 697 String partId = part.getId(); 698 if (part.getId() != null) { 699 actualId = partId; 700 } 701 break; 702 } 703 default: 704 throw new UnsupportedOperationException(objectType.fieldName()); 705 } 706 707 String byId = getById(); 708 if (getById() == null && TargetType.CONTROL.equals(objectType)) { 709 retval = getControl().equals(obj); 710 } else { 711 retval = byId != null && byId.equals(actualId); 712 } 713 } 714 return retval; 715 } 716 } 717}