1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
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       * Get the target type associated with the provided {@code clazz}.
72       *
73       * @param clazz
74       *          the class to identify the target type for
75       * @return the associated target type or {@code null} if the class is not
76       *         associated with a target type
77       */
78      @Nullable
79      public static TargetType forClass(@NonNull Class<?> clazz) {
80        Class<?> target = clazz;
81        TargetType retval;
82        // recurse over parent classes to find a match
83        do {
84          retval = CLASS_TO_TYPE.get(target);
85        } while (retval == null && (target = target.getSuperclass()) != null);
86        return retval;
87      }
88  
89      /**
90       * Get the target type associated with the provided field {@code name}.
91       *
92       * @param name
93       *          the field name to identify the target type for
94       * @return the associated target type or {@code null} if the name is not
95       *         associated with a target type
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      * Get the field name associated with the target type.
109      *
110      * @return the name
111      */
112     public String fieldName() {
113       return fieldName;
114     }
115 
116     /**
117      * Get the bound class associated with the target type.
118      *
119      * @return the class
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       // if the item type is applicable, attempt to remove any items
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           // ignore removed items and their children
160         } else if (handler != null && handleChildren) {
161           // handle child items since they are applicable to the search criteria
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    * Apply the remove directive.
177    *
178    * @param control
179    *          the control target
180    * @param objectName
181    *          the name flag of a matching node to remove
182    * @param objectClass
183    *          the class flag of a matching node to remove
184    * @param objectId
185    *          the id flag of a matching node to remove
186    * @param objectNamespace
187    *          the namespace flag of a matching node to remove
188    * @param itemType
189    *          the type of a matching node to remove
190    * @return {@code true} if the modification was made or {@code false} otherwise
191    * @throws ProfileResolutionEvaluationException
192    *           if a processing error occurred during profile resolution
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     // not required
209     throw new UnsupportedOperationException("not needed");
210   }
211 
212   @Override
213   public Boolean visitGroup(CatalogGroup group, Context context) {
214     // not required
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     // visit params
228     boolean retval = handle(
229         TargetType.PARAM,
230         () -> modifiableListOrEmpty(control.getParams()),
231         child -> visitParameter(ObjectUtils.notNull(child), context),
232         context);
233 
234     // visit props
235     retval = retval || handle(
236         TargetType.PROP,
237         () -> modifiableListOrEmpty(control.getProps()),
238         null,
239         context);
240 
241     // visit links
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     // visit props
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    * Visit the control part.
275    *
276    * @param part
277    *          the bound part object
278    * @param context
279    *          the visitor context
280    * @return {@code true} if the removal was applied or {@code false} otherwise
281    */
282   public boolean visitPart(ControlPart part, Context context) {
283     assert context != null;
284 
285     // visit props
286     boolean retval = handle(
287         TargetType.PROP,
288         () -> modifiableListOrEmpty(part.getProps()),
289         null,
290         context);
291 
292     // visit links
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      * Types with an "name" flag.
309      */
310     @NonNull
311     private static final Set<TargetType> NAME_TYPES = ObjectUtils.notNull(
312         Set.of(TargetType.PART, TargetType.PROP));
313     /**
314      * Types with an "class" flag.
315      */
316     @NonNull
317     private static final Set<TargetType> CLASS_TYPES = ObjectUtils.notNull(
318         Set.of(TargetType.PARAM, TargetType.PART, TargetType.PROP));
319     /**
320      * Types with an "id" flag.
321      */
322     @NonNull
323     private static final Set<TargetType> ID_TYPES = ObjectUtils.notNull(
324         Set.of(TargetType.PARAM, TargetType.PART));
325     /**
326      * Types with an "ns" flag.
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       // determine the set of effective item types to search for
371       // this helps with short-circuit searching for parts of the graph that cannot
372       // match
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         // check other criteria
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           // do nothing
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 }