View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.oscal.lib.profile.resolver;
28  
29  import gov.nist.secauto.metaschema.binding.io.BindingException;
30  import gov.nist.secauto.metaschema.binding.io.DeserializationFeature;
31  import gov.nist.secauto.metaschema.binding.io.IBoundLoader;
32  import gov.nist.secauto.metaschema.binding.model.IAssemblyClassBinding;
33  import gov.nist.secauto.metaschema.binding.model.RootAssemblyDefinition;
34  import gov.nist.secauto.metaschema.model.common.metapath.DynamicContext;
35  import gov.nist.secauto.metaschema.model.common.metapath.MetapathExpression;
36  import gov.nist.secauto.metaschema.model.common.metapath.StaticContext;
37  import gov.nist.secauto.metaschema.model.common.metapath.format.IPathFormatter;
38  import gov.nist.secauto.metaschema.model.common.metapath.item.DefaultNodeItemFactory;
39  import gov.nist.secauto.metaschema.model.common.metapath.item.IDocumentNodeItem;
40  import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueAssemblyNodeItem;
41  import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueModelNodeItem;
42  import gov.nist.secauto.metaschema.model.common.metapath.item.IRequiredValueNodeItem;
43  import gov.nist.secauto.metaschema.model.common.metapath.item.IRootAssemblyNodeItem;
44  import gov.nist.secauto.metaschema.model.common.util.CollectionUtil;
45  import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
46  import gov.nist.secauto.oscal.lib.OscalBindingContext;
47  import gov.nist.secauto.oscal.lib.OscalUtils;
48  import gov.nist.secauto.oscal.lib.model.BackMatter;
49  import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
50  import gov.nist.secauto.oscal.lib.model.Catalog;
51  import gov.nist.secauto.oscal.lib.model.Control;
52  import gov.nist.secauto.oscal.lib.model.Merge;
53  import gov.nist.secauto.oscal.lib.model.Metadata;
54  import gov.nist.secauto.oscal.lib.model.Modify;
55  import gov.nist.secauto.oscal.lib.model.Modify.ProfileSetParameter;
56  import gov.nist.secauto.oscal.lib.model.Parameter;
57  import gov.nist.secauto.oscal.lib.model.Profile;
58  import gov.nist.secauto.oscal.lib.model.ProfileImport;
59  import gov.nist.secauto.oscal.lib.model.Property;
60  import gov.nist.secauto.oscal.lib.model.metadata.AbstractLink;
61  import gov.nist.secauto.oscal.lib.model.metadata.AbstractProperty;
62  import gov.nist.secauto.oscal.lib.profile.resolver.alter.AddVisitor;
63  import gov.nist.secauto.oscal.lib.profile.resolver.alter.RemoveVisitor;
64  import gov.nist.secauto.oscal.lib.profile.resolver.merge.FlatteningStructuringVisitor;
65  import gov.nist.secauto.oscal.lib.profile.resolver.selection.Import;
66  import gov.nist.secauto.oscal.lib.profile.resolver.selection.ImportCycleException;
67  import gov.nist.secauto.oscal.lib.profile.resolver.support.BasicIndexer;
68  import gov.nist.secauto.oscal.lib.profile.resolver.support.ControlIndexingVisitor;
69  import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem;
70  import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType;
71  import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer;
72  
73  import org.apache.logging.log4j.LogManager;
74  import org.apache.logging.log4j.Logger;
75  import org.xml.sax.EntityResolver;
76  import org.xml.sax.InputSource;
77  import org.xml.sax.SAXException;
78  
79  import java.io.File;
80  import java.io.IOException;
81  import java.net.URI;
82  import java.net.URISyntaxException;
83  import java.net.URL;
84  import java.nio.file.Path;
85  import java.time.ZoneOffset;
86  import java.time.ZonedDateTime;
87  import java.util.EnumSet;
88  import java.util.LinkedList;
89  import java.util.List;
90  import java.util.Stack;
91  import java.util.UUID;
92  import java.util.stream.Collectors;
93  
94  import edu.umd.cs.findbugs.annotations.NonNull;
95  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
96  
97  public class ProfileResolver {
98    private static final Logger LOGGER = LogManager.getLogger(ProfileResolver.class);
99    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 }