001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.oscal.lib.profile.resolver.selection;
007
008import com.fasterxml.jackson.core.Version;
009import com.fasterxml.jackson.core.util.VersionUtil;
010
011import java.net.URI;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.stream.Collectors;
015
016import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
017import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
018import dev.metaschema.core.metapath.item.node.INodeItem;
019import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
020import dev.metaschema.core.util.CollectionUtil;
021import dev.metaschema.core.util.ObjectUtils;
022import dev.metaschema.oscal.lib.model.BackMatter;
023import dev.metaschema.oscal.lib.model.BackMatter.Resource;
024import dev.metaschema.oscal.lib.model.Catalog;
025import dev.metaschema.oscal.lib.model.CatalogGroup;
026import dev.metaschema.oscal.lib.model.Control;
027import dev.metaschema.oscal.lib.model.Metadata;
028import dev.metaschema.oscal.lib.model.Metadata.Location;
029import dev.metaschema.oscal.lib.model.Metadata.Party;
030import dev.metaschema.oscal.lib.model.Metadata.Role;
031import dev.metaschema.oscal.lib.model.Parameter;
032import dev.metaschema.oscal.lib.model.ProfileImport;
033import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
034import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionException;
035import dev.metaschema.oscal.lib.profile.resolver.ProfileResolver.UriResolver;
036import dev.metaschema.oscal.lib.profile.resolver.policy.ReferenceCountingVisitor;
037import dev.metaschema.oscal.lib.profile.resolver.support.BasicIndexer;
038import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem;
039import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer;
040import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer.SelectionStatus;
041import edu.umd.cs.findbugs.annotations.NonNull;
042
043public class Import {
044
045  @NonNull
046  private final IRootAssemblyNodeItem profile;
047  @NonNull
048  private final IAssemblyNodeItem profileImportItem;
049
050  public Import(
051      @NonNull IRootAssemblyNodeItem profile,
052      @NonNull IAssemblyNodeItem profileImportItem) {
053
054    this.profile = profile;
055    this.profileImportItem = profileImportItem;
056  }
057
058  protected IRootAssemblyNodeItem getProfileItem() {
059    return profile;
060  }
061
062  protected IAssemblyNodeItem getProfileImportItem() {
063    return profileImportItem;
064  }
065
066  @NonNull
067  protected ProfileImport getProfileImport() {
068    return ObjectUtils.requireNonNull((ProfileImport) profileImportItem.getValue());
069  }
070
071  private static Catalog toCatalog(@NonNull IDocumentNodeItem catalogDocument) {
072    return (Catalog) INodeItem.toValue(catalogDocument);
073  }
074
075  @NonNull
076  protected IControlFilter newControlFilter() {
077    return IControlFilter.newInstance(getProfileImport());
078  }
079
080  @NonNull
081  protected IIndexer newIndexer() {
082    // TODO: add support for reassignment
083    // IIdentifierMapper mapper = IIdentifierMapper.IDENTITY;
084    // IIndexer indexer = new ReassignmentIndexer(mapper);
085    return new BasicIndexer();
086  }
087
088  @NonNull
089  public IIndexer resolve(
090      @NonNull IDocumentNodeItem importedCatalogDocument,
091      @NonNull Catalog resolvedCatalog,
092      @NonNull UriResolver uriResolver)
093      throws ProfileResolutionException {
094    ProfileImport profileImport = getProfileImport();
095    URI uri = ObjectUtils.requireNonNull(profileImport.getHref(), "profile import href is null");
096
097    // determine which controls and groups to keep
098    IControlFilter filter = newControlFilter();
099    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}