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