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