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