ProfileResolver.java

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

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

import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
import gov.nist.secauto.metaschema.core.metapath.IDocumentLoader;
import gov.nist.secauto.metaschema.core.metapath.ISequence;
import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.core.metapath.StaticContext;
import gov.nist.secauto.metaschema.core.metapath.format.IPathFormatter;
import gov.nist.secauto.metaschema.core.metapath.function.FunctionUtils;
import gov.nist.secauto.metaschema.core.metapath.item.IItem;
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.INodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.model.IBoundObject;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.io.BindingException;
import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
import gov.nist.secauto.oscal.lib.OscalBindingContext;
import gov.nist.secauto.oscal.lib.OscalModelConstants;
import gov.nist.secauto.oscal.lib.OscalUtils;
import gov.nist.secauto.oscal.lib.model.BackMatter;
import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
import gov.nist.secauto.oscal.lib.model.BackMatter.Resource.Base64;
import gov.nist.secauto.oscal.lib.model.BackMatter.Resource.Rlink;
import gov.nist.secauto.oscal.lib.model.Catalog;
import gov.nist.secauto.oscal.lib.model.Control;
import gov.nist.secauto.oscal.lib.model.Merge;
import gov.nist.secauto.oscal.lib.model.Metadata;
import gov.nist.secauto.oscal.lib.model.Metadata.Location;
import gov.nist.secauto.oscal.lib.model.Metadata.Party;
import gov.nist.secauto.oscal.lib.model.Metadata.Role;
import gov.nist.secauto.oscal.lib.model.Modify;
import gov.nist.secauto.oscal.lib.model.Modify.ProfileSetParameter;
import gov.nist.secauto.oscal.lib.model.Parameter;
import gov.nist.secauto.oscal.lib.model.Profile;
import gov.nist.secauto.oscal.lib.model.ProfileImport;
import gov.nist.secauto.oscal.lib.model.Property;
import gov.nist.secauto.oscal.lib.model.metadata.AbstractLink;
import gov.nist.secauto.oscal.lib.model.metadata.AbstractProperty;
import gov.nist.secauto.oscal.lib.profile.resolver.alter.AddVisitor;
import gov.nist.secauto.oscal.lib.profile.resolver.alter.RemoveVisitor;
import gov.nist.secauto.oscal.lib.profile.resolver.merge.FlatteningStructuringVisitor;
import gov.nist.secauto.oscal.lib.profile.resolver.selection.Import;
import gov.nist.secauto.oscal.lib.profile.resolver.selection.ImportCycleException;
import gov.nist.secauto.oscal.lib.profile.resolver.support.BasicIndexer;
import gov.nist.secauto.oscal.lib.profile.resolver.support.ControlIndexingVisitor;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer;

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

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.xml.namespace.QName;

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

public class ProfileResolver {
  private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
  @NonNull
  private static final QName IMPORT_QNAME = new QName(OscalModelConstants.NS_OSCAL, "import");

