View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.oscal.lib.profile.resolver.alter;
28  
29  import gov.nist.secauto.metaschema.model.common.util.CollectionUtil;
30  import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
31  import gov.nist.secauto.oscal.lib.model.Catalog;
32  import gov.nist.secauto.oscal.lib.model.CatalogGroup;
33  import gov.nist.secauto.oscal.lib.model.Control;
34  import gov.nist.secauto.oscal.lib.model.ControlPart;
35  import gov.nist.secauto.oscal.lib.model.Link;
36  import gov.nist.secauto.oscal.lib.model.Parameter;
37  import gov.nist.secauto.oscal.lib.model.Property;
38  import gov.nist.secauto.oscal.lib.model.control.catalog.ICatalogVisitor;
39  import gov.nist.secauto.oscal.lib.model.metadata.IProperty;
40  import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
41  
42  import java.util.Collection;
43  import java.util.Collections;
44  import java.util.EnumMap;
45  import java.util.EnumSet;
46  import java.util.Iterator;
47  import java.util.Locale;
48  import java.util.Map;
49  import java.util.Set;
50  import java.util.concurrent.ConcurrentHashMap;
51  import java.util.function.Function;
52  import java.util.function.Supplier;
53  
54  import edu.umd.cs.findbugs.annotations.NonNull;
55  import edu.umd.cs.findbugs.annotations.Nullable;
56  
57  public class RemoveVisitor implements ICatalogVisitor<Boolean, RemoveVisitor.Context> {
58    public enum TargetType {
59      PARAM("param", Parameter.class),
60      PROP("prop", Property.class),
61      LINK("link", Link.class),
62      PART("part", ControlPart.class);
63  
64      @NonNull
65      private static final Map<Class<?>, TargetType> CLASS_TO_TYPE;
66      @NonNull
67      private static final Map<String, TargetType> NAME_TO_TYPE;
68      @NonNull
69      private final String fieldName;
70      @NonNull
71      private final Class<?> clazz;
72  
73      static {
74        {
75          Map<Class<?>, TargetType> map = new ConcurrentHashMap<>();
76          for (TargetType type : TargetType.values()) {
77            map.put(type.getClazz(), type);
78          }
79          CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map);
80        }
81  
82        {
83          Map<String, TargetType> map = new ConcurrentHashMap<>();
84          for (TargetType type : TargetType.values()) {
85            map.put(type.fieldName(), type);
86          }
87          NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map);
88        }
89      }
90  
91      /**
92       * Get the target type associated with the provided {@code clazz}.
93       *
94       * @param clazz
95       *          the class to identify the target type for
96       * @return the associated target type or {@code null} if the class is not
97       *         associated with a target type
98       */
99      @Nullable
100     public static TargetType forClass(@NonNull Class<?> clazz) {
101       Class<?> target = clazz;
102       TargetType retval;
103       // recurse over parent classes to find a match
104       do {
105         retval = CLASS_TO_TYPE.get(target);
106       } while (retval == null && (target = target.getSuperclass()) != null);
107       return retval;
108     }
109 
110     /**
111      * Get the target type associated with the provided field {@code name}.
112      *
113      * @param name
114      *          the field name to identify the target type for
115      * @return the associated target type or {@code null} if the name is not
116      *         associated with a target type
117      */
118     @Nullable
119     public static TargetType forFieldName(@Nullable String name) {
120       return name == null ? null : NAME_TO_TYPE.get(name);
121     }
122 
123     TargetType(@NonNull String fieldName, @NonNull Class<?> clazz) {
124       this.fieldName = fieldName;
125       this.clazz = clazz;
126     }
127 
128     /**
129      * Get the field name associated with the target type.
130      *
131      * @return the name
132      */
133     public String fieldName() {
134       return fieldName;
135     }
136 
137     /**
138      * Get the bound class associated with the target type.
139      *
140      * @return the class
141      */
142     public Class<?> getClazz() {
143       return clazz;
144     }
145 
146   }
147 
148   @NonNull
149   private static final RemoveVisitor INSTANCE = new RemoveVisitor();
150 
151   private static final Map<TargetType, Set<TargetType>> APPLICABLE_TARGETS;
152 
153   static {
154     APPLICABLE_TARGETS = new EnumMap<>(TargetType.class);
155     APPLICABLE_TARGETS.put(TargetType.PARAM, Set.of(TargetType.PROP, TargetType.LINK));
156     APPLICABLE_TARGETS.put(TargetType.PART, Set.of(TargetType.PART, TargetType.PROP, TargetType.LINK));
157   }
158 
159   private static Set<TargetType> getApplicableTypes(@NonNull TargetType type) {
160     return APPLICABLE_TARGETS.getOrDefault(type, CollectionUtil.emptySet());
161   }
162 
163   private static <T> boolean handle(
164       @NonNull TargetType itemType,
165       @NonNull Supplier<? extends Collection<T>> supplier,
166       @Nullable Function<T, Boolean> handler,
167       @NonNull Context context) {
168 
169     boolean handleChildren = !Collections.disjoint(context.getTargetItemTypes(), getApplicableTypes(itemType));
170     boolean retval = false;
171     if (context.isMatchingType(itemType)) {
172       // if the item type is applicable, attempt to remove any items
173       Iterator<T> iter = supplier.get().iterator();
174       while (iter.hasNext()) {
175         T item = iter.next();
176 
177         if (item == null || context.isApplicableTo(item)) {
178           iter.remove();
179           retval = true;
180           // ignore removed items and their children
181         } else if (handler != null && handleChildren) {
182           // handle child items since they are applicable to the search criteria
183           retval = retval || handler.apply(item);
184         }
185       }
186     } else if (handleChildren && handler != null) {
187       // if the child item type is applicable and there is a handler, iterate over
188       // children
189       Iterator<T> iter = supplier.get().iterator();
190       while (iter.hasNext()) {
191         T item = iter.next();
192         if (item != null) {
193           retval = retval || handler.apply(item);
194         }
195       }
196     }
197     return retval;
198   }
199 
200   /**
201    * Apply the remove directive.
202    *
203    * @param control
204    *          the control target
205    * @param objectName
206    *          the name flag of a matching node to remove
207    * @param objectClass
208    *          the class flag of a matching node to remove
209    * @param objectId
210    *          the id flag of a matching node to remove
211    * @param objectNamespace
212    *          the namespace flag of a matching node to remove
213    * @param itemType
214    *          the type of a matching node to remove
215    * @return {@code true} if the modification was made or {@code false} otherwise
216    * @throws ProfileResolutionEvaluationException
217    *           if a processing error occurred during profile resolution
218    */
219   public static boolean remove(
220       @NonNull Control control,
221       @Nullable String objectName,
222       @Nullable String objectClass,
223       @Nullable String objectId,
224       @Nullable String objectNamespace,
225       @Nullable TargetType itemType) {
226     return INSTANCE.visitControl(
227         control,
228         new Context(objectName, objectClass, objectId, objectNamespace, itemType));
229   }
230 
231   @Override
232   public Boolean visitCatalog(Catalog catalog, Context context) {
233     // not required
234     throw new UnsupportedOperationException("not needed");
235   }
236 
237   @Override
238   public Boolean visitGroup(CatalogGroup group, Context context) {
239     // not required
240     throw new UnsupportedOperationException("not needed");
241   }
242 
243   @Override
244   public Boolean visitControl(Control control, Context context) {
245     assert context != null;
246 
247     // visit params
248     boolean retval = handle(
249         TargetType.PARAM,
250         () -> CollectionUtil.listOrEmpty(control.getParams()),
251         child -> visitParameter(ObjectUtils.notNull(child), context),
252         context);
253 
254     // visit props
255     retval = retval || handle(
256         TargetType.PROP,
257         () -> CollectionUtil.listOrEmpty(control.getProps()),
258         null,
259         context);
260 
261     // visit links
262     retval = retval || handle(
263         TargetType.LINK,
264         () -> CollectionUtil.listOrEmpty(control.getLinks()),
265         null,
266         context);
267 
268     // visit parts
269     retval = retval || handle(
270         TargetType.PART,
271         () -> CollectionUtil.listOrEmpty(control.getParts()),
272         child -> visitPart(child, context),
273         context);
274 
275     return retval;
276   }
277 
278   @Override
279   public Boolean visitParameter(Parameter parameter, Context context) {
280     assert context != null;
281 
282     // visit props
283     boolean retval = handle(
284         TargetType.PROP,
285         () -> CollectionUtil.listOrEmpty(parameter.getProps()),
286         null,
287         context);
288 
289     // visit links
290     retval = retval || handle(
291         TargetType.LINK,
292         () -> CollectionUtil.listOrEmpty(parameter.getLinks()),
293         null,
294         context);
295     return retval;
296   }
297 
298   /**
299    * Visit the control part.
300    *
301    * @param part
302    *          the bound part object
303    * @param context
304    *          the visitor context
305    * @return {@code true} if the removal was applied or {@code false} otherwise
306    */
307   public boolean visitPart(ControlPart part, Context context) {
308     assert context != null;
309 
310     // visit props
311     boolean retval = handle(
312         TargetType.PROP,
313         () -> CollectionUtil.listOrEmpty(part.getProps()),
314         null,
315         context);
316 
317     // visit links
318     retval = retval || handle(
319         TargetType.LINK,
320         () -> CollectionUtil.listOrEmpty(part.getLinks()),
321         null,
322         context);
323 
324     // visit parts
325     retval = retval || handle(
326         TargetType.PART,
327         () -> CollectionUtil.listOrEmpty(part.getParts()),
328         child -> visitPart(child, context),
329         context);
330     return retval;
331   }
332 
333   static final class Context {
334     /**
335      * Types with an "name" flag.
336      */
337     @NonNull
338     private static final Set<TargetType> NAME_TYPES = ObjectUtils.notNull(
339         Set.of(TargetType.PART, TargetType.PROP));
340     /**
341      * Types with an "class" flag.
342      */
343     @NonNull
344     private static final Set<TargetType> CLASS_TYPES = ObjectUtils.notNull(
345         Set.of(TargetType.PARAM, TargetType.PART, TargetType.PROP));
346     /**
347      * Types with an "id" flag.
348      */
349     @NonNull
350     private static final Set<TargetType> ID_TYPES = ObjectUtils.notNull(
351         Set.of(TargetType.PARAM, TargetType.PART));
352     /**
353      * Types with an "ns" flag.
354      */
355     @NonNull
356     private static final Set<TargetType> NAMESPACE_TYPES = ObjectUtils.notNull(
357         Set.of(TargetType.PART, TargetType.PROP));
358 
359     @Nullable
360     private final String objectName;
361     @Nullable
362     private final String objectClass;
363     @Nullable
364     private final String objectId;
365     @Nullable
366     private final String objectNamespace;
367     @NonNull
368     private final Set<TargetType> targetItemTypes;
369 
370     private static boolean filterTypes(
371         @NonNull Set<TargetType> effectiveTypes,
372         @NonNull String criteria,
373         @NonNull Set<TargetType> allowedTypes,
374         @Nullable String value,
375         @Nullable TargetType itemType) {
376       boolean retval = false;
377       if (value != null) {
378         retval = effectiveTypes.retainAll(allowedTypes);
379         if (itemType != null && !allowedTypes.contains(itemType)) {
380           throw new ProfileResolutionEvaluationException(
381               String.format("%s='%s' is not supported for items of type '%s'",
382                   criteria,
383                   value,
384                   itemType.fieldName()));
385         }
386       }
387       return retval;
388     }
389 
390     private Context(
391         @Nullable String objectName,
392         @Nullable String objectClass,
393         @Nullable String objectId,
394         @Nullable String objectNamespace,
395         @Nullable TargetType itemType) {
396 
397       // determine the set of effective item types to search for
398       // this helps with short-circuit searching for parts of the graph that cannot
399       // match
400       @NonNull Set<TargetType> targetItemTypes = ObjectUtils.notNull(EnumSet.allOf(TargetType.class));
401       filterTypes(targetItemTypes, "by-name", NAME_TYPES, objectName, itemType);
402       filterTypes(targetItemTypes, "by-class", CLASS_TYPES, objectClass, itemType);
403       filterTypes(targetItemTypes, "by-id", ID_TYPES, objectId, itemType);
404       filterTypes(targetItemTypes, "by-ns", NAMESPACE_TYPES, objectNamespace, itemType);
405 
406       if (itemType != null) {
407         targetItemTypes.retainAll(Set.of(itemType));
408       }
409 
410       if (targetItemTypes.isEmpty()) {
411         throw new ProfileResolutionEvaluationException("The filter matches no available item types");
412       }
413 
414       this.objectName = objectName;
415       this.objectClass = objectClass;
416       this.objectId = objectId;
417       this.objectNamespace = objectNamespace;
418       this.targetItemTypes = CollectionUtil.unmodifiableSet(targetItemTypes);
419     }
420 
421     @Nullable
422     public String getObjectName() {
423       return objectName;
424     }
425 
426     @Nullable
427     public String getObjectClass() {
428       return objectClass;
429     }
430 
431     @Nullable
432     public String getObjectId() {
433       return objectId;
434     }
435 
436     @NonNull
437     public Set<TargetType> getTargetItemTypes() {
438       return targetItemTypes;
439     }
440 
441     public boolean isMatchingType(@NonNull TargetType type) {
442       return getTargetItemTypes().contains(type);
443     }
444 
445     @Nullable
446     public String getObjectNamespace() {
447       return objectNamespace;
448     }
449 
450     private static boolean checkValue(@Nullable String actual, @Nullable String expected) {
451       return expected == null || expected.equals(actual);
452     }
453 
454     public boolean isApplicableTo(@NonNull Object obj) {
455       TargetType objectType = TargetType.forClass(obj.getClass());
456 
457       boolean retval = objectType != null && getTargetItemTypes().contains(objectType);
458       if (retval) {
459         assert objectType != null;
460 
461         // check other criteria
462         String actualName = null;
463         String actualClass = null;
464         String actualId = null;
465         String actualNamespace = null;
466 
467         switch (objectType) {
468         case PARAM: {
469           Parameter param = (Parameter) obj;
470           actualClass = param.getClazz();
471           actualId = param.getId();
472           break;
473         }
474         case PROP: {
475           Property prop = (Property) obj;
476           actualName = prop.getName();
477           actualClass = prop.getClazz();
478           actualNamespace = prop.getNs() == null ? IProperty.OSCAL_NAMESPACE.toString() : prop.getNs().toString();
479           break;
480         }
481         case PART: {
482           ControlPart part = (ControlPart) obj;
483           actualName = part.getName();
484           actualClass = part.getClazz();
485           actualId = part.getId() == null ? null : part.getId().toString();
486           actualNamespace = part.getNs() == null ? IProperty.OSCAL_NAMESPACE.toString() : part.getNs().toString();
487           break;
488         }
489         case LINK:
490           // do nothing
491           break;
492         default:
493           throw new UnsupportedOperationException(objectType.name().toLowerCase(Locale.ROOT));
494         }
495 
496         retval = checkValue(actualName, getObjectName())
497             && checkValue(actualClass, getObjectClass())
498             && checkValue(actualId, getObjectId())
499             && checkValue(actualNamespace, getObjectNamespace());
500       }
501       return retval;
502     }
503   }
504 }