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