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