001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.oscal.lib.profile.resolver.alter;
028
029import gov.nist.secauto.metaschema.model.common.util.CollectionUtil;
030import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
031import gov.nist.secauto.oscal.lib.model.Catalog;
032import gov.nist.secauto.oscal.lib.model.CatalogGroup;
033import gov.nist.secauto.oscal.lib.model.Control;
034import gov.nist.secauto.oscal.lib.model.ControlPart;
035import gov.nist.secauto.oscal.lib.model.Link;
036import gov.nist.secauto.oscal.lib.model.Parameter;
037import gov.nist.secauto.oscal.lib.model.Property;
038import gov.nist.secauto.oscal.lib.model.control.catalog.ICatalogVisitor;
039import gov.nist.secauto.oscal.lib.model.metadata.IProperty;
040import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
041
042import java.util.Collection;
043import java.util.Collections;
044import java.util.EnumMap;
045import java.util.EnumSet;
046import java.util.Iterator;
047import java.util.Locale;
048import java.util.Map;
049import java.util.Set;
050import java.util.concurrent.ConcurrentHashMap;
051import java.util.function.Function;
052import java.util.function.Supplier;
053
054import edu.umd.cs.findbugs.annotations.NonNull;
055import edu.umd.cs.findbugs.annotations.Nullable;
056
057public class RemoveVisitor implements ICatalogVisitor<Boolean, RemoveVisitor.Context> {
058  public enum TargetType {
059    PARAM("param", Parameter.class),
060    PROP("prop", Property.class),
061    LINK("link", Link.class),
062    PART("part", ControlPart.class);
063
064    @NonNull
065    private static final Map<Class<?>, TargetType> CLASS_TO_TYPE;
066    @NonNull
067    private static final Map<String, TargetType> NAME_TO_TYPE;
068    @NonNull
069    private final String fieldName;
070    @NonNull
071    private final Class<?> clazz;
072
073    static {
074      {
075        Map<Class<?>, TargetType> map = new ConcurrentHashMap<>();
076        for (TargetType type : TargetType.values()) {
077          map.put(type.getClazz(), type);
078        }
079        CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map);
080      }
081
082      {
083        Map<String, TargetType> map = new ConcurrentHashMap<>();
084        for (TargetType type : TargetType.values()) {
085          map.put(type.fieldName(), type);
086        }
087        NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map);
088      }
089    }
090
091    /**
092     * Get the target type associated with the provided {@code clazz}.
093     *
094     * @param clazz
095     *          the class to identify the target type for
096     * @return the associated target type or {@code null} if the class is not
097     *         associated with a target type
098     */
099    @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}