ControlSelectionVisitor.java

/*
 * Portions of this software was developed by employees of the National Institute
 * of Standards and Technology (NIST), an agency of the Federal Government and is
 * being made available as a public service. Pursuant to title 17 United States
 * Code Section 105, works of NIST employees are not subject to copyright
 * protection in the United States. This software may be subject to foreign
 * copyright. Permission in the United States and in foreign countries, to the
 * extent that NIST may hold copyright, to use, copy, modify, create derivative
 * works, and distribute this software and its documentation without fee is hereby
 * granted on a non-exclusive basis, provided that this notice and disclaimer
 * of warranty appears in all copies.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
 */

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

import gov.nist.secauto.metaschema.model.common.metapath.item.IDocumentNodeItem;
import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueModelNodeItem;
import gov.nist.secauto.metaschema.model.common.metapath.item.IRootAssemblyNodeItem;
import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
import gov.nist.secauto.oscal.lib.model.Catalog;
import gov.nist.secauto.oscal.lib.model.CatalogGroup;
import gov.nist.secauto.oscal.lib.model.Control;
import gov.nist.secauto.oscal.lib.model.ControlPart;
import gov.nist.secauto.oscal.lib.profile.resolver.support.AbstractIndexingVisitor;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer.SelectionStatus;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

/**
 * Walks a {@link Catalog} indexing all nodes that can be referenced.
 * <p>
 * For each {@link CatalogGroup}, {@link Control}, and {@link ControlPart},
 * determine if that object is {@link SelectionStatus#SELECTED} or
 * {@link SelectionStatus#UNSELECTED}.
 * <p>
 * A {@link Control} is {@link SelectionStatus#SELECTED} if it matches the
 * configured {@link IControlFilter}, otherwise it is
 * {@link SelectionStatus#UNSELECTED}.
 * <p>
 * A {@link CatalogGroup} is {@link SelectionStatus#SELECTED} if it contains a
 * {@link SelectionStatus#SELECTED} descendant {@link Control}, otherwise it is
 * {@link SelectionStatus#UNSELECTED}.
 * <p>
 * A {@link ControlPart} is {@link SelectionStatus#SELECTED} if its containing
 * control is {@link SelectionStatus#SELECTED}.
 * <p>
 * All other indexed nodes will have the {@link SelectionStatus#UNKNOWN}, since
 * these nodes require reference counting to determine if they are to be kept or
 * not.
 */
public class ControlSelectionVisitor
    extends AbstractIndexingVisitor<IControlSelectionState, Boolean> {
  private static final Logger LOGGER = LogManager.getLogger(ControlSelectionVisitor.class);

  private static final ControlSelectionVisitor SINGLETON = new ControlSelectionVisitor();

  public static ControlSelectionVisitor instance() {
    return SINGLETON;
  }

  @Override
  protected IIndexer getIndexer(IControlSelectionState state) {
    return state.getIndex();
  }

  @Override
  protected Boolean newDefaultResult(IControlSelectionState state) {
    return false;
  }

  @Override
  protected Boolean aggregateResults(Boolean first, Boolean second, IControlSelectionState state) {
    return first || second;
  }

  public void visit(@NonNull IDocumentNodeItem catalogDocument, @NonNull IControlSelectionState state) {
    visitCatalog(catalogDocument, state);
  }

  public void visitProfile(
      @NonNull IDocumentNodeItem catalogDocument,
      @NonNull IDocumentNodeItem profileDocument,
      @NonNull IControlSelectionState state) {
    visit(catalogDocument, state);

    IRootAssemblyNodeItem root = profileDocument.getRootAssemblyNodeItem();

    visitMetadata(root, state);
    visitBackMatter(root, state);
  }

  @Override
  public Boolean visitCatalog(IDocumentNodeItem catalogDocument, IControlSelectionState state) {
    getIndexer(state).setSelectionStatus(catalogDocument, SelectionStatus.SELECTED);
    return super.visitCatalog(catalogDocument, state);
  }

  @Override
  public Boolean visitGroup(IRequiredValueModelNodeItem groupItem, Boolean childSelected,
      IControlSelectionState state) {
    super.visitGroup(groupItem, childSelected, state);
    if (LOGGER.isTraceEnabled()) {
      CatalogGroup group = (CatalogGroup) groupItem.getValue();
      LOGGER.atTrace().log("Selecting group '{}'. match={}", group.getId(), childSelected);
    }

    // these should agree
    assert state.isSelected(groupItem) == childSelected;

    if (childSelected) {
      getIndexer(state).setSelectionStatus(groupItem, SelectionStatus.SELECTED);
    } else {
      getIndexer(state).setSelectionStatus(groupItem, SelectionStatus.UNSELECTED);
    }

    handlePartSelection(groupItem, childSelected, state);
    return childSelected;
  }

  private void handlePartSelection(
      @NonNull IRequiredValueModelNodeItem groupOrControlItem,
      boolean selected,
      IControlSelectionState state) {
    if (isVisitedItemType(IEntityItem.ItemType.PART)) {
      SelectionStatus selectionStatus = selected ? SelectionStatus.SELECTED : SelectionStatus.UNSELECTED;

      IIndexer index = getIndexer(state);
      CHILD_PART_METAPATH.evaluate(groupOrControlItem).asStream()
          .map(item -> (IRequiredValueModelNodeItem) item)
          .forEachOrdered(partItem -> {
            index.setSelectionStatus(ObjectUtils.requireNonNull(partItem), selectionStatus);
          });
    }
  }

  @Override
  public Boolean visitControl(
      IRequiredValueModelNodeItem controlItem,
      Boolean childResult,
      IControlSelectionState state) {
    super.visitControl(controlItem, childResult, state);

    boolean selected = state.isSelected(controlItem);
    if (selected) {
      getIndexer(state).setSelectionStatus(controlItem, SelectionStatus.SELECTED);
    } else {
      getIndexer(state).setSelectionStatus(controlItem, SelectionStatus.UNSELECTED);
    }

    handlePartSelection(controlItem, selected, state);
    return selected;
  }
}