ReferenceCountingVisitor.java

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

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

import com.vladsch.flexmark.ast.InlineLinkNode;
import com.vladsch.flexmark.util.ast.Node;

import gov.nist.secauto.metaschema.core.datatype.markup.IMarkupString;
import gov.nist.secauto.metaschema.core.datatype.markup.flexmark.InsertAnchorExtension.InsertAnchorNode;
import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.core.metapath.format.IPathFormatter;
import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IMarkupItem;
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.IFieldNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IModelNodeItem;
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 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.model.Link;
import gov.nist.secauto.oscal.lib.model.Property;
import gov.nist.secauto.oscal.lib.model.metadata.AbstractProperty;
import gov.nist.secauto.oscal.lib.model.metadata.IProperty;
import gov.nist.secauto.oscal.lib.profile.resolver.support.AbstractCatalogEntityVisitor;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer;

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

import java.net.URI;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;

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 final class ReferenceCountingVisitor
    extends AbstractCatalogEntityVisitor<ReferenceCountingVisitor.Context, Void>
    implements IReferenceVisitor<ReferenceCountingVisitor.Context> {
  private static final Logger LOGGER = LogManager.getLogger(ReferenceCountingVisitor.class);

  private static final ReferenceCountingVisitor SINGLETON = new ReferenceCountingVisitor();

  @NonNull
  private static final MetapathExpression PARAM_MARKUP_METAPATH
      = MetapathExpression
          .compile(
              "label|usage|constraint/(description|tests/remarks)|guideline/prose|select/choice|remarks",
              OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression ROLE_MARKUP_METAPATH
      = MetapathExpression.compile("title|description|remarks",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression LOCATION_MARKUP_METAPATH
      = MetapathExpression.compile("title|remarks",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression PARTY_MARKUP_METAPATH
      = MetapathExpression.compile("title|remarks",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
  @NonNull
  private static final MetapathExpression RESOURCE_MARKUP_METAPATH
      = MetapathExpression.compile("title|description|remarks",
          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);

  @NonNull
  private static final IReferencePolicy<Property> PROPERTY_POLICY_IGNORE = IReferencePolicy.ignore();
  @NonNull
  private static final IReferencePolicy<Link> LINK_POLICY_IGNORE = IReferencePolicy.ignore();

  @NonNull
  private static final Map<QName, IReferencePolicy<Property>> PROPERTY_POLICIES;
  @NonNull
  private static final Map<String, IReferencePolicy<Link>> LINK_POLICIES;
  @NonNull
  private static final InsertReferencePolicy INSERT_POLICY = new InsertReferencePolicy();
  @NonNull
  private static final AnchorReferencePolicy ANCHOR_POLICY = new AnchorReferencePolicy();

  static {
    PROPERTY_POLICIES = new HashMap<>();
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "resolution-tool"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "label"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "sort-id"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "alt-label"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "alt-identifier"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "method"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "keep"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.RMF_NAMESPACE, "method"), PROPERTY_POLICY_IGNORE);
    PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.RMF_NAMESPACE, "aggregates"),
        PropertyReferencePolicy.create(IIdentifierParser.IDENTITY_PARSER, IEntityItem.ItemType.PARAMETER));

    LINK_POLICIES = new HashMap<>();
    LINK_POLICIES.put("source-profile", LINK_POLICY_IGNORE);
    LINK_POLICIES.put("citation", LinkReferencePolicy.create(IEntityItem.ItemType.RESOURCE));
    LINK_POLICIES.put("reference", LinkReferencePolicy.create(IEntityItem.ItemType.RESOURCE));
    LINK_POLICIES.put("related", LinkReferencePolicy.create(IEntityItem.ItemType.CONTROL));
    LINK_POLICIES.put("required", LinkReferencePolicy.create(IEntityItem.ItemType.CONTROL));
    LINK_POLICIES.put("corresp", LinkReferencePolicy.create(IEntityItem.ItemType.PART));
  }

  @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED", justification = "class initialization")
  public static ReferenceCountingVisitor instance() {
    return SINGLETON;
  }

  private ReferenceCountingVisitor() {
    // visit everything except parts, roles, locations, parties, parameters, and
    // resources, which are
    // handled differently by this visitor
    super(ObjectUtils.notNull(EnumSet.complementOf(
        EnumSet.of(
            IEntityItem.ItemType.PART,
            IEntityItem.ItemType.ROLE,
            IEntityItem.ItemType.LOCATION,
            IEntityItem.ItemType.PARTY,
            IEntityItem.ItemType.PARAMETER,
            IEntityItem.ItemType.RESOURCE))));
  }

  @Override
  protected Void newDefaultResult(Context context) {
    // do nothing
    return null;
  }

  @Override
  protected Void aggregateResults(Void first, Void second, Context context) {
    // do nothing
    return null;
  }

  //
  // public void visitProfile(@NonNull Profile profile) {
  // // process children
  // Metadata metadata = profile.getMetadata();
  // if (metadata != null) {
  // visitMetadata(metadata);
  // }
  //
  // BackMatter backMatter = profile.getBackMatter();
  // if (backMatter != null) {
  // for (BackMatter.Resource resource :
  // CollectionUtil.listOrEmpty(backMatter.getResources())) {
  // visitResource(resource);
  // }
  // }
  // }

  public void visitCatalog(@NonNull IDocumentNodeItem catalogItem, @NonNull IIndexer indexer, @NonNull URI baseUri) {
    Context context = new Context(indexer, baseUri);
    visitCatalog(catalogItem, context);

    IIndexer index = context.getIndexer();
    // resolve the entities picked up by the original indexing operation
    // FIXME: Is this necessary?
    IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.ROLE))
        .forEachOrdered(
            item -> resolveEntity(ObjectUtils.notNull(item), context, ReferenceCountingVisitor::resolveRole));
    IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.LOCATION))
        .forEachOrdered(
            item -> resolveEntity(ObjectUtils.notNull(item), context,
                ReferenceCountingVisitor::resolveLocation));
    IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.PARTY))
        .forEachOrdered(
            item -> resolveEntity(ObjectUtils.notNull(item), context,
                ReferenceCountingVisitor::resolveParty));
    IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.PARAMETER))
        .forEachOrdered(
            item -> resolveEntity(ObjectUtils.notNull(item), context,
                ReferenceCountingVisitor::resolveParameter));
    IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.RESOURCE))
        .forEachOrdered(
            item -> resolveEntity(ObjectUtils.notNull(item), context,
                ReferenceCountingVisitor::resolveResource));
  }

  @Override
  public Void visitGroup(
      IAssemblyNodeItem item,
      Void childResult,
      Context context) {
    IIndexer index = context.getIndexer();
    // handle the group if it is selected
    // a group will only be selected if it contains a descendant control that is
    // selected
    if (IIndexer.SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
      CatalogGroup group = ObjectUtils.requireNonNull((CatalogGroup) item.getValue());
      String id = group.getId();

      boolean resolve;
      if (id == null) {
        // always resolve a group without an identifier
        resolve = true;
      } else {
        IEntityItem entity = index.getEntity(IEntityItem.ItemType.GROUP, id, false);
        if (entity != null && !context.isResolved(entity)) {
          // only resolve if not already resolved
          context.markResolved(entity);
          resolve = true;
        } else {
          resolve = false;
        }
      }

      // resolve only if requested
      if (resolve) {
        resolveGroup(item, context);
      }
    }
    return null;
  }

  @Override
  public Void visitControl(
      IAssemblyNodeItem item,
      Void childResult,
      Context context) {
    IIndexer index = context.getIndexer();
    // handle the control if it is selected
    if (IIndexer.SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
      Control control = ObjectUtils.requireNonNull((Control) item.getValue());
      IEntityItem entity
          = context.getIndexer().getEntity(IEntityItem.ItemType.CONTROL, ObjectUtils.notNull(control.getId()), false);

      // the control must always appear in the index
      assert entity != null;

      if (!context.isResolved(entity)) {
        context.markResolved(entity);
        if (IIndexer.SelectionStatus.SELECTED.equals(context.getIndexer().getSelectionStatus(item))) {
          resolveControl(item, context);
        }
      }
    }
    return null;
  }

  @Override
  protected void visitParts(
      IAssemblyNodeItem groupOrControlItem,
      Context context) {
    // visits all descendant parts
    CHILD_PART_METAPATH.evaluate(groupOrControlItem).stream()
        .map(item -> (IAssemblyNodeItem) item)
        .forEachOrdered(partItem -> {
          visitPart(ObjectUtils.notNull(partItem), groupOrControlItem, context);
        });
  }

  @Override
  protected void visitPart(
      IAssemblyNodeItem item,
      IAssemblyNodeItem groupOrControlItem,
      Context context) {
    assert context != null;

    ControlPart part = ObjectUtils.requireNonNull((ControlPart) item.getValue());
    String id = part.getId();

    boolean resolve;
    if (id == null) {
      // always resolve a part without an identifier
      resolve = true;
    } else {
      IEntityItem entity = context.getIndexer().getEntity(IEntityItem.ItemType.PART, id, false);
      if (entity != null && !context.isResolved(entity)) {
        // only resolve if not already resolved
        context.markResolved(entity);
        resolve = true;
      } else {
        resolve = false;
      }
    }

    if (resolve) {
      resolvePart(item, context);
    }
  }

  protected void resolveGroup(
      @NonNull IAssemblyNodeItem item,
      @NonNull Context context) {
    if (IIndexer.SelectionStatus.SELECTED.equals(context.getIndexer().getSelectionStatus(item))) {

      // process children
      item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
          .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
      item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
          .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
      item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
          .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));

      // always visit parts
      visitParts(item, context);

      // skip parameters for now. These will be processed by a separate pass.
    }
  }

  protected void resolveControl(
      @NonNull IAssemblyNodeItem item,
      @NonNull Context context) {
    // process non-control, non-param children
    item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
        .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
        .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));

    // always visit parts
    visitParts(item, context);

    // skip parameters for now. These will be processed by a separate pass.
  }

  private static void resolveRole(@NonNull IEntityItem entity, @NonNull Context context) {
    IModelNodeItem<?, ?> item = entity.getInstance();
    item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
        .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
        .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    ROLE_MARKUP_METAPATH.evaluate(item).getValue()
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
  }

  private static void resolveParty(@NonNull IEntityItem entity, @NonNull Context context) {
    IModelNodeItem<?, ?> item = entity.getInstance();
    item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
        .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
        .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    PARTY_MARKUP_METAPATH.evaluate(item).getValue()
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
  }

  public static void resolveLocation(@NonNull IEntityItem entity, @NonNull Context context) {
    IModelNodeItem<?, ?> item = entity.getInstance();
    item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
        .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
        .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    LOCATION_MARKUP_METAPATH.evaluate(item).getValue()
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
  }

  public static void resolveResource(@NonNull IEntityItem entity, @NonNull Context context) {
    IModelNodeItem<?, ?> item = entity.getInstance();

    item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
        .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));

    item.getModelItemsByName(OscalModelConstants.QNAME_CITATION).forEach(child -> {
      if (child != null) {
        child.getModelItemsByName(OscalModelConstants.QNAME_TEXT)
            .forEach(citationChild -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) citationChild), context));
        child.getModelItemsByName(OscalModelConstants.QNAME_PROP)
            .forEach(citationChild -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) citationChild), context));
        child.getModelItemsByName(OscalModelConstants.QNAME_LINK)
            .forEach(citationChild -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) citationChild), context));
      }
    });

    RESOURCE_MARKUP_METAPATH.evaluate(item).getValue()
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
  }

  public static void resolveParameter(@NonNull IEntityItem entity, @NonNull Context context) {
    IModelNodeItem<?, ?> item = entity.getInstance();

    item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
        .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
        .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    PARAM_MARKUP_METAPATH.evaluate(item).getValue()
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
  }

  private static void resolvePart(
      @NonNull IAssemblyNodeItem item,
      @NonNull Context context) {
    item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
        .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
        .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
    item.getModelItemsByName(OscalModelConstants.QNAME_PROSE)
        .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
    // item.getModelItemsByName("part").forEach(child ->
    // visitor.visitPart(ObjectUtils.notNull(child),
    // context));
  }

  private static void handleMarkup(
      @NonNull IFieldNodeItem item,
      @NonNull Context context) {
    IMarkupItem markupItem = (IMarkupItem) FnData.fnDataItem(item);
    IMarkupString<?> markup = markupItem.asMarkup();
    handleMarkup(item, markup, context);
  }

  private static void handleMarkup(
      @NonNull IFieldNodeItem contextItem,
      @NonNull IMarkupString<?> text,
      @NonNull Context context) {
    for (Node node : CollectionUtil.toIterable(
        ObjectUtils.notNull(text.getNodesAsStream().iterator()))) {
      if (node instanceof InsertAnchorNode) {
        handleInsert(contextItem, (InsertAnchorNode) node, context);
      } else if (node instanceof InlineLinkNode) {
        handleAnchor(contextItem, (InlineLinkNode) node, context);
      }
    }
  }

  private static void handleInsert(
      @NonNull IFieldNodeItem contextItem,
      @NonNull InsertAnchorNode node,
      @NonNull Context context) {
    boolean retval = INSERT_POLICY.handleReference(contextItem, node, context);
    if (LOGGER.isWarnEnabled() && !retval) {
      LOGGER.atWarn().log("Unsupported insert type '{}' at '{}'",
          node.getType().toString(),
          contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
    }
  }

  private static void handleAnchor(
      @NonNull IFieldNodeItem contextItem,
      @NonNull InlineLinkNode node,
      @NonNull Context context) {
    boolean result = ANCHOR_POLICY.handleReference(contextItem, node, context);
    if (LOGGER.isWarnEnabled() && !result) {
      LOGGER.atWarn().log("Unsupported anchor with href '{}' at '{}'",
          node.getUrl().toString(),
          contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
    }
  }

  private static void handleProperty(
      @NonNull IAssemblyNodeItem item,
      @NonNull Context context) {
    Property property = ObjectUtils.requireNonNull((Property) item.getValue());
    QName qname = property.getQName();

    IReferencePolicy<Property> policy = PROPERTY_POLICIES.get(qname);

    boolean result = policy != null && policy.handleReference(item, property, context);
    if (LOGGER.isWarnEnabled() && !result) {
      LOGGER.atWarn().log("Unsupported property '{}' at '{}'",
          property.getQName(),
          item.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
    }
  }

  private static void handleLink(
      @NonNull IAssemblyNodeItem item,
      @NonNull Context context) {
    Link link = ObjectUtils.requireNonNull((Link) item.getValue());
    IReferencePolicy<Link> policy = null;
    String rel = link.getRel();
    if (rel != null) {
      policy = LINK_POLICIES.get(rel);
    }

    boolean result = policy != null && policy.handleReference(item, link, context);
    if (LOGGER.isWarnEnabled() && !result) {
      LOGGER.atWarn().log("unsupported link rel '{}' at '{}'",
          link.getRel(),
          item.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
    }
  }

  protected void resolveEntity(
      @NonNull IEntityItem entity,
      @NonNull Context context,
      @NonNull BiConsumer<IEntityItem, Context> handler) {

    if (!context.isResolved(entity)) {
      context.markResolved(entity);

      if (LOGGER.isDebugEnabled()) {
        LOGGER.atDebug().log("Resolving {} identified as '{}'",
            entity.getItemType().name(),
            entity.getIdentifier());
      }

      if (!IIndexer.SelectionStatus.UNSELECTED
          .equals(context.getIndexer().getSelectionStatus(entity.getInstance()))) {
        // only resolve selected and unknown entities
        handler.accept(entity, context);
      }
    }
  }

  public void resolveEntity(
      @NonNull IEntityItem entity,
      @NonNull Context context) {
    resolveEntity(entity, context, (theEntity, theContext) -> entityDispatch(
        ObjectUtils.notNull(theEntity),
        ObjectUtils.notNull(theContext)));
  }

  protected void entityDispatch(@NonNull IEntityItem entity, @NonNull Context context) {
    IAssemblyNodeItem item = (IAssemblyNodeItem) entity.getInstance();
    switch (entity.getItemType()) {
    case CONTROL:
      resolveControl(item, context);
      break;
    case GROUP:
      resolveGroup(item, context);
      break;
    case LOCATION:
      resolveLocation(entity, context);
      break;
    case PARAMETER:
      resolveParameter(entity, context);
      break;
    case PART:
      resolvePart(item, context);
      break;
    case PARTY:
      resolveParty(entity, context);
      break;
    case RESOURCE:
      resolveResource(entity, context);
      break;
    case ROLE:
      resolveRole(entity, context);
      break;
    default:
      throw new UnsupportedOperationException(entity.getItemType().name());
    }
  }
  //
  // @Override
  // protected Void newDefaultResult(Object context) {
  // return null;
  // }
  //
  // @Override
  // protected Void aggregateResults(Object first, Object second, Object context)
  // {
  // return null;
  // }

  public static final class Context {
    @NonNull
    private final IIndexer indexer;
    @NonNull
    private final URI source;
    @NonNull
    private final Set<IEntityItem> resolvedEntities = new HashSet<>();

    private Context(@NonNull IIndexer indexer, @NonNull URI source) {
      this.indexer = indexer;
      this.source = source;
    }

    @NonNull
    @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intending to expose this field")
    public IIndexer getIndexer() {
      return indexer;
    }

    @Nullable
    public IEntityItem getEntity(@NonNull IEntityItem.ItemType itemType, @NonNull String identifier) {
      return getIndexer().getEntity(itemType, identifier);
    }

    @SuppressWarnings("unused")
    @NonNull
    private URI getSource() {
      return source;
    }

    public void markResolved(@NonNull IEntityItem entity) {
      resolvedEntities.add(entity);
    }

    public boolean isResolved(@NonNull IEntityItem entity) {
      return resolvedEntities.contains(entity);
    }

    public void incrementReferenceCount(
        @NonNull IModelNodeItem<?, ?> contextItem,
        @NonNull IEntityItem.ItemType type,
        @NonNull UUID identifier) {
      incrementReferenceCountInternal(
          contextItem,
          type,
          ObjectUtils.notNull(identifier.toString()),
          false);
    }

    public void incrementReferenceCount(
        @NonNull IModelNodeItem<?, ?> contextItem,
        @NonNull IEntityItem.ItemType type,
        @NonNull String identifier) {
      incrementReferenceCountInternal(
          contextItem,
          type,
          identifier,
          type.isUuid());
    }

    private void incrementReferenceCountInternal(
        @NonNull IModelNodeItem<?, ?> contextItem,
        @NonNull IEntityItem.ItemType type,
        @NonNull String identifier,
        boolean normalize) {
      IEntityItem item = getIndexer().getEntity(type, identifier, normalize);
      if (item == null) {
        if (LOGGER.isErrorEnabled()) {
          LOGGER.atError().log("Unknown reference to {} '{}' at '{}'",
              type.toString().toLowerCase(Locale.ROOT),
              identifier,
              contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
        }
      } else {
        item.incrementReferenceCount();
      }
    }
  }
}