1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.oscal.lib.profile.resolver;
7   
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  
11  import java.io.File;
12  import java.io.IOException;
13  import java.net.URI;
14  import java.net.URISyntaxException;
15  import java.net.URL;
16  import java.nio.ByteBuffer;
17  import java.nio.file.Path;
18  import java.time.ZoneOffset;
19  import java.time.ZonedDateTime;
20  import java.util.EnumSet;
21  import java.util.LinkedList;
22  import java.util.List;
23  import java.util.Stack;
24  import java.util.UUID;
25  import java.util.stream.Collectors;
26  
27  import dev.metaschema.core.metapath.DynamicContext;
28  import dev.metaschema.core.metapath.IDocumentLoader;
29  import dev.metaschema.core.metapath.IMetapathExpression;
30  import dev.metaschema.core.metapath.StaticContext;
31  import dev.metaschema.core.metapath.format.IPathFormatter;
32  import dev.metaschema.core.metapath.function.FunctionUtils;
33  import dev.metaschema.core.metapath.item.IItem;
34  import dev.metaschema.core.metapath.item.ISequence;
35  import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
36  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
37  import dev.metaschema.core.metapath.item.node.INodeItem;
38  import dev.metaschema.core.metapath.item.node.INodeItemFactory;
39  import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
40  import dev.metaschema.core.model.IBoundObject;
41  import dev.metaschema.core.qname.IEnhancedQName;
42  import dev.metaschema.core.util.CollectionUtil;
43  import dev.metaschema.core.util.ObjectUtils;
44  import dev.metaschema.databind.io.BindingException;
45  import dev.metaschema.databind.io.DeserializationFeature;
46  import dev.metaschema.databind.io.IBoundLoader;
47  import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
48  import dev.metaschema.oscal.lib.OscalBindingContext;
49  import dev.metaschema.oscal.lib.OscalModelConstants;
50  import dev.metaschema.oscal.lib.OscalUtils;
51  import dev.metaschema.oscal.lib.model.BackMatter;
52  import dev.metaschema.oscal.lib.model.BackMatter.Resource;
53  import dev.metaschema.oscal.lib.model.BackMatter.Resource.Base64;
54  import dev.metaschema.oscal.lib.model.BackMatter.Resource.Rlink;
55  import dev.metaschema.oscal.lib.model.Catalog;
56  import dev.metaschema.oscal.lib.model.Control;
57  import dev.metaschema.oscal.lib.model.Merge;
58  import dev.metaschema.oscal.lib.model.Metadata;
59  import dev.metaschema.oscal.lib.model.Metadata.Location;
60  import dev.metaschema.oscal.lib.model.Metadata.Party;
61  import dev.metaschema.oscal.lib.model.Metadata.Role;
62  import dev.metaschema.oscal.lib.model.Modify;
63  import dev.metaschema.oscal.lib.model.Modify.ProfileSetParameter;
64  import dev.metaschema.oscal.lib.model.Parameter;
65  import dev.metaschema.oscal.lib.model.Profile;
66  import dev.metaschema.oscal.lib.model.ProfileImport;
67  import dev.metaschema.oscal.lib.model.Property;
68  import dev.metaschema.oscal.lib.model.metadata.AbstractLink;
69  import dev.metaschema.oscal.lib.model.metadata.AbstractProperty;
70  import dev.metaschema.oscal.lib.profile.resolver.alter.AddVisitor;
71  import dev.metaschema.oscal.lib.profile.resolver.alter.RemoveVisitor;
72  import dev.metaschema.oscal.lib.profile.resolver.merge.FlatteningStructuringVisitor;
73  import dev.metaschema.oscal.lib.profile.resolver.selection.Import;
74  import dev.metaschema.oscal.lib.profile.resolver.selection.ImportCycleException;
75  import dev.metaschema.oscal.lib.profile.resolver.support.BasicIndexer;
76  import dev.metaschema.oscal.lib.profile.resolver.support.ControlIndexingVisitor;
77  import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem;
78  import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem.ItemType;
79  import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer;
80  import edu.umd.cs.findbugs.annotations.NonNull;
81  import edu.umd.cs.findbugs.annotations.Nullable;
82  
83  public class ProfileResolver {
84  
85    public enum StructuringDirective {
86      FLAT,
87      AS_IS,
88      CUSTOM;
89    }
90  
91    private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
92    @NonNull
93    private static final IEnhancedQName IMPORT_QNAME = IEnhancedQName.of(OscalModelConstants.NS_OSCAL, "import");
94  
95    @NonNull
96    private static final IMetapathExpression METAPATH_SET_PARAMETER
97        = IMetapathExpression.compile("modify/set-parameter",
98            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
99    @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 }