1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.oscal.lib.profile.resolver.support;
7   
8   import gov.nist.secauto.metaschema.core.datatype.adapter.UuidAdapter;
9   import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
10  import gov.nist.secauto.metaschema.core.metapath.MetapathExpression.ResultType;
11  import gov.nist.secauto.metaschema.core.metapath.item.node.IModelNodeItem;
12  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
13  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
14  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
15  import gov.nist.secauto.oscal.lib.OscalBindingContext;
16  import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
17  import gov.nist.secauto.oscal.lib.model.CatalogGroup;
18  import gov.nist.secauto.oscal.lib.model.Control;
19  import gov.nist.secauto.oscal.lib.model.ControlPart;
20  import gov.nist.secauto.oscal.lib.model.Metadata.Location;
21  import gov.nist.secauto.oscal.lib.model.Metadata.Party;
22  import gov.nist.secauto.oscal.lib.model.Metadata.Role;
23  import gov.nist.secauto.oscal.lib.model.Parameter;
24  import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
25  import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType;
26  
27  import org.apache.logging.log4j.LogManager;
28  import org.apache.logging.log4j.Logger;
29  
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.EnumMap;
33  import java.util.LinkedHashMap;
34  import java.util.Locale;
35  import java.util.Map;
36  import java.util.UUID;
37  import java.util.concurrent.ConcurrentHashMap;
38  import java.util.stream.Collectors;
39  
40  import edu.umd.cs.findbugs.annotations.NonNull;
41  
42  public class BasicIndexer implements IIndexer {
43    private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
44    private static final MetapathExpression CONTAINER_METAPATH
45        = MetapathExpression.compile("(ancestor::control|ancestor::group)[1])",
46            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
47  
48    @NonNull
49    private final Map<IEntityItem.ItemType, Map<String, IEntityItem>> entityTypeToIdentifierToEntityMap;
50    @NonNull
51    private Map<INodeItem, SelectionStatus> nodeItemToSelectionStatusMap;
52  
53    @Override
54    public void append(@NonNull IIndexer other) {
55      for (ItemType itemType : ItemType.values()) {
56        assert itemType != null;
57        for (IEntityItem entity : other.getEntitiesByItemType(itemType)) {
58          assert entity != null;
59          addItem(entity);
60        }
61      }
62  
63      this.nodeItemToSelectionStatusMap.putAll(other.getSelectionStatusMap());
64    }
65  
66    public BasicIndexer() {
67      this.entityTypeToIdentifierToEntityMap = new EnumMap<>(IEntityItem.ItemType.class);
68      this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>();
69    }
70  
71    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // needed
72    public BasicIndexer(IIndexer other) {
73      // copy entity map
74      this.entityTypeToIdentifierToEntityMap = other.getEntities();
75  
76      // copy selection map
77      this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(other.getSelectionStatusMap());
78    }
79  
80    @Override
81    public void setSelectionStatus(@NonNull INodeItem item, @NonNull SelectionStatus selectionStatus) {
82      nodeItemToSelectionStatusMap.put(item, selectionStatus);
83    }
84  
85    @Override
86    public Map<INodeItem, SelectionStatus> getSelectionStatusMap() {
87      return CollectionUtil.unmodifiableMap(nodeItemToSelectionStatusMap);
88    }
89  
90    @Override
91    public SelectionStatus getSelectionStatus(@NonNull INodeItem item) {
92      SelectionStatus retval = nodeItemToSelectionStatusMap.get(item);
93      return retval == null ? SelectionStatus.UNKNOWN : retval;
94    }
95  
96    @Override
97    public void resetSelectionStatus() {
98      nodeItemToSelectionStatusMap = new ConcurrentHashMap<>();
99    }
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 }