001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.oscal.lib.profile.resolver.support; 007 008import gov.nist.secauto.metaschema.core.datatype.adapter.UuidAdapter; 009import gov.nist.secauto.metaschema.core.metapath.MetapathExpression; 010import gov.nist.secauto.metaschema.core.metapath.MetapathExpression.ResultType; 011import gov.nist.secauto.metaschema.core.metapath.item.node.IModelNodeItem; 012import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem; 013import gov.nist.secauto.metaschema.core.util.CollectionUtil; 014import gov.nist.secauto.metaschema.core.util.ObjectUtils; 015import gov.nist.secauto.oscal.lib.OscalBindingContext; 016import gov.nist.secauto.oscal.lib.model.BackMatter.Resource; 017import gov.nist.secauto.oscal.lib.model.CatalogGroup; 018import gov.nist.secauto.oscal.lib.model.Control; 019import gov.nist.secauto.oscal.lib.model.ControlPart; 020import gov.nist.secauto.oscal.lib.model.Metadata.Location; 021import gov.nist.secauto.oscal.lib.model.Metadata.Party; 022import gov.nist.secauto.oscal.lib.model.Metadata.Role; 023import gov.nist.secauto.oscal.lib.model.Parameter; 024import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver; 025import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType; 026 027import org.apache.logging.log4j.LogManager; 028import org.apache.logging.log4j.Logger; 029 030import java.util.Collection; 031import java.util.Collections; 032import java.util.EnumMap; 033import java.util.LinkedHashMap; 034import java.util.Locale; 035import java.util.Map; 036import java.util.UUID; 037import java.util.concurrent.ConcurrentHashMap; 038import java.util.stream.Collectors; 039 040import edu.umd.cs.findbugs.annotations.NonNull; 041 042public class BasicIndexer implements IIndexer { 043 private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class); 044 private static final MetapathExpression CONTAINER_METAPATH 045 = MetapathExpression.compile("(ancestor::control|ancestor::group)[1]", 046 OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT); 047 048 @NonNull 049 private final Map<IEntityItem.ItemType, Map<String, IEntityItem>> entityTypeToIdentifierToEntityMap; 050 @NonNull 051 private Map<INodeItem, SelectionStatus> nodeItemToSelectionStatusMap; 052 053 @Override 054 public void append(@NonNull IIndexer other) { 055 for (ItemType itemType : ItemType.values()) { 056 assert itemType != null; 057 for (IEntityItem entity : other.getEntitiesByItemType(itemType)) { 058 assert entity != null; 059 addItem(entity); 060 } 061 } 062 063 this.nodeItemToSelectionStatusMap.putAll(other.getSelectionStatusMap()); 064 } 065 066 public BasicIndexer() { 067 this.entityTypeToIdentifierToEntityMap = new EnumMap<>(IEntityItem.ItemType.class); 068 this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(); 069 } 070 071 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // needed 072 public BasicIndexer(IIndexer other) { 073 // copy entity map 074 this.entityTypeToIdentifierToEntityMap = other.getEntities(); 075 076 // copy selection map 077 this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(other.getSelectionStatusMap()); 078 } 079 080 @Override 081 public void setSelectionStatus(@NonNull INodeItem item, @NonNull SelectionStatus selectionStatus) { 082 nodeItemToSelectionStatusMap.put(item, selectionStatus); 083 } 084 085 @Override 086 public Map<INodeItem, SelectionStatus> getSelectionStatusMap() { 087 return CollectionUtil.unmodifiableMap(nodeItemToSelectionStatusMap); 088 } 089 090 @Override 091 public SelectionStatus getSelectionStatus(@NonNull INodeItem item) { 092 SelectionStatus retval = nodeItemToSelectionStatusMap.get(item); 093 return retval == null ? SelectionStatus.UNKNOWN : retval; 094 } 095 096 @Override 097 public void resetSelectionStatus() { 098 nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(); 099 } 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.ITEM); 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}