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.alter; 028 029import gov.nist.secauto.metaschema.model.common.util.CollectionUtil; 030import gov.nist.secauto.metaschema.model.common.util.ObjectUtils; 031import gov.nist.secauto.oscal.lib.model.Catalog; 032import gov.nist.secauto.oscal.lib.model.CatalogGroup; 033import gov.nist.secauto.oscal.lib.model.Control; 034import gov.nist.secauto.oscal.lib.model.ControlPart; 035import gov.nist.secauto.oscal.lib.model.Link; 036import gov.nist.secauto.oscal.lib.model.Parameter; 037import gov.nist.secauto.oscal.lib.model.Property; 038import gov.nist.secauto.oscal.lib.model.control.catalog.ICatalogVisitor; 039import gov.nist.secauto.oscal.lib.model.metadata.IProperty; 040import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionEvaluationException; 041 042import java.util.Collection; 043import java.util.Collections; 044import java.util.EnumMap; 045import java.util.EnumSet; 046import java.util.Iterator; 047import java.util.Locale; 048import java.util.Map; 049import java.util.Set; 050import java.util.concurrent.ConcurrentHashMap; 051import java.util.function.Function; 052import java.util.function.Supplier; 053 054import edu.umd.cs.findbugs.annotations.NonNull; 055import edu.umd.cs.findbugs.annotations.Nullable; 056 057public class RemoveVisitor implements ICatalogVisitor<Boolean, RemoveVisitor.Context> { 058 public enum TargetType { 059 PARAM("param", Parameter.class), 060 PROP("prop", Property.class), 061 LINK("link", Link.class), 062 PART("part", ControlPart.class); 063 064 @NonNull 065 private static final Map<Class<?>, TargetType> CLASS_TO_TYPE; 066 @NonNull 067 private static final Map<String, TargetType> NAME_TO_TYPE; 068 @NonNull 069 private final String fieldName; 070 @NonNull 071 private final Class<?> clazz; 072 073 static { 074 { 075 Map<Class<?>, TargetType> map = new ConcurrentHashMap<>(); 076 for (TargetType type : TargetType.values()) { 077 map.put(type.getClazz(), type); 078 } 079 CLASS_TO_TYPE = CollectionUtil.unmodifiableMap(map); 080 } 081 082 { 083 Map<String, TargetType> map = new ConcurrentHashMap<>(); 084 for (TargetType type : TargetType.values()) { 085 map.put(type.fieldName(), type); 086 } 087 NAME_TO_TYPE = CollectionUtil.unmodifiableMap(map); 088 } 089 } 090 091 /** 092 * Get the target type associated with the provided {@code clazz}. 093 * 094 * @param clazz 095 * the class to identify the target type for 096 * @return the associated target type or {@code null} if the class is not 097 * associated with a target type 098 */ 099 @Nullable 100 public static TargetType forClass(@NonNull Class<?> clazz) { 101 Class<?> target = clazz; 102 TargetType retval; 103 // recurse over parent classes to find a match 104 do { 105 retval = CLASS_TO_TYPE.get(target); 106 } while (retval == null && (target = target.getSuperclass()) != null); 107 return retval; 108 } 109 110 /** 111 * Get the target type associated with the provided field {@code name}. 112 * 113 * @param name 114 * the field name to identify the target type for 115 * @return the associated target type or {@code null} if the name is not 116 * associated with a target type 117 */ 118 @Nullable 119 public static TargetType forFieldName(@Nullable String name) { 120 return name == null ? null : NAME_TO_TYPE.get(name); 121 } 122 123 TargetType(@NonNull String fieldName, @NonNull Class<?> clazz) { 124 this.fieldName = fieldName; 125 this.clazz = clazz; 126 } 127 128 /** 129 * Get the field name associated with the target type. 130 * 131 * @return the name 132 */ 133 public String fieldName() { 134 return fieldName; 135 } 136 137 /** 138 * Get the bound class associated with the target type. 139 * 140 * @return the class 141 */ 142 public Class<?> getClazz() { 143 return clazz; 144 } 145 146 } 147 148 @NonNull 149 private static final RemoveVisitor INSTANCE = new RemoveVisitor(); 150 151 private static final Map<TargetType, Set<TargetType>> APPLICABLE_TARGETS; 152 153 static { 154 APPLICABLE_TARGETS = new EnumMap<>(TargetType.class); 155 APPLICABLE_TARGETS.put(TargetType.PARAM, Set.of(TargetType.PROP, TargetType.LINK)); 156 APPLICABLE_TARGETS.put(TargetType.PART, Set.of(TargetType.PART, TargetType.PROP, TargetType.LINK)); 157 } 158 159 private static Set<TargetType> getApplicableTypes(@NonNull TargetType type) { 160 return APPLICABLE_TARGETS.getOrDefault(type, CollectionUtil.emptySet()); 161 } 162 163 private static <T> boolean handle( 164 @NonNull TargetType itemType, 165 @NonNull Supplier<? extends Collection<T>> supplier, 166 @Nullable Function<T, Boolean> handler, 167 @NonNull Context context) { 168 169 boolean handleChildren = !Collections.disjoint(context.getTargetItemTypes(), getApplicableTypes(itemType)); 170 boolean retval = false; 171 if (context.isMatchingType(itemType)) { 172 // if the item type is applicable, attempt to remove any items 173 Iterator<T> iter = supplier.get().iterator(); 174 while (iter.hasNext()) { 175 T item = iter.next(); 176 177 if (item == null || context.isApplicableTo(item)) { 178 iter.remove(); 179 retval = true; 180 // ignore removed items and their children 181 } else if (handler != null && handleChildren) { 182 // handle child items since they are applicable to the search criteria 183 retval = retval || handler.apply(item); 184 } 185 } 186 } else if (handleChildren && handler != null) { 187 // if the child item type is applicable and there is a handler, iterate over 188 // children 189 Iterator<T> iter = supplier.get().iterator(); 190 while (iter.hasNext()) { 191 T item = iter.next(); 192 if (item != null) { 193 retval = retval || handler.apply(item); 194 } 195 } 196 } 197 return retval; 198 } 199 200 /** 201 * Apply the remove directive. 202 * 203 * @param control 204 * the control target 205 * @param objectName 206 * the name flag of a matching node to remove 207 * @param objectClass 208 * the class flag of a matching node to remove 209 * @param objectId 210 * the id flag of a matching node to remove 211 * @param objectNamespace 212 * the namespace flag of a matching node to remove 213 * @param itemType 214 * the type of a matching node to remove 215 * @return {@code true} if the modification was made or {@code false} otherwise 216 * @throws ProfileResolutionEvaluationException 217 * if a processing error occurred during profile resolution 218 */ 219 public static boolean remove( 220 @NonNull Control control, 221 @Nullable String objectName, 222 @Nullable String objectClass, 223 @Nullable String objectId, 224 @Nullable String objectNamespace, 225 @Nullable TargetType itemType) { 226 return INSTANCE.visitControl( 227 control, 228 new Context(objectName, objectClass, objectId, objectNamespace, itemType)); 229 } 230 231 @Override 232 public Boolean visitCatalog(Catalog catalog, Context context) { 233 // not required 234 throw new UnsupportedOperationException("not needed"); 235 } 236 237 @Override 238 public Boolean visitGroup(CatalogGroup group, Context context) { 239 // not required 240 throw new UnsupportedOperationException("not needed"); 241 } 242 243 @Override 244 public Boolean visitControl(Control control, Context context) { 245 assert context != null; 246 247 // visit params 248 boolean retval = handle( 249 TargetType.PARAM, 250 () -> CollectionUtil.listOrEmpty(control.getParams()), 251 child -> visitParameter(ObjectUtils.notNull(child), context), 252 context); 253 254 // visit props 255 retval = retval || handle( 256 TargetType.PROP, 257 () -> CollectionUtil.listOrEmpty(control.getProps()), 258 null, 259 context); 260 261 // visit links 262 retval = retval || handle( 263 TargetType.LINK, 264 () -> CollectionUtil.listOrEmpty(control.getLinks()), 265 null, 266 context); 267 268 // visit parts 269 retval = retval || handle( 270 TargetType.PART, 271 () -> CollectionUtil.listOrEmpty(control.getParts()), 272 child -> visitPart(child, context), 273 context); 274 275 return retval; 276 } 277 278 @Override 279 public Boolean visitParameter(Parameter parameter, Context context) { 280 assert context != null; 281 282 // visit props 283 boolean retval = handle( 284 TargetType.PROP, 285 () -> CollectionUtil.listOrEmpty(parameter.getProps()), 286 null, 287 context); 288 289 // visit links 290 retval = retval || handle( 291 TargetType.LINK, 292 () -> CollectionUtil.listOrEmpty(parameter.getLinks()), 293 null, 294 context); 295 return retval; 296 } 297 298 /** 299 * Visit the control part. 300 * 301 * @param part 302 * the bound part object 303 * @param context 304 * the visitor context 305 * @return {@code true} if the removal was applied or {@code false} otherwise 306 */ 307 public boolean visitPart(ControlPart part, Context context) { 308 assert context != null; 309 310 // visit props 311 boolean retval = handle( 312 TargetType.PROP, 313 () -> CollectionUtil.listOrEmpty(part.getProps()), 314 null, 315 context); 316 317 // visit links 318 retval = retval || handle( 319 TargetType.LINK, 320 () -> CollectionUtil.listOrEmpty(part.getLinks()), 321 null, 322 context); 323 324 // visit parts 325 retval = retval || handle( 326 TargetType.PART, 327 () -> CollectionUtil.listOrEmpty(part.getParts()), 328 child -> visitPart(child, context), 329 context); 330 return retval; 331 } 332 333 static final class Context { 334 /** 335 * Types with an "name" flag. 336 */ 337 @NonNull 338 private static final Set<TargetType> NAME_TYPES = ObjectUtils.notNull( 339 Set.of(TargetType.PART, TargetType.PROP)); 340 /** 341 * Types with an "class" flag. 342 */ 343 @NonNull 344 private static final Set<TargetType> CLASS_TYPES = ObjectUtils.notNull( 345 Set.of(TargetType.PARAM, TargetType.PART, TargetType.PROP)); 346 /** 347 * Types with an "id" flag. 348 */ 349 @NonNull 350 private static final Set<TargetType> ID_TYPES = ObjectUtils.notNull( 351 Set.of(TargetType.PARAM, TargetType.PART)); 352 /** 353 * Types with an "ns" flag. 354 */ 355 @NonNull 356 private static final Set<TargetType> NAMESPACE_TYPES = ObjectUtils.notNull( 357 Set.of(TargetType.PART, TargetType.PROP)); 358 359 @Nullable 360 private final String objectName; 361 @Nullable 362 private final String objectClass; 363 @Nullable 364 private final String objectId; 365 @Nullable 366 private final String objectNamespace; 367 @NonNull 368 private final Set<TargetType> targetItemTypes; 369 370 private static boolean filterTypes( 371 @NonNull Set<TargetType> effectiveTypes, 372 @NonNull String criteria, 373 @NonNull Set<TargetType> allowedTypes, 374 @Nullable String value, 375 @Nullable TargetType itemType) { 376 boolean retval = false; 377 if (value != null) { 378 retval = effectiveTypes.retainAll(allowedTypes); 379 if (itemType != null && !allowedTypes.contains(itemType)) { 380 throw new ProfileResolutionEvaluationException( 381 String.format("%s='%s' is not supported for items of type '%s'", 382 criteria, 383 value, 384 itemType.fieldName())); 385 } 386 } 387 return retval; 388 } 389 390 private Context( 391 @Nullable String objectName, 392 @Nullable String objectClass, 393 @Nullable String objectId, 394 @Nullable String objectNamespace, 395 @Nullable TargetType itemType) { 396 397 // determine the set of effective item types to search for 398 // this helps with short-circuit searching for parts of the graph that cannot 399 // match 400 @NonNull Set<TargetType> targetItemTypes = ObjectUtils.notNull(EnumSet.allOf(TargetType.class)); 401 filterTypes(targetItemTypes, "by-name", NAME_TYPES, objectName, itemType); 402 filterTypes(targetItemTypes, "by-class", CLASS_TYPES, objectClass, itemType); 403 filterTypes(targetItemTypes, "by-id", ID_TYPES, objectId, itemType); 404 filterTypes(targetItemTypes, "by-ns", NAMESPACE_TYPES, objectNamespace, itemType); 405 406 if (itemType != null) { 407 targetItemTypes.retainAll(Set.of(itemType)); 408 } 409 410 if (targetItemTypes.isEmpty()) { 411 throw new ProfileResolutionEvaluationException("The filter matches no available item types"); 412 } 413 414 this.objectName = objectName; 415 this.objectClass = objectClass; 416 this.objectId = objectId; 417 this.objectNamespace = objectNamespace; 418 this.targetItemTypes = CollectionUtil.unmodifiableSet(targetItemTypes); 419 } 420 421 @Nullable 422 public String getObjectName() { 423 return objectName; 424 } 425 426 @Nullable 427 public String getObjectClass() { 428 return objectClass; 429 } 430 431 @Nullable 432 public String getObjectId() { 433 return objectId; 434 } 435 436 @NonNull 437 public Set<TargetType> getTargetItemTypes() { 438 return targetItemTypes; 439 } 440 441 public boolean isMatchingType(@NonNull TargetType type) { 442 return getTargetItemTypes().contains(type); 443 } 444 445 @Nullable 446 public String getObjectNamespace() { 447 return objectNamespace; 448 } 449 450 private static boolean checkValue(@Nullable String actual, @Nullable String expected) { 451 return expected == null || expected.equals(actual); 452 } 453 454 public boolean isApplicableTo(@NonNull Object obj) { 455 TargetType objectType = TargetType.forClass(obj.getClass()); 456 457 boolean retval = objectType != null && getTargetItemTypes().contains(objectType); 458 if (retval) { 459 assert objectType != null; 460 461 // check other criteria 462 String actualName = null; 463 String actualClass = null; 464 String actualId = null; 465 String actualNamespace = null; 466 467 switch (objectType) { 468 case PARAM: { 469 Parameter param = (Parameter) obj; 470 actualClass = param.getClazz(); 471 actualId = param.getId(); 472 break; 473 } 474 case PROP: { 475 Property prop = (Property) obj; 476 actualName = prop.getName(); 477 actualClass = prop.getClazz(); 478 actualNamespace = prop.getNs() == null ? IProperty.OSCAL_NAMESPACE.toString() : prop.getNs().toString(); 479 break; 480 } 481 case PART: { 482 ControlPart part = (ControlPart) obj; 483 actualName = part.getName(); 484 actualClass = part.getClazz(); 485 actualId = part.getId() == null ? null : part.getId().toString(); 486 actualNamespace = part.getNs() == null ? IProperty.OSCAL_NAMESPACE.toString() : part.getNs().toString(); 487 break; 488 } 489 case LINK: 490 // do nothing 491 break; 492 default: 493 throw new UnsupportedOperationException(objectType.name().toLowerCase(Locale.ROOT)); 494 } 495 496 retval = checkValue(actualName, getObjectName()) 497 && checkValue(actualClass, getObjectClass()) 498 && checkValue(actualId, getObjectId()) 499 && checkValue(actualNamespace, getObjectNamespace()); 500 } 501 return retval; 502 } 503 } 504}