1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.item.node;
7   
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  
11  import java.util.Collection;
12  import java.util.Collections;
13  import java.util.LinkedHashMap;
14  import java.util.LinkedList;
15  import java.util.List;
16  import java.util.Map;
17  
18  import dev.metaschema.core.metapath.DynamicContext;
19  import dev.metaschema.core.metapath.MetapathException;
20  import dev.metaschema.core.metapath.StaticContext;
21  import dev.metaschema.core.metapath.item.ISequence;
22  import dev.metaschema.core.model.IModule;
23  import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
24  import dev.metaschema.core.model.constraint.ILet;
25  import edu.umd.cs.findbugs.annotations.NonNull;
26  
27  /**
28   * A visitor that traverses a Metaschema module's node items and collects all
29   * allowed-values constraints, organized by the target node they apply to.
30   * <p>
31   * This visitor extends {@link AbstractRecursionPreventingNodeItemVisitor} to
32   * safely handle recursive assembly definitions without infinite looping.
33   * <p>
34   * Usage example:
35   *
36   * <pre>
37   * AllowedValueCollectingNodeItemVisitor visitor
38   *     = new AllowedValueCollectingNodeItemVisitor();
39   * visitor.visit(module);
40   * Collection&lt;NodeItemRecord&gt; locations = visitor.getAllowedValueLocations();
41   * </pre>
42   */
43  public class AllowedValueCollectingNodeItemVisitor
44      extends AbstractRecursionPreventingNodeItemVisitor<DynamicContext, Void> {
45    private static final Logger LOGGER = LogManager.getLogger(AllowedValueCollectingNodeItemVisitor.class);
46  
47    @NonNull
48    private final Map<IDefinitionNodeItem<?, ?>, NodeItemRecord> nodeItemAnalysis = new LinkedHashMap<>();
49  
50    /**
51     * Get the collected allowed-values constraint locations found during
52     * visitation.
53     *
54     * @return a collection of records, each containing a definition node item and
55     *         the allowed-values constraints that target it
56     */
57    @NonNull
58    public Collection<NodeItemRecord> getAllowedValueLocations() {
59      return nodeItemAnalysis.values();
60    }
61  
62    /**
63     * Visit all definitions in the provided module to collect allowed-values
64     * constraints.
65     * <p>
66     * This method creates a new {@link DynamicContext} configured with the module's
67     * default namespace.
68     *
69     * @param module
70     *          the Metaschema module to visit
71     */
72    public void visit(@NonNull IModule module) {
73      DynamicContext context = new DynamicContext(
74          StaticContext.builder()
75              .defaultModelNamespace(module.getXmlNamespace())
76              .build());
77  
78      visit(INodeItemFactory.instance().newModuleNodeItem(module), context);
79    }
80  
81    /**
82     * Visit all definitions in the provided module node item using the given
83     * dynamic context.
84     * <p>
85     * The provided context is configured for module-definition walking: predicate
86     * evaluation is disabled and no-data atomization is treated as empty. These
87     * settings are applied regardless of their state on the incoming context so
88     * that expressions which reach instance-only functions (for example,
89     * {@code fn:doc} via {@code oscal:resolve-reference(@href)}) degrade to empty
90     * sequences rather than raising {@code InvalidTypeFunctionException} and
91     * predicates on definitional targets do not attempt evaluation that requires
92     * instance data.
93     *
94     * @param module
95     *          the module node item to visit
96     * @param context
97     *          the dynamic context to use for constraint evaluation
98     */
99    public void visit(@NonNull IModuleNodeItem module, @NonNull DynamicContext context) {
100     context.disablePredicateEvaluation();
101     context.enableAtomizeNoDataAsEmpty();
102     visitMetaschema(module, context);
103   }
104 
105   @SuppressWarnings("PMD.AvoidCatchingGenericException")
106   private void handleAllowedValuesAtLocation(
107       @NonNull IDefinitionNodeItem<?, ?> itemLocation,
108       @NonNull DynamicContext context) {
109     itemLocation.getDefinition().getAllowedValuesConstraints().stream()
110         .forEachOrdered(allowedValues -> {
111           try {
112             ISequence<?> result = allowedValues.getTarget().evaluate(itemLocation, context);
113             result.stream().forEachOrdered(target -> {
114               assert target != null;
115               handleAllowedValues(allowedValues, itemLocation, (IDefinitionNodeItem<?, ?>) target);
116             });
117           } catch (MetapathException ex) {
118             LOGGER.atWarn().log(
119                 "Skipping allowed-values constraint target '{}' at '{}' because it cannot be evaluated"
120                     + " against the module definition: {}",
121                 allowedValues.getTarget().getPath(), itemLocation.getMetapath(), ex.getLocalizedMessage());
122           }
123         });
124   }
125 
126   private void handleAllowedValues(
127       @NonNull IAllowedValuesConstraint allowedValues,
128       @NonNull IDefinitionNodeItem<?, ?> location,
129       @NonNull IDefinitionNodeItem<?, ?> target) {
130     NodeItemRecord itemRecord = nodeItemAnalysis.get(target);
131     if (itemRecord == null) {
132       itemRecord = new NodeItemRecord(target);
133       nodeItemAnalysis.put(target, itemRecord);
134     }
135 
136     AllowedValuesRecord allowedValuesRecord = new AllowedValuesRecord(allowedValues, location, target);
137     itemRecord.addAllowedValues(allowedValuesRecord);
138   }
139 
140   @Override
141   public Void visitFlag(IFlagNodeItem item, DynamicContext context) {
142     assert context != null;
143     DynamicContext subContext = handleLetStatements(item, context);
144     handleAllowedValuesAtLocation(item, subContext);
145     return super.visitFlag(item, subContext);
146   }
147 
148   @Override
149   public Void visitField(IFieldNodeItem item, DynamicContext context) {
150     assert context != null;
151     DynamicContext subContext = handleLetStatements(item, context);
152     handleAllowedValuesAtLocation(item, subContext);
153     return super.visitField(item, subContext);
154   }
155 
156   @Override
157   public Void visitAssembly(IAssemblyNodeItem item, DynamicContext context) {
158     assert context != null;
159     DynamicContext subContext = handleLetStatements(item, context);
160     handleAllowedValuesAtLocation(item, subContext);
161     return super.visitAssembly(item, subContext);
162   }
163 
164   @SuppressWarnings("PMD.AssignmentInOperand")
165   private DynamicContext handleLetStatements(IDefinitionNodeItem<?, ?> item, DynamicContext context) {
166     assert context != null;
167     DynamicContext subContext = context;
168     for (ILet let : item.getDefinition().getLetExpressions().values()) {
169       try {
170         ISequence<?> result = let.getValueExpression().evaluate(item,
171             subContext).reusable();
172         subContext = subContext.bindVariableValue(let.getName(), result);
173       } catch (MetapathException ex) {
174         // Let expressions may reference runtime-only data (e.g. atomization of a
175         // flag whose typed value is only available on an instance document) that
176         // cannot be evaluated while walking the module definition. Skip binding
177         // the variable; dependent expressions will be skipped downstream.
178         LOGGER.atWarn().log(
179             "Skipping let expression '${}' at '{}' because it cannot be evaluated against"
180                 + " the module definition: {}",
181             let.getName(), item.getMetapath(), ex.getLocalizedMessage());
182       }
183     }
184     return subContext;
185   }
186 
187   @Override
188   public Void visitAssembly(IAssemblyInstanceGroupedNodeItem item, DynamicContext context) {
189     return visitAssembly((IAssemblyNodeItem) item, context);
190   }
191 
192   @Override
193   protected Void defaultResult() {
194     return null;
195   }
196 
197   /**
198    * A record that associates a definition node item with all the allowed-values
199    * constraints that target it.
200    */
201   public static final class NodeItemRecord {
202     @NonNull
203     private final IDefinitionNodeItem<?, ?> item;
204     @NonNull
205     private final List<AllowedValuesRecord> allowedValues = new LinkedList<>();
206 
207     private NodeItemRecord(@NonNull IDefinitionNodeItem<?, ?> item) {
208       this.item = item;
209     }
210 
211     /**
212      * Get the definition node item that is targeted by the allowed-values
213      * constraints.
214      *
215      * @return the target node item
216      */
217     @NonNull
218     public IDefinitionNodeItem<?, ?> getItem() {
219       return item;
220     }
221 
222     /**
223      * Get the list of allowed-values constraint records targeting this node item.
224      *
225      * @return the list of allowed-values records
226      */
227     @NonNull
228     public List<AllowedValuesRecord> getAllowedValues() {
229       return Collections.unmodifiableList(allowedValues);
230     }
231 
232     /**
233      * Add an allowed-values constraint record to this node item.
234      *
235      * @param record
236      *          the allowed-values record to add
237      */
238     public void addAllowedValues(@NonNull AllowedValuesRecord record) {
239       this.allowedValues.add(record);
240     }
241   }
242 
243   /**
244    * A record capturing the relationship between an allowed-values constraint, the
245    * definition where it is declared, and the target node it applies to.
246    */
247   public static final class AllowedValuesRecord {
248     @NonNull
249     private final IAllowedValuesConstraint allowedValues;
250     @NonNull
251     private final IDefinitionNodeItem<?, ?> location;
252     @NonNull
253     private final IDefinitionNodeItem<?, ?> target;
254 
255     /**
256      * Construct a new allowed-values record.
257      *
258      * @param allowedValues
259      *          the allowed-values constraint
260      * @param location
261      *          the definition node item where the constraint is declared
262      * @param target
263      *          the definition node item that the constraint targets
264      */
265     public AllowedValuesRecord(
266         @NonNull IAllowedValuesConstraint allowedValues,
267         @NonNull IDefinitionNodeItem<?, ?> location,
268         @NonNull IDefinitionNodeItem<?, ?> target) {
269       this.allowedValues = allowedValues;
270       this.location = location;
271       this.target = target;
272     }
273 
274     /**
275      * Get the allowed-values constraint.
276      *
277      * @return the allowed-values constraint
278      */
279     @NonNull
280     public IAllowedValuesConstraint getAllowedValues() {
281       return allowedValues;
282     }
283 
284     /**
285      * Get the definition node item where the constraint is declared.
286      *
287      * @return the location node item
288      */
289     @NonNull
290     public IDefinitionNodeItem<?, ?> getLocation() {
291       return location;
292     }
293 
294     /**
295      * Get the definition node item that the constraint targets.
296      *
297      * @return the target node item
298      */
299     @NonNull
300     public IDefinitionNodeItem<?, ?> getTarget() {
301       return target;
302     }
303   }
304 }