  @NonNull
  private static final MetapathExpression METAPATH_SET_PARAMETER
      = MetapathExpression.compile("modify/set-parameter",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression METAPATH_ALTER
      = MetapathExpression.compile("modify/alter",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression METAPATH_ALTER_REMOVE
      = MetapathExpression.compile("remove",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression METAPATH_ALTER_ADD
      = MetapathExpression.compile("add",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression CATALOG_OR_PROFILE
      = MetapathExpression.compile("/(catalog|profile)",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression CATALOG
      = MetapathExpression.compile("/catalog",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);

  public enum StructuringDirective {
    FLAT,
    AS_IS,
    CUSTOM;
  }

  private IBoundLoader loader;
  private DynamicContext dynamicContext;

  /**
   * Gets the configured loader or creates a new default loader if no loader was
   * configured.
   *
   * @return the bound loader
   */
  @NonNull
  public IBoundLoader getBoundLoader() {
    synchronized (this) {
      if (loader == null) {
        loader = OscalBindingContext.instance().newBoundLoader();
        loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
      }
      assert loader != null;
      return loader;
    }
  }

  public void setBoundLoader(@NonNull IBoundLoader loader) {
    synchronized (this) {
      this.loader = loader;
    }
  }

  @NonNull
  @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intending to expose this field")
  public DynamicContext getDynamicContext() {
    synchronized (this) {
      if (dynamicContext == null) {
        dynamicContext = new DynamicContext(StaticContext.builder()
            .defaultModelNamespace(OscalModelConstants.NS_URI_OSCAL)
            .build());
        dynamicContext.setDocumentLoader(getBoundLoader());
      }
      assert dynamicContext != null;
      return dynamicContext;
    }
  }

  @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intending to store this parameter")
  public void setDynamicContext(@NonNull DynamicContext dynamicContext) {
    synchronized (this) {
      this.dynamicContext = dynamicContext;
    }
  }

  @Nullable
  private static IRootAssemblyNodeItem getRoot(
      @NonNull IDocumentNodeItem document,
      @NonNull MetapathExpression rootPath) {
    ISequence<?> result = rootPath.evaluate(document);
    IItem item = result.getFirstItem(false);

    return item == null ? null : FunctionUtils.asType(item);
  }

  @NonNull
  public IDocumentNodeItem resolve(@NonNull URL url)
      throws URISyntaxException, IOException, ProfileResolutionException {
    IBoundLoader loader = getBoundLoader();
    IDocumentNodeItem catalogOrProfile = loader.loadAsNodeItem(url);
    return resolve(catalogOrProfile, new Stack<>());
  }

  @NonNull
  public IDocumentNodeItem resolve(@NonNull File file) throws IOException, ProfileResolutionException {
    return resolve(ObjectUtils.notNull(file.toPath()));
  }

  @NonNull
  public IDocumentNodeItem resolve(@NonNull Path path) throws IOException, ProfileResolutionException {
    IBoundLoader loader = getBoundLoader();
    IDocumentNodeItem catalogOrProfile = loader.loadAsNodeItem(path);
    return resolve(catalogOrProfile, new Stack<>());
  }

  @NonNull
  public IDocumentNodeItem resolve(
      @NonNull IDocumentNodeItem profileOrCatalogDocument)
      throws IOException, ProfileResolutionException {
    return resolve(profileOrCatalogDocument, new Stack<>());
  }

  @NonNull
  public IDocumentNodeItem resolve(
      @NonNull IDocumentNodeItem profileOrCatalogDocument,
      @NonNull Stack<URI> importHistory)
      throws IOException, ProfileResolutionException {
    IRootAssemblyNodeItem profileOrCatalog = getRoot(
        profileOrCatalogDocument,
        CATALOG_OR_PROFILE);
    if (profileOrCatalog == null) {
      throw new ProfileResolutionException(
          String.format("The provided document '%s' does not contain a catalog or profile.",
              profileOrCatalogDocument.getDocumentUri()));
    }
    return resolve(profileOrCatalog, importHistory);
  }

  @NonNull
  public IDocumentNodeItem resolve(
      @NonNull IRootAssemblyNodeItem profileOrCatalog,
      @NonNull Stack<URI> importHistory)
      throws IOException, ProfileResolutionException {
    Object profileObject = profileOrCatalog.getValue();

    IDocumentNodeItem retval;
    if (profileObject instanceof Catalog) {
      // already a catalog
      retval = profileOrCatalog.getParentNodeItem();
    } else {
      // must be a profile
      retval = resolveProfile(profileOrCatalog, importHistory);
    }
    return retval;
  }

  /**
   * Resolve the profile to a catalog.
   *
   * @param profileItem
   *          a {@link IDocumentNodeItem} containing the profile to resolve
   * @param importHistory
   *          the import stack for cycle detection
   * @return the resolved profile
   * @throws IOException
   *           if an error occurred while loading the profile or an import
   * @throws ProfileResolutionException
   *           if an error occurred while resolving the profile
   */
  @NonNull
  protected IDocumentNodeItem resolveProfile(
      @NonNull IRootAssemblyNodeItem profileItem,
      @NonNull Stack<URI> importHistory) throws IOException, ProfileResolutionException {
    Catalog resolvedCatalog = new Catalog();

    generateMetadata(resolvedCatalog, profileItem);

    IIndexer index = resolveImports(resolvedCatalog, profileItem, importHistory);
    handleReferences(resolvedCatalog, profileItem, index);
    handleMerge(resolvedCatalog, profileItem, index);
    handleModify(resolvedCatalog, profileItem);

    return INodeItemFactory.instance().newDocumentNodeItem(
        ObjectUtils.requireNonNull(
            (IBoundDefinitionModelAssembly) OscalBindingContext.instance().getBoundDefinitionForClass(Catalog.class)),
        ObjectUtils.requireNonNull(profileItem.getBaseUri()),
        resolvedCatalog);
  }

  @NonNull
  private static Profile toProfile(@NonNull IRootAssemblyNodeItem profileItem) {
    Object object = profileItem.getValue();
    assert object != null;

    return (Profile) object;
  }

  private static void generateMetadata(
      @NonNull Catalog resolvedCatalog,
      @NonNull IRootAssemblyNodeItem profileItem) {
    resolvedCatalog.setUuid(UUID.randomUUID());

    Profile profile = toProfile(profileItem);
    Metadata profileMetadata = profile.getMetadata();

    Metadata resolvedMetadata = new Metadata();
    resolvedMetadata.setTitle(profileMetadata.getTitle());

    if (profileMetadata.getVersion() != null) {
      resolvedMetadata.setVersion(profileMetadata.getVersion());
    }

    // metadata.setOscalVersion(OscalUtils.OSCAL_VERSION);
    resolvedMetadata.setOscalVersion(profileMetadata.getOscalVersion());

    resolvedMetadata.setLastModified(ZonedDateTime.now(ZoneOffset.UTC));

    resolvedMetadata.addProp(AbstractProperty.builder("resolution-tool").value("libOSCAL-Java").build());

    URI profileUri = ObjectUtils.requireNonNull(profileItem.getParentNodeItem().getDocumentUri());
    resolvedMetadata.addLink(AbstractLink.builder(profileUri).relation("source-profile").build());

    resolvedCatalog.setMetadata(resolvedMetadata);
  }

  @NonNull
  private IIndexer resolveImports(
      @NonNull Catalog resolvedCatalog,
      @NonNull IRootAssemblyNodeItem profileItem,
      @NonNull Stack<URI> importHistory)
      throws IOException, ProfileResolutionException {

    // first verify there is at least one import
    @SuppressWarnings("unchecked") List<IAssemblyNodeItem> profileImports
        = (List<IAssemblyNodeItem>) profileItem.getModelItemsByName(IMPORT_QNAME);
    if (profileImports.isEmpty()) {
      throw new ProfileResolutionException(String.format("Profile '%s' has no imports", profileItem.getBaseUri()));
    }

    // now process each import
    IIndexer retval = new BasicIndexer();
    for (IAssemblyNodeItem profileImportItem : profileImports) {
      IIndexer result = resolveImport(
          ObjectUtils.notNull(profileImportItem),
          profileItem,
          importHistory,
          resolvedCatalog);
      retval.append(result);
    }
    return retval;
  }

  @NonNull
  protected IIndexer resolveImport(
      @NonNull IAssemblyNodeItem profileImportItem,
      @NonNull IRootAssemblyNodeItem profileItem,
      @NonNull Stack<URI> importHistory,
      @NonNull Catalog resolvedCatalog) throws IOException, ProfileResolutionException {
    ProfileImport profileImport = ObjectUtils.requireNonNull((ProfileImport) profileImportItem.getValue());

    URI importUri = profileImport.getHref();
    if (importUri == null) {
      throw new ProfileResolutionException("profileImport.getHref() must return a non-null URI");
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.atDebug().log("resolving profile import '{}'", importUri);
    }

    IDocumentNodeItem importedDocument = getImport(importUri, profileItem);
    URI importedUri = importedDocument.getDocumentUri();
    assert importedUri != null; // always non-null

    // Import import = Import.
    // InputSource source = newImportSource(importUri, profileItem);
    // URI sourceUri = ObjectUtils.notNull(URI.create(source.getSystemId()));

    // check for import cycle
    try {
      requireNonCycle(
          importedUri,
          importHistory);
    } catch (ImportCycleException ex) {
      throw new IOException(ex);
    }

    // track the import in the import history
    importHistory.push(importedUri);
    try {
      IDocumentNodeItem importedCatalog = resolve(importedDocument, importHistory);

      // Create a defensive deep copy of the document and associated values, since we
      // will be making
      // changes to the data.
      try {
        IRootAssemblyNodeItem importedCatalogRoot = ObjectUtils.requireNonNull(getRoot(importedCatalog, CATALOG));
        Catalog catalogCopy = (Catalog) OscalBindingContext.instance().deepCopy(
            (IBoundObject) ObjectUtils.requireNonNull(importedCatalogRoot).getValue(), null);

        importedCatalog = INodeItemFactory.instance().newDocumentNodeItem(
            importedCatalogRoot.getDefinition(),
            ObjectUtils.requireNonNull(importedCatalog.getDocumentUri()),
            catalogCopy);

        return new Import(profileItem, profileImportItem).resolve(importedCatalog, resolvedCatalog);
      } catch (BindingException ex) {
        throw new IOException(ex);
      }
    } finally {
      // pop the resolved catalog from the import history
      URI poppedUri = ObjectUtils.notNull(importHistory.pop());
      assert importedUri.equals(poppedUri);
    }
  }

  private IDocumentNodeItem getImport(
      @NonNull URI importUri,
      @NonNull IRootAssemblyNodeItem importingProfile) throws IOException {

    URI importingDocumentUri = ObjectUtils.requireNonNull(importingProfile.getParentNodeItem().getDocumentUri());

    IDocumentNodeItem retval;
    if (OscalUtils.isInternalReference(importUri)) {
      // handle internal reference
      String uuid = OscalUtils.internalReferenceFragmentToId(importUri);

      Profile profile = INodeItem.toValue(importingProfile);
      Resource resource = profile.getResourceByUuid(ObjectUtils.notNull(UUID.fromString(uuid)));
      if (resource == null) {
        throw new IOException(
            String.format("unable to find the resource identified by '%s' used in profile import", importUri));
      }

      retval = getImport(resource, importingDocumentUri);
    } else {
      URI uri = importingDocumentUri.resolve(importUri);
      assert uri != null;

      retval = getDynamicContext().getDocumentLoader().loadAsNodeItem(uri);
    }
    return retval;
  }

  @Nullable
  private IDocumentNodeItem getImport(
      @NonNull Resource resource,
      @NonNull URI baseUri) throws IOException {

    IDocumentLoader loader = getDynamicContext().getDocumentLoader();

    IDocumentNodeItem retval = null;
    // first try base64 data
    Base64 base64 = resource.getBase64();
    ByteBuffer buffer = base64 == null ? null : base64.getValue();
    if (buffer != null) {
      URI resourceUri = baseUri.resolve("#" + resource.getUuid());
      assert resourceUri != null;
      retval = loader.loadAsNodeItem(resourceUri);
    }

    if (retval == null) {
      Rlink rlink = OscalUtils.findMatchingRLink(resource, null);
      URI uri = rlink == null ? null : rlink.getHref();

      if (uri == null) {
        throw new IOException(String.format("unable to determine URI for resource '%s'", resource.getUuid()));
      }

      uri = baseUri.resolve(uri);
      assert uri != null;
      retval = loader.loadAsNodeItem(uri);
    }
    return retval;
  }

  private static void requireNonCycle(@NonNull URI uri, @NonNull Stack<URI> importHistory)
      throws ImportCycleException {
    List<URI> cycle = checkCycle(uri, importHistory);
    if (!cycle.isEmpty()) {
      throw new ImportCycleException(String.format("Importing resource '%s' would result in the import cycle: %s", uri,
          cycle.stream().map(URI::toString).collect(Collectors.joining(" -> ", " -> ", ""))));
    }
  }

  @NonNull
  private static List<URI> checkCycle(@NonNull URI uri, @NonNull Stack<URI> importHistory) {
    int index = importHistory.indexOf(uri);

    List<URI> retval;
    if (index == -1) {
      retval = CollectionUtil.emptyList();
    } else {
      retval = CollectionUtil.unmodifiableList(
          ObjectUtils.notNull(importHistory.subList(0, index + 1)));
    }
    return retval;
  }

  // TODO: move this to an abstract method on profile
  private static StructuringDirective getStructuringDirective(Profile profile) {
    Merge merge = profile.getMerge();

    StructuringDirective retval;
    if (merge == null) {
      retval = StructuringDirective.FLAT;
    } else if (merge.getAsIs() != null && merge.getAsIs()) {
      retval = StructuringDirective.AS_IS;
    } else if (merge.getCustom() != null) {
      retval = StructuringDirective.CUSTOM;
    } else {
      retval = StructuringDirective.FLAT;
    }
    return retval;
  }

  protected void handleMerge(
      @NonNull Catalog resolvedCatalog,
      @NonNull IRootAssemblyNodeItem profileItem,
      @NonNull IIndexer importIndex) {
    // handle combine

    // handle structuring
    switch (getStructuringDirective(toProfile(profileItem))) {
    case AS_IS:
      // do nothing
      break;
    case CUSTOM:
      throw new UnsupportedOperationException("custom structuring");
    case FLAT:
    default:
      structureFlat(resolvedCatalog, profileItem, importIndex);
      break;
    }

  }

  protected void structureFlat(@NonNull Catalog resolvedCatalog, @NonNull IRootAssemblyNodeItem profileItem,
      @NonNull IIndexer importIndex) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("applying flat structuring directive");
    }

    // {
    // // rebuild an index
    // IDocumentNodeItem resolvedCatalogItem =
    // DefaultNodeItemFactory.instance().newDocumentNodeItem(
    // new RootAssemblyDefinition(
    // ObjectUtils.notNull(
    // (IAssemblyClassBinding)
    // OscalBindingContext.instance().getClassBinding(Catalog.class))),
    // resolvedCatalog,
    // profileDocument.getBaseUri());
    //
    // // FIXME: need to find a better way to create an index that doesn't auto
    // select groups
    // IIndexer indexer = new BasicIndexer();
    // ControlSelectionVisitor selectionVisitor
    // = new ControlSelectionVisitor(IControlFilter.ALWAYS_MATCH, indexer);
    // selectionVisitor.visitCatalog(resolvedCatalogItem);
    // }

    // rebuild the document, since the paths have changed
    IDocumentNodeItem resolvedCatalogItem = INodeItemFactory.instance().newDocumentNodeItem(
        ObjectUtils.requireNonNull(
            (IBoundDefinitionModelAssembly) OscalBindingContext.instance().getBoundDefinitionForClass(Catalog.class)),
        ObjectUtils.requireNonNull(profileItem.getBaseUri()),
        resolvedCatalog);

    FlatteningStructuringVisitor.instance().visitCatalog(resolvedCatalogItem, importIndex);
  }

  @SuppressWarnings("PMD.ExceptionAsFlowControl") // ok
  protected void handleModify(@NonNull Catalog resolvedCatalog, @NonNull IRootAssemblyNodeItem profileItem)
      throws ProfileResolutionException {
    IDocumentNodeItem resolvedCatalogDocument = INodeItemFactory.instance().newDocumentNodeItem(
        ObjectUtils.requireNonNull(
            (IBoundDefinitionModelAssembly) OscalBindingContext.instance().getBoundDefinitionForClass(Catalog.class)),
        ObjectUtils.requireNonNull(profileItem.getBaseUri()),
        resolvedCatalog);

    try {
      IIndexer indexer = new BasicIndexer();
      ControlIndexingVisitor visitor = new ControlIndexingVisitor(
          ObjectUtils.notNull(EnumSet.of(IEntityItem.ItemType.CONTROL, IEntityItem.ItemType.PARAMETER)));
      visitor.visitCatalog(resolvedCatalogDocument, indexer);

      METAPATH_SET_PARAMETER.evaluate(profileItem)
          .forEach(item -> {
            IAssemblyNodeItem setParameter = (IAssemblyNodeItem) item;
            try {
              handleSetParameter(setParameter, indexer);
            } catch (ProfileResolutionEvaluationException ex) {
              throw new ProfileResolutionEvaluationException(
                  String.format("Unable to apply the set-parameter at '%s'. %s",
                      setParameter.toPath(IPathFormatter.METAPATH_PATH_FORMATER),
                      ex.getLocalizedMessage()),
                  ex);
            }
          });

      METAPATH_ALTER.evaluate(profileItem)
          .forEach(item -> {
            handleAlter((IAssemblyNodeItem) item, indexer);
          });
    } catch (ProfileResolutionEvaluationException ex) {
      throw new ProfileResolutionException(ex.getLocalizedMessage(), ex);
    }
  }

  protected void handleSetParameter(IAssemblyNodeItem item, IIndexer indexer) {
    ProfileSetParameter setParameter = ObjectUtils.requireNonNull((Modify.ProfileSetParameter) item.getValue());
    String paramId = ObjectUtils.requireNonNull(setParameter.getParamId());
    IEntityItem entity = indexer.getEntity(IEntityItem.ItemType.PARAMETER, paramId, false);
    if (entity == null) {
      throw new ProfileResolutionEvaluationException(
          String.format(
              "The parameter '%s' does not exist in the resolved catalog.",
              paramId));
    }

    Parameter param = entity.getInstanceValue();

    // apply the set parameter values
    param.setClazz(ModifyPhaseUtils.mergeItem(param.getClazz(), setParameter.getClazz()));
    param.setProps(ModifyPhaseUtils.merge(param.getProps(), setParameter.getProps(),
        ModifyPhaseUtils.identifierKey(Property::getUuid)));
    param.setLinks(ModifyPhaseUtils.merge(param.getLinks(), setParameter.getLinks(), ModifyPhaseUtils.identityKey()));
    param.setLabel(ModifyPhaseUtils.mergeItem(param.getLabel(), setParameter.getLabel()));
    param.setUsage(ModifyPhaseUtils.mergeItem(param.getUsage(), setParameter.getUsage()));
    param.setConstraints(
        ModifyPhaseUtils.merge(param.getConstraints(), setParameter.getConstraints(), ModifyPhaseUtils.identityKey()));
    param.setGuidelines(
        ModifyPhaseUtils.merge(param.getGuidelines(), setParameter.getGuidelines(), ModifyPhaseUtils.identityKey()));
    param.setValues(new LinkedList<>(setParameter.getValues()));
    param.setSelect(setParameter.getSelect());
  }

  @SuppressWarnings("PMD.ExceptionAsFlowControl")
  protected void handleAlter(IAssemblyNodeItem item, IIndexer indexer) {
    Modify.Alter alter = ObjectUtils.requireNonNull((Modify.Alter) item.getValue());
    String controlId = ObjectUtils.requireNonNull(alter.getControlId());
    IEntityItem entity = indexer.getEntity(IEntityItem.ItemType.CONTROL, controlId, false);
    if (entity == null) {
      throw new ProfileResolutionEvaluationException(
          String.format(
              "Unable to apply the alter targeting control '%s' at '%s'."
                  + " The control does not exist in the resolved catalog.",
              controlId,
              item.toPath(IPathFormatter.METAPATH_PATH_FORMATER)));
    }
    Control control = entity.getInstanceValue();

    METAPATH_ALTER_REMOVE.evaluate(item)
        .forEach(nodeItem -> {
          INodeItem removeItem = (INodeItem) nodeItem;
          Modify.Alter.Remove remove = ObjectUtils.notNull((Modify.Alter.Remove) removeItem.getValue());

          try {
            if (!RemoveVisitor.remove(
                control,
                remove.getByName(),
                remove.getByClass(),
                remove.getById(),
                remove.getByNs(),
                RemoveVisitor.TargetType.forFieldName(remove.getByItemName()))) {
              throw new ProfileResolutionEvaluationException(
                  String.format("The remove did not match a valid target"));
            }
          } catch (ProfileResolutionEvaluationException ex) {
            throw new ProfileResolutionEvaluationException(
                String.format("Unable to apply the remove targeting control '%s' at '%s'. %s",
                    control.getId(),
                    removeItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER),
                    ex.getLocalizedMessage()),
                ex);
          }
        });
    METAPATH_ALTER_ADD.evaluate(item)
        .forEach(nodeItem -> {
          INodeItem addItem = (INodeItem) nodeItem;
          Modify.Alter.Add add = ObjectUtils.notNull((Modify.Alter.Add) addItem.getValue());
          String byId = add.getById();
          try {
            if (!AddVisitor.add(
                control,
                AddVisitor.Position.forName(add.getPosition()),
                byId,
                add.getTitle(),
                CollectionUtil.listOrEmpty(add.getParams()),
                CollectionUtil.listOrEmpty(add.getProps()),
                CollectionUtil.listOrEmpty(add.getLinks()),
                CollectionUtil.listOrEmpty(add.getParts()))) {

              throw new ProfileResolutionEvaluationException(
                  String.format("The add did not match a valid target"));
            }
          } catch (ProfileResolutionEvaluationException ex) {
            throw new ProfileResolutionEvaluationException(
                String.format("Unable to apply the add targeting control '%s'%s at '%s'. %s",
                    control.getId(),
                    byId == null ? "" : String.format(" having by-id '%s'", byId),
                    addItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER),
                    ex.getLocalizedMessage()),
                ex);
          }
        });
  }

  private static void handleReferences(@NonNull Catalog resolvedCatalog, @NonNull IRootAssemblyNodeItem profileItem,
      @NonNull IIndexer index) {

    BasicIndexer profileIndex = new BasicIndexer();

    new ControlIndexingVisitor(ObjectUtils.notNull(EnumSet.allOf(ItemType.class)))
        .visitProfile(profileItem, profileIndex);

    // copy roles, parties, and locations with prop name:keep and any referenced
    Metadata resolvedMetadata = resolvedCatalog.getMetadata();
    resolvedMetadata.setRoles(
        IIndexer.filterDistinct(
            ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getRoles()).stream()),
            profileIndex.getEntitiesByItemType(IEntityItem.ItemType.ROLE),
            Role::getId)
            .collect(Collectors.toCollection(LinkedList::new)));
    resolvedMetadata.setParties(
        IIndexer.filterDistinct(
            ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getParties()).stream()),
            profileIndex.getEntitiesByItemType(IEntityItem.ItemType.PARTY),
            Party::getUuid)
            .collect(Collectors.toCollection(LinkedList::new)));
    resolvedMetadata.setLocations(
        IIndexer.filterDistinct(
            ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getLocations()).stream()),
            profileIndex.getEntitiesByItemType(IEntityItem.ItemType.LOCATION),
            Location::getUuid)
            .collect(Collectors.toCollection(LinkedList::new)));

    // copy resources
    BackMatter resolvedBackMatter = resolvedCatalog.getBackMatter();
    List<Resource> resolvedResources = resolvedBackMatter == null ? CollectionUtil.emptyList()
        : CollectionUtil.listOrEmpty(resolvedBackMatter.getResources());

    List<Resource> resources = IIndexer.filterDistinct(
        ObjectUtils.notNull(resolvedResources.stream()),
        profileIndex.getEntitiesByItemType(IEntityItem.ItemType.RESOURCE),
        Resource::getUuid)
        .collect(Collectors.toCollection(LinkedList::new));

    if (!resources.isEmpty()) {
      if (resolvedBackMatter == null) {
        resolvedBackMatter = new BackMatter();
        resolvedCatalog.setBackMatter(resolvedBackMatter);
      }

      resolvedBackMatter.setResources(resources);
    }

    index.append(profileIndex);
  }

}