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