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