001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.oscal.lib.profile.resolver.support;
007
008import gov.nist.secauto.metaschema.core.datatype.adapter.UuidAdapter;
009import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
010import gov.nist.secauto.metaschema.core.metapath.MetapathExpression.ResultType;
011import gov.nist.secauto.metaschema.core.metapath.item.node.IModelNodeItem;
012import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
013import gov.nist.secauto.metaschema.core.util.CollectionUtil;
014import gov.nist.secauto.metaschema.core.util.ObjectUtils;
015import gov.nist.secauto.oscal.lib.OscalBindingContext;
016import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
017import gov.nist.secauto.oscal.lib.model.CatalogGroup;
018import gov.nist.secauto.oscal.lib.model.Control;
019import gov.nist.secauto.oscal.lib.model.ControlPart;
020import gov.nist.secauto.oscal.lib.model.Metadata.Location;
021import gov.nist.secauto.oscal.lib.model.Metadata.Party;
022import gov.nist.secauto.oscal.lib.model.Metadata.Role;
023import gov.nist.secauto.oscal.lib.model.Parameter;
024import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
025import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType;
026
027import org.apache.logging.log4j.LogManager;
028import org.apache.logging.log4j.Logger;
029
030import java.util.Collection;
031import java.util.Collections;
032import java.util.EnumMap;
033import java.util.LinkedHashMap;
034import java.util.Locale;
035import java.util.Map;
036import java.util.UUID;
037import java.util.concurrent.ConcurrentHashMap;
038import java.util.stream.Collectors;
039
040import edu.umd.cs.findbugs.annotations.NonNull;
041
042public class BasicIndexer implements IIndexer {
043  private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
044  private static final MetapathExpression CONTAINER_METAPATH
045      = MetapathExpression.compile("(ancestor::control|ancestor::group)[1])",
046          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
047
048  @NonNull
049  private final Map<IEntityItem.ItemType, Map<String, IEntityItem>> entityTypeToIdentifierToEntityMap;
050  @NonNull
051  private Map<INodeItem, SelectionStatus> nodeItemToSelectionStatusMap;
052
053  @Override
054  public void append(@NonNull IIndexer other) {
055    for (ItemType itemType : ItemType.values()) {
056      assert itemType != null;
057      for (IEntityItem entity : other.getEntitiesByItemType(itemType)) {
058        assert entity != null;
059        addItem(entity);
060      }
061    }
062
063    this.nodeItemToSelectionStatusMap.putAll(other.getSelectionStatusMap());
064  }
065
066  public BasicIndexer() {
067    this.entityTypeToIdentifierToEntityMap = new EnumMap<>(IEntityItem.ItemType.class);
068    this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>();
069  }
070
071  @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // needed
072  public BasicIndexer(IIndexer other) {
073    // copy entity map
074    this.entityTypeToIdentifierToEntityMap = other.getEntities();
075
076    // copy selection map
077    this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(other.getSelectionStatusMap());
078  }
079
080  @Override
081  public void setSelectionStatus(@NonNull INodeItem item, @NonNull SelectionStatus selectionStatus) {
082    nodeItemToSelectionStatusMap.put(item, selectionStatus);
083  }
084
085  @Override
086  public Map<INodeItem, SelectionStatus> getSelectionStatusMap() {
087    return CollectionUtil.unmodifiableMap(nodeItemToSelectionStatusMap);
088  }
089
090  @Override
091  public SelectionStatus getSelectionStatus(@NonNull INodeItem item) {
092    SelectionStatus retval = nodeItemToSelectionStatusMap.get(item);
093    return retval == null ? SelectionStatus.UNKNOWN : retval;
094  }
095
096  @Override
097  public void resetSelectionStatus() {
098    nodeItemToSelectionStatusMap = new ConcurrentHashMap<>();
099  }
100
101  @Override
102  public boolean isSelected(@NonNull IEntityItem entity) {
103    boolean retval;
104    switch (entity.getItemType()) {
105    case CONTROL:
106    case GROUP:
107      retval = IIndexer.SelectionStatus.SELECTED.equals(getSelectionStatus(entity.getInstance()));
108      break;
109    case PART: {
110      IModelNodeItem<?, ?> instance = entity.getInstance();
111      IIndexer.SelectionStatus status = getSelectionStatus(instance);
112      if (IIndexer.SelectionStatus.UNKNOWN.equals(status)) {
113        // lookup the status if not known
114        IModelNodeItem<?, ?> containerItem = CONTAINER_METAPATH.evaluateAs(instance, ResultType.NODE);
115        assert containerItem != null;
116        status = getSelectionStatus(containerItem);
117
118        // cache the status
119        setSelectionStatus(instance, status);
120      }
121      retval = IIndexer.SelectionStatus.SELECTED.equals(status);
122      break;
123    }
124    case PARAMETER:
125    case LOCATION:
126    case PARTY:
127    case RESOURCE:
128    case ROLE:
129      // always "selected"
130      retval = true;
131      break;
132    default:
133      throw new UnsupportedOperationException(entity.getItemType().name());
134    }
135    return retval;
136  }
137
138  @Override
139  public Map<ItemType, Map<String, IEntityItem>> getEntities() {
140    // make a copy
141    Map<ItemType, Map<String, IEntityItem>> copy = entityTypeToIdentifierToEntityMap.entrySet().stream()
142        .map(entry -> {
143          ItemType key = entry.getKey();
144          Map<String, IEntityItem> oldMap = entry.getValue();
145
146          Map<String, IEntityItem> newMap = oldMap.entrySet().stream()
147              .collect(Collectors.toMap(
148                  Map.Entry::getKey,
149                  Map.Entry::getValue,
150                  (key1, key2) -> key1,
151                  LinkedHashMap::new)); // need ordering
152          assert newMap != null;
153          // use a synchronized map to ensure thread safety
154          return Map.entry(key, Collections.synchronizedMap(newMap));
155        })
156        .collect(Collectors.toMap(
157            Map.Entry::getKey,
158            Map.Entry::getValue,
159            (key1, key2) -> key1,
160            ConcurrentHashMap::new));
161
162    assert copy != null;
163    return copy;
164  }
165
166  @Override
167  @NonNull
168  // TODO: rename to getEntitiesForItemType
169  public Collection<IEntityItem> getEntitiesByItemType(@NonNull IEntityItem.ItemType itemType) {
170    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.get(itemType);
171    return entityGroup == null ? CollectionUtil.emptyList() : ObjectUtils.notNull(entityGroup.values());
172  }
173  //
174  // public EntityItem getEntity(@NonNull ItemType itemType, @NonNull UUID
175  // identifier) {
176  // return getEntity(itemType, ObjectUtils.notNull(identifier.toString()),
177  // false);
178  // }
179  //
180  // public EntityItem getEntity(@NonNull ItemType itemType, @NonNull String
181  // identifier) {
182  // return getEntity(itemType, identifier, itemType.isUuid());
183  // }
184
185  @Override
186  public IEntityItem getEntity(@NonNull ItemType itemType, @NonNull String identifier, boolean normalize) {
187    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.get(itemType);
188    String normalizedIdentifier = normalize ? normalizeIdentifier(identifier) : identifier;
189    return entityGroup == null ? null : entityGroup.get(normalizedIdentifier);
190  }
191
192  protected IEntityItem addItem(@NonNull IEntityItem item) {
193    IEntityItem.ItemType type = item.getItemType();
194
195    @SuppressWarnings("PMD.UseConcurrentHashMap") // need ordering
196    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.computeIfAbsent(
197        type,
198        (key) -> Collections.synchronizedMap(new LinkedHashMap<>()));
199    IEntityItem oldEntity = entityGroup.put(item.getIdentifier(), item);
200
201    if (oldEntity != null && LOGGER.isWarnEnabled()) {
202      LOGGER.atWarn().log("Duplicate {} found with identifier {} in index.",
203          oldEntity.getItemType().name().toLowerCase(Locale.ROOT),
204          oldEntity.getIdentifier());
205    }
206    return oldEntity;
207  }
208
209  @NonNull
210  protected IEntityItem addItem(@NonNull AbstractEntityItem.Builder builder) {
211    IEntityItem retval = builder.build();
212    addItem(retval);
213    return retval;
214  }
215
216  @Override
217  public boolean removeItem(@NonNull IEntityItem entity) {
218    IEntityItem.ItemType type = entity.getItemType();
219    Map<String, IEntityItem> entityGroup = entityTypeToIdentifierToEntityMap.get(type);
220
221    boolean retval = false;
222    if (entityGroup != null) {
223      retval = entityGroup.remove(entity.getIdentifier(), entity);
224
225      // remove if present
226      nodeItemToSelectionStatusMap.remove(entity.getInstance());
227
228      if (retval) {
229        if (LOGGER.isDebugEnabled()) {
230          LOGGER.atDebug().log("Removing {} '{}' from index.", type.name(), entity.getIdentifier());
231        }
232      } else if (LOGGER.isDebugEnabled()) {
233        LOGGER.atDebug().log("The {} entity '{}' was not found in the index to remove.",
234            type.name(),
235            entity.getIdentifier());
236      }
237    }
238    return retval;
239  }
240
241  @Override
242  public IEntityItem addRole(IModelNodeItem<?, ?> item) {
243    Role role = ObjectUtils.requireNonNull((Role) item.getValue());
244    String identifier = ObjectUtils.requireNonNull(role.getId());
245
246    return addItem(newBuilder(item, ItemType.ROLE, identifier));
247  }
248
249  @Override
250  public IEntityItem addLocation(IModelNodeItem<?, ?> item) {
251    Location location = ObjectUtils.requireNonNull((Location) item.getValue());
252    UUID identifier = ObjectUtils.requireNonNull(location.getUuid());
253
254    return addItem(newBuilder(item, ItemType.LOCATION, identifier));
255  }
256
257  @Override
258  public IEntityItem addParty(IModelNodeItem<?, ?> item) {
259    Party party = ObjectUtils.requireNonNull((Party) item.getValue());
260    UUID identifier = ObjectUtils.requireNonNull(party.getUuid());
261
262    return addItem(newBuilder(item, ItemType.PARTY, identifier));
263  }
264
265  @Override
266  public IEntityItem addGroup(IModelNodeItem<?, ?> item) {
267    CatalogGroup group = ObjectUtils.requireNonNull((CatalogGroup) item.getValue());
268    String identifier = group.getId();
269    return identifier == null ? null : addItem(newBuilder(item, ItemType.GROUP, identifier));
270  }
271
272  @Override
273  public IEntityItem addControl(IModelNodeItem<?, ?> item) {
274    Control control = ObjectUtils.requireNonNull((Control) item.getValue());
275    String identifier = ObjectUtils.requireNonNull(control.getId());
276    return addItem(newBuilder(item, ItemType.CONTROL, identifier));
277  }
278
279  @Override
280  public IEntityItem addParameter(IModelNodeItem<?, ?> item) {
281    Parameter parameter = ObjectUtils.requireNonNull((Parameter) item.getValue());
282    String identifier = ObjectUtils.requireNonNull(parameter.getId());
283
284    return addItem(newBuilder(item, ItemType.PARAMETER, identifier));
285  }
286
287  @Override
288  public IEntityItem addPart(IModelNodeItem<?, ?> item) {
289    ControlPart part = ObjectUtils.requireNonNull((ControlPart) item.getValue());
290    String identifier = part.getId();
291
292    return identifier == null ? null : addItem(newBuilder(item, ItemType.PART, identifier));
293  }
294
295  @Override
296  public IEntityItem addResource(IModelNodeItem<?, ?> item) {
297    Resource resource = ObjectUtils.requireNonNull((Resource) item.getValue());
298    UUID identifier = ObjectUtils.requireNonNull(resource.getUuid());
299
300    return addItem(newBuilder(item, ItemType.RESOURCE, identifier));
301  }
302
303  @NonNull
304  protected final AbstractEntityItem.Builder newBuilder(
305      @NonNull IModelNodeItem<?, ?> item,
306      @NonNull ItemType itemType,
307      @NonNull UUID identifier) {
308    return newBuilder(item, itemType, ObjectUtils.notNull(identifier.toString()));
309  }
310
311  /**
312   * Create a new builder with the provided info.
313   * <p>
314   * This method can be overloaded to support applying additional data to the
315   * returned builder.
316   * <p>
317   * When working with identifiers that are case insensitve, it is important to
318   * ensure that the identifiers are normalized to lower case.
319   *
320   * @param item
321   *          the Metapath node to associate with the entity
322   * @param itemType
323   *          the type of entity
324   * @param identifier
325   *          the entity's identifier
326   * @return the entity builder
327   */
328  @NonNull
329  protected AbstractEntityItem.Builder newBuilder(
330      @NonNull IModelNodeItem<?, ?> item,
331      @NonNull ItemType itemType,
332      @NonNull String identifier) {
333    return new AbstractEntityItem.Builder()
334        .instance(item, itemType)
335        .originalIdentifier(identifier)
336        .source(ObjectUtils.requireNonNull(item.getBaseUri(), "item must have an associated URI"));
337  }
338
339  /**
340   * Lower case UUID-based identifiers and leave others unmodified.
341   *
342   * @param identifier
343   *          the identifier
344   * @return the resulting normalized identifier
345   */
346  @NonNull
347  public String normalizeIdentifier(@NonNull String identifier) {
348    return UuidAdapter.UUID_PATTERN.matcher(identifier).matches()
349        ? ObjectUtils.notNull(identifier.toLowerCase(Locale.ROOT))
350        : identifier;
351  }
352  //
353  // private static class ItemGroup {
354  // @NonNull
355  // private final ItemType itemType;
356  // Map<String, IEntityItem> idToEntityMap;
357  //
358  // public ItemGroup(@NonNull ItemType itemType) {
359  // this.itemType = itemType;
360  // this.idToEntityMap = new LinkedHashMap<>();
361  // }
362  //
363  // public IEntityItem getEntity(@NonNull String identifier) {
364  // return idToEntityMap.get(identifier);
365  // }
366  //
367  // @SuppressWarnings("null")
368  // @NonNull
369  // public Collection<IEntityItem> getEntities() {
370  // return idToEntityMap.values();
371  // }
372  //
373  // public IEntityItem add(@NonNull IEntityItem entity) {
374  // assert itemType.equals(entity.getItemType());
375  // return idToEntityMap.put(entity.getOriginalIdentifier(), entity);
376  // }
377  // }
378}