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