001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.oscal.lib.profile.resolver.alter;
007
008import java.util.Collection;
009import java.util.Collections;
010import java.util.EnumMap;
011import java.util.EnumSet;
012import java.util.Iterator;
013import java.util.List;
014import java.util.Locale;
015import java.util.Map;
016import java.util.Set;
017import java.util.concurrent.ConcurrentHashMap;
018import java.util.function.Function;
019import java.util.function.Supplier;
020
021import dev.metaschema.core.util.CollectionUtil;
022import dev.metaschema.core.util.ObjectUtils;
023import dev.metaschema.oscal.lib.model.Catalog;
024import dev.metaschema.oscal.lib.model.CatalogGroup;
025import dev.metaschema.oscal.lib.model.Control;
026import dev.metaschema.oscal.lib.model.ControlPart;
027import dev.metaschema.oscal.lib.model.Link;
028import dev.metaschema.oscal.lib.model.Parameter;
029import dev.metaschema.oscal.lib.model.Property;
030import dev.metaschema.oscal.lib.model.control.catalog.ICatalogVisitor;
031import dev.metaschema.oscal.lib.model.metadata.IProperty;
032import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
033import edu.umd.cs.findbugs.annotations.NonNull;
034import edu.umd.cs.findbugs.annotations.Nullable;
035
036public class RemoveVisitor implements ICatalogVisitor<Boolean, RemoveVisitor.Context> {
037  public enum TargetType {
038    PARAM("param", Parameter.class),
039    PROP("prop", Property.class),
040    LINK("link", Link.class),
041    PART("part", ControlPart.class);
042
043    @NonNull
044    private static final Map<Class<?>, TargetType> CLASS_TO_TYPE;
045    @NonNull
046    private static final Map<String, TargetType> NAME_TO_TYPE;
047    @NonNull
048    private final String fieldName;
049    @NonNull
050    private final Class<?> clazz;
051
052    static {
053      {
054        Map<Class<?>, TargetType> map = new ConcurrentHashMap<>();
055        for (TargetType type : values()) {
056          map.put(type.getClazz(), type);
057        }
058        CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map);
059      }
060
061      {
062        Map<String, TargetType> map = new ConcurrentHashMap<>();
063        for (TargetType type : values()) {
064          map.put(type.fieldName(), type);
065        }
066        NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map);
067      }
068    }
069
070    /**
071     * Get the target type associated with the provided {@code clazz}.
072     *
073     * @param clazz
074     *          the class to identify the target type for
075     * @return the associated target type or {@code null} if the class is not
076     *         associated with a target type
077     */
078    @Nullable
079    public static TargetType forClass(@NonNull Class<?> clazz) {
080      Class<?> target = clazz;
081      TargetType retval;
082      // recurse over parent classes to find a match
083      do {
084        retval = CLASS_TO_TYPE.get(target);
085      } while (retval == null && (target = target.getSuperclass()) != null);
086      return retval;
087    }
088
089    /**
090     * Get the target type associated with the provided field {@code name}.
091     *
092     * @param name
093     *          the field name to identify the target type for
094     * @return the associated target type or {@code null} if the name is not
095     *         associated with a target type
096     */
097    @Nullable
098    public static TargetType forFieldName(@Nullable String name) {
099      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}