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