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