BasicIndexer.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.support;

import gov.nist.secauto.metaschema.model.common.datatype.adapter.UuidAdapter;
import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression.ResultType;
import gov.nist.secauto.metaschema.model.common.metapath.item.INodeItem;
import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueModelNodeItem;
import gov.nist.secauto.metaschema.model.common.util.CollectionUtil;
import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
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.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.Parameter;
import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType;

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

import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

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

public class BasicIndexer implements IIndexer {
  private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
  private static final MetapathExpression CONTAINER_METAPATH
      = MetapathExpression.compile("(ancestor::control|ancestor::group)[1])");

  @NonNull
  private final Map<IEntityItem.ItemType, Map<String, IEntityItem>> entityTypeToIdentifierToEntityMap;
  @NonNull
  private Map<INodeItem, SelectionStatus> nodeItemToSelectionStatusMap;

  @Override
  public void append(@NonNull IIndexer other) {
    for (ItemType itemType : ItemType.values()) {
      assert itemType != null;
      for (IEntityItem entity : other.getEntitiesByItemType(itemType)) {
        assert entity != null;
        addItem(entity);
      }
    }

    this.nodeItemToSelectionStatusMap.putAll(other.getSelectionStatusMap());
  }

  public BasicIndexer() {
    this.entityTypeToIdentifierToEntityMap = new EnumMap<>(IEntityItem.ItemType.class);
    this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>();
  }

  @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // needed
  public BasicIndexer(IIndexer other) {
    // copy entity map
    this.entityTypeToIdentifierToEntityMap = other.getEntities();

    // copy selection map
    this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(other.getSelectionStatusMap());
  }

  @Override
  public void setSelectionStatus(@NonNull INodeItem item, @NonNull SelectionStatus selectionStatus) {
    nodeItemToSelectionStatusMap.put(item, selectionStatus);
  }

  @Override
  public Map<INodeItem, SelectionStatus> getSelectionStatusMap() {
    return CollectionUtil.unmodifiableMap(nodeItemToSelectionStatusMap);
  }

  @Override
  public SelectionStatus getSelectionStatus(@NonNull INodeItem item) {
    SelectionStatus retval = nodeItemToSelectionStatusMap.get(item);
    return retval == null ? SelectionStatus.UNKNOWN : retval;
  }

  @Override
  public void resetSelectionStatus() {
    nodeItemToSelectionStatusMap = new ConcurrentHashMap<>();
  }

  @Override
  public boolean isSelected(@NonNull IEntityItem entity) {
    boolean retval;
    switch (entity.getItemType()) {
    case CONTROL:
    case GROUP:
      retval = IIndexer.SelectionStatus.SELECTED.equals(getSelectionStatus(entity.getInstance()));
      break;
    case PART: {
      IRequiredValueModelNodeItem instance = entity.getInstance();
      IIndexer.SelectionStatus status = getSelectionStatus(instance);
      if (IIndexer.SelectionStatus.UNKNOWN.equals(status)) {
        // lookup the status if not known
        IRequiredValueModelNodeItem containerItem = CONTAINER_METAPATH.evaluateAs(instance, ResultType.NODE);
        assert containerItem != null;
        status = getSelectionStatus(containerItem);

        // cache the status
        setSelectionStatus(instance, status);
      }
      retval = IIndexer.SelectionStatus.SELECTED.equals(status);
      break;
    }
    case PARAMETER:
    case LOCATION:
    case PARTY:
    case RESOURCE:
    case ROLE:
      // always "selected"
      retval = true;
      break;
    default:
      throw new UnsupportedOperationException(entity.getItemType().name());
    }
    return retval;
  }

