001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.oscal.lib.profile.resolver; 007 008import org.apache.logging.log4j.LogManager; 009import org.apache.logging.log4j.Logger; 010 011import java.io.File; 012import java.io.IOException; 013import java.net.URI; 014import java.net.URISyntaxException; 015import java.net.URL; 016import java.nio.ByteBuffer; 017import java.nio.file.Path; 018import java.time.ZoneOffset; 019import java.time.ZonedDateTime; 020import java.util.EnumSet; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.Stack; 024import java.util.UUID; 025import java.util.stream.Collectors; 026 027import dev.metaschema.core.metapath.DynamicContext; 028import dev.metaschema.core.metapath.IDocumentLoader; 029import dev.metaschema.core.metapath.IMetapathExpression; 030import dev.metaschema.core.metapath.StaticContext; 031import dev.metaschema.core.metapath.format.IPathFormatter; 032import dev.metaschema.core.metapath.function.FunctionUtils; 033import dev.metaschema.core.metapath.item.IItem; 034import dev.metaschema.core.metapath.item.ISequence; 035import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem; 036import dev.metaschema.core.metapath.item.node.IDocumentNodeItem; 037import dev.metaschema.core.metapath.item.node.INodeItem; 038import dev.metaschema.core.metapath.item.node.INodeItemFactory; 039import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem; 040import dev.metaschema.core.model.IBoundObject; 041import dev.metaschema.core.qname.IEnhancedQName; 042import dev.metaschema.core.util.CollectionUtil; 043import dev.metaschema.core.util.ObjectUtils; 044import dev.metaschema.databind.io.BindingException; 045import dev.metaschema.databind.io.DeserializationFeature; 046import dev.metaschema.databind.io.IBoundLoader; 047import dev.metaschema.databind.model.IBoundDefinitionModelAssembly; 048import dev.metaschema.oscal.lib.OscalBindingContext; 049import dev.metaschema.oscal.lib.OscalModelConstants; 050import dev.metaschema.oscal.lib.OscalUtils; 051import dev.metaschema.oscal.lib.model.BackMatter; 052import dev.metaschema.oscal.lib.model.BackMatter.Resource; 053import dev.metaschema.oscal.lib.model.BackMatter.Resource.Base64; 054import dev.metaschema.oscal.lib.model.BackMatter.Resource.Rlink; 055import dev.metaschema.oscal.lib.model.Catalog; 056import dev.metaschema.oscal.lib.model.Control; 057import dev.metaschema.oscal.lib.model.Merge; 058import dev.metaschema.oscal.lib.model.Metadata; 059import dev.metaschema.oscal.lib.model.Metadata.Location; 060import dev.metaschema.oscal.lib.model.Metadata.Party; 061import dev.metaschema.oscal.lib.model.Metadata.Role; 062import dev.metaschema.oscal.lib.model.Modify; 063import dev.metaschema.oscal.lib.model.Modify.ProfileSetParameter; 064import dev.metaschema.oscal.lib.model.Parameter; 065import dev.metaschema.oscal.lib.model.Profile; 066import dev.metaschema.oscal.lib.model.ProfileImport; 067import dev.metaschema.oscal.lib.model.Property; 068import dev.metaschema.oscal.lib.model.metadata.AbstractLink; 069import dev.metaschema.oscal.lib.model.metadata.AbstractProperty; 070import dev.metaschema.oscal.lib.profile.resolver.alter.AddVisitor; 071import dev.metaschema.oscal.lib.profile.resolver.alter.RemoveVisitor; 072import dev.metaschema.oscal.lib.profile.resolver.merge.FlatteningStructuringVisitor; 073import dev.metaschema.oscal.lib.profile.resolver.selection.Import; 074import dev.metaschema.oscal.lib.profile.resolver.selection.ImportCycleException; 075import dev.metaschema.oscal.lib.profile.resolver.support.BasicIndexer; 076import dev.metaschema.oscal.lib.profile.resolver.support.ControlIndexingVisitor; 077import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem; 078import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem.ItemType; 079import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer; 080import edu.umd.cs.findbugs.annotations.NonNull; 081import edu.umd.cs.findbugs.annotations.Nullable; 082 083public class ProfileResolver { 084 085 public enum StructuringDirective { 086 FLAT, 087 AS_IS, 088 CUSTOM; 089 } 090 091 private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class); 092 @NonNull 093 private static final IEnhancedQName IMPORT_QNAME = IEnhancedQName.of(OscalModelConstants.NS_OSCAL, "import"); 094 095 @NonNull 096 private static final IMetapathExpression METAPATH_SET_PARAMETER 097 = IMetapathExpression.compile("modify/set-parameter", 098 OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT); 099 @NonNull 100 private static final IMetapathExpression METAPATH_ALTER 101 = IMetapathExpression.compile("modify/alter", 102 OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT); 103 @NonNull 104 private static final IMetapathExpression METAPATH_ALTER_REMOVE 105 = IMetapathExpression.compile("remove", 106 OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT); 107 @NonNull 108 private static final IMetapathExpression METAPATH_ALTER_ADD 109 = IMetapathExpression.compile("add", 110 OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT); 111 @NonNull 112 private static final IMetapathExpression CATALOG_OR_PROFILE 113 = IMetapathExpression.compile("/(catalog|profile)", 114 OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT); 115 @NonNull 116 private static final IMetapathExpression CATALOG 117 = IMetapathExpression.compile("/catalog", 118 OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT); 119 120 @NonNull 121 private final DynamicContext dynamicContext; 122 @NonNull 123 private final UriResolver uriResolver; 124 125 public ProfileResolver() { 126 this(newDynamicContext(), (uri, source) -> source.resolve(uri)); 127 } 128 129 public ProfileResolver( 130 @NonNull DynamicContext dynamicContext, 131 @NonNull UriResolver uriResolver) { 132 this.dynamicContext = dynamicContext; 133 this.uriResolver = uriResolver; 134 } 135 136 @NonNull 137 private static DynamicContext newDynamicContext() { 138 IBoundLoader loader = OscalBindingContext.instance().newBoundLoader(); 139 loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS); 140 141 DynamicContext retval = new DynamicContext(StaticContext.builder() 142 .defaultModelNamespace(OscalModelConstants.NS_OSCAL) 143 .build()); 144 retval.setDocumentLoader(loader); 145 return retval; 146 } 147 148 /** 149 * Gets the configured loader or creates a new default loader if no loader was 150 * configured. 151 * 152 * @return the bound loader 153 * @since 5.0.0 154 */ 155 @NonNull 156 public IDocumentLoader getDocumentLoader() { 157 return getDynamicContext().getDocumentLoader(); 158 } 159 160 @NonNull 161 public DynamicContext getDynamicContext() { 162 return dynamicContext; 163 } 164 165 @Nullable 166 private static IRootAssemblyNodeItem getRoot( 167 @NonNull IDocumentNodeItem document, 168 @NonNull IMetapathExpression rootPath) { 169 ISequence<?> result = rootPath.evaluate(document); 170 IItem item = result.getFirstItem(false); 171 172 return item == null ? null : FunctionUtils.asType(item); 173 } 174 175 @NonNull 176 public IDocumentNodeItem resolve(@NonNull URL url) 177 throws URISyntaxException, IOException, ProfileResolutionException { 178 IDocumentLoader loader = getDocumentLoader(); 179 IDocumentNodeItem catalogOrProfile = loader.loadAsNodeItem(url); 180 return resolve(catalogOrProfile, new Stack<>()); 181 } 182 183 @NonNull 184 public IDocumentNodeItem resolve(@NonNull File file) throws IOException, ProfileResolutionException { 185 return resolve(ObjectUtils.notNull(file.toPath())); 186 } 187 188 @NonNull 189 public IDocumentNodeItem resolve(@NonNull Path path) throws IOException, ProfileResolutionException { 190 IDocumentLoader loader = getDocumentLoader(); 191 IDocumentNodeItem catalogOrProfile = loader.loadAsNodeItem(path); 192 return resolve(catalogOrProfile, new Stack<>()); 193 } 194 195 @NonNull 196 public IDocumentNodeItem resolve( 197 @NonNull IDocumentNodeItem profileOrCatalogDocument) 198 throws IOException, ProfileResolutionException { 199 return resolve(profileOrCatalogDocument, new Stack<>()); 200 } 201 202 @NonNull 203 public IDocumentNodeItem resolve( 204 @NonNull IDocumentNodeItem profileOrCatalogDocument, 205 @NonNull Stack<URI> importHistory) 206 throws IOException, ProfileResolutionException { 207 IRootAssemblyNodeItem profileOrCatalog = getRoot( 208 profileOrCatalogDocument, 209 CATALOG_OR_PROFILE); 210 if (profileOrCatalog == null) { 211 throw new ProfileResolutionException( 212 String.format("The provided document '%s' does not contain a catalog or profile.", 213 profileOrCatalogDocument.getDocumentUri())); 214 } 215 return resolve(profileOrCatalog, importHistory); 216 } 217 218 @NonNull 219 public IDocumentNodeItem resolve( 220 @NonNull IRootAssemblyNodeItem profileOrCatalog, 221 @NonNull Stack<URI> importHistory) 222 throws IOException, ProfileResolutionException { 223 Object profileObject = profileOrCatalog.getValue(); 224 225 IDocumentNodeItem retval; 226 if (profileObject instanceof Catalog) { 227 // already a catalog 228 retval = profileOrCatalog.getParentNodeItem(); 229 } else { 230 // must be a profile 231 retval = resolveProfile(profileOrCatalog, importHistory); 232 } 233 return retval; 234 } 235 236 /** 237 * Resolve the profile to a catalog. 238 * 239 * @param profileItem 240 * a {@link IDocumentNodeItem} containing the profile to resolve 241 * @param importHistory 242 * the import stack for cycle detection 243 * @return the resolved profile 244 * @throws IOException 245 * if an error occurred while loading the profile or an import 246 * @throws ProfileResolutionException 247 * if an error occurred while resolving the profile 248 */ 249 @NonNull 250 protected IDocumentNodeItem resolveProfile( 251 @NonNull IRootAssemblyNodeItem profileItem, 252 @NonNull Stack<URI> importHistory) throws IOException, ProfileResolutionException { 253 Catalog resolvedCatalog = new Catalog(); 254 255 generateMetadata(resolvedCatalog, profileItem); 256 257 IIndexer index = resolveImports(resolvedCatalog, profileItem, importHistory); 258 handleReferences(resolvedCatalog, profileItem, index); 259 handleMerge(resolvedCatalog, profileItem, index); 260 handleModify(resolvedCatalog, profileItem); 261 262 return INodeItemFactory.instance().newDocumentNodeItem( 263 ObjectUtils.requireNonNull( 264 (IBoundDefinitionModelAssembly) OscalBindingContext.instance().getBoundDefinitionForClass(Catalog.class)), 265 ObjectUtils.requireNonNull(profileItem.getBaseUri()), 266 resolvedCatalog); 267 } 268 269 @NonNull 270 private static Profile toProfile(@NonNull IRootAssemblyNodeItem profileItem) { 271 Object object = profileItem.getValue(); 272 assert object != null; 273 274 return (Profile) object; 275 } 276 277 private void generateMetadata( 278 @NonNull Catalog resolvedCatalog, 279 @NonNull IRootAssemblyNodeItem profileItem) { 280 resolvedCatalog.setUuid(UUID.randomUUID()); 281 282 Profile profile = toProfile(profileItem); 283 Metadata profileMetadata = profile.getMetadata(); 284 285 Metadata resolvedMetadata = new Metadata(); 286 resolvedMetadata.setTitle(profileMetadata.getTitle()); 287 288 if (profileMetadata.getVersion() != null) { 289 resolvedMetadata.setVersion(profileMetadata.getVersion()); 290 } 291 292 // metadata.setOscalVersion(OscalUtils.OSCAL_VERSION); 293 resolvedMetadata.setOscalVersion(profileMetadata.getOscalVersion()); 294 295 resolvedMetadata.setLastModified(ZonedDateTime.now(ZoneOffset.UTC)); 296 297 resolvedMetadata.addProp(AbstractProperty.builder("resolution-tool").value("libOSCAL-Java").build()); 298 299 URI documentUri = profileItem.getParentNodeItem().getDocumentUri(); 300 URI profileUri = ObjectUtils.requireNonNull(uriResolver.resolve(documentUri, documentUri)); 301 resolvedMetadata.addLink(AbstractLink.builder(profileUri) 302 .relation("source-profile") 303 .build()); 304 305 resolvedCatalog.setMetadata(resolvedMetadata); 306 } 307 308 @NonNull 309 private IIndexer resolveImports( 310 @NonNull Catalog resolvedCatalog, 311 @NonNull IRootAssemblyNodeItem profileItem, 312 @NonNull Stack<URI> importHistory) 313 throws IOException, ProfileResolutionException { 314 315 // first verify there is at least one import 316 @SuppressWarnings("unchecked") 317 List<IAssemblyNodeItem> profileImports 318 = (List<IAssemblyNodeItem>) profileItem.getModelItemsByName(IMPORT_QNAME); 319 if (profileImports.isEmpty()) { 320 throw new ProfileResolutionException(String.format("Profile '%s' has no imports", profileItem.getBaseUri())); 321 } 322 323 // now process each import 324 IIndexer retval = new BasicIndexer(); 325 for (IAssemblyNodeItem profileImportItem : profileImports) { 326 IIndexer result = resolveImport( 327 ObjectUtils.notNull(profileImportItem), 328 profileItem, 329 importHistory, 330 resolvedCatalog); 331 retval.append(result); 332 } 333 return retval; 334 } 335 336 @NonNull 337 protected IIndexer resolveImport( 338 @NonNull IAssemblyNodeItem profileImportItem, 339 @NonNull IRootAssemblyNodeItem profileItem, 340 @NonNull Stack<URI> importHistory, 341 @NonNull Catalog resolvedCatalog) throws IOException, ProfileResolutionException { 342 ProfileImport profileImport = ObjectUtils.requireNonNull((ProfileImport) profileImportItem.getValue()); 343 344 URI importUri = profileImport.getHref(); 345 if (importUri == null) { 346 throw new ProfileResolutionException("profileImport.getHref() must return a non-null URI"); 347 } 348 349 if (LOGGER.isDebugEnabled()) { 350 LOGGER.atDebug().log("resolving profile import '{}'", importUri); 351 } 352 353 IDocumentNodeItem importedDocument = getImport(importUri, profileItem); 354 URI importedUri = importedDocument.getDocumentUri(); 355 assert importedUri != null; // always non-null 356 357 // Import import = Import. 358 // InputSource source = newImportSource(importUri, profileItem); 359 // URI sourceUri = ObjectUtils.notNull(URI.create(source.getSystemId())); 360 361 // check for import cycle 362 try { 363 requireNonCycle( 364 importedUri, 365 importHistory); 366 } catch (ImportCycleException ex) { 367 throw new IOException(ex); 368 } 369 370 // track the import in the import history 371 importHistory.push(importedUri); 372 try { 373 IDocumentNodeItem importedCatalog = resolve(importedDocument, importHistory); 374 375 // Create a defensive deep copy of the document and associated values, since we 376 // will be making 377 // changes to the data. 378 try { 379 IRootAssemblyNodeItem importedCatalogRoot = ObjectUtils.requireNonNull(getRoot(importedCatalog, CATALOG)); 380 Catalog catalogCopy = (Catalog) OscalBindingContext.instance().deepCopy( 381 ObjectUtils.requireNonNull((IBoundObject) importedCatalogRoot.getValue()), 382 null); 383 384 importedCatalog = INodeItemFactory.instance().newDocumentNodeItem( 385 importedCatalogRoot.getDefinition(), 386 ObjectUtils.requireNonNull(importedCatalog.getDocumentUri()), 387 catalogCopy); 388 389 return new Import(profileItem, profileImportItem).resolve(importedCatalog, resolvedCatalog, uriResolver); 390 } catch (BindingException ex) { 391 throw new IOException(ex); 392 } 393 } finally { 394 // pop the resolved catalog from the import history 395 URI poppedUri = ObjectUtils.notNull(importHistory.pop()); 396 assert importedUri.equals(poppedUri); 397 } 398 } 399 400 private IDocumentNodeItem getImport( 401 @NonNull URI importUri, 402 @NonNull IRootAssemblyNodeItem importingProfile) throws IOException { 403 404 URI importingDocumentUri = ObjectUtils.requireNonNull(importingProfile.getParentNodeItem().getDocumentUri()); 405 406 IDocumentNodeItem retval; 407 if (OscalUtils.isInternalReference(importUri)) { 408 // handle internal reference 409 String uuid = OscalUtils.internalReferenceFragmentToId(importUri); 410 411 Profile profile = INodeItem.toValue(importingProfile); 412 Resource resource = profile.getResourceByUuid(ObjectUtils.notNull(UUID.fromString(uuid))); 413 if (resource == null) { 414 throw new IOException( 415 String.format("unable to find the resource identified by '%s' used in profile import", importUri)); 416 } 417 418 retval = getImport(resource, importingDocumentUri); 419 } else { 420 URI uri = importingDocumentUri.resolve(importUri); 421 assert uri != null; 422 423 retval = getDynamicContext().getDocumentLoader().loadAsNodeItem(uri); 424 } 425 return retval; 426 } 427 428 @Nullable 429 private IDocumentNodeItem getImport( 430 @NonNull Resource resource, 431 @NonNull URI baseUri) throws IOException { 432 433 IDocumentLoader loader = getDynamicContext().getDocumentLoader(); 434 435 IDocumentNodeItem retval = null; 436 // first try base64 data 437 Base64 base64 = resource.getBase64(); 438 ByteBuffer buffer = base64 == null ? null : base64.getValue(); 439 if (buffer != null) { 440 URI resourceUri = baseUri.resolve("#" + resource.getUuid()); 441 assert resourceUri != null; 442 retval = loader.loadAsNodeItem(resourceUri); 443 } 444 445 if (retval == null) { 446 Rlink rlink = OscalUtils.findMatchingRLink(resource, null); 447 URI uri = rlink == null ? null : rlink.getHref(); 448 449 if (uri == null) { 450 throw new IOException( 451 String.format("Unable to determine URI for resource '%s'.", resource.getUuid())); 452 } 453 454 uri = baseUri.resolve(uri); 455 assert uri != null; 456 try { 457 retval = loader.loadAsNodeItem(uri); 458 } catch (IOException ex) { 459 throw new IOException(String.format("Unable to load resource '%s'.", uri), ex); 460 } 461 } 462 return retval; 463 } 464 465 private static void requireNonCycle(@NonNull URI uri, @NonNull Stack<URI> importHistory) 466 throws ImportCycleException { 467 List<URI> cycle = checkCycle(uri, importHistory); 468 if (!cycle.isEmpty()) { 469 throw new ImportCycleException(String.format("Importing resource '%s' would result in the import cycle: %s", uri, 470 cycle.stream().map(URI::toString).collect(Collectors.joining(" -> ", " -> ", "")))); 471 } 472 } 473 474 @NonNull 475 private static List<URI> checkCycle(@NonNull URI uri, @NonNull Stack<URI> importHistory) { 476 int index = importHistory.indexOf(uri); 477 478 List<URI> retval; 479 if (index == -1) { 480 retval = CollectionUtil.emptyList(); 481 } else { 482 retval = CollectionUtil.unmodifiableList( 483 ObjectUtils.notNull(importHistory.subList(0, index + 1))); 484 } 485 return retval; 486 } 487 488 // TODO: move this to an abstract method on profile 489 private static StructuringDirective getStructuringDirective(Profile profile) { 490 Merge merge = profile.getMerge(); 491 492 StructuringDirective retval; 493 if (merge == null) { 494 retval = StructuringDirective.FLAT; 495 } else if (Boolean.TRUE.equals(merge.getAsIs())) { 496 retval = StructuringDirective.AS_IS; 497 } else if (merge.getCustom() != null) { 498 retval = StructuringDirective.CUSTOM; 499 } else { 500 retval = StructuringDirective.FLAT; 501 } 502 return retval; 503 } 504 505 protected void handleMerge( 506 @NonNull Catalog resolvedCatalog, 507 @NonNull IRootAssemblyNodeItem profileItem, 508 @NonNull IIndexer importIndex) { 509 // handle combine 510 511 // handle structuring 512 switch (getStructuringDirective(toProfile(profileItem))) { 513 case AS_IS: 514 // do nothing 515 break; 516 case CUSTOM: 517 throw new UnsupportedOperationException("custom structuring"); 518 case FLAT: 519 default: 520 structureFlat(resolvedCatalog, profileItem, importIndex); 521 break; 522 } 523 524 } 525 526 protected void structureFlat(@NonNull Catalog resolvedCatalog, @NonNull IRootAssemblyNodeItem profileItem, 527 @NonNull IIndexer importIndex) { 528 if (LOGGER.isDebugEnabled()) { 529 LOGGER.debug("applying flat structuring directive"); 530 } 531 532 // { 533 // // rebuild an index 534 // IDocumentNodeItem resolvedCatalogItem = 535 // DefaultNodeItemFactory.instance().newDocumentNodeItem( 536 // new RootAssemblyDefinition( 537 // ObjectUtils.notNull( 538 // (IAssemblyClassBinding) 539 // OscalBindingContext.instance().getClassBinding(Catalog.class))), 540 // resolvedCatalog, 541 // profileDocument.getBaseUri()); 542 // 543 // // FIXME: need to find a better way to create an index that doesn't auto 544 // select groups 545 // IIndexer indexer = new BasicIndexer(); 546 // ControlSelectionVisitor selectionVisitor 547 // = new ControlSelectionVisitor(IControlFilter.ALWAYS_MATCH, indexer); 548 // selectionVisitor.visitCatalog(resolvedCatalogItem); 549 // } 550 551 // rebuild the document, since the paths have changed 552 IDocumentNodeItem resolvedCatalogItem = INodeItemFactory.instance().newDocumentNodeItem( 553 ObjectUtils.requireNonNull( 554 (IBoundDefinitionModelAssembly) OscalBindingContext.instance().getBoundDefinitionForClass(Catalog.class)), 555 ObjectUtils.requireNonNull(profileItem.getBaseUri()), 556 resolvedCatalog); 557 558 new FlatteningStructuringVisitor(uriResolver).visitCatalog(resolvedCatalogItem, importIndex); 559 } 560 561 @SuppressWarnings("PMD.ExceptionAsFlowControl") // ok 562 protected void handleModify(@NonNull Catalog resolvedCatalog, @NonNull IRootAssemblyNodeItem profileItem) 563 throws ProfileResolutionException { 564 IDocumentNodeItem resolvedCatalogDocument = INodeItemFactory.instance().newDocumentNodeItem( 565 ObjectUtils.requireNonNull( 566 (IBoundDefinitionModelAssembly) OscalBindingContext.instance().getBoundDefinitionForClass(Catalog.class)), 567 ObjectUtils.requireNonNull(profileItem.getBaseUri()), 568 resolvedCatalog); 569 570 try { 571 IIndexer indexer = new BasicIndexer(); 572 ControlIndexingVisitor visitor = new ControlIndexingVisitor( 573 ObjectUtils.notNull(EnumSet.of(IEntityItem.ItemType.CONTROL, IEntityItem.ItemType.PARAMETER))); 574 visitor.visitCatalog(resolvedCatalogDocument, indexer); 575 576 METAPATH_SET_PARAMETER.evaluate(profileItem) 577 .forEach(item -> { 578 IAssemblyNodeItem setParameter = (IAssemblyNodeItem) item; 579 try { 580 handleSetParameter(setParameter, indexer); 581 } catch (ProfileResolutionEvaluationException ex) { 582 throw new ProfileResolutionEvaluationException( 583 String.format("Unable to apply the set-parameter at '%s'. %s", 584 setParameter.toPath(IPathFormatter.METAPATH_PATH_FORMATER), 585 ex.getLocalizedMessage()), 586 ex); 587 } 588 }); 589 590 METAPATH_ALTER.evaluate(profileItem) 591 .forEach(item -> { 592 handleAlter((IAssemblyNodeItem) item, indexer); 593 }); 594 } catch (ProfileResolutionEvaluationException ex) { 595 throw new ProfileResolutionException(ex.getLocalizedMessage(), ex); 596 } 597 } 598 599 protected void handleSetParameter(IAssemblyNodeItem item, IIndexer indexer) { 600 ProfileSetParameter setParameter = ObjectUtils.requireNonNull((Modify.ProfileSetParameter) item.getValue()); 601 String paramId = ObjectUtils.requireNonNull(setParameter.getParamId()); 602 IEntityItem entity = indexer.getEntity(IEntityItem.ItemType.PARAMETER, paramId, false); 603 if (entity == null) { 604 throw new ProfileResolutionEvaluationException( 605 String.format( 606 "The parameter '%s' does not exist in the resolved catalog.", 607 paramId)); 608 } 609 610 Parameter param = entity.getInstanceValue(); 611 612 // apply the set parameter values 613 param.setClazz(ModifyPhaseUtils.mergeItem(param.getClazz(), setParameter.getClazz())); 614 param.setProps(ModifyPhaseUtils.merge(param.getProps(), setParameter.getProps(), 615 ModifyPhaseUtils.identifierKey(Property::getUuid))); 616 param.setLinks(ModifyPhaseUtils.merge(param.getLinks(), setParameter.getLinks(), ModifyPhaseUtils.identityKey())); 617 param.setLabel(ModifyPhaseUtils.mergeItem(param.getLabel(), setParameter.getLabel())); 618 param.setUsage(ModifyPhaseUtils.mergeItem(param.getUsage(), setParameter.getUsage())); 619 param.setConstraints( 620 ModifyPhaseUtils.merge(param.getConstraints(), setParameter.getConstraints(), ModifyPhaseUtils.identityKey())); 621 param.setGuidelines( 622 ModifyPhaseUtils.merge(param.getGuidelines(), setParameter.getGuidelines(), ModifyPhaseUtils.identityKey())); 623 param.setValues(new LinkedList<>(setParameter.getValues())); 624 param.setSelect(setParameter.getSelect()); 625 } 626 627 @SuppressWarnings("PMD.ExceptionAsFlowControl") 628 protected void handleAlter(IAssemblyNodeItem item, IIndexer indexer) { 629 Modify.Alter alter = ObjectUtils.requireNonNull((Modify.Alter) item.getValue()); 630 String controlId = ObjectUtils.requireNonNull(alter.getControlId()); 631 IEntityItem entity = indexer.getEntity(IEntityItem.ItemType.CONTROL, controlId, false); 632 if (entity == null) { 633 throw new ProfileResolutionEvaluationException( 634 String.format( 635 "Unable to apply the alter targeting control '%s' at '%s'." 636 + " The control does not exist in the resolved catalog.", 637 controlId, 638 item.toPath(IPathFormatter.METAPATH_PATH_FORMATER))); 639 } 640 Control control = entity.getInstanceValue(); 641 642 METAPATH_ALTER_REMOVE.evaluate(item) 643 .forEach(nodeItem -> { 644 INodeItem removeItem = (INodeItem) nodeItem; 645 Modify.Alter.Remove remove = ObjectUtils.notNull((Modify.Alter.Remove) removeItem.getValue()); 646 647 String removeNS = remove.getByNs() == null ? null : remove.getByNs().toASCIIString(); 648 649 try { 650 if (!RemoveVisitor.remove( 651 control, 652 remove.getByName(), 653 remove.getByClass(), 654 remove.getById(), 655 removeNS, 656 RemoveVisitor.TargetType.forFieldName(remove.getByItemName()))) { 657 throw new ProfileResolutionEvaluationException( 658 String.format("The remove did not match a valid target")); 659 } 660 } catch (ProfileResolutionEvaluationException ex) { 661 throw new ProfileResolutionEvaluationException( 662 String.format("Unable to apply the remove targeting control '%s' at '%s'. %s", 663 control.getId(), 664 removeItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER), 665 ex.getLocalizedMessage()), 666 ex); 667 } 668 }); 669 METAPATH_ALTER_ADD.evaluate(item) 670 .forEach(nodeItem -> { 671 INodeItem addItem = (INodeItem) nodeItem; 672 Modify.Alter.Add add = ObjectUtils.notNull((Modify.Alter.Add) addItem.getValue()); 673 String byId = add.getById(); 674 try { 675 if (!AddVisitor.add( 676 control, 677 AddVisitor.Position.forName(add.getPosition()), 678 byId, 679 add.getTitle(), 680 CollectionUtil.listOrEmpty(add.getParams()), 681 CollectionUtil.listOrEmpty(add.getProps()), 682 CollectionUtil.listOrEmpty(add.getLinks()), 683 CollectionUtil.listOrEmpty(add.getParts()))) { 684 685 throw new ProfileResolutionEvaluationException( 686 String.format("The add did not match a valid target")); 687 } 688 } catch (ProfileResolutionEvaluationException ex) { 689 throw new ProfileResolutionEvaluationException( 690 String.format("Unable to apply the add targeting control '%s'%s at '%s'. %s", 691 control.getId(), 692 byId == null ? "" : String.format(" having by-id '%s'", byId), 693 addItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER), 694 ex.getLocalizedMessage()), 695 ex); 696 } 697 }); 698 } 699 700 private static void handleReferences(@NonNull Catalog resolvedCatalog, @NonNull IRootAssemblyNodeItem profileItem, 701 @NonNull IIndexer index) { 702 703 BasicIndexer profileIndex = new BasicIndexer(); 704 705 new ControlIndexingVisitor(ObjectUtils.notNull(EnumSet.allOf(ItemType.class))) 706 .visitProfile(profileItem, profileIndex); 707 708 // copy roles, parties, and locations with prop name:keep and any referenced 709 Metadata resolvedMetadata = resolvedCatalog.getMetadata(); 710 resolvedMetadata.setRoles( 711 IIndexer.filterDistinct( 712 ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getRoles()).stream()), 713 profileIndex.getEntitiesByItemType(IEntityItem.ItemType.ROLE), 714 Role::getId) 715 .collect(Collectors.toCollection(LinkedList::new))); 716 resolvedMetadata.setParties( 717 IIndexer.filterDistinct( 718 ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getParties()).stream()), 719 profileIndex.getEntitiesByItemType(IEntityItem.ItemType.PARTY), 720 Party::getUuid) 721 .collect(Collectors.toCollection(LinkedList::new))); 722 resolvedMetadata.setLocations( 723 IIndexer.filterDistinct( 724 ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolvedMetadata.getLocations()).stream()), 725 profileIndex.getEntitiesByItemType(IEntityItem.ItemType.LOCATION), 726 Location::getUuid) 727 .collect(Collectors.toCollection(LinkedList::new))); 728 729 // copy resources 730 BackMatter resolvedBackMatter = resolvedCatalog.getBackMatter(); 731 List<Resource> resolvedResources = resolvedBackMatter == null ? CollectionUtil.emptyList() 732 : CollectionUtil.listOrEmpty(resolvedBackMatter.getResources()); 733 734 List<Resource> resources = IIndexer.filterDistinct( 735 ObjectUtils.notNull(resolvedResources.stream()), 736 profileIndex.getEntitiesByItemType(IEntityItem.ItemType.RESOURCE), 737 Resource::getUuid) 738 .collect(Collectors.toCollection(LinkedList::new)); 739 740 if (!resources.isEmpty()) { 741 if (resolvedBackMatter == null) { 742 resolvedBackMatter = new BackMatter(); 743 resolvedCatalog.setBackMatter(resolvedBackMatter); 744 } 745 746 resolvedBackMatter.setResources(resources); 747 } 748 749 index.append(profileIndex); 750 } 751 752 @FunctionalInterface 753 public interface UriResolver { 754 @NonNull 755 URI resolve(@NonNull URI uri, @NonNull URI source); 756 } 757}