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