ProfileResolver.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;

import gov.nist.secauto.metaschema.binding.io.BindingException;
import gov.nist.secauto.metaschema.binding.io.DeserializationFeature;
import gov.nist.secauto.metaschema.binding.io.IBoundLoader;
import gov.nist.secauto.metaschema.binding.model.IAssemblyClassBinding;
import gov.nist.secauto.metaschema.binding.model.RootAssemblyDefinition;
import gov.nist.secauto.metaschema.model.common.metapath.DynamicContext;
import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.model.common.metapath.StaticContext;
import gov.nist.secauto.metaschema.model.common.metapath.format.IPathFormatter;
import gov.nist.secauto.metaschema.model.common.metapath.item.DefaultNodeItemFactory;
import gov.nist.secauto.metaschema.model.common.metapath.item.IDocumentNodeItem;
import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueAssemblyNodeItem;
import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueModelNodeItem;
import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueNodeItem;
import gov.nist.secauto.metaschema.model.common.metapath.item.IRootAssemblyNodeItem;
import gov.nist.secauto.metaschema.model.common.util.CollectionUtil;
import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
import gov.nist.secauto.oscal.lib.OscalBindingContext;
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.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.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 org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
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 edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

public class ProfileResolver {
  private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
  private static final MetapathExpression METAPATH_SET_PARAMETER
      = MetapathExpression.compile("/profile/modify/set-parameter");
  private static final MetapathExpression METAPATH_ALTER
      = MetapathExpression.compile("/profile/modify/alter");
  private static final MetapathExpression METAPATH_ALTER_REMOVE
      = MetapathExpression.compile("remove");
  private static final MetapathExpression METAPATH_ALTER_ADD
      = MetapathExpression.compile("add");

  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 StaticContext().newDynamicContext();
        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;
    }
  }

  @NonNull
  protected EntityResolver getEntityResolver(@NonNull URI documentUri) {
    return new DocumentEntityResolver(documentUri);
  }

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

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

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

  /**
   * Resolve the profile to a catalog.
   *
   * @param profileDocument
   *          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 IDocumentNodeItem profileDocument,
      @NonNull Stack<URI> importHistory) throws IOException, ProfileResolutionException {
    Catalog resolvedCatalog = new Catalog();

    generateMetadata(resolvedCatalog, profileDocument);

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

    return DefaultNodeItemFactory.instance().newDocumentNodeItem(
        new RootAssemblyDefinition(
            ObjectUtils.notNull(
                (IAssemblyClassBinding) OscalBindingContext.instance().getClassBinding(Catalog.class))),
        resolvedCatalog,
        profileDocument.getBaseUri());
  }

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

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

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

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

    return (Profile) object;
  }

  @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 IDocumentNodeItem profileDocument) {
    resolvedCatalog.setUuid(UUID.randomUUID());

    Profile profile = toProfile(profileDocument);
    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 = profileDocument.getDocumentUri();
    resolvedMetadata.addLink(AbstractLink.builder(profileUri).relation("source-profile").build());

    resolvedCatalog.setMetadata(resolvedMetadata);
  }

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

    IRootAssemblyNodeItem profileItem = profileDocument.getRootAssemblyNodeItem();

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

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

  @NonNull
  protected IIndexer resolveImport(
      @NonNull IRequiredValueModelNodeItem profileImportItem,
      @NonNull IDocumentNodeItem profileDocument,
      @NonNull Stack<URI> importHistory,
      @NonNull Catalog resolvedCatalog) throws IOException, ProfileResolutionException {
    ProfileImport profileImport = (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);
    }

    InputSource source = newImportSource(importUri, profileDocument);
    URI sourceUri = ObjectUtils.notNull(URI.create(source.getSystemId()));

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

    // track the import in the import history
    importHistory.push(sourceUri);
    try {
      IDocumentNodeItem document = getDynamicContext().getDocumentLoader().loadAsNodeItem(source);
      IDocumentNodeItem importedCatalog = resolve(document, importHistory);

      // Create a defensive deep copy of the document and associated values, since we
      // will be making
      // changes to the data.
      try {
        importedCatalog = DefaultNodeItemFactory.instance().newDocumentNodeItem(
            importedCatalog.getRootAssemblyNodeItem().getDefinition(),
            OscalBindingContext.instance().copyBoundObject(importedCatalog.getValue(), null),
            importedCatalog.getDocumentUri());

        return new Import(profileDocument, 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 sourceUri.equals(poppedUri);
    }
  }

  @NonNull
  protected InputSource newImportSource(
      @NonNull URI importUri,
      @NonNull IDocumentNodeItem profileDocument) throws IOException {

    // Get the entity resolver to resolve relative references in the profile
    EntityResolver resolver = getEntityResolver(profileDocument.getDocumentUri());

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

      IRootAssemblyNodeItem profileItem = profileDocument.getRootAssemblyNodeItem();
      Profile profile = toProfile(profileItem);
      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));
      }

      source = OscalUtils.newInputSource(resource, resolver, null);
    } else {
      try {
        source = resolver.resolveEntity(null, importUri.toASCIIString());
      } catch (SAXException ex) {
        throw new IOException(ex);
      }
    }

    if (source == null || source.getSystemId() == null) {
      throw new IOException(String.format("Unable to resolve import '%s'.", importUri.toString()));
    }

    return source;
  }

  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(cycleUri -> cycleUri.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 IDocumentNodeItem profileDocument,
      @NonNull IIndexer importIndex) {
    // handle combine

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

  }

  protected void structureFlat(@NonNull Catalog resolvedCatalog, @NonNull IDocumentNodeItem profileDocument,
      @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 = DefaultNodeItemFactory.instance().newDocumentNodeItem(
        new RootAssemblyDefinition(
            ObjectUtils.notNull(
                (IAssemblyClassBinding) OscalBindingContext.instance().getClassBinding(Catalog.class))),
        resolvedCatalog,
        profileDocument.getBaseUri());

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

  @SuppressWarnings("PMD.ExceptionAsFlowControl") // ok
  protected void handleModify(@NonNull Catalog resolvedCatalog, @NonNull IDocumentNodeItem profileDocument)
      throws ProfileResolutionException {
    IDocumentNodeItem resolvedCatalogDocument = DefaultNodeItemFactory.instance().newDocumentNodeItem(
        new RootAssemblyDefinition(
            ObjectUtils.notNull(
                (IAssemblyClassBinding) OscalBindingContext.instance().getClassBinding(Catalog.class))),
        resolvedCatalog,
        profileDocument.getBaseUri());

    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(profileDocument)
          .forEach(item -> {
            IRequiredValueAssemblyNodeItem setParameter = (IRequiredValueAssemblyNodeItem) 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(profileDocument)
          .forEach(item -> {
            handleAlter((IRequiredValueAssemblyNodeItem) item, indexer);
          });
    } catch (ProfileResolutionEvaluationException ex) {
      throw new ProfileResolutionException(ex.getLocalizedMessage(), ex);
    }
  }

  protected void handleSetParameter(IRequiredValueAssemblyNodeItem item, IIndexer indexer) {
    ProfileSetParameter setParameter = (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());
  }

  protected void handleAlter(IRequiredValueAssemblyNodeItem item, IIndexer indexer) {
    Modify.Alter alter = (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 -> {
          IRequiredValueNodeItem removeItem = (IRequiredValueNodeItem) 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 -> {
          IRequiredValueNodeItem addItem = (IRequiredValueNodeItem) 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 IDocumentNodeItem profileDocument,
      @NonNull IIndexer index) {

    BasicIndexer profileIndex = new BasicIndexer();

    new ControlIndexingVisitor(ObjectUtils.notNull(EnumSet.allOf(ItemType.class)))
        .visitProfile(profileDocument, 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),
            item -> item.getId())
            .collect(Collectors.toCollection(LinkedList::new)));
    resolvedMetadata.setParties(
        IIndexer.filterDistinct(
            ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getParties()).stream()),
            profileIndex.getEntitiesByItemType(IEntityItem.ItemType.PARTY),
            item -> item.getUuid())
            .collect(Collectors.toCollection(LinkedList::new)));
    resolvedMetadata.setLocations(
        IIndexer.filterDistinct(
            ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getLocations()).stream()),
            profileIndex.getEntitiesByItemType(IEntityItem.ItemType.LOCATION),
            item -> item.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),
        item -> item.getUuid())
        .collect(Collectors.toCollection(LinkedList::new));

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

      resolvedBackMatter.setResources(resources);
    }

    index.append(profileIndex);
  }

  private class DocumentEntityResolver implements EntityResolver {
    @NonNull
    private final URI documentUri;

    public DocumentEntityResolver(@NonNull URI documentUri) {
      this.documentUri = documentUri;
    }

    @NonNull
    protected URI getDocumentUri() {
      return documentUri;
    }

    @Override
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {

      URI resolvedUri = getDocumentUri().resolve(systemId);

      EntityResolver resolver = getDynamicContext().getDocumentLoader().getEntityResolver();

      InputSource retval;
      if (resolver == null) {
        retval = new InputSource(resolvedUri.toASCIIString());
      } else {
        retval = resolver.resolveEntity(publicId, resolvedUri.toASCIIString());
      }
      return retval;
    }

  }
}