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}