1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.oscal.lib.profile.resolver.selection;
7   
8   import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyNodeItem;
9   import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
10  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
11  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
12  import gov.nist.secauto.oscal.lib.OscalModelConstants;
13  import gov.nist.secauto.oscal.lib.model.BackMatter;
14  import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
15  import gov.nist.secauto.oscal.lib.model.Catalog;
16  import gov.nist.secauto.oscal.lib.model.CatalogGroup;
17  import gov.nist.secauto.oscal.lib.model.Control;
18  import gov.nist.secauto.oscal.lib.model.ControlPart;
19  import gov.nist.secauto.oscal.lib.model.Metadata;
20  import gov.nist.secauto.oscal.lib.model.Metadata.Location;
21  import gov.nist.secauto.oscal.lib.model.Metadata.Party;
22  import gov.nist.secauto.oscal.lib.model.Metadata.Role;
23  import gov.nist.secauto.oscal.lib.model.Parameter;
24  import gov.nist.secauto.oscal.lib.profile.resolver.support.AbstractCatalogEntityVisitor;
25  import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem;
26  import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem.ItemType;
27  import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer;
28  import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer.SelectionStatus;
29  
30  import org.apache.logging.log4j.LogManager;
31  import org.apache.logging.log4j.Logger;
32  
33  import java.util.EnumSet;
34  import java.util.stream.Collectors;
35  
36  import edu.umd.cs.findbugs.annotations.NonNull;
37  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
38  
39  public class FilterNonSelectedVisitor
40      extends AbstractCatalogEntityVisitor<FilterNonSelectedVisitor.Context, DefaultResult> {
41    private static final Logger LOGGER = LogManager.getLogger(FilterNonSelectedVisitor.class);
42    @NonNull
43    private static final FilterNonSelectedVisitor SINGLETON = new FilterNonSelectedVisitor();
44  
45    @NonNull
46    @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED", justification = "class initialization")
47    public static FilterNonSelectedVisitor instance() {
48      return SINGLETON;
49    }
50  
51    @SuppressWarnings("null")
52  
53    @SuppressFBWarnings(value = "SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR", justification = "allows for extension")
54    protected FilterNonSelectedVisitor() {
55      // all other entity types are handled in a special way by this visitor
56      super(EnumSet.of(IEntityItem.ItemType.GROUP, IEntityItem.ItemType.CONTROL, IEntityItem.ItemType.PARAMETER));
57    }
58  
59    public void visitCatalog(@NonNull IDocumentNodeItem catalogItem, @NonNull IIndexer indexer) {
60      Context context = new Context(indexer);
61      IResult result = visitCatalog(catalogItem, context);
62  
63      Catalog catalog = (Catalog) INodeItem.toValue(catalogItem);
64      result.applyTo(catalog);
65  
66      catalogItem.modelItems().forEachOrdered(root -> {
67        root.getModelItemsByName(OscalModelConstants.QNAME_METADATA).stream()
68            .map(child -> (IAssemblyNodeItem) child)
69            .forEachOrdered(child -> {
70              assert child != null;
71              visitMetadata(child, context);
72            });
73  
74        root.getModelItemsByName(OscalModelConstants.QNAME_BACK_MATTER).stream()
75            .map(child -> (IAssemblyNodeItem) child)
76            .forEachOrdered(child -> {
77              assert child != null;
78              visitBackMatter(child, context);
79            });
80      });
81    }
82  
83    @Override
84    protected DefaultResult newDefaultResult(Context state) {
85      return new DefaultResult();
86    }
87  
88    @Override
89    protected DefaultResult aggregateResults(DefaultResult first, DefaultResult second, Context state) {
90      return first.append(ObjectUtils.notNull(second));
91    }
92  
93    protected void visitMetadata(@NonNull IAssemblyNodeItem metadataItem, Context context) {
94      Metadata metadata = ObjectUtils.requireNonNull((Metadata) metadataItem.getValue());
95  
96      IIndexer index = context.getIndexer();
97      // prune roles, parties, and locations
98      // keep entries with prop name:keep and any referenced
99      for (IEntityItem entity : IIndexer.getUnreferencedEntitiesAsStream(index.getEntitiesByItemType(ItemType.ROLE))
100         .collect(Collectors.toList())) {
101       Role role = entity.getInstanceValue();
102       if (LOGGER.isDebugEnabled()) {
103         LOGGER.atDebug().log("Removing role '{}'", role.getId());
104       }
105       metadata.removeRole(role);
106       index.removeItem(entity);
107     }
108 
109     for (IEntityItem entity : IIndexer.getUnreferencedEntitiesAsStream(index.getEntitiesByItemType(ItemType.PARTY))
110         .collect(Collectors.toList())) {
111       Party party = entity.getInstanceValue();
112       if (LOGGER.isDebugEnabled()) {
113         LOGGER.atDebug().log("Removing party '{}'", party.getUuid());
114       }
115       metadata.removeParty(party);
116       index.removeItem(entity);
117     }
118 
119     for (IEntityItem entity : IIndexer.getUnreferencedEntitiesAsStream(index.getEntitiesByItemType(ItemType.LOCATION))
120         .collect(Collectors.toList())) {
121       Location location = entity.getInstanceValue();
122       if (LOGGER.isDebugEnabled()) {
123         LOGGER.atDebug().log("Removing location '{}'", location.getUuid());
124       }
125       metadata.removeLocation(location);
126       index.removeItem(entity);
127     }
128   }
129 
130   @SuppressWarnings("static-method")
131   private void visitBackMatter(@NonNull IAssemblyNodeItem backMatterItem, Context context) {
132     BackMatter backMatter = ObjectUtils.requireNonNull((BackMatter) backMatterItem.getValue());
133 
134     IIndexer index = context.getIndexer();
135     for (IEntityItem entity : IIndexer.getUnreferencedEntitiesAsStream(index.getEntitiesByItemType(ItemType.RESOURCE))
136         .collect(Collectors.toList())) {
137       Resource resource = entity.getInstanceValue();
138       if (LOGGER.isDebugEnabled()) {
139         LOGGER.atDebug().log("Removing resource '{}'", resource.getUuid());
140       }
141       backMatter.removeResource(resource);
142       index.removeItem(entity);
143     }
144   }
145 
146   @Override
147   public DefaultResult visitGroup(
148       IAssemblyNodeItem item,
149       DefaultResult childResult,
150       Context context) {
151     CatalogGroup group = ObjectUtils.requireNonNull((CatalogGroup) item.getValue());
152 
153     IIndexer index = context.getIndexer();
154     String groupId = group.getId();
155     DefaultResult retval = new DefaultResult();
156     if (SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
157       if (groupId != null) {
158         // this group should always be found in the index
159         IEntityItem entity = ObjectUtils.requireNonNull(index.getEntity(ItemType.GROUP, groupId, false));
160         // update the id
161         group.setId(entity.getIdentifier());
162       }
163       childResult.applyTo(group);
164     } else {
165       retval.removeGroup(group);
166       retval.appendPromoted(ObjectUtils.notNull(childResult));
167 
168       if (groupId != null) {
169         // this group should always be found in the index
170         IEntityItem entity = ObjectUtils.requireNonNull(index.getEntity(ItemType.GROUP, groupId, false));
171         index.removeItem(entity);
172       }
173 
174       // remove any associated parts from the index
175       removePartsFromIndex(item, index);
176     }
177     return retval;
178   }
179 
180   @Override
181   public DefaultResult visitControl(
182       IAssemblyNodeItem item,
183       DefaultResult childResult,
184       Context context) {
185     Control control = ObjectUtils.requireNonNull((Control) item.getValue());
186     IIndexer index = context.getIndexer();
187     // this control should always be found in the index
188     IEntityItem entity = ObjectUtils.requireNonNull(
189         index.getEntity(ItemType.CONTROL, ObjectUtils.requireNonNull(control.getId()), false));
190 
191     IAssemblyNodeItem parent = ObjectUtils.notNull(item.getParentContentNodeItem());
192     DefaultResult retval = new DefaultResult();
193     if (SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
194       // keep this control
195       // update the id
196       control.setId(entity.getIdentifier());
197 
198       if (!SelectionStatus.SELECTED.equals(index.getSelectionStatus(parent))) {
199         // promote this control
200         retval.promoteControl(control);
201       }
202       childResult.applyTo(control);
203     } else {
204       // remove this control and promote any needed children
205 
206       if (SelectionStatus.SELECTED.equals(index.getSelectionStatus(parent))) {
207         retval.removeControl(control);
208       }
209       retval.appendPromoted(ObjectUtils.notNull(childResult));
210       index.removeItem(entity);
211 
212       // remove any associated parts from the index
213       removePartsFromIndex(item, index);
214     }
215     return retval;
216   }
217 
218   protected static void removePartsFromIndex(@NonNull IAssemblyNodeItem groupOrControlItem,
219       @NonNull IIndexer index) {
220     CHILD_PART_METAPATH.evaluate(groupOrControlItem).stream()
221         .map(item -> (IAssemblyNodeItem) item)
222         .forEachOrdered(partItem -> {
223           ControlPart part = ObjectUtils.requireNonNull((ControlPart) partItem.getValue());
224           String id = part.getId();
225           if (id != null) {
226             IEntityItem entity = index.getEntity(IEntityItem.ItemType.PART, id);
227             if (entity != null) {
228               index.removeItem(entity);
229             }
230           }
231         });
232   }
233 
234   @Override
235   protected DefaultResult visitParameter(IAssemblyNodeItem item, IAssemblyNodeItem parent,
236       Context context) {
237     Parameter param = ObjectUtils.requireNonNull((Parameter) item.getValue());
238     IIndexer index = context.getIndexer();
239     // this parameter should always be found in the index
240     IEntityItem entity = ObjectUtils.requireNonNull(
241         index.getEntity(ItemType.PARAMETER, ObjectUtils.requireNonNull(param.getId()), false));
242 
243     DefaultResult retval = new DefaultResult();
244     if (IIndexer.isReferencedEntity(entity)) {
245       // keep the parameter
246       // update the id
247       param.setId(entity.getIdentifier());
248 
249       // a parameter is selected if it has a reference count greater than 0
250       index.setSelectionStatus(item, SelectionStatus.SELECTED);
251 
252       // promote this parameter
253       if (SelectionStatus.UNSELECTED.equals(index.getSelectionStatus(parent))) {
254         retval.promoteParameter(param);
255       }
256     } else {
257       // don't keep the parameter
258       if (SelectionStatus.SELECTED.equals(index.getSelectionStatus(parent))) {
259         retval.removeParameter(param);
260       }
261       index.removeItem(entity);
262     }
263     return retval;
264   }
265 
266   protected static final class Context {
267 
268     @NonNull
269     private final IIndexer indexer;
270 
271     private Context(@NonNull IIndexer indexer) {
272       this.indexer = indexer;
273     }
274 
275     @NonNull
276     @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "provides intentional access to index state")
277     public IIndexer getIndexer() {
278       return indexer;
279     }
280   }
281 }