001/* 002 * Portions of this software was developed by employees of the National Institute 003 * of Standards and Technology (NIST), an agency of the Federal Government and is 004 * being made available as a public service. Pursuant to title 17 United States 005 * Code Section 105, works of NIST employees are not subject to copyright 006 * protection in the United States. This software may be subject to foreign 007 * copyright. Permission in the United States and in foreign countries, to the 008 * extent that NIST may hold copyright, to use, copy, modify, create derivative 009 * works, and distribute this software and its documentation without fee is hereby 010 * granted on a non-exclusive basis, provided that this notice and disclaimer 011 * of warranty appears in all copies. 012 * 013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER 014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY 015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF 016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM 017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE 018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT 019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, 020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, 021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, 022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR 023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT 024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. 025 */ 026 027package gov.nist.secauto.oscal.lib.profile.resolver.support; 028 029import gov.nist.secauto.metaschema.model.common.datatype.adapter.UuidAdapter; 030import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression; 031import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression.ResultType; 032import gov.nist.secauto.metaschema.model.common.metapath.item.INodeItem; 033import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueModelNodeItem; 034import gov.nist.secauto.metaschema.model.common.util.CollectionUtil; 035import gov.nist.secauto.metaschema.model.common.util.ObjectUtils; 036import gov.nist.secauto.oscal.lib.model.BackMatter.Resource; 037import gov.nist.secauto.oscal.lib.model.CatalogGroup; 038import gov.nist.secauto.oscal.lib.model.Control; 039import gov.nist.secauto.oscal.lib.model.ControlPart; 040import gov.nist.secauto.oscal.lib.model.Metadata.Location; 041import gov.nist.secauto.oscal.lib.model.Metadata.Party; 042import gov.nist.secauto.oscal.lib.model.Metadata.Role; 043import gov.nist.secauto.oscal.lib.model.Parameter; 044import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver; 045import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType; 046 047import org.apache.logging.log4j.LogManager; 048import org.apache.logging.log4j.Logger; 049 050import java.util.Collection; 051import java.util.Collections; 052import java.util.EnumMap; 053import java.util.LinkedHashMap; 054import java.util.Locale; 055import java.util.Map; 056import java.util.UUID; 057import java.util.concurrent.ConcurrentHashMap; 058import java.util.stream.Collectors; 059 060import edu.umd.cs.findbugs.annotations.NonNull; 061 062public class BasicIndexer implements IIndexer { 063 private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class); 064 private static final MetapathExpression CONTAINER_METAPATH 065 = MetapathExpression.compile("(ancestor::control|ancestor::group)[1])"); 066 067 @NonNull 068 private final Map<IEntityItem.ItemType, Map<String, IEntityItem>> entityTypeToIdentifierToEntityMap; 069 @NonNull 070 private Map<INodeItem, SelectionStatus> nodeItemToSelectionStatusMap; 071 072 @Override 073 public void append(@NonNull IIndexer other) { 074 for (ItemType itemType : ItemType.values()) { 075 assert itemType != null; 076 for (IEntityItem entity : other.getEntitiesByItemType(itemType)) { 077 assert entity != null; 078 addItem(entity); 079 } 080 } 081 082 this.nodeItemToSelectionStatusMap.putAll(other.getSelectionStatusMap()); 083 } 084 085 public BasicIndexer() { 086 this.entityTypeToIdentifierToEntityMap = new EnumMap<>(IEntityItem.ItemType.class); 087 this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(); 088 } 089 090 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // needed 091 public BasicIndexer(IIndexer other) { 092 // copy entity map 093 this.entityTypeToIdentifierToEntityMap = other.getEntities(); 094 095 // copy selection map 096 this.nodeItemToSelectionStatusMap = new ConcurrentHashMap<>(other.getSelectionStatusMap()); 097 } 098 099 @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}