1
2
3
4
5
6 package dev.metaschema.oscal.lib.profile.resolver.alter;
7
8 import java.util.Collection;
9 import java.util.Collections;
10 import java.util.EnumMap;
11 import java.util.EnumSet;
12 import java.util.Iterator;
13 import java.util.List;
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.Function;
19 import java.util.function.Supplier;
20
21 import dev.metaschema.core.util.CollectionUtil;
22 import dev.metaschema.core.util.ObjectUtils;
23 import dev.metaschema.oscal.lib.model.Catalog;
24 import dev.metaschema.oscal.lib.model.CatalogGroup;
25 import dev.metaschema.oscal.lib.model.Control;
26 import dev.metaschema.oscal.lib.model.ControlPart;
27 import dev.metaschema.oscal.lib.model.Link;
28 import dev.metaschema.oscal.lib.model.Parameter;
29 import dev.metaschema.oscal.lib.model.Property;
30 import dev.metaschema.oscal.lib.model.control.catalog.ICatalogVisitor;
31 import dev.metaschema.oscal.lib.model.metadata.IProperty;
32 import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
33 import edu.umd.cs.findbugs.annotations.NonNull;
34 import edu.umd.cs.findbugs.annotations.Nullable;
35
36 public class RemoveVisitor implements ICatalogVisitor<Boolean, RemoveVisitor.Context> {
37 public enum TargetType {
38 PARAM("param", Parameter.class),
39 PROP("prop", Property.class),
40 LINK("link", Link.class),
41 PART("part", ControlPart.class);
42
43 @NonNull
44 private static final Map<Class<?>, TargetType> CLASS_TO_TYPE;
45 @NonNull
46 private static final Map<String, TargetType> NAME_TO_TYPE;
47 @NonNull
48 private final String fieldName;
49 @NonNull
50 private final Class<?> clazz;
51
52 static {
53 {
54 Map<Class<?>, TargetType> map = new ConcurrentHashMap<>();
55 for (TargetType type : values()) {
56 map.put(type.getClazz(), type);
57 }
58 CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map);
59 }
60
61 {
62 Map<String, TargetType> map = new ConcurrentHashMap<>();
63 for (TargetType type : values()) {
64 map.put(type.fieldName(), type);
65 }
66 NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map);
67 }
68 }
69
70
71
72
73
74
75
76
77
78 @Nullable
79 public static TargetType forClass(@NonNull Class<?> clazz) {
80 Class<?> target = clazz;
81 TargetType retval;
82
83 do {
84 retval = CLASS_TO_TYPE.get(target);
85 } while (retval == null && (target = target.getSuperclass()) != null);
86 return retval;
87 }
88
89
90
91
92
93
94
95
96
97 @Nullable
98 public static TargetType forFieldName(@Nullable String name) {
99 return name == null ? null : NAME_TO_TYPE.get(name);
100 }
101
102 TargetType(@NonNull String fieldName, @NonNull Class<?> clazz) {
103 this.fieldName = fieldName;
104 this.clazz = clazz;
105 }
106
107
108
109
110
111
112 public String fieldName() {
113 return fieldName;
114 }
115
116
117
118
119
120
121 public Class<?> getClazz() {
122 return clazz;
123 }
124
125 }
126
127 @NonNull
128 private static final RemoveVisitor INSTANCE = new RemoveVisitor();
129
130 private static final Map<TargetType, Set<TargetType>> APPLICABLE_TARGETS;
131
132 static {
133 APPLICABLE_TARGETS = new EnumMap<>(TargetType.class);
134 APPLICABLE_TARGETS.put(TargetType.PARAM, Set.of(TargetType.PROP, TargetType.LINK));
135 APPLICABLE_TARGETS.put(TargetType.PART, Set.of(TargetType.PART, TargetType.PROP, TargetType.LINK));
136 }
137
138 private static Set<TargetType> getApplicableTypes(@NonNull TargetType type) {
139 return APPLICABLE_TARGETS.getOrDefault(type, CollectionUtil.emptySet());
140 }
141
142 private static <T> boolean handle(
143 @NonNull TargetType itemType,
144 @NonNull Supplier<? extends Collection<T>> supplier,
145 @Nullable Function<T, Boolean> handler,
146 @NonNull Context context) {
147
148 boolean handleChildren = !Collections.disjoint(context.getTargetItemTypes(), getApplicableTypes(itemType));
149 boolean retval = false;
150 if (context.isMatchingType(itemType)) {
151
152 Iterator<T> iter = supplier.get().iterator();
153 while (iter.hasNext()) {
154 T item = iter.next();
155
156 if (item == null || context.isApplicableTo(item)) {
157 iter.remove();
158 retval = true;
159
160 } else if (handler != null && handleChildren) {
161
162 retval = retval || handler.apply(item);
163 }
164 }
165 } else if (handleChildren && handler != null) {
166 for (T item : supplier.get()) {
167 if (item != null) {
168 retval = retval || handler.apply(item);
169 }
170 }
171 }
172 return retval;
173 }
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194 public static boolean remove(
195 @NonNull Control control,
196 @Nullable String objectName,
197 @Nullable String objectClass,
198 @Nullable String objectId,
199 @Nullable String objectNamespace,
200 @Nullable TargetType itemType) {
201 return INSTANCE.visitControl(
202 control,
203 new Context(objectName, objectClass, objectId, objectNamespace, itemType));
204 }
205
206 @Override
207 public Boolean visitCatalog(Catalog catalog, Context context) {
208
209 throw new UnsupportedOperationException("not needed");
210 }
211
212 @Override
213 public Boolean visitGroup(CatalogGroup group, Context context) {
214
215 throw new UnsupportedOperationException("not needed");
216 }
217
218 @NonNull
219 private static <T> List<T> modifiableListOrEmpty(@Nullable List<T> list) {
220 return list == null ? CollectionUtil.emptyList() : list;
221 }
222
223 @Override
224 public Boolean visitControl(Control control, Context context) {
225 assert context != null;
226
227
228 boolean retval = handle(
229 TargetType.PARAM,
230 () -> modifiableListOrEmpty(control.getParams()),
231 child -> visitParameter(ObjectUtils.notNull(child), context),
232 context);
233
234
235 retval = retval || handle(
236 TargetType.PROP,
237 () -> modifiableListOrEmpty(control.getProps()),
238 null,
239 context);
240
241
242 retval = retval || handle(
243 TargetType.LINK,
244 () -> modifiableListOrEmpty(control.getLinks()),
245 null,
246 context);
247
248 return retval || handle(
249 TargetType.PART,
250 () -> modifiableListOrEmpty(control.getParts()),
251 child -> visitPart(child, context),
252 context);
253 }
254
255 @Override
256 public Boolean visitParameter(Parameter parameter, Context context) {
257 assert context != null;
258
259
260 boolean retval = handle(
261 TargetType.PROP,
262 () -> modifiableListOrEmpty(parameter.getProps()),
263 null,
264 context);
265
266 return retval || handle(
267 TargetType.LINK,
268 () -> modifiableListOrEmpty(parameter.getLinks()),
269 null,
270 context);
271 }
272
273
274
275
276
277
278
279
280
281
282 public boolean visitPart(ControlPart part, Context context) {
283 assert context != null;
284
285
286 boolean retval = handle(
287 TargetType.PROP,
288 () -> modifiableListOrEmpty(part.getProps()),
289 null,
290 context);
291
292
293 retval = retval || handle(
294 TargetType.LINK,
295 () -> modifiableListOrEmpty(part.getLinks()),
296 null,
297 context);
298
299 return retval || handle(
300 TargetType.PART,
301 () -> modifiableListOrEmpty(part.getParts()),
302 child -> visitPart(child, context),
303 context);
304 }
305
306 static final class Context {
307
308
309
310 @NonNull
311 private static final Set<TargetType> NAME_TYPES = ObjectUtils.notNull(
312 Set.of(TargetType.PART, TargetType.PROP));
313
314
315
316 @NonNull
317 private static final Set<TargetType> CLASS_TYPES = ObjectUtils.notNull(
318 Set.of(TargetType.PARAM, TargetType.PART, TargetType.PROP));
319
320
321
322 @NonNull
323 private static final Set<TargetType> ID_TYPES = ObjectUtils.notNull(
324 Set.of(TargetType.PARAM, TargetType.PART));
325
326
327
328 @NonNull
329 private static final Set<TargetType> NAMESPACE_TYPES = ObjectUtils.notNull(
330 Set.of(TargetType.PART, TargetType.PROP));
331
332 @Nullable
333 private final String objectName;
334 @Nullable
335 private final String objectClass;
336 @Nullable
337 private final String objectId;
338 @Nullable
339 private final String objectNamespace;
340 @NonNull
341 private final Set<TargetType> targetItemTypes;
342
343 private static boolean filterTypes(
344 @NonNull Set<TargetType> effectiveTypes,
345 @NonNull String criteria,
346 @NonNull Set<TargetType> allowedTypes,
347 @Nullable String value,
348 @Nullable TargetType itemType) {
349 boolean retval = false;
350 if (value != null) {
351 retval = effectiveTypes.retainAll(allowedTypes);
352 if (itemType != null && !allowedTypes.contains(itemType)) {
353 throw new ProfileResolutionEvaluationException(
354 String.format("%s='%s' is not supported for items of type '%s'",
355 criteria,
356 value,
357 itemType.fieldName()));
358 }
359 }
360 return retval;
361 }
362
363 private Context(
364 @Nullable String objectName,
365 @Nullable String objectClass,
366 @Nullable String objectId,
367 @Nullable String objectNamespace,
368 @Nullable TargetType itemType) {
369
370
371
372
373 @NonNull
374 Set<TargetType> targetItemTypes = ObjectUtils.notNull(EnumSet.allOf(TargetType.class));
375 filterTypes(targetItemTypes, "by-name", NAME_TYPES, objectName, itemType);
376 filterTypes(targetItemTypes, "by-class", CLASS_TYPES, objectClass, itemType);
377 filterTypes(targetItemTypes, "by-id", ID_TYPES, objectId, itemType);
378 filterTypes(targetItemTypes, "by-ns", NAMESPACE_TYPES, objectNamespace, itemType);
379
380 if (itemType != null) {
381 targetItemTypes.retainAll(Set.of(itemType));
382 }
383
384 if (targetItemTypes.isEmpty()) {
385 throw new ProfileResolutionEvaluationException("The filter matches no available item types");
386 }
387
388 this.objectName = objectName;
389 this.objectClass = objectClass;
390 this.objectId = objectId;
391 this.objectNamespace = objectNamespace;
392 this.targetItemTypes = CollectionUtil.unmodifiableSet(targetItemTypes);
393 }
394
395 @Nullable
396 public String getObjectName() {
397 return objectName;
398 }
399
400 @Nullable
401 public String getObjectClass() {
402 return objectClass;
403 }
404
405 @Nullable
406 public String getObjectId() {
407 return objectId;
408 }
409
410 @NonNull
411 public Set<TargetType> getTargetItemTypes() {
412 return targetItemTypes;
413 }
414
415 public boolean isMatchingType(@NonNull TargetType type) {
416 return getTargetItemTypes().contains(type);
417 }
418
419 @Nullable
420 public String getObjectNamespace() {
421 return objectNamespace;
422 }
423
424 private static boolean checkValue(@Nullable String actual, @Nullable String expected) {
425 return expected == null || expected.equals(actual);
426 }
427
428 public boolean isApplicableTo(@NonNull Object obj) {
429 TargetType objectType = TargetType.forClass(obj.getClass());
430
431 boolean retval = objectType != null && getTargetItemTypes().contains(objectType);
432 if (retval) {
433 assert objectType != null;
434
435
436 String actualName = null;
437 String actualClass = null;
438 String actualId = null;
439 String actualNamespace = null;
440
441 switch (objectType) {
442 case PARAM: {
443 Parameter param = (Parameter) obj;
444 actualClass = param.getClazz();
445 actualId = param.getId();
446 break;
447 }
448 case PROP: {
449 Property prop = (Property) obj;
450 actualName = prop.getName();
451 actualClass = prop.getClazz();
452 actualNamespace = IProperty.normalizeNamespace(prop.getNs()).toASCIIString();
453 break;
454 }
455 case PART: {
456 ControlPart part = (ControlPart) obj;
457 actualName = part.getName();
458 actualClass = part.getClazz();
459 String partId = part.getId();
460 if (partId != null) {
461 actualId = partId;
462 }
463 actualNamespace = IProperty.normalizeNamespace(part.getNs()).toASCIIString();
464 break;
465 }
466 case LINK:
467
468 break;
469 default:
470 throw new UnsupportedOperationException(objectType.name().toLowerCase(Locale.ROOT));
471 }
472
473 retval = checkValue(actualName, getObjectName())
474 && checkValue(actualClass, getObjectClass())
475 && checkValue(actualId, getObjectId())
476 && checkValue(actualNamespace, getObjectNamespace());
477 }
478 return retval;
479 }
480 }
481 }