AbstractCatalogEntityVisitor.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.oscal.lib.profile.resolver.support;

import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.oscal.lib.OscalBindingContext;
import gov.nist.secauto.oscal.lib.OscalModelConstants;

import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
 * Visits a catalog document and its children as designated.
 * <p>
 * This implementation is stateless. The {@code T} parameter can be used to
 * convey state as needed.
 *
 * @param <T>
 *          the state type
 * @param <R>
 *          the result type
 */
public abstract class AbstractCatalogEntityVisitor<T, R>
    extends AbstractCatalogVisitor<T, R> {
  @NonNull
  public static final MetapathExpression CHILD_PART_METAPATH
      = MetapathExpression.compile("part|part//part",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression BACK_MATTER_RESOURCES_METAPATH
      = MetapathExpression.compile("back-matter/resource",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final Set<IEntityItem.ItemType> GROUP_CONTAINER_TYPES
      = ObjectUtils.notNull(EnumSet.of(
          IEntityItem.ItemType.GROUP,
          IEntityItem.ItemType.CONTROL,
          IEntityItem.ItemType.PARAMETER,
          IEntityItem.ItemType.PART));
  @NonNull
  private static final Set<IEntityItem.ItemType> CONTROL_CONTAINER_TYPES
      = ObjectUtils.notNull(EnumSet.of(
          IEntityItem.ItemType.CONTROL,
          IEntityItem.ItemType.PARAMETER,
          IEntityItem.ItemType.PART));
  @NonNull
  private final Set<IEntityItem.ItemType> itemTypesToVisit;

  /**
   * Create a new visitor that will visit the item types identified by
   * {@code itemTypesToVisit}.
   *
   * @param itemTypesToVisit
   *          the item type the visitor will visit
   */
  public AbstractCatalogEntityVisitor(@NonNull Set<IEntityItem.ItemType> itemTypesToVisit) {
    this.itemTypesToVisit = CollectionUtil.unmodifiableSet(itemTypesToVisit);
  }

  public Set<IEntityItem.ItemType> getItemTypesToVisit() {
    return CollectionUtil.unmodifiableSet(itemTypesToVisit);
  }

  protected boolean isVisitedItemType(@NonNull IEntityItem.ItemType type) {
    return itemTypesToVisit.contains(type);
  }

  @Override
  public R visitCatalog(IDocumentNodeItem catalogDocument, T state) {
    R result = super.visitCatalog(catalogDocument, state);

    catalogDocument.modelItems().forEachOrdered(item -> {
      IRootAssemblyNodeItem root = ObjectUtils.requireNonNull((IRootAssemblyNodeItem) item);
      visitMetadata(root, state);
      visitBackMatter(root, state);
    });
    return result;
  }

  @Override
  protected R visitGroupContainer(IAssemblyNodeItem catalogOrGroup, R initialResult, T state) {
    R retval;
    if (Collections.disjoint(getItemTypesToVisit(), GROUP_CONTAINER_TYPES)) {
      retval = initialResult;
    } else {
      retval = super.visitGroupContainer(catalogOrGroup, initialResult, state);
    }
    return retval;
  }

  @Override
  protected R visitControlContainer(IAssemblyNodeItem catalogOrGroupOrControl, R initialResult, T state) {
    R retval;
    if (Collections.disjoint(getItemTypesToVisit(), CONTROL_CONTAINER_TYPES)) {
      retval = initialResult;
    } else {
      // first descend to all control container children
      retval = super.visitControlContainer(catalogOrGroupOrControl, initialResult, state);

      // handle parameters
      if (isVisitedItemType(IEntityItem.ItemType.PARAMETER)) {
        retval = catalogOrGroupOrControl.getModelItemsByName(OscalModelConstants.QNAME_PARAM).stream()
            .map(paramItem -> visitParameter(
                ObjectUtils.requireNonNull((IAssemblyNodeItem) paramItem),
                catalogOrGroupOrControl,
                state))
            .reduce(retval, (first, second) -> aggregateResults(first, second, state));
      }
    }
    return retval;
  }

  protected void visitParts(@NonNull IAssemblyNodeItem groupOrControlItem, T state) {
    // handle parts
    if (isVisitedItemType(IEntityItem.ItemType.PART)) {
      CHILD_PART_METAPATH.evaluate(groupOrControlItem).stream()
          .map(item -> (IAssemblyNodeItem) item)
          .forEachOrdered(partItem -> {
            visitPart(ObjectUtils.requireNonNull(partItem), groupOrControlItem, state);
          });
    }
  }

  @Override
  protected R visitGroupInternal(@NonNull IAssemblyNodeItem item, R childResult, T state) {
    if (isVisitedItemType(IEntityItem.ItemType.PART)) {
      visitParts(item, state);
    }

    R retval = childResult;
    if (isVisitedItemType(IEntityItem.ItemType.GROUP)) {
      retval = visitGroup(item, retval, state);
    }
    return retval;
  }

  @Override
  protected R visitControlInternal(IAssemblyNodeItem item, R childResult, T state) {
    if (isVisitedItemType(IEntityItem.ItemType.PART)) {
      visitParts(item, state);
    }

    R retval = childResult;
    if (isVisitedItemType(IEntityItem.ItemType.CONTROL)) {
      retval = visitControl(item, retval, state);
    }
    return retval;
  }

  /**
   * Called when visiting a parameter.
   * <p>
   * Can be overridden by classes extending this interface to support processing
   * of the visited object.
   *
   * @param item
   *          the Metapath item for the parameter
   * @param catalogOrGroupOrControl
   *          the parameter's parent Metapath item
   * @param state
   *          the calling context information
   * @return a meaningful result of the given type
   */
  protected R visitParameter(
      @NonNull IAssemblyNodeItem item,
      @NonNull IAssemblyNodeItem catalogOrGroupOrControl,
      T state) {
    // do nothing
    return newDefaultResult(state);
  }

  /**
   * Called when visiting a part.
   * <p>
   * Can be overridden by classes extending this interface to support processing
   * of the visited object.
   *
   * @param item
   *          the Metapath item for the part
   * @param groupOrControl
   *          the part's parent Metapath item
   * @param state
   *          the calling context information
   */
  protected void visitPart( // NOPMD noop default
      @NonNull IAssemblyNodeItem item,
      @NonNull IAssemblyNodeItem groupOrControl,
      T state) {
    // do nothing
  }

  /**
   * Called when visiting the "metadata" section of an OSCAL document.
   * <p>
   * Visits each contained role, location, and party.
   *
   * @param rootItem
   *          the root Module node item containing the "metadata" node
   * @param state
   *          the calling context information
   */
  protected void visitMetadata(@NonNull IRootAssemblyNodeItem rootItem, T state) {
    rootItem.getModelItemsByName(OscalModelConstants.QNAME_METADATA).stream()
        .map(metadataItem -> (IAssemblyNodeItem) metadataItem)
        .forEach(metadataItem -> {
          if (isVisitedItemType(IEntityItem.ItemType.ROLE)) {
            metadataItem.getModelItemsByName(OscalModelConstants.QNAME_ROLE).stream()
                .map(roleItem -> (IAssemblyNodeItem) roleItem)
                .forEachOrdered(roleItem -> {
                  visitRole(ObjectUtils.requireNonNull(roleItem), metadataItem, state);
                });
          }

          if (isVisitedItemType(IEntityItem.ItemType.LOCATION)) {
            metadataItem.getModelItemsByName(OscalModelConstants.QNAME_LOCATION).stream()
                .map(locationItem -> (IAssemblyNodeItem) locationItem)
                .forEachOrdered(locationItem -> {
                  visitLocation(ObjectUtils.requireNonNull(locationItem), metadataItem, state);
                });
          }

          if (isVisitedItemType(IEntityItem.ItemType.PARTY)) {
            metadataItem.getModelItemsByName(OscalModelConstants.QNAME_PARTY).stream()
                .map(partyItem -> (IAssemblyNodeItem) partyItem)
                .forEachOrdered(partyItem -> {
                  visitParty(ObjectUtils.requireNonNull(partyItem), metadataItem, state);
                });
          }
        });
  }

  /**
   * Called when visiting a role in the "metadata" section of an OSCAL document.
   * <p>
   * Can be overridden by classes extending this interface to support processing
   * of the visited object.
   *
   * @param item
   *          the role Module node item which is a child of the "metadata" node
   * @param metadataItem
   *          the "metadata" Module node item containing the role
   * @param state
   *          the calling context information
   */
  protected void visitRole( // NOPMD noop default
      @NonNull IAssemblyNodeItem item,
      @NonNull IAssemblyNodeItem metadataItem,
      T state) {
    // do nothing
  }

  /**
   * Called when visiting a location in the "metadata" section of an OSCAL
   * document.
   * <p>
   * Can be overridden by classes extending this interface to support processing
   * of the visited object.
   *
   * @param item
   *          the location Module node item which is a child of the "metadata"
   *          node
   * @param metadataItem
   *          the "metadata" Module node item containing the location
   * @param state
   *          the calling context information
   */
  protected void visitLocation( // NOPMD noop default
      @NonNull IAssemblyNodeItem item,
      @NonNull IAssemblyNodeItem metadataItem,
      T state) {
    // do nothing
  }

  /**
   * Called when visiting a party in the "metadata" section of an OSCAL document.
   * <p>
   * Can be overridden by classes extending this interface to support processing
   * of the visited object.
   *
   * @param item
   *          the party Module node item which is a child of the "metadata" node
   * @param metadataItem
   *          the "metadata" Module node item containing the party
   * @param state
   *          the calling context information
   */
  protected void visitParty( // NOPMD noop default
      @NonNull IAssemblyNodeItem item,
      @NonNull IAssemblyNodeItem metadataItem,
      T state) {
    // do nothing
  }

  /**
   * Called when visiting the "back-matter" section of an OSCAL document.
   * <p>
   * Visits each contained resource.
   *
   * @param rootItem
   *          the root Module node item containing the "back-matter" node
   * @param state
   *          the calling context information
   */
  protected void visitBackMatter(@NonNull IRootAssemblyNodeItem rootItem, T state) {
    if (isVisitedItemType(IEntityItem.ItemType.RESOURCE)) {
      BACK_MATTER_RESOURCES_METAPATH.evaluate(rootItem).stream()
          .map(item -> (IAssemblyNodeItem) item)
          .forEachOrdered(resourceItem -> {
            visitResource(ObjectUtils.requireNonNull(resourceItem), rootItem, state);
          });
    }
  }

  /**
   * Called when visiting a resource in the "back-matter" section of an OSCAL
   * document.
   * <p>
   * Can be overridden by classes extending this interface to support processing
   * of the visited object.
   *
   * @param resource
   *          the resource Module node item which is a child of the "metadata"
   *          node
   * @param backMatter
   *          the resource Module node item containing the party
   * @param state
   *          the calling context information
   */
  protected void visitResource( // NOPMD noop default
      @NonNull IAssemblyNodeItem resource,
      @NonNull IRootAssemblyNodeItem backMatter,
      T state) {
    // do nothing
  }
}