1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package gov.nist.secauto.oscal.lib.profile.resolver.alter;
28
29 import gov.nist.secauto.metaschema.model.common.datatype.markup.MarkupLine;
30 import gov.nist.secauto.metaschema.model.common.util.CollectionUtil;
31 import gov.nist.secauto.metaschema.model.common.util.CustomCollectors;
32 import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
33 import gov.nist.secauto.oscal.lib.model.Catalog;
34 import gov.nist.secauto.oscal.lib.model.CatalogGroup;
35 import gov.nist.secauto.oscal.lib.model.Control;
36 import gov.nist.secauto.oscal.lib.model.ControlPart;
37 import gov.nist.secauto.oscal.lib.model.Link;
38 import gov.nist.secauto.oscal.lib.model.Parameter;
39 import gov.nist.secauto.oscal.lib.model.Property;
40 import gov.nist.secauto.oscal.lib.model.control.catalog.ICatalogVisitor;
41 import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
42
43 import java.util.Collections;
44 import java.util.EnumMap;
45 import java.util.EnumSet;
46 import java.util.LinkedList;
47 import java.util.List;
48 import java.util.ListIterator;
49 import java.util.Locale;
50 import java.util.Map;
51 import java.util.Set;
52 import java.util.concurrent.ConcurrentHashMap;
53 import java.util.function.Consumer;
54 import java.util.function.Function;
55 import java.util.function.Supplier;
56
57 import edu.umd.cs.findbugs.annotations.NonNull;
58 import edu.umd.cs.findbugs.annotations.Nullable;
59
60 public class AddVisitor implements ICatalogVisitor<Boolean, AddVisitor.Context> {
61 public enum TargetType {
62 CONTROL("control", Control.class),
63 PARAM("param", Parameter.class),
64 PART("part", ControlPart.class);
65
66 @NonNull
67 private static final Map<Class<?>, TargetType> CLASS_TO_TYPE;
68 @NonNull
69 private static final Map<String, TargetType> NAME_TO_TYPE;
70 @NonNull
71 private final String fieldName;
72 @NonNull
73 private final Class<?> clazz;
74
75 static {
76 {
77 Map<Class<?>, TargetType> map = new ConcurrentHashMap<>();
78 for (TargetType type : TargetType.values()) {
79 map.put(type.getClazz(), type);
80 }
81 CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map);
82 }
83
84 {
85 Map<String, TargetType> map = new ConcurrentHashMap<>();
86 for (TargetType type : TargetType.values()) {
87 map.put(type.fieldName(), type);
88 }
89 NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map);
90 }
91 }
92
93
94
95
96
97
98
99
100
101 @Nullable
102 public static TargetType forClass(@NonNull Class<?> clazz) {
103 Class<?> target = clazz;
104 TargetType retval;
105
106 do {
107 retval = CLASS_TO_TYPE.get(target);
108 } while (retval == null && (target = target.getSuperclass()) != null);
109 return retval;
110 }
111
112
113
114
115
116
117
118
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
132
133
134
135 public String fieldName() {
136 return fieldName;
137 }
138
139
140
141
142
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
168
169
170
171
172
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
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
243 throw new UnsupportedOperationException("not needed");
244 }
245
246 @Override
247 public Boolean visitGroup(CatalogGroup group, Context context) {
248
249 throw new UnsupportedOperationException("not needed");
250 }
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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
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 {
323 oldItems.addAll(newItems);
324 }
325 }
326 }
327 }
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
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
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
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
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
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
448 retval = retval || handleChild(
449 TargetType.PARAM,
450 () -> control.getParams(),
451 () -> context.getParams(),
452 child -> visitParameter(ObjectUtils.notNull(child), context),
453 context);
454
455
456 retval = retval || handleChild(
457 TargetType.PART,
458 () -> control.getParts(),
459 () -> context.getParts(),
460 child -> visitPart(child, context),
461 context);
462
463
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
496
497
498
499
500
501
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
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
709
710
711
712
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
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 }