  @Override
  public Map<ItemType, Map<String, IEntityItem>> getEntities() {
    // make a copy
    Map<ItemType, Map<String, IEntityItem>> copy = entityTypeToIdentifierToEntityMap.entrySet().stream()
        .map(entry -> {
          ItemType key = entry.getKey();
          Map<String, IEntityItem> oldMap = entry.getValue();

          Map<String, IEntityItem> newMap = oldMap.entrySet().stream()
              .collect(Collectors.toMap(
                  Map.Entry::getKey,
                  Map.Entry::getValue,
                  (key1, key2) -> key1,
                  LinkedHashMap::new)); // need ordering
          assert newMap != null;
          // use a synchronized map to ensure thread safety
          return Map.entry(key, Collections.synchronizedMap(newMap));
        })
        .collect(Collectors.toMap(
            Map.Entry::getKey,
            Map.Entry::getValue,
            (key1, key2) -> key1,
            ConcurrentHashMap::new));

    assert copy != null;
    return copy;
  }

  @Override
  @NonNull
  // TODO: rename to getEntitiesForItemType
  public Collection<IEntityItem> getEntitiesByItemType(@NonNull IEntityItem.ItemType itemType) {
    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.get(itemType);
    return entityGroup == null ? CollectionUtil.emptyList() : ObjectUtils.notNull(entityGroup.values());
  }
  //
  // public EntityItem getEntity(@NonNull ItemType itemType, @NonNull UUID
  // identifier) {
  // return getEntity(itemType, ObjectUtils.notNull(identifier.toString()),
  // false);
  // }
  //
  // public EntityItem getEntity(@NonNull ItemType itemType, @NonNull String
  // identifier) {
  // return getEntity(itemType, identifier, itemType.isUuid());
  // }

  @Override
  public IEntityItem getEntity(@NonNull ItemType itemType, @NonNull String identifier, boolean normalize) {
    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.get(itemType);
    String normalizedIdentifier = normalize ? normalizeIdentifier(identifier) : identifier;
    return entityGroup == null ? null : entityGroup.get(normalizedIdentifier);
  }

  protected IEntityItem addItem(@NonNull IEntityItem item) {
    IEntityItem.ItemType type = item.getItemType();

    @SuppressWarnings("PMD.UseConcurrentHashMap") // need ordering
    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.computeIfAbsent(
        type,
        (key) -> Collections.synchronizedMap(new LinkedHashMap<>()));
    IEntityItem oldEntity = entityGroup.put(item.getIdentifier(), item);

    if (oldEntity != null && LOGGER.isWarnEnabled()) {
      LOGGER.atWarn().log("Duplicate {} found with identifier {} in index.",
          oldEntity.getItemType().name().toLowerCase(Locale.ROOT),
          oldEntity.getIdentifier());
    }
    return oldEntity;
  }

  @NonNull
  protected IEntityItem addItem(@NonNull AbstractEntityItem.Builder builder) {
    IEntityItem retval = builder.build();
    addItem(retval);
    return retval;
  }

