TEIV: yang-parser updates
[smo/teiv.git] / yang-parser / src / main / java / org / oran / smo / yangtools / parser / model / resolvers / AugmentResolver.java
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2024 Ericsson
4  *  Modifications Copyright (C) 2024 OpenInfra Foundation Europe
5  *  ================================================================================
6  *  Licensed under the Apache License, Version 2.0 (the "License");
7  *  you may not use this file except in compliance with the License.
8  *  You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21 package org.oran.smo.yangtools.parser.model.resolvers;
22
23 import java.util.Arrays;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Set;
27
28 import org.oran.smo.yangtools.parser.ParserExecutionContext;
29 import org.oran.smo.yangtools.parser.findings.Finding;
30 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
31 import org.oran.smo.yangtools.parser.model.schema.Schema;
32 import org.oran.smo.yangtools.parser.model.schema.SchemaProcessor;
33 import org.oran.smo.yangtools.parser.model.statements.AbstractStatement;
34 import org.oran.smo.yangtools.parser.model.statements.StatementModuleAndName;
35 import org.oran.smo.yangtools.parser.model.statements.yang.CY;
36 import org.oran.smo.yangtools.parser.model.statements.yang.YAugment;
37 import org.oran.smo.yangtools.parser.model.statements.yang.YIfFeature;
38 import org.oran.smo.yangtools.parser.model.statements.yang.YStatus;
39 import org.oran.smo.yangtools.parser.model.statements.yang.YWhen;
40 import org.oran.smo.yangtools.parser.model.util.StringHelper;
41
42 /**
43  * Resolves 'augment' statements that sit at the root of YAMs.
44  *
45  * @author Mark Hollmann
46  */
47 public abstract class AugmentResolver {
48
49     /**
50      * Resolves all augments by placing these into the correct part of the statement tree.
51      * The DOM element tree is not modified.
52      */
53     public static void resolveAugments(final ParserExecutionContext context, final Schema schema) {
54
55         boolean atLeastOneResolved = true;
56
57         @SuppressWarnings("unchecked") final List<YAugment> allAugments = (List<YAugment>) Helper
58                 .findStatementsAtModuleRootInSchema(CY.STMT_AUGMENT, schema);
59
60         /*
61          * There are edge cases where an 'augment' refers to data nodes part of yet another 'augment'.
62          * This means that the resolution of an augment will fail unless another augment is resolved
63          * first. We handle this below by simply retrying.
64          */
65         while (atLeastOneResolved && !allAugments.isEmpty()) {
66
67             atLeastOneResolved = false;
68
69             for (int i = 0; i < allAugments.size();) {
70                 try {
71                     final YAugment augment = allAugments.get(i);
72                     final boolean resolved = resolveAugment(context, augment, schema);
73                     /*
74                      * If the augments was resolved we remove it from the list - otherwise move to
75                      * the next one.
76                      */
77                     if (resolved) {
78                         atLeastOneResolved = true;
79                         allAugments.remove(i);
80                     } else {
81                         i++;
82                     }
83                 } catch (final Exception ex) {
84                     // Swallow and move to next. Best effort here, keep trying other augments.
85                     i++;
86                 }
87             }
88         }
89
90         /*
91          * If after all that there are still 'augment' statement left, then these are not resolvable.
92          */
93         for (final YAugment augment : allAugments) {
94             context.addFinding(new Finding(augment, ParserFindingType.P054_UNRESOLVABLE_PATH,
95                     "Path to schema node '" + augment
96                             .getAugmentTargetNode() + "', part of 'augment' statement, cannot be resolved."));
97         }
98     }
99
100     /**
101      * These are the statements that may be the target of an augmentation.
102      * <p>
103      * See RFC 7950, chapter 7.17
104      */
105     private static final Set<StatementModuleAndName> ALLOWABLE_TARGETS_OF_AUGMENT = new HashSet<>(Arrays.asList(
106             CY.STMT_CONTAINER, CY.STMT_LIST, CY.STMT_CHOICE, CY.STMT_CASE, CY.STMT_INPUT, CY.STMT_OUTPUT,
107             CY.STMT_NOTIFICATION));
108
109     /**
110      * These are the statements that can sit under the augment that we will merge-in under the target of the augmentation.
111      */
112     private static final Set<StatementModuleAndName> STATEMENTS_UNDER_AUGMENT_TO_HANDLE = new HashSet<>(Arrays.asList(
113             CY.STMT_ACTION, CY.STMT_ANYDATA, CY.STMT_ANYXML, CY.STMT_CASE, CY.STMT_CHOICE, CY.STMT_CONTAINER,
114             CY.STMT_LEAF_LIST, CY.STMT_LEAF, CY.STMT_LIST, CY.STMT_NOTIFICATION));
115
116     private static boolean resolveAugment(final ParserExecutionContext context, final YAugment augment,
117             final Schema schema) {
118
119         final String augmentTargetNode = augment.getAugmentTargetNode();
120         if (augmentTargetNode.isEmpty()) {
121             /*
122              * Pointless trying to resolve the path. No point issuing a finding either, a
123              * P015_INVALID_SYNTAX_IN_DOCUMENT would have been issued already. We return TRUE
124              * to pretend that we handled the augment as it makes no sense re-trying it over
125              * and over again.
126              */
127             return true;
128         }
129
130         final AbstractStatement augmentedStatement = findTargetSchemaNode(context, augment, schema);
131         if (augmentedStatement == null) {
132             /*
133              * Possibly not found because the schema node is missing since it will be
134              * merged-in by another augment statement that will be processed later on.
135              * So delay processing.
136              */
137             return false;
138         }
139
140         /*
141          * Check that the target (schema node) of the augments can actually be augmented.
142          */
143         if (!ALLOWABLE_TARGETS_OF_AUGMENT.contains(augmentedStatement.getStatementModuleAndName())) {
144             final String allowableTargetsAsString = StringHelper.toString(ALLOWABLE_TARGETS_OF_AUGMENT, "[", "]", ", ", "'",
145                     "'");
146             context.addFinding(new Finding(augment, ParserFindingType.P151_TARGET_NODE_CANNOT_BE_AUGMENTED,
147                     "Statement '" + augmentedStatement.getStatementModuleAndName() + "' pointed to by '" + augment
148                             .getAugmentTargetNode() + "' cannot be augmented (only statements " + allowableTargetsAsString + ")."));
149             /*
150              * We return TRUE to pretend that we handled the augment as it makes no sense
151              * re-trying it over and over again.
152              */
153             return true;
154         }
155
156         /*
157          * Special handling: if the target of the augmentation is a choice statement, then the RFC
158          * allows the shorthand notation to be used (i.e. not to augment with a 'case' but some
159          * other data node). Example:
160          *
161          * Augmenting module:
162          * ==================
163          *
164          * augment /abc:cont1/foo-choice {
165          *   container bar { ... }
166          * }
167          *
168          * Augmented module:
169          * =================
170          *
171          * container cont1 {
172          *   choice foo-choice {
173          *     ...
174          *   }
175          * }
176          *
177          * In the example, we would not want to directly place the container under the 'choice', as this
178          * may cause problems elsewhere - instead, we inject a 'case' statement, so we will end up with
179          * this here in the end which is cleaner:
180          *
181          * Augmenting module:
182          * ==================
183          *
184          * augment /abc:cont1/foo-choice {
185          *   case bar {
186          *     container bar { ... }
187          *   }
188          * }
189          */
190         if (augmentedStatement.is(CY.STMT_CHOICE)) {
191             SchemaProcessor.injectCaseForShorthandedStatements(augment);
192         }
193
194         /*
195          * Collect all the statements that will be moved under the augment's target node in a moment
196          * and mark these as having been augmented-in.
197          *
198          * Note: Any extensions sitting directly under the 'augment' statement are considered to
199          * relate to the 'augment' itself, i.e. will NOT be moved under the target of the augments. The
200          * RFC does not mention extensions at all when it comes to 'augments' (not even that is it
201          * 'undefined') so the assumption here is that extensions can only ever be "added" to other
202          * statements by using a "deviate add".
203          */
204         final List<AbstractStatement> statementsToMoveUnderTargetNode = augment.getChildren(
205                 STATEMENTS_UNDER_AUGMENT_TO_HANDLE);
206
207         /*
208          * Check that whatever sits under the 'augment' statement is actually allowed under the
209          * target of the augment. We can use the CY class for this that gets us the allowed optional
210          * children.
211          */
212         final List<String> allowedOptionalChildren = CY.getOptionalMultipleChildren(augmentedStatement.getStatementName());
213
214         for (final AbstractStatement statementToMove : statementsToMoveUnderTargetNode) {
215             if (!allowedOptionalChildren.contains(statementToMove.getStatementName())) {
216                 context.addFinding(new Finding(statementToMove, ParserFindingType.P151_TARGET_NODE_CANNOT_BE_AUGMENTED,
217                         "Statement '" + statementToMove.getStatementName() + "' is not allowed under '" + augmentedStatement
218                                 .getStatementName() + "' and therefore cannot be augmented-in."));
219                 /*
220                  * We return TRUE to pretend that we handled the augment as it makes no sense
221                  * re-trying it over and over again.
222                  */
223                 return true;
224             }
225         }
226
227         /*
228          * 'when' and 'if-feature' must be cloned as well.
229          */
230         handleWhenAndIfFeature(augment, statementsToMoveUnderTargetNode);
231
232         /*
233          * Also inherit down the status if needs be.
234          */
235         handleStatus(augment, statementsToMoveUnderTargetNode);
236
237         /*
238          * Mark the children as having been augmented-in.
239          */
240         for (final AbstractStatement oneStatement : statementsToMoveUnderTargetNode) {
241             addAugmentingReference(oneStatement, augment);
242             addAugmentAppData(oneStatement, "statement augmented-in by 'augment' in " + StringHelper.getModuleLineString(
243                     augment));
244         }
245
246         /*
247          * Now take all of the statements that are under the augment statement and re-parent
248          * them under the target schema node of the augment. They retain their own namespace
249          * and prefix resolver. The DOM element tree is not modified.
250          */
251         augmentedStatement.addChildren(statementsToMoveUnderTargetNode);
252
253         return true;
254     }
255
256     /**
257      * Handles any 'when' or 'if-feature' under the augment.
258      */
259     private static void handleWhenAndIfFeature(final YAugment augment,
260             final List<AbstractStatement> statementsToMoveUnderTargetNode) {
261         /*
262          * If the 'augment' has a 'when' clause this gets cloned to all statements within
263          * the 'augment'. For example:
264          *
265          * Augmented module XYZ:
266          * =====================
267          *
268          * container foo {
269          *   leaf bar { type string; }
270          * }
271          *
272          * Augmenting module
273          * ==================
274          *
275          * augment /xyz:foo {
276          *   when "bar = 'Hello!'";
277          *
278          *   leaf rock { type int32; }
279          *   leaf roll { type int16; }
280          * }
281          *
282          * We can't just drop the 'when' statement, it must be retained in order to make the
283          * "rock" and "roll" leafs conditional. We want to be ending up with this here after
284          * the augments has been resolved:
285          *
286          * Augmented module XYZ:
287          * =====================
288          *
289          * container foo {
290          *   leaf bar { type string; }
291          *   leaf rock {
292          *     when "bar = 'Hello!'";
293          *     type int32;
294          *   }
295          *   leaf roll {
296          *     type int16;
297          *     when "bar = 'Hello!'";
298          *   }
299          * }
300          *
301          * Of course the issue now is that the 'when' statement inside the 'augment' refers to
302          * the augment's target node, which is the container "foo". When we clone the 'when'
303          * statement, we can only clone it to the *child* of the target node (here, the leafs).
304          * The path inside the 'when' statement is now wrong - it applies to the parent of the
305          * leafs. This is the reason why "appliesToParentSchemaNode" exists inside the YWhen class.
306          *
307          * So why not simply update the path of the 'when' statement? Because it is really hard
308          * to do that. In this example here, it would be easy - simply place a "../" in front of
309          * the path. However, the path could be really complex, involving multi-level navigation,
310          * predicates, etc., and to correctly clean that up is a real challenge.
311          *
312          * Also note that the "when" statement is *added* to the child statement of the augment,
313          * not *replaced*. That's a bit of a hack, as YANG only allows for a single occurrence
314          * of 'when' for statements. However, it could conceivably be the case that each of the
315          * statements amended thus has itself already a 'when' statement - and that must be
316          * retained, so using a *replace* would be wrong. (And that's the reason why various
317          * type-safe statement classes return 0..n 'when' statements, as opposed to 0..1.)
318          */
319         final YWhen whenUnderAugment = augment.getWhen();
320         if (whenUnderAugment != null) {
321             for (final AbstractStatement oneStatement : statementsToMoveUnderTargetNode) {
322                 final YWhen clonedWhen = new YWhen(oneStatement, whenUnderAugment.getDomElement());
323                 clonedWhen.cloneFrom(whenUnderAugment);
324                 clonedWhen.setAppliesToParentSchemaNode();
325                 addAugmentAppData(clonedWhen,
326                         "This 'when' statement has been inherited from the 'when' statement that sits under the augment in " + StringHelper
327                                 .getModuleLineString(whenUnderAugment));
328             }
329         }
330
331         /*
332          * If the 'augment' has one or multiple 'if-feature' statements then these will be cloned
333          * as well, similar to how this is done above. Only we don't have to worry about a path
334          * here, we can simply clone the if-feature(s).
335          */
336         for (final YIfFeature ifFeature : augment.getIfFeatures()) {
337             for (final AbstractStatement oneStatement : statementsToMoveUnderTargetNode) {
338                 final YIfFeature clonedIfFeature = new YIfFeature(oneStatement, ifFeature.getDomElement());
339                 clonedIfFeature.cloneFrom(ifFeature);
340                 addAugmentAppData(clonedIfFeature,
341                         "This 'if-feature' statement has been inherited from the 'if-feature' statement that sits under the augment in " + StringHelper
342                                 .getModuleLineString(ifFeature));
343             }
344         }
345     }
346
347     /**
348      * Handle a possible 'status' statement under the 'augment'. We must do this here during the merge of
349      * the 'augment' content, as the 'augment' and its child 'status' will disappear from the schema tree,
350      * hence the 'status' will be lost. To retain the information, we must clone the 'status' statement
351      * into the contents of the 'augment'.
352      *
353      * Note there is some special handling - if the status is more restrictive under the augmented
354      * statement then this would not be replaced. For example, if the status is DEPRECATED under the
355      * augment, but it is explicitly OBSOLETE under a container being a child of the augment, this would
356      * not be updated.
357      */
358     private static void handleStatus(final YAugment augment,
359             final List<AbstractStatement> statementsToMoveUnderTargetNode) {
360
361         final YStatus statusUnderAugment = augment.getStatus();
362         if (statusUnderAugment == null) {
363             return;
364         }
365
366         for (final AbstractStatement oneStatement : statementsToMoveUnderTargetNode) {
367
368             final YStatus childExplicitStatus = oneStatement.getChild(CY.STMT_STATUS);
369             boolean clone = true;
370
371             if (childExplicitStatus == null) {
372                 /*
373                  * There is no 'status' statement under the child, so then we will simply
374                  * clone down the parent 'status' in a moment.
375                  */
376             } else if (childExplicitStatus.getStatusOrder() >= statusUnderAugment.getStatusOrder()) {
377                 /*
378                  * There is an explicit 'status' statement under the child. If the child 'status'
379                  * is more restrictive, or the same, as the 'status' of the parent we don't have
380                  * to do anything, i.e. don't clone.
381                  *
382                  * For example, child is DEPRECATED, parent is CURRENT - hence child is more
383                  * restrictive, so don't overwrite the 'status' (don't clone the parent 'status').
384                  */
385                 clone = false;
386             }
387
388             if (clone) {
389                 /*
390                  * Must clone, so first remove the 'status' statement under the child (if it exists).
391                  */
392                 if (childExplicitStatus != null) {
393                     oneStatement.removeChild(childExplicitStatus);
394                 }
395                 /*
396                  * Now clone down the parent's (the augment's) 'status' into the child.
397                  */
398                 final YStatus clonedStatus = new YStatus(oneStatement, statusUnderAugment.getDomElement());
399                 clonedStatus.cloneFrom(statusUnderAugment);
400                 addAugmentAppData(clonedStatus,
401                         "This 'status' statement has been inherited from the 'status' statement that sits under the augment in " + StringHelper
402                                 .getModuleLineString(statusUnderAugment));
403             }
404         }
405     }
406
407     private static AbstractStatement findTargetSchemaNode(final ParserExecutionContext context, final YAugment augment,
408             final Schema schema) {
409
410         final AbstractStatement targetSchemaNode = Helper.findSchemaNode(context, augment, augment.getAugmentTargetNode(),
411                 schema);
412         if (targetSchemaNode == null) {
413             return null;
414         }
415
416         /*
417          * Check whether the 'augment' and the target node sit inside the very same module. Poor modeling.
418          */
419         if (augment.getDomElement().getYangModel() == targetSchemaNode.getDomElement().getYangModel()) {
420             context.addFinding(new Finding(augment, ParserFindingType.P152_AUGMENT_TARGET_NODE_IN_SAME_MODULE,
421                     "Both 'augment' and it's target node sit in the same (sub-)module."));
422         }
423
424         return targetSchemaNode;
425     }
426
427     private static final String AUGMENTED_IN_REFERENCE = "AUGMENTED_IN_REFERENCE";
428
429     private static void addAugmentingReference(final AbstractStatement statementAugmentedIn, final YAugment origAugment) {
430         Helper.addAppDataListInfo(statementAugmentedIn, AUGMENTED_IN_REFERENCE, origAugment);
431     }
432
433     public static List<YAugment> getAugmentingReference(final AbstractStatement augmentedStatement) {
434         return Helper.getAppDataListInfo(augmentedStatement, AUGMENTED_IN_REFERENCE);
435     }
436
437     private static final String AUGMENTED_IN_INFO = "AUGMENTED_IN_INFO";
438
439     private static void addAugmentAppData(final AbstractStatement statement, final String info) {
440         Helper.addAppDataListInfo(statement, AUGMENTED_IN_INFO, info);
441     }
442
443     public static List<String> getAugmentedInInfosForStatement(final AbstractStatement statement) {
444         return Helper.getAppDataListInfo(statement, AUGMENTED_IN_INFO);
445     }
446 }