1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.oscal.lib.profile.resolver.alter;
7   
8   import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
9   import gov.nist.secauto.metaschema.core.util.CollectionUtil;
10  import gov.nist.secauto.metaschema.core.util.CustomCollectors;
11  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
12  import gov.nist.secauto.oscal.lib.model.Catalog;
13  import gov.nist.secauto.oscal.lib.model.CatalogGroup;
14  import gov.nist.secauto.oscal.lib.model.Control;
15  import gov.nist.secauto.oscal.lib.model.ControlPart;
16  import gov.nist.secauto.oscal.lib.model.Link;
17  import gov.nist.secauto.oscal.lib.model.Parameter;
18  import gov.nist.secauto.oscal.lib.model.Property;
19  import gov.nist.secauto.oscal.lib.model.control.catalog.ICatalogVisitor;
20  import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
21  
22  import java.util.Collections;
23  import java.util.EnumMap;
24  import java.util.EnumSet;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.ListIterator;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.concurrent.ConcurrentHashMap;
32  import java.util.function.Consumer;
33  import java.util.function.Function;
34  import java.util.function.Supplier;
35  
36  import edu.umd.cs.findbugs.annotations.NonNull;
37  import edu.umd.cs.findbugs.annotations.Nullable;
38  
39  @SuppressWarnings("PMD.CouplingBetweenObjects")
40  public class AddVisitor implements ICatalogVisitor<Boolean, AddVisitor.Context> {
41    public enum TargetType {
42      CONTROL("control", Control.class),
43      PARAM("param", Parameter.class),
44      PART("part", ControlPart.class);
45  
46      @NonNull
47      private static final Map<Class<?>, TargetType> CLASS_TO_TYPE;
48      @NonNull
49      private static final Map<String, TargetType> NAME_TO_TYPE;
50      @NonNull
51      private final String fieldName;
52      @NonNull
53      private final Class<?> clazz;
54  
55      static {
56        {
57          Map<Class<?>, TargetType> map = new ConcurrentHashMap<>();
58          for (TargetType type : values()) {
59            map.put(type.getClazz(), type);
60          }
61          CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map);
62        }
63  
64        {
65          Map<String, TargetType> map = new ConcurrentHashMap<>();
66          for (TargetType type : values()) {
67            map.put(type.fieldName(), type);
68          }
69          NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map);
70        }
71      }
72  
73      /**
74       * Get the target type associated with the provided {@code clazz}.
75       *
76       * @param clazz
77       *          the class to identify the target type for
78       * @return the associated target type or {@code null} if the class is not
79       *         associated with a target type
80       */
81      @Nullable
82      public static TargetType forClass(@NonNull Class<?> clazz) {
83        Class<?> target = clazz;
84        TargetType retval;
85        // recurse over parent classes to find a match
86        do {
87          retval = CLASS_TO_TYPE.get(target);
88        } while (retval == null && (target = target.getSuperclass()) != null);
89        return retval;
90      }
91  
92      /**
93       * Get the target type associated with the provided field {@code name}.
94       *
95       * @param name
96       *          the field name to identify the target type for
97       * @return the associated target type or {@code null} if the name is not
98       *         associated with a target type
99       */
100     @Nullable
101     public static TargetType forFieldName(@Nullable String name) {
102       return name == null ? null : NAME_TO_TYPE.get(name);
103     }
104 
105     TargetType(@NonNull String fieldName, @NonNull Class<?> clazz) {
106       this.fieldName = fieldName;
107       this.clazz = clazz;
108     }
109 
110     /**
111      * Get the field name associated with the target type.
112      *
113      * @return the name
114      */
115     public String fieldName() {
116       return fieldName;
117     }
118 
119     /**
120      * Get the bound class associated with the target type.
121      *
122      * @return the class
123      */
124     public Class<?> getClazz() {
125       return clazz;
126     }
127   }
128 
129   public enum Position {
130     BEFORE,
131     AFTER,
132     STARTING,
133     ENDING;
134 
135     @NonNull
136     private static final Map<String, Position> NAME_TO_POSITION;
137 
138     static {
139       Map<String, Position> map = new ConcurrentHashMap<>();
140       for (Position position : values()) {
141         map.put(position.name().toLowerCase(Locale.ROOT), position);
142       }
143       NAME_TO_POSITION = CollectionUtil.unmodifiableMap(map);
144     }
145 
146     /**
147      * Get the position associated with the provided {@code name}.
148      *
149      * @param name
150      *          the name to identify the position for
151      * @return the associated position or {@code null} if the name is not associated
152      *         with a position
153      */
154     @Nullable
155     public static Position forName(@Nullable String name) {
156       return name == null ? null : NAME_TO_POSITION.get(name);
157     }
158   }
159 
160   @NonNull
161   private static final AddVisitor INSTANCE = new AddVisitor();
162   private static final Map<TargetType, Set<TargetType>> APPLICABLE_TARGETS;
163 
164   static {
165     APPLICABLE_TARGETS = new EnumMap<>(TargetType.class);
166     APPLICABLE_TARGETS.put(TargetType.CONTROL, Set.of(TargetType.CONTROL, TargetType.PARAM, TargetType.PART));
167     APPLICABLE_TARGETS.put(TargetType.PARAM, Set.of(TargetType.PARAM));
168     APPLICABLE_TARGETS.put(TargetType.PART, Set.of(TargetType.PART));
169   }
170 
171   private static Set<TargetType> getApplicableTypes(@NonNull TargetType type) {
172     return APPLICABLE_TARGETS.getOrDefault(type, CollectionUtil.emptySet());
173   }
174 
175   /**
176    * Apply the add directive.
177    *
178    * @param control
179    *          the control target
180    * @param position
181    *          the position to apply the content or {@code null}
182    * @param byId
183    *          the identifier of the target or {@code null}
184    * @param title
185    *          a title to set
186    * @param params
187    *          parameters to add
188    * @param props
189    *          properties to add
190    * @param links
191    *          links to add
192    * @param parts
193    *          parts to add
194    * @return {@code true} if the modification was made or {@code false} otherwise
195    * @throws ProfileResolutionEvaluationException
196    *           if a processing error occurred during profile resolution
197    */
198   public static boolean add(
199       @NonNull Control control,
200       @Nullable Position position,
201       @Nullable String byId,
202       @Nullable MarkupLine title,
203       @NonNull List<Parameter> params,
204       @NonNull List<Property> props,
205       @NonNull List<Link> links,
206       @NonNull List<ControlPart> parts) {
207     return INSTANCE.visitControl(
208         control,
209         Context.newContext(
210             control,
211             position == null ? Position.ENDING : position,
212             byId,
213             title,
214             params,
215             props,
216             links,
217             parts));
218   }
219 
220   @Override
221   public Boolean visitCatalog(Catalog catalog, Context context) {
222     // not required
223     throw new UnsupportedOperationException("not needed");
224   }
225 
226   @Override
227   public Boolean visitGroup(CatalogGroup group, Context context) {
228     // not required
229     throw new UnsupportedOperationException("not needed");
230   }
231 
232   /**
233    * If the add applies to the current object, then apply the child objects.
234    * <p>
235    * An add applies if:
236    * <ol>
237    * <li>the {@code targetItem} supports all of the children</li>
238    * <li>the context matches if:
239    * <ul>
240    * <li>the target item's id matches the "by-id"; or</li>
241    * <li>the "by-id" is not defined and the target item is the control matching
242    * the target context</li>
243    * </ul>
244    * </li>
245    * </ol>
246    *
247    * @param <T>
248    *          the type of the {@code targetItem}
249    * @param targetItem
250    *          the current target to process
251    * @param titleConsumer
252    *          a consumer to apply a title to or {@code null} if the object has no
253    *          title field
254    * @param paramsSupplier
255    *          a supplier for the child {@link Parameter} collection
256    * @param propsSupplier
257    *          a supplier for the child {@link Property} collection
258    * @param linksSupplier
259    *          a supplier for the child {@link Link} collection
260    * @param partsSupplier
261    *          a supplier for the child {@link ControlPart} collection
262    * @param context
263    *          the add context
264    * @return {@code true} if a modification was made or {@code false} otherwise
265    */
266   private static <T> boolean handleCurrent(
267       @NonNull T targetItem,
268       @Nullable Consumer<MarkupLine> titleConsumer,
269       @Nullable Supplier<? extends List<Parameter>> paramsSupplier,
270       @Nullable Supplier<? extends List<Property>> propsSupplier,
271       @Nullable Supplier<? extends List<Link>> linksSupplier,
272       @Nullable Supplier<? extends List<ControlPart>> partsSupplier,
273       @NonNull Context context) {
274     boolean retval = false;
275     Position position = context.getPosition();
276     if (context.appliesTo(targetItem) && !context.isSequenceTargeted(targetItem)) {
277       // the target item is the target of the add
278       MarkupLine newTitle = context.getTitle();
279       if (newTitle != null) {
280         assert titleConsumer != null;
281         titleConsumer.accept(newTitle);
282       }
283 
284       handleCollection(position, context.getParams(), paramsSupplier);
285       handleCollection(position, context.getProps(), propsSupplier);
286       handleCollection(position, context.getLinks(), linksSupplier);
287       handleCollection(position, context.getParts(), partsSupplier);
288       retval = true;
289     }
290     return retval;
291   }
292 
293   private static <T> void handleCollection(
294       @NonNull Position position,
295       @NonNull List<T> newItems,
296       @Nullable Supplier<? extends List<T>> originalCollectionSupplier) {
297     if (originalCollectionSupplier != null) {
298       List<T> oldItems = originalCollectionSupplier.get();
299       if (!newItems.isEmpty()) {
300         if (Position.STARTING.equals(position)) {
301           oldItems.addAll(0, newItems);
302         } else { // ENDING
303           oldItems.addAll(newItems);
304         }
305       }
306     }
307   }
308 
309   // private static <T> void handleChild(
310   // @NonNull TargetType itemType,
311   // @NonNull Supplier<? extends List<T>> collectionSupplier,
312   // @Nullable Consumer<T> handler,
313   // @NonNull Context context) {
314   // boolean handleChildren = !Collections.disjoint(context.getTargetItemTypes(),
315   // getApplicableTypes(itemType));
316   // if (handleChildren && handler != null) {
317   // // if the child item type is applicable and there is a handler, iterate over
318   // children
319   // Iterator<T> iter = collectionSupplier.get().iterator();
320   // while (iter.hasNext()) {
321   // T item = iter.next();
322   // if (item != null) {
323   // handler.accept(item);
324   // }
325   // }
326   // }
327   // }
328 
329   @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" })
330   private static <T> boolean handleChild(
331       @NonNull TargetType itemType,
332       @NonNull Supplier<? extends List<T>> originalCollectionSupplier,
333       @NonNull Supplier<? extends List<T>> newItemsSupplier,
334       @Nullable Function<T, Boolean> handler,
335       @NonNull Context context) {
336 
337     // determine if this child type can match
338     boolean isItemTypeMatch = context.isMatchingType(itemType);
339 
340     Set<TargetType> applicableTypes = getApplicableTypes(itemType);
341     boolean descendChild = handler != null && !Collections.disjoint(context.getTargetItemTypes(), applicableTypes);
342 
343     boolean retval = false;
344     if (isItemTypeMatch || descendChild) {
345       // if the item type is applicable, attempt to match by id
346       List<T> collection = originalCollectionSupplier.get();
347       ListIterator<T> iter = collection.listIterator();
348       boolean deferred = false;
349       while (iter.hasNext()) {
350         T item = ObjectUtils.requireNonNull(iter.next());
351 
352         if (isItemTypeMatch && context.appliesTo(item) && context.isSequenceTargeted(item)) {
353           // if id match, inject the new items into the collection
354           switch (context.getPosition()) {
355           case AFTER: {
356             newItemsSupplier.get().forEach(iter::add);
357             retval = true;
358             break;
359           }
360           case BEFORE: {
361             iter.previous();
362             List<T> adds = newItemsSupplier.get();
363             adds.forEach(iter::add);
364             item = iter.next();
365             retval = true;
366             break;
367           }
368           case STARTING:
369           case ENDING:
370             deferred = true;
371             break;
372           default:
373             throw new UnsupportedOperationException(context.getPosition().name().toLowerCase(Locale.ROOT));
374           }
375         }
376 
377         if (descendChild) {
378           assert handler != null;
379 
380           // handle child items since they are applicable to the search criteria
381           retval = retval || handler.apply(item);
382         }
383       }
384 
385       if (deferred) {
386         List<T> newItems = newItemsSupplier.get();
387         if (Position.ENDING.equals(context.getPosition())) {
388           collection.addAll(newItems);
389           retval = true;
390         } else if (Position.STARTING.equals(context.getPosition())) {
391           collection.addAll(0, newItems);
392           retval = true;
393         }
394       }
395     }
396     return retval;
397   }
398 
399   @Override
400   public Boolean visitControl(Control control, Context context) {
401     assert context != null;
402 
403     if (control.getParams() == null) {
404       control.setParams(new LinkedList<>());
405     }
406 
407     if (control.getProps() == null) {
408       control.setProps(new LinkedList<>());
409     }
410 
411     if (control.getLinks() == null) {
412       control.setLinks(new LinkedList<>());
413     }
414 
415     if (control.getParts() == null) {
416       control.setParts(new LinkedList<>());
417     }
418 
419     boolean retval = handleCurrent(
420         control,
421         control::setTitle,
422         control::getParams,
423         control::getProps,
424         control::getLinks,
425         control::getParts,
426         context);
427 
428     // visit params
429     retval = retval || handleChild(
430         TargetType.PARAM,
431         control::getParams,
432         context::getParams,
433         child -> visitParameter(ObjectUtils.notNull(child), context),
434         context);
435 
436     // visit parts
437     retval = retval || handleChild(
438         TargetType.PART,
439         control::getParts,
440         context::getParts,
441         child -> visitPart(child, context),
442         context);
443 
444     // visit control children
445     for (Control childControl : CollectionUtil.listOrEmpty(control.getControls())) {
446       Set<TargetType> applicableTypes = getApplicableTypes(TargetType.CONTROL);
447       if (!Collections.disjoint(context.getTargetItemTypes(), applicableTypes)) {
448         retval = retval || visitControl(ObjectUtils.requireNonNull(childControl), context);
449       }
450     }
451     return retval;
452   }
453 
454   @Override
455   public Boolean visitParameter(Parameter parameter, Context context) {
456     assert context != null;
457     if (parameter.getProps() == null) {
458       parameter.setProps(new LinkedList<>());
459     }
460 
461     if (parameter.getLinks() == null) {
462       parameter.setLinks(new LinkedList<>());
463     }
464 
465     return handleCurrent(
466         parameter,
467         null,
468         null,
469         parameter::getProps,
470         parameter::getLinks,
471         null,
472         context);
473   }
474 
475   /**
476    * Visit the control part.
477    *
478    * @param part
479    *          the bound part object
480    * @param context
481    *          the visitor context
482    * @return {@code true} if the removal was applied or {@code false} otherwise
483    */
484   public boolean visitPart(ControlPart part, Context context) {
485     assert context != null;
486     if (part.getProps() == null) {
487       part.setProps(new LinkedList<>());
488     }
489 
490     if (part.getLinks() == null) {
491       part.setLinks(new LinkedList<>());
492     }
493 
494     if (part.getParts() == null) {
495       part.setParts(new LinkedList<>());
496     }
497 
498     boolean retval = handleCurrent(
499         part,
500         null,
501         null,
502         part::getProps,
503         part::getLinks,
504         part::getParts,
505         context);
506 
507     return retval || handleChild(
508         TargetType.PART,
509         part::getParts,
510         context::getParts,
511         child -> visitPart(child, context),
512         context);
513   }
514 
515   static final class Context {
516     @NonNull
517     private static final Set<TargetType> TITLE_TYPES = ObjectUtils.notNull(
518         Set.of(TargetType.CONTROL, TargetType.PART));
519     @NonNull
520     private static final Set<TargetType> PARAM_TYPES = ObjectUtils.notNull(
521         Set.of(TargetType.CONTROL, TargetType.PARAM));
522     @NonNull
523     private static final Set<TargetType> PROP_TYPES = ObjectUtils.notNull(
524         Set.of(TargetType.CONTROL, TargetType.PARAM, TargetType.PART));
525     @NonNull
526     private static final Set<TargetType> LINK_TYPES = ObjectUtils.notNull(
527         Set.of(TargetType.CONTROL, TargetType.PARAM, TargetType.PART));
528     @NonNull
529     private static final Set<TargetType> PART_TYPES = ObjectUtils.notNull(
530         Set.of(TargetType.CONTROL, TargetType.PART));
531 
532     @NonNull
533     private final Control control;
534     @NonNull
535     private final Position position;
536     @Nullable
537     private final String byId;
538     @Nullable
539     private final MarkupLine title;
540     @NonNull
541     private final List<Parameter> params;
542     @NonNull
543     private final List<Property> props;
544     @NonNull
545     private final List<Link> links;
546     @NonNull
547     private final List<ControlPart> parts;
548     @NonNull
549     private final Set<TargetType> targetItemTypes;
550 
551     @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", "PMD.NPathComplexity" })
552     public static Context newContext(
553         @NonNull Control control,
554         @NonNull Position position,
555         @Nullable String byId,
556         @Nullable MarkupLine title,
557         @NonNull List<Parameter> params,
558         @NonNull List<Property> props,
559         @NonNull List<Link> links,
560         @NonNull List<ControlPart> parts) {
561       Set<TargetType> targetItemTypes = ObjectUtils.notNull(EnumSet.allOf(TargetType.class));
562       List<String> additionObjects = new LinkedList<>();
563 
564       boolean sequenceTarget = true;
565       if (title != null) {
566         targetItemTypes.retainAll(TITLE_TYPES);
567         additionObjects.add("title");
568         sequenceTarget = false;
569       }
570 
571       if (!params.isEmpty()) {
572         targetItemTypes.retainAll(PARAM_TYPES);
573         additionObjects.add("param");
574       }
575 
576       if (!props.isEmpty()) {
577         targetItemTypes.retainAll(PROP_TYPES);
578         additionObjects.add("prop");
579         sequenceTarget = false;
580       }
581 
582       if (!links.isEmpty()) {
583         targetItemTypes.retainAll(LINK_TYPES);
584         additionObjects.add("link");
585         sequenceTarget = false;
586       }
587 
588       if (!parts.isEmpty()) {
589         targetItemTypes.retainAll(PART_TYPES);
590         additionObjects.add("part");
591       }
592 
593       if (Position.BEFORE.equals(position) || Position.AFTER.equals(position)) {
594         if (!sequenceTarget) {
595           throw new ProfileResolutionEvaluationException(
596               "When using position before or after, one collection of parameters or parts can be specified."
597                   + " Other additions must not be used.");
598         }
599         if (!params.isEmpty() && parts.isEmpty()) {
600           targetItemTypes.retainAll(Set.of(TargetType.PARAM));
601         } else if (!parts.isEmpty() && params.isEmpty()) {
602           targetItemTypes.retainAll(Set.of(TargetType.PART));
603         } else {
604           throw new ProfileResolutionEvaluationException(
605               "When using position before or after, only one collection of parameters or parts can be specified.");
606         }
607       }
608 
609       if (targetItemTypes.isEmpty()) {
610         throw new ProfileResolutionEvaluationException("No parent object supports the requested objects to add: " +
611             additionObjects.stream().collect(CustomCollectors.joiningWithOxfordComma("or")));
612       }
613 
614       return new Context(
615           control,
616           position,
617           byId,
618           title,
619           params,
620           props,
621           links,
622           parts,
623           targetItemTypes);
624     }
625 
626     private Context(
627         @NonNull Control control,
628         @NonNull Position position,
629         @Nullable String byId,
630         @Nullable MarkupLine title,
631         @NonNull List<Parameter> params,
632         @NonNull List<Property> props,
633         @NonNull List<Link> links,
634         @NonNull List<ControlPart> parts,
635         @NonNull Set<TargetType> targetItemTypes) {
636       this.control = control;
637       this.position = position;
638       this.byId = byId;
639       this.title = title;
640       this.params = params;
641       this.props = props;
642       this.links = links;
643       this.parts = parts;
644       this.targetItemTypes = CollectionUtil.unmodifiableSet(targetItemTypes);
645     }
646 
647     @NonNull
648     private Control getControl() {
649       return control;
650     }
651 
652     @NonNull
653     private Position getPosition() {
654       return position;
655     }
656 
657     @Nullable
658     private String getById() {
659       return byId;
660     }
661 
662     @Nullable
663     private MarkupLine getTitle() {
664       return title;
665     }
666 
667     @NonNull
668     private List<Parameter> getParams() {
669       return params;
670     }
671 
672     @NonNull
673     private List<Property> getProps() {
674       return props;
675     }
676 
677     @NonNull
678     private List<Link> getLinks() {
679       return links;
680     }
681 
682     @NonNull
683     private List<ControlPart> getParts() {
684       return parts;
685     }
686 
687     @NonNull
688     private Set<TargetType> getTargetItemTypes() {
689       return targetItemTypes;
690     }
691 
692     private boolean isMatchingType(@NonNull TargetType type) {
693       return getTargetItemTypes().contains(type);
694     }
695 
696     private <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     /**
704      * Determine if the provided {@code obj} is the target of the add.
705      *
706      * @param obj
707      *          the current object
708      * @return {@code true} if the current object applies or {@code false} otherwise
709      */
710     private boolean appliesTo(@NonNull Object obj) {
711       TargetType objectType = TargetType.forClass(obj.getClass());
712 
713       boolean retval = objectType != null && isMatchingType(objectType);
714       if (retval) {
715         assert objectType != null;
716 
717         // check other criteria
718         String actualId = null;
719         switch (objectType) {
720         case CONTROL: {
721           Control control = (Control) obj;
722           actualId = control.getId();
723           break;
724         }
725         case PARAM: {
726           Parameter param = (Parameter) obj;
727           actualId = param.getId();
728           break;
729         }
730         case PART: {
731           ControlPart part = (ControlPart) obj;
732           String partId = part.getId();
733           if (part.getId() != null) {
734             actualId = partId;
735           }
736           break;
737         }
738         default:
739           throw new UnsupportedOperationException(objectType.fieldName());
740         }
741 
742         String byId = getById();
743         if (getById() == null && TargetType.CONTROL.equals(objectType)) {
744           retval = getControl().equals(obj);
745         } else {
746           retval = byId != null && byId.equals(actualId);
747         }
748       }
749       return retval;
750     }
751   }
752 }