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}