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 org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  
11  import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
12  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
13  import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
14  import dev.metaschema.core.util.ObjectUtils;
15  import dev.metaschema.oscal.lib.model.Catalog;
16  import dev.metaschema.oscal.lib.model.CatalogGroup;
17  import dev.metaschema.oscal.lib.model.Control;
18  import dev.metaschema.oscal.lib.model.ControlPart;
19  import dev.metaschema.oscal.lib.profile.resolver.support.AbstractIndexingVisitor;
20  import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem;
21  import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer;
22  import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer.SelectionStatus;
23  import edu.umd.cs.findbugs.annotations.NonNull;
24  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
25  
26  /**
27   * Walks a {@link Catalog} indexing all nodes that can be referenced.
28   * <p>
29   * For each {@link CatalogGroup}, {@link Control}, and {@link ControlPart},
30   * determine if that object is {@link SelectionStatus#SELECTED} or
31   * {@link SelectionStatus#UNSELECTED}.
32   * <p>
33   * A {@link Control} is {@link SelectionStatus#SELECTED} if it matches the
34   * configured {@link IControlFilter}, otherwise it is
35   * {@link SelectionStatus#UNSELECTED}.
36   * <p>
37   * A {@link CatalogGroup} is {@link SelectionStatus#SELECTED} if it contains a
38   * {@link SelectionStatus#SELECTED} descendant {@link Control}, otherwise it is
39   * {@link SelectionStatus#UNSELECTED}.
40   * <p>
41   * A {@link ControlPart} is {@link SelectionStatus#SELECTED} if its containing
42   * control is {@link SelectionStatus#SELECTED}.
43   * <p>
44   * All other indexed nodes will have the {@link SelectionStatus#UNKNOWN}, since
45   * these nodes require reference counting to determine if they are to be kept or
46   * not.
47   */
48  public final class ControlSelectionVisitor
49      extends AbstractIndexingVisitor<IControlSelectionState, Boolean> {
50    private static final Logger LOGGER = LogManager.getLogger(ControlSelectionVisitor.class);
51  
52    private static final ControlSelectionVisitor SINGLETON = new ControlSelectionVisitor();
53  
54    @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED", justification = "class initialization")
55    public static ControlSelectionVisitor instance() {
56      return SINGLETON;
57    }
58  
59    private ControlSelectionVisitor() {
60      // disable construction
61    }
62  
63    @Override
64    protected IIndexer getIndexer(IControlSelectionState state) {
65      return state.getIndex();
66    }
67  
68    @Override
69    protected Boolean newDefaultResult(IControlSelectionState state) {
70      return false;
71    }
72  
73    @Override
74    protected Boolean aggregateResults(Boolean first, Boolean second, IControlSelectionState state) {
75      return first || second;
76    }
77  
78    public void visit(@NonNull IDocumentNodeItem catalogDocument, @NonNull IControlSelectionState state) {
79      visitCatalog(catalogDocument, state);
80    }
81  
82    public void visitProfile(
83        @NonNull IDocumentNodeItem catalogDocument,
84        @NonNull IDocumentNodeItem profileDocument,
85        @NonNull IControlSelectionState state) {
86      visit(catalogDocument, state);
87  
88      profileDocument.modelItems().forEachOrdered(item -> {
89        IRootAssemblyNodeItem root = ObjectUtils.requireNonNull((IRootAssemblyNodeItem) item);
90  
91        visitMetadata(root, state);
92        visitBackMatter(root, state);
93      });
94    }
95  
96    @Override
97    public Boolean visitCatalog(IDocumentNodeItem catalogDocument, IControlSelectionState state) {
98      getIndexer(state).setSelectionStatus(catalogDocument, SelectionStatus.SELECTED);
99      return super.visitCatalog(catalogDocument, state);
100   }
101 
102   @Override
103   public Boolean visitGroup(IAssemblyNodeItem groupItem, Boolean childSelected,
104       IControlSelectionState state) {
105     super.visitGroup(groupItem, childSelected, state);
106     if (LOGGER.isTraceEnabled()) {
107       CatalogGroup group = ObjectUtils.requireNonNull((CatalogGroup) groupItem.getValue());
108       LOGGER.atTrace().log("Selecting group '{}'. match={}", group.getId(), childSelected);
109     }
110 
111     // these should agree
112     assert state.isSelected(groupItem) == childSelected;
113 
114     if (childSelected) {
115       getIndexer(state).setSelectionStatus(groupItem, SelectionStatus.SELECTED);
116     } else {
117       getIndexer(state).setSelectionStatus(groupItem, SelectionStatus.UNSELECTED);
118     }
119 
120     handlePartSelection(groupItem, childSelected, state);
121     return childSelected;
122   }
123 
124   private void handlePartSelection(
125       @NonNull IAssemblyNodeItem groupOrControlItem,
126       boolean selected,
127       IControlSelectionState state) {
128     if (isVisitedItemType(IEntityItem.ItemType.PART)) {
129       SelectionStatus selectionStatus = selected ? SelectionStatus.SELECTED : SelectionStatus.UNSELECTED;
130 
131       IIndexer index = getIndexer(state);
132       CHILD_PART_METAPATH.evaluate(groupOrControlItem).stream()
133           .map(item -> (IAssemblyNodeItem) item)
134           .forEachOrdered(partItem -> {
135             index.setSelectionStatus(ObjectUtils.requireNonNull(partItem), selectionStatus);
136           });
137     }
138   }
139 
140   @Override
141   public Boolean visitControl(
142       IAssemblyNodeItem controlItem,
143       Boolean childResult,
144       IControlSelectionState state) {
145     super.visitControl(controlItem, childResult, state);
146 
147     boolean selected = state.isSelected(controlItem);
148     if (selected) {
149       getIndexer(state).setSelectionStatus(controlItem, SelectionStatus.SELECTED);
150     } else {
151       getIndexer(state).setSelectionStatus(controlItem, SelectionStatus.UNSELECTED);
152     }
153 
154     handlePartSelection(controlItem, selected, state);
155     return selected;
156   }
157 }