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