1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.oscal.lib.profile.resolver.selection;
7   
8   import com.fasterxml.jackson.core.Version;
9   import com.fasterxml.jackson.core.util.VersionUtil;
10  
11  import java.net.URI;
12  import java.util.LinkedList;
13  import java.util.List;
14  import java.util.stream.Collectors;
15  
16  import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
17  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
18  import dev.metaschema.core.metapath.item.node.INodeItem;
19  import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
20  import dev.metaschema.core.util.CollectionUtil;
21  import dev.metaschema.core.util.ObjectUtils;
22  import dev.metaschema.oscal.lib.model.BackMatter;
23  import dev.metaschema.oscal.lib.model.BackMatter.Resource;
24  import dev.metaschema.oscal.lib.model.Catalog;
25  import dev.metaschema.oscal.lib.model.CatalogGroup;
26  import dev.metaschema.oscal.lib.model.Control;
27  import dev.metaschema.oscal.lib.model.Metadata;
28  import dev.metaschema.oscal.lib.model.Metadata.Location;
29  import dev.metaschema.oscal.lib.model.Metadata.Party;
30  import dev.metaschema.oscal.lib.model.Metadata.Role;
31  import dev.metaschema.oscal.lib.model.Parameter;
32  import dev.metaschema.oscal.lib.model.ProfileImport;
33  import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
34  import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionException;
35  import dev.metaschema.oscal.lib.profile.resolver.ProfileResolver.UriResolver;
36  import dev.metaschema.oscal.lib.profile.resolver.policy.ReferenceCountingVisitor;
37  import dev.metaschema.oscal.lib.profile.resolver.support.BasicIndexer;
38  import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem;
39  import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer;
40  import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer.SelectionStatus;
41  import edu.umd.cs.findbugs.annotations.NonNull;
42  
43  public class Import {
44  
45    @NonNull
46    private final IRootAssemblyNodeItem profile;
47    @NonNull
48    private final IAssemblyNodeItem profileImportItem;
49  
50    public Import(
51        @NonNull IRootAssemblyNodeItem profile,
52        @NonNull IAssemblyNodeItem profileImportItem) {
53  
54      this.profile = profile;
55      this.profileImportItem = profileImportItem;
56    }
57  
58    protected IRootAssemblyNodeItem getProfileItem() {
59      return profile;
60    }
61  
62    protected IAssemblyNodeItem getProfileImportItem() {
63      return profileImportItem;
64    }
65  
66    @NonNull
67    protected ProfileImport getProfileImport() {
68      return ObjectUtils.requireNonNull((ProfileImport) profileImportItem.getValue());
69    }
70  
71    private static Catalog toCatalog(@NonNull IDocumentNodeItem catalogDocument) {
72      return (Catalog) INodeItem.toValue(catalogDocument);
73    }
74  
75    @NonNull
76    protected IControlFilter newControlFilter() {
77      return IControlFilter.newInstance(getProfileImport());
78    }
79  
80    @NonNull
81    protected IIndexer newIndexer() {
82      // TODO: add support for reassignment
83      // IIdentifierMapper mapper = IIdentifierMapper.IDENTITY;
84      // IIndexer indexer = new ReassignmentIndexer(mapper);
85      return new BasicIndexer();
86    }
87  
88    @NonNull
89    public IIndexer resolve(
90        @NonNull IDocumentNodeItem importedCatalogDocument,
91        @NonNull Catalog resolvedCatalog,
92        @NonNull UriResolver uriResolver)
93        throws ProfileResolutionException {
94      ProfileImport profileImport = getProfileImport();
95      URI uri = ObjectUtils.requireNonNull(profileImport.getHref(), "profile import href is null");
96  
97      // determine which controls and groups to keep
98      IControlFilter filter = newControlFilter();
99      IIndexer indexer = newIndexer();
100     // the catalog is always selected. This ensures that controls defined at the
101     // catalog level are kept and not duplicated
102     indexer.setSelectionStatus(importedCatalogDocument.getRootAssemblyNodeItem(), SelectionStatus.SELECTED);
103     IControlSelectionState state = new ControlSelectionState(indexer, filter);
104 
105     try {
106       ControlSelectionVisitor.instance().visitCatalog(importedCatalogDocument, state);
107 
108       // process references
109       ReferenceCountingVisitor.instance().visitCatalog(importedCatalogDocument, indexer, uriResolver);
110 
111       // filter based on selections
112       FilterNonSelectedVisitor.instance().visitCatalog(importedCatalogDocument, indexer);
113     } catch (ProfileResolutionEvaluationException ex) {
114       throw new ProfileResolutionException(
115           String.format("Import: Unable to resolve profile import '%s'. %s", uri.toString(), ex.getMessage()), ex);
116     }
117 
118     Catalog importedCatalog = toCatalog(importedCatalogDocument);
119     for (Parameter param : CollectionUtil.listOrEmpty(importedCatalog.getParams())) {
120       if (param != null) {
121         resolvedCatalog.addParam(param);
122       }
123     }
124     for (Control control : CollectionUtil.listOrEmpty(importedCatalog.getControls())) {
125       if (control != null) {
126         resolvedCatalog.addControl(control);
127       }
128     }
129     for (CatalogGroup group : CollectionUtil.listOrEmpty(importedCatalog.getGroups())) {
130       if (group != null) {
131         resolvedCatalog.addGroup(group);
132       }
133     }
134 
135     generateMetadata(importedCatalogDocument, resolvedCatalog, indexer);
136     generateBackMatter(importedCatalogDocument, resolvedCatalog, indexer);
137     return indexer;
138   }
139 
140   private static void generateMetadata(
141       @NonNull IDocumentNodeItem importedCatalogDocument,
142       @NonNull Catalog resolvedCatalog,
143       @NonNull IIndexer indexer) {
144     Metadata importedMetadata = toCatalog(importedCatalogDocument).getMetadata();
145 
146     if (importedMetadata != null) {
147       resolveMetadata(importedMetadata, resolvedCatalog.getMetadata(), indexer);
148     }
149   }
150 
151   private static void resolveMetadata(
152       @NonNull Metadata imported,
153       @NonNull Metadata resolved,
154       @NonNull IIndexer indexer) {
155     String importedVersion = imported.getOscalVersion();
156     if (importedVersion != null) {
157       Version importOscalVersion = VersionUtil.parseVersion(importedVersion, null, null);
158 
159       Version resolvedCatalogVersion
160           = VersionUtil.parseVersion(resolved.getOscalVersion(), null, null);
161 
162       if (importOscalVersion.compareTo(resolvedCatalogVersion) > 0) {
163         resolved.setOscalVersion(importOscalVersion.toString());
164       }
165     }
166 
167     // copy roles, parties, and locations with prop name:keep and any referenced
168     resolved.setRoles(
169         IIndexer.filterDistinct(
170             ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolved.getRoles()).stream()),
171             indexer.getEntitiesByItemType(IEntityItem.ItemType.ROLE),
172             Role::getId)
173             .collect(Collectors.toCollection(LinkedList::new)));
174     resolved.setParties(
175         IIndexer.filterDistinct(
176             ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolved.getParties()).stream()),
177             indexer.getEntitiesByItemType(IEntityItem.ItemType.PARTY),
178             Party::getUuid)
179             .collect(Collectors.toCollection(LinkedList::new)));
180     resolved.setLocations(
181         IIndexer.filterDistinct(
182             ObjectUtils.notNull(CollectionUtil.listOrEmpty(resolved.getLocations()).stream()),
183             indexer.getEntitiesByItemType(IEntityItem.ItemType.LOCATION),
184             Location::getUuid)
185             .collect(Collectors.toCollection(LinkedList::new)));
186   }
187 
188   @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts") // not worth a function call
189   private static void generateBackMatter(
190       @NonNull IDocumentNodeItem importedCatalogDocument,
191       @NonNull Catalog resolvedCatalog,
192       @NonNull IIndexer indexer) {
193     BackMatter importedBackMatter = toCatalog(importedCatalogDocument).getBackMatter();
194 
195     if (importedBackMatter != null) {
196       BackMatter resolvedBackMatter = resolvedCatalog.getBackMatter();
197 
198       List<Resource> resolvedResources = resolvedBackMatter == null ? CollectionUtil.emptyList()
199           : CollectionUtil.listOrEmpty(resolvedBackMatter.getResources());
200 
201       List<Resource> resources = IIndexer.filterDistinct(
202           ObjectUtils.notNull(resolvedResources.stream()),
203           indexer.getEntitiesByItemType(IEntityItem.ItemType.RESOURCE),
204           Resource::getUuid)
205           .collect(Collectors.toCollection(LinkedList::new));
206 
207       if (!resources.isEmpty()) {
208         if (resolvedBackMatter == null) {
209           resolvedBackMatter = new BackMatter();
210           resolvedCatalog.setBackMatter(resolvedBackMatter);
211         }
212 
213         resolvedBackMatter.setResources(resources);
214       }
215     }
216   }
217 }