1
2
3
4
5
6 package dev.metaschema.oscal.lib.profile.resolver.alter;
7
8 import java.util.Collections;
9 import java.util.EnumMap;
10 import java.util.EnumSet;
11 import java.util.LinkedList;
12 import java.util.List;
13 import java.util.ListIterator;
14 import java.util.Locale;
15 import java.util.Map;
16 import java.util.Set;
17 import java.util.concurrent.ConcurrentHashMap;
18 import java.util.function.Consumer;
19 import java.util.function.Function;
20 import java.util.function.Supplier;
21
22 import dev.metaschema.core.datatype.markup.MarkupLine;
23 import dev.metaschema.core.util.CollectionUtil;
24 import dev.metaschema.core.util.CustomCollectors;
25 import dev.metaschema.core.util.ObjectUtils;
26 import dev.metaschema.oscal.lib.model.Catalog;
27 import dev.metaschema.oscal.lib.model.CatalogGroup;
28 import dev.metaschema.oscal.lib.model.Control;
29 import dev.metaschema.oscal.lib.model.ControlPart;
30 import dev.metaschema.oscal.lib.model.Link;
31 import dev.metaschema.oscal.lib.model.Parameter;
32 import dev.metaschema.oscal.lib.model.Property;
33 import dev.metaschema.oscal.lib.model.control.catalog.ICatalogVisitor;
34 import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
35 import edu.umd.cs.findbugs.annotations.NonNull;
36 import edu.umd.cs.findbugs.annotations.Nullable;
37
38 @SuppressWarnings("PMD.CouplingBetweenObjects")
39 public class AddVisitor implements ICatalogVisitor<Boolean, AddVisitor.Context> {
40 public enum TargetType {
41 CONTROL("control", Control.class),
42 PARAM("param", Parameter.class),
43 PART("part", ControlPart.class);
44
45 @NonNull
46 private static final Map<Class<?>, TargetType> CLASS_TO_TYPE;
47 @NonNull
48 private static final Map<String, TargetType> NAME_TO_TYPE;
49 @NonNull
50 private final String fieldName;
51 @NonNull
52 private final Class<?> clazz;
53
54 static {
55 {
56 Map<Class<?>, TargetType> map = new ConcurrentHashMap<>();
57 for (TargetType type : values()) {
58 map.put(type.getClazz(), type);
59 }
60 CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map);
61 }
62
63 {
64 Map<String, TargetType> map = new ConcurrentHashMap<>();
65 for (TargetType type : values()) {
66 map.put(type.fieldName(), type);
67 }
68 NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map);
69 }
70 }
71
72
73
74
75
76
77
78
79
80 @Nullable
81 public static TargetType forClass(@NonNull Class<?> clazz) {
82 Class<?> target = clazz;
83 TargetType retval;
84
85 do {
86 retval = CLASS_TO_TYPE.get(target);
87 } while (retval == null && (target = target.getSuperclass()) != null);
88 return retval;
89 }
90
91
92
93
94
95
96
97
98
99 @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
111
112
113
114 public String fieldName() {
115 return fieldName;
116 }
117
118
119
120
121
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
147
148
149
150
151
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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
222 throw new UnsupportedOperationException("not needed");
223 }
224
225 @Override
226 public Boolean visitGroup(CatalogGroup group, Context context) {
227
228 throw new UnsupportedOperationException("not needed");
229 }
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
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
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 {
302 oldItems.addAll(newItems);
303 }
304 }
305 }
306 }
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
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
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
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
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
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
412 retval = retval || handleChild(
413 TargetType.PARAM,
414 control::getParams,
415 context::getParams,
416 child -> visitParameter(ObjectUtils.notNull(child), context),
417 context);
418
419
420 retval = retval || handleChild(
421 TargetType.PART,
422 control::getParts,
423 context::getParts,
424 child -> visitPart(child, context),
425 context);
426
427
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
453
454
455
456
457
458
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
670
671
672
673
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
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 }