View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.oscal.lib.profile.resolver.support;
28  
29  import gov.nist.secauto.metaschema.model.common.datatype.adapter.UuidAdapter;
30  import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression;
31  import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression.ResultType;
32  import gov.nist.secauto.metaschema.model.common.metapath.item.INodeItem;
33  import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueModelNodeItem;
34  import gov.nist.secauto.metaschema.model.common.util.CollectionUtil;
35  import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
36  import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
37  import gov.nist.secauto.oscal.lib.model.CatalogGroup;
38  import gov.nist.secauto.oscal.lib.model.Control;
39  import gov.nist.secauto.oscal.lib.model.ControlPart;
40  import gov.nist.secauto.oscal.lib.model.Metadata.Location;
41  import gov.nist.secauto.oscal.lib.model.Metadata.Party;
42  import gov.nist.secauto.oscal.lib.model.Metadata.Role;
43  import gov.nist.secauto.oscal.lib.model.Parameter;
44  import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
45  import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType;
46  
47  import org.apache.logging.log4j.LogManager;
48  import org.apache.logging.log4j.Logger;
49  
50  import java.util.Collection;
51  import java.util.Collections;
52  import java.util.EnumMap;
53  import java.util.LinkedHashMap;
54  import java.util.Locale;
55  import java.util.Map;
56  import java.util.UUID;
57  import java.util.concurrent.ConcurrentHashMap;
58  import java.util.stream.Collectors;
59  
60  import edu.umd.cs.findbugs.annotations.NonNull;
61  
62  public class BasicIndexer implements IIndexer {
63    private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
64    private static final MetapathExpression CONTAINER_METAPATH
65        = MetapathExpression.compile("(ancestor::control|ancestor::group)[1])");
66  
67    @NonNull
68    private final Map<IEntityItem.ItemType, Map<String, IEntityItem>> entityTypeToIdentifierToEntityMap;
69    @NonNull
70    private Map<INodeItem, SelectionStatus> nodeItemToSelectionStatusMap;
71  
72    @Override
73    public void append(@NonNull IIndexer other) {
74      for (ItemType itemType : ItemType.values()) {
75        assert itemType != null;
76        for (IEntityItem entity : other.getEntitiesByItemType(itemType)) {
77          assert entity != null;
78          addItem(entity);
79        }
80      }
81  
82      this.nodeItemToSelectionStatusMap.putAll(other.getSelectionStatusMap());
83    }
84  
85    public BasicIndexer() {
86      this.entityTypeToIdentifierToEntityMap = new EnumMap<>(IEntityItem.ItemType.class);
87      this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>();
88    }
89  
90    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // needed
91    public BasicIndexer(IIndexer other) {
92      // copy entity map
93      this.entityTypeToIdentifierToEntityMap = other.getEntities();
94  
95      // copy selection map
96      this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(other.getSelectionStatusMap());
97    }
98  
99    @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 }