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