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