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