  @Override
  public boolean removeItem(@NonNull IEntityItem entity) {
    IEntityItem.ItemType type = entity.getItemType();
    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.get(type);

    boolean retval = false;
    if (entityGroup != null) {
      retval = entityGroup.remove(entity.getIdentifier(), entity);

      // remove if present
      nodeItemToSelectionStatusMap.remove(entity.getInstance());

      if (retval) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.atDebug().log("Removing {} '{}' from index.", type.name(), entity.getIdentifier());
        }
      } else if (LOGGER.isDebugEnabled()) {
        LOGGER.atDebug().log("The {} entity '{}' was not found in the index to remove.",
            type.name(),
            entity.getIdentifier());
      }
    }
    return retval;
  }

  @Override
  public IEntityItem addRole(IRequiredValueModelNodeItem item) {
    Role role = (Role) item.getValue();
    String identifier = ObjectUtils.requireNonNull(role.getId());

    return addItem(newBuilder(item, ItemType.ROLE, identifier));
  }

  @Override
  public IEntityItem addLocation(IRequiredValueModelNodeItem item) {
    Location location = (Location) item.getValue();
    UUID identifier = ObjectUtils.requireNonNull(location.getUuid());

    return addItem(newBuilder(item, ItemType.LOCATION, identifier));
  }

  @Override
  public IEntityItem addParty(IRequiredValueModelNodeItem item) {
    Party party = (Party) item.getValue();
    UUID identifier = ObjectUtils.requireNonNull(party.getUuid());

    return addItem(newBuilder(item, ItemType.PARTY, identifier));
  }

  @Override
  public IEntityItem addGroup(IRequiredValueModelNodeItem item) {
    CatalogGroup group = (CatalogGroup) item.getValue();
    String identifier = group.getId();
    return identifier == null ? null : addItem(newBuilder(item, ItemType.GROUP, identifier));
  }

  @Override
  public IEntityItem addControl(IRequiredValueModelNodeItem item) {
    Control control = (Control) item.getValue();
    String identifier = ObjectUtils.requireNonNull(control.getId());
    return addItem(newBuilder(item, ItemType.CONTROL, identifier));
  }

  @Override
  public IEntityItem addParameter(IRequiredValueModelNodeItem item) {
    Parameter parameter = (Parameter) item.getValue();
    String identifier = ObjectUtils.requireNonNull(parameter.getId());

    return addItem(newBuilder(item, ItemType.PARAMETER, identifier));
  }

  @Override
  public IEntityItem addPart(IRequiredValueModelNodeItem item) {
    ControlPart part = (ControlPart) item.getValue();
    String identifier = part.getId();

    return identifier == null ? null : addItem(newBuilder(item, ItemType.PART, identifier));
  }

  @Override
  public IEntityItem addResource(IRequiredValueModelNodeItem item) {
    Resource resource = (Resource) item.getValue();
    UUID identifier = ObjectUtils.requireNonNull(resource.getUuid());

    return addItem(newBuilder(item, ItemType.RESOURCE, identifier));
  }

  @NonNull
  protected final AbstractEntityItem.Builder newBuilder(
      @NonNull IRequiredValueModelNodeItem item,
      @NonNull ItemType itemType,
      @NonNull UUID identifier) {
    return newBuilder(item, itemType, ObjectUtils.notNull(identifier.toString()));
  }

  /**
   * Create a new builder with the provided info.
   * <p>
   * This method can be overloaded to support applying additional data to the
   * returned builder.
   * <p>
   * When working with identifiers that are case insensitve, it is important to
   * ensure that the identifiers are normalized to lower case.
   *
   * @param item
   *          the Metapath node to associate with the entity
   * @param itemType
   *          the type of entity
   * @param identifier
   *          the entity's identifier
   * @return the entity builder
   */
  @NonNull
  protected AbstractEntityItem.Builder newBuilder(
      @NonNull IRequiredValueModelNodeItem item,
      @NonNull ItemType itemType,
      @NonNull String identifier) {
    return new AbstractEntityItem.Builder()
        .instance(item, itemType)
        .originalIdentifier(identifier)
        .source(ObjectUtils.requireNonNull(item.getBaseUri(), "item must have an associated URI"));
  }

  /**
   * Lower case UUID-based identifiers and leave others unmodified.
   *
   * @param identifier
   *          the identifier
   * @return the resulting normalized identifier
   */
  @NonNull
  public String normalizeIdentifier(@NonNull String identifier) {
    return UuidAdapter.UUID_PATTERN.matcher(identifier).matches()
        ? ObjectUtils.notNull(identifier.toLowerCase(Locale.ROOT))
        : identifier;
  }
  //
  // private static class ItemGroup {
  // @NonNull
  // private final ItemType itemType;
  // Map<String, IEntityItem> idToEntityMap;
  //
  // public ItemGroup(@NonNull ItemType itemType) {
  // this.itemType = itemType;
  // this.idToEntityMap = new LinkedHashMap<>();
  // }
  //
  // public IEntityItem getEntity(@NonNull String identifier) {
  // return idToEntityMap.get(identifier);
  // }
  //
  // @SuppressWarnings("null")
  // @NonNull
  // public Collection<IEntityItem> getEntities() {
  // return idToEntityMap.values();
  // }
  //
  // public IEntityItem add(@NonNull IEntityItem entity) {
  // assert itemType.equals(entity.getItemType());
  // return idToEntityMap.put(entity.getOriginalIdentifier(), entity);
  // }
  // }
}