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