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
10 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
21 package org.oran.smo.yangtools.parser.model.resolvers;
23 import java.util.Arrays;
24 import java.util.HashSet;
25 import java.util.List;
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;
43 * Resolves 'augment' statements that sit at the root of YAMs.
45 * @author Mark Hollmann
47 public abstract class AugmentResolver {
50 * Resolves all augments by placing these into the correct part of the statement tree.
51 * The DOM element tree is not modified.
53 public static void resolveAugments(final ParserExecutionContext context, final Schema schema) {
55 boolean atLeastOneResolved = true;
57 @SuppressWarnings("unchecked") final List<YAugment> allAugments = (List<YAugment>) Helper
58 .findStatementsAtModuleRootInSchema(CY.STMT_AUGMENT, schema);
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.
65 while (atLeastOneResolved && !allAugments.isEmpty()) {
67 atLeastOneResolved = false;
69 for (int i = 0; i < allAugments.size();) {
71 final YAugment augment = allAugments.get(i);
72 final boolean resolved = resolveAugment(context, augment, schema);
74 * If the augments was resolved we remove it from the list - otherwise move to
78 atLeastOneResolved = true;
79 allAugments.remove(i);
83 } catch (final Exception ex) {
84 // Swallow and move to next. Best effort here, keep trying other augments.
91 * If after all that there are still 'augment' statement left, then these are not resolvable.
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."));
101 * These are the statements that may be the target of an augmentation.
103 * See RFC 7950, chapter 7.17
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));
110 * These are the statements that can sit under the augment that we will merge-in under the target of the augmentation.
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));
116 private static boolean resolveAugment(final ParserExecutionContext context, final YAugment augment,
117 final Schema schema) {
119 final String augmentTargetNode = augment.getAugmentTargetNode();
120 if (augmentTargetNode.isEmpty()) {
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
130 final AbstractStatement augmentedStatement = findTargetSchemaNode(context, augment, schema);
131 if (augmentedStatement == null) {
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.
141 * Check that the target (schema node) of the augments can actually be augmented.
143 if (!ALLOWABLE_TARGETS_OF_AUGMENT.contains(augmentedStatement.getStatementModuleAndName())) {
144 final String allowableTargetsAsString = StringHelper.toString(ALLOWABLE_TARGETS_OF_AUGMENT, "[", "]", ", ", "'",
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 + ")."));
150 * We return TRUE to pretend that we handled the augment as it makes no sense
151 * re-trying it over and over again.
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:
164 * augment /abc:cont1/foo-choice {
165 * container bar { ... }
172 * choice foo-choice {
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:
184 * augment /abc:cont1/foo-choice {
186 * container bar { ... }
190 if (augmentedStatement.is(CY.STMT_CHOICE)) {
191 SchemaProcessor.injectCaseForShorthandedStatements(augment);
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.
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".
204 final List<AbstractStatement> statementsToMoveUnderTargetNode = augment.getChildren(
205 STATEMENTS_UNDER_AUGMENT_TO_HANDLE);
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
212 final List<String> allowedOptionalChildren = CY.getOptionalMultipleChildren(augmentedStatement.getStatementName());
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."));
220 * We return TRUE to pretend that we handled the augment as it makes no sense
221 * re-trying it over and over again.
228 * 'when' and 'if-feature' must be cloned as well.
230 handleWhenAndIfFeature(augment, statementsToMoveUnderTargetNode);
233 * Also inherit down the status if needs be.
235 handleStatus(augment, statementsToMoveUnderTargetNode);
238 * Mark the children as having been augmented-in.
240 for (final AbstractStatement oneStatement : statementsToMoveUnderTargetNode) {
241 addAugmentingReference(oneStatement, augment);
242 addAugmentAppData(oneStatement, "statement augmented-in by 'augment' in " + StringHelper.getModuleLineString(
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.
251 augmentedStatement.addChildren(statementsToMoveUnderTargetNode);
257 * Handles any 'when' or 'if-feature' under the augment.
259 private static void handleWhenAndIfFeature(final YAugment augment,
260 final List<AbstractStatement> statementsToMoveUnderTargetNode) {
262 * If the 'augment' has a 'when' clause this gets cloned to all statements within
263 * the 'augment'. For example:
265 * Augmented module XYZ:
266 * =====================
269 * leaf bar { type string; }
276 * when "bar = 'Hello!'";
278 * leaf rock { type int32; }
279 * leaf roll { type int16; }
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:
286 * Augmented module XYZ:
287 * =====================
290 * leaf bar { type string; }
292 * when "bar = 'Hello!'";
297 * when "bar = 'Hello!'";
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.
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.
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.)
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));
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).
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));
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'.
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
358 private static void handleStatus(final YAugment augment,
359 final List<AbstractStatement> statementsToMoveUnderTargetNode) {
361 final YStatus statusUnderAugment = augment.getStatus();
362 if (statusUnderAugment == null) {
366 for (final AbstractStatement oneStatement : statementsToMoveUnderTargetNode) {
368 final YStatus childExplicitStatus = oneStatement.getChild(CY.STMT_STATUS);
369 boolean clone = true;
371 if (childExplicitStatus == null) {
373 * There is no 'status' statement under the child, so then we will simply
374 * clone down the parent 'status' in a moment.
376 } else if (childExplicitStatus.getStatusOrder() >= statusUnderAugment.getStatusOrder()) {
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.
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').
390 * Must clone, so first remove the 'status' statement under the child (if it exists).
392 if (childExplicitStatus != null) {
393 oneStatement.removeChild(childExplicitStatus);
396 * Now clone down the parent's (the augment's) 'status' into the child.
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));
407 private static AbstractStatement findTargetSchemaNode(final ParserExecutionContext context, final YAugment augment,
408 final Schema schema) {
410 final AbstractStatement targetSchemaNode = Helper.findSchemaNode(context, augment, augment.getAugmentTargetNode(),
412 if (targetSchemaNode == null) {
417 * Check whether the 'augment' and the target node sit inside the very same module. Poor modeling.
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."));
424 return targetSchemaNode;
427 private static final String AUGMENTED_IN_REFERENCE = "AUGMENTED_IN_REFERENCE";
429 private static void addAugmentingReference(final AbstractStatement statementAugmentedIn, final YAugment origAugment) {
430 Helper.addAppDataListInfo(statementAugmentedIn, AUGMENTED_IN_REFERENCE, origAugment);
433 public static List<YAugment> getAugmentingReference(final AbstractStatement augmentedStatement) {
434 return Helper.getAppDataListInfo(augmentedStatement, AUGMENTED_IN_REFERENCE);
437 private static final String AUGMENTED_IN_INFO = "AUGMENTED_IN_INFO";
439 private static void addAugmentAppData(final AbstractStatement statement, final String info) {
440 Helper.addAppDataListInfo(statement, AUGMENTED_IN_INFO, info);
443 public static List<String> getAugmentedInInfosForStatement(final AbstractStatement statement) {
444 return Helper.getAppDataListInfo(statement, AUGMENTED_IN_INFO);