bc12e1b62be58b3d0781ad19bbba36cfba0207a3
[smo/teiv.git] /
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.data.instance;
22
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.Optional;
30 import java.util.stream.Collectors;
31
32 import org.oran.smo.yangtools.parser.data.YangData;
33 import org.oran.smo.yangtools.parser.data.dom.YangDataDomDocumentRoot.SourceDataType;
34 import org.oran.smo.yangtools.parser.data.dom.YangDataDomNode;
35 import org.oran.smo.yangtools.parser.findings.Finding;
36 import org.oran.smo.yangtools.parser.findings.FindingsManager;
37 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
38 import org.oran.smo.yangtools.parser.model.YangModel;
39 import org.oran.smo.yangtools.parser.model.resolvers.Helper;
40 import org.oran.smo.yangtools.parser.model.schema.ModuleRegistry;
41 import org.oran.smo.yangtools.parser.model.statements.AbstractStatement;
42 import org.oran.smo.yangtools.parser.model.statements.yang.CY;
43 import org.oran.smo.yangtools.parser.model.statements.yang.YList;
44 import org.oran.smo.yangtools.parser.model.statements.yang.YType;
45 import org.oran.smo.yangtools.parser.model.util.DataTypeHelper;
46 import org.oran.smo.yangtools.parser.model.util.GrammarHelper;
47 import org.oran.smo.yangtools.parser.yanglibrary.IetfYangLibraryParser;
48
49 /**
50  * Builds a type-safe Yang instance data tree from Yang data DOM trees.
51  *
52  * @author Mark Hollmann
53  */
54 public class InstanceDataTreeBuilder {
55
56     /**
57      * Given a number of data DOM trees, merges these together and forms a (single) tree with type-safe
58      * Yang instance data.
59      * <p/>
60      * This class requires the underlying Yang Model to be available, i.e. cannot be used with data only.
61      * <p/>
62      * If the input data was in JSON, module-name -&gt; namespace resolution must have been performed on
63      * the data first.
64      */
65     public static RootInstance buildCombinedDataTree(final FindingsManager findingsManager, final List<YangData> yangDatas,
66             final ModuleRegistry moduleRegistry, final DataTreeBuilderPredicate topLevelInstancePredicate) {
67
68         /*
69          * In a first step, the instance tree is build for each data file. Once this has been done the
70          * trees will be merged together.
71          */
72         final List<RootInstance> rootInstances = new ArrayList<>();
73
74         for (final YangData yangData : yangDatas) {
75
76             if (yangData.getYangDataDomDocumentRoot() == null) {
77                 continue;
78             }
79
80             if (containsYangLibraryInstanceOnly(yangData) && !yangLibraryModelPresent(moduleRegistry)) {
81                 /*
82                  * In case the data input only contains the data for the yang library, but the yang library
83                  * module itself was not part of the model inputs, we will not attempt to generate the data
84                  * instance tree, as this would fail - and the yang library is used as in effect BOM.
85                  */
86             } else {
87                 final RootInstance rootInstance = new RootInstance();
88                 /*
89                  * Schema root is handled slightly different from child-handling further down the tree...
90                  */
91                 final List<AbstractStatement> allDataNodesAtTopLevel = getAllDataNodesAndChoiceAtTopLevel(moduleRegistry);
92                 for (final YangDataDomNode dataDomNode : yangData.getYangDataDomDocumentRoot().getChildren()) {
93                     if (topLevelInstancePredicate.test(dataDomNode)) {
94                         processDomNode(findingsManager, dataDomNode, rootInstance, allDataNodesAtTopLevel);
95                     }
96                 }
97
98                 rootInstances.add(rootInstance);
99             }
100         }
101
102         /*
103          * Now all of the trees are merged together. This is a "smart" merge - the contents of the containers
104          * and lists are merged together; where content already exists and it is of the same value no finding
105          * will be issued.
106          */
107         final RootInstance result = new RootInstance();
108         for (final RootInstance rootInstance : rootInstances) {
109             mergeInDataTree(findingsManager, result, rootInstance);
110         }
111
112         return result;
113     }
114
115     public static List<AbstractStatement> getAllDataNodesAndChoiceAtTopLevel(final ModuleRegistry moduleRegistry) {
116
117         final List<AbstractStatement> result = new ArrayList<>();
118
119         for (final YangModel yangModelFile : moduleRegistry.getAllYangModels()) {
120             if (yangModelFile.getYangModelRoot().isModule()) {
121                 result.addAll(getAllDataNodesAndChoiceUnderStatement(yangModelFile.getYangModelRoot().getModule()));
122             }
123         }
124
125         return result;
126     }
127
128     private static List<AbstractStatement> getAllDataNodesAndChoiceUnderStatement(final AbstractStatement statement) {
129         return statement.getChildStatements().stream().filter(child -> child.definesDataNode() || child.is(CY.STMT_CHOICE))
130                 .collect(Collectors.toList());
131     }
132
133     /**
134      * Returns whether this input only contains the YANG instance data.
135      */
136     private static boolean containsYangLibraryInstanceOnly(final YangData yangData) {
137
138         if (yangData.getYangDataDomDocumentRoot() == null) {
139             return false;
140         }
141
142         final List<YangDataDomNode> childrenUnderRoot = yangData.getYangDataDomDocumentRoot().getChildren();
143         if (childrenUnderRoot.size() != 1) {
144             return false;
145         }
146
147         final YangDataDomNode yangDataDomNode = childrenUnderRoot.get(0);
148
149         if (!IetfYangLibraryParser.IETF_YANG_LIBRARY_NAMESPACE.equals(yangDataDomNode.getNamespace())) {
150             return false;
151         }
152
153         return IetfYangLibraryParser.YANG_LIBRARY_MODULES_STATE.equals(yangDataDomNode
154                 .getName()) || IetfYangLibraryParser.YANG_LIBRARY_YANG_LIBRARY.equals(yangDataDomNode.getName());
155     }
156
157     /**
158      * Returns whether any of the YANG library containers exist in the data nodes tree - so basically
159      * if the yang-library model was in the input.
160      */
161     private static boolean yangLibraryModelPresent(final ModuleRegistry moduleRegistry) {
162
163         final List<AbstractStatement> allDataNodesAtTopLevel = getAllDataNodesAndChoiceAtTopLevel(moduleRegistry);
164
165         final Optional<AbstractStatement> YangLibContainer = allDataNodesAtTopLevel.stream().filter(statement -> statement
166                 .is(CY.STMT_CONTAINER)).filter(statement -> IetfYangLibraryParser.IETF_YANG_LIBRARY_NAMESPACE.equals(
167                         statement.getEffectiveNamespace())).filter(statement -> {
168                             final String containerName = statement.getStatementIdentifier();
169                             return IetfYangLibraryParser.YANG_LIBRARY_MODULES_STATE.equals(
170                                     containerName) || IetfYangLibraryParser.YANG_LIBRARY_YANG_LIBRARY.equals(containerName);
171                         }).findFirst();
172
173         return YangLibContainer.isPresent();
174     }
175
176     private static void processDomNode(final FindingsManager findingsManager, final YangDataDomNode dataDomNode,
177             final AbstractStructureInstance parentInstance, final List<AbstractStatement> candidateDataNodes) {
178
179         final AbstractStatement matchingDataNode = Helper.findSchemaDataNode(candidateDataNodes, dataDomNode.getNamespace(),
180                 dataDomNode.getName());
181         if (matchingDataNode == null) {
182             /*
183              * Well possible that the prefix is wrong / missing on the XML element, and hence the
184              * namespace of the DOM node is wrong and can't be found. Check if a schema node with
185              * the same name exists to give the user better feedback.
186              */
187             final AbstractStatement childWithSameName = Helper.findSchemaDataNode(candidateDataNodes, dataDomNode
188                     .getName());
189
190             if (childWithSameName != null) {
191                 findingsManager.addFinding(new Finding(dataDomNode,
192                         ParserFindingType.P075_CORRESPONDING_SCHEMA_NODE_NOT_FOUND.toString(),
193                         "No corresponding schema node was found in the model for data instance '" + dataDomNode
194                                 .getPath() + "' in namespace '" + dataDomNode
195                                         .getNamespace() + "', but there exists a schema node with the same name in namespace '" + childWithSameName
196                                                 .getEffectiveNamespace() + "'. Adjust namespace of the data instance."));
197             } else {
198                 findingsManager.addFinding(new Finding(dataDomNode,
199                         ParserFindingType.P075_CORRESPONDING_SCHEMA_NODE_NOT_FOUND.toString(),
200                         "No corresponding schema node was found in the model for data instance '" + dataDomNode
201                                 .getPath() + "' (ns='" + dataDomNode.getNamespace() + "')."));
202             }
203             return;
204         }
205
206         if (matchingDataNode.is(CY.STMT_CONTAINER)) {
207             processContainer(findingsManager, dataDomNode, parentInstance, matchingDataNode);
208         } else if (matchingDataNode.is(CY.STMT_LEAF)) {
209             processLeaf(findingsManager, dataDomNode, parentInstance, matchingDataNode);
210         } else if (matchingDataNode.is(CY.STMT_LEAF_LIST)) {
211             processLeafList(findingsManager, dataDomNode, parentInstance, matchingDataNode);
212         } else if (matchingDataNode.is(CY.STMT_LIST)) {
213             processList(findingsManager, dataDomNode, parentInstance, matchingDataNode);
214         } else if (matchingDataNode.is(CY.STMT_ANYXML)) {
215             processAnyxml(findingsManager, dataDomNode, parentInstance, matchingDataNode);
216         } else if (matchingDataNode.is(CY.STMT_ANYDATA)) {
217             processAnydata(findingsManager, dataDomNode, parentInstance, matchingDataNode);
218         }
219     }
220
221     private static void processContainer(final FindingsManager findingsManager, final YangDataDomNode dataDomNode,
222             final AbstractStructureInstance parentInstance, final AbstractStatement container) {
223
224         final ContainerInstance containerInstance = new ContainerInstance(container, dataDomNode, parentInstance);
225         if (parentInstance.hasContainerInstance(containerInstance.getNamespace(), containerInstance.getName())) {
226             findingsManager.addFinding(new Finding(dataDomNode, ParserFindingType.P076_DUPLICATE_INSTANCE_DATA.toString(),
227                     "Container '" + dataDomNode.getPath() + "' already defined in this input."));
228             return;
229         }
230
231         parentInstance.addStructureChild(containerInstance);
232
233         final List<AbstractStatement> allDataNodesUnderContainerStatement = getAllDataNodesAndChoiceUnderStatement(
234                 container);
235         for (final YangDataDomNode childDomNode : dataDomNode.getChildren()) {
236             processDomNode(findingsManager, childDomNode, containerInstance, allDataNodesUnderContainerStatement);
237         }
238     }
239
240     private static void processLeaf(final FindingsManager findingsManager, final YangDataDomNode domNode,
241             final AbstractStructureInstance parentInstance, final AbstractStatement leaf) {
242
243         Object leafValue = domNode.getValue();
244         if (leafValue == null && domNode.getSourceDataType() == SourceDataType.JSON) {
245             leafValue = adjustNullValueForEmpty(leaf);
246         }
247         if (leafValue == null) {
248             findingsManager.addFinding(new Finding(domNode, ParserFindingType.P080_NULL_VALUE.toString(), "Leaf '" + domNode
249                     .getPath() + "' does not have a value."));
250             return;
251         }
252
253         final LeafInstance leafInstance = new LeafInstance(leaf, domNode, parentInstance, leafValue);
254         if (parentInstance.hasLeafInstance(leafInstance.getNamespace(), leafInstance.getName())) {
255             findingsManager.addFinding(new Finding(domNode, ParserFindingType.P076_DUPLICATE_INSTANCE_DATA.toString(),
256                     "Leaf '" + domNode.getPath() + "' already defined in this input."));
257             return;
258         }
259
260         parentInstance.addContentChild(leafInstance);
261     }
262
263     private static void processAnydata(final FindingsManager findingsManager, final YangDataDomNode domNode,
264             final AbstractStructureInstance parentInstance, final AbstractStatement schemaLeaf) {
265
266         final String nodeValue = domNode.getReassembledChildren();
267
268         final AnyDataInstance anyDataInstance = new AnyDataInstance(schemaLeaf, domNode, parentInstance, nodeValue);
269         if (parentInstance.hasAnyDataInstance(anyDataInstance.getNamespace(), anyDataInstance.getName())) {
270             findingsManager.addFinding(new Finding(domNode, ParserFindingType.P076_DUPLICATE_INSTANCE_DATA.toString(),
271                     "Anydata '" + domNode.getPath() + "' already defined in this input."));
272             return;
273         }
274
275         parentInstance.addContentChild(anyDataInstance);
276     }
277
278     private static void processAnyxml(final FindingsManager findingsManager, final YangDataDomNode domNode,
279             final AbstractStructureInstance parentInstance, final AbstractStatement schemaLeaf) {
280
281         final String nodeValue = domNode.getReassembledChildren();
282
283         final AnyXmlInstance anyXmlInstance = new AnyXmlInstance(schemaLeaf, domNode, parentInstance, nodeValue);
284         if (parentInstance.hasAnyXmlInstance(anyXmlInstance.getNamespace(), anyXmlInstance.getName())) {
285             findingsManager.addFinding(new Finding(domNode, ParserFindingType.P076_DUPLICATE_INSTANCE_DATA.toString(),
286                     "Anyxml '" + domNode.getPath() + "' already defined in this input."));
287             return;
288         }
289
290         parentInstance.addContentChild(anyXmlInstance);
291     }
292
293     private static void processLeafList(final FindingsManager findingsManager, final YangDataDomNode domNode,
294             final AbstractStructureInstance parentInstance, final AbstractStatement leafList) {
295
296         Object leafListValue = domNode.getValue();
297         if (leafListValue == null && domNode.getSourceDataType() == SourceDataType.JSON) {
298             leafListValue = adjustNullValueForEmpty(leafList);
299         }
300         if (leafListValue == null) {
301             findingsManager.addFinding(new Finding(domNode, ParserFindingType.P080_NULL_VALUE.toString(),
302                     "Leaf-list '" + domNode.getPath() + "' does not have a value."));
303             return;
304         }
305
306         final LeafListInstance leafListInstance = new LeafListInstance(leafList, domNode, parentInstance, leafListValue);
307
308         /*
309          * leaf-list is a bit different. The RFC states that values have to be unique in config data, so we need to check for that.
310          */
311         if (leafList.isEffectiveConfigTrue()) {
312             if (parentInstance.hasLeafListInstance(leafListInstance.getNamespace(), leafListInstance.getName(),
313                     leafListInstance.getValue())) {
314                 findingsManager.addFinding(new Finding(domNode, ParserFindingType.P073_LEAF_VALUE_ALREADY_SET.toString(),
315                         "'config true' leaf-list '" + domNode
316                                 .getPath() + "' instance with value '" + leafListValue + "' already defined in this input."));
317                 return;
318             }
319         }
320
321         parentInstance.addContentChild(leafListInstance);
322     }
323
324     private static void processList(final FindingsManager findingsManager, final YangDataDomNode domNode,
325             final AbstractStructureInstance parentInstance, final AbstractStatement list) {
326         /*
327          * Create the list and check it doesn't exist yet. Then hook it up,
328          * and go recursively down the tree.
329          */
330         final ListInstance listInstance = createListInstance(findingsManager, parentInstance, domNode, list);
331         if (listInstance == null) {
332             /*
333              * No need for extra finding, would have been issued when creating the list instance.
334              */
335             return;
336         }
337         if (parentInstance.hasListInstance(listInstance.getNamespace(), listInstance.getName(), listInstance
338                 .getKeyValues())) {
339             findingsManager.addFinding(new Finding(domNode, ParserFindingType.P076_DUPLICATE_INSTANCE_DATA.toString(),
340                     "List '" + domNode.getPath() + "' with key '" + listInstance
341                             .getKeyValues() + "' already defined in this input."));
342             return;
343         }
344         parentInstance.addStructureChild(listInstance);
345
346         final List<AbstractStatement> allDataNodesUnderListStatement = getAllDataNodesAndChoiceUnderStatement(list);
347         for (final YangDataDomNode childDomNode : domNode.getChildren()) {
348             processDomNode(findingsManager, childDomNode, listInstance, allDataNodesUnderListStatement);
349         }
350     }
351
352     private static ListInstance createListInstance(final FindingsManager findingsManager,
353             final AbstractStructureInstance parentStructure, final YangDataDomNode dataDomNode,
354             final AbstractStatement list) {
355
356         /*
357          * So it's a YANG list, get key(s), as these are important to identify the correct instance.
358          */
359         final YList yangList = (YList) list;
360
361         final List<String> keyNames = yangList.getKey() != null ?
362                 GrammarHelper.parseToStringList(yangList.getKey().getValue()) :
363                 Collections.<String> emptyList();
364         final Map<String, String> keyValues = new HashMap<>();
365
366         for (final String keyName : keyNames) {
367             final String value = getValueOfKeyLeaf(dataDomNode, keyName);
368             if (value == null) {
369                 /*
370                  * Note that RFC7950 states:
371                  *
372                  * "All key leafs MUST be given values when a list entry is created." So if we don't have a value that is an error.
373                  */
374                 findingsManager.addFinding(new Finding(dataDomNode, ParserFindingType.P072_MISSING_KEY_VALUE.toString(),
375                         "No value, or null, supplied for key leaf '" + keyName + "' for list instance '" + dataDomNode
376                                 .getPath() + "'."));
377                 return null;
378             }
379             keyValues.put(keyName, value);
380         }
381
382         return new ListInstance(list, dataDomNode, parentStructure, keyNames, keyValues);
383     }
384
385     /**
386      * Returns the value of the key leaf with the given name. Note that null
387      * will be returned if the key does not exist, or has an explicit null value.
388      */
389     private static String getValueOfKeyLeaf(final YangDataDomNode dataDomNode, final String keyName) {
390
391         for (final YangDataDomNode child : dataDomNode.getChildren()) {
392             if (child.getName().equals(keyName)) {
393                 return child.getStringValue();
394             }
395         }
396
397         return null;
398     }
399
400     /**
401      * In JSON, an instance of a data node of type "empty" is encoded as '"my-leaf" : [null]', resulting
402      * in a DOM node with a null value. If the leaf in question is of type empty, then we will convert
403      * this to an empty string (as in NETCONF), so to allow further processing.
404      */
405     private static String adjustNullValueForEmpty(final AbstractStatement leafOrLeafList) {
406
407         final YType type = leafOrLeafList.getChild(CY.STMT_TYPE);
408         /*
409          * Sanity check - should never happen, unless the schema has defined a leaf / leaf-list
410          * without type (this would have been issued as a finding a long time ago).
411          */
412         if (type == null) {
413             return null;
414         }
415
416         /*
417          * Handle union as well - although having an empty as part of a union does not make much sense?
418          */
419         final List<YType> types = DataTypeHelper.isUnionType(type.getDataType()) ?
420                 type.getTypes() :
421                 Collections.singletonList(type);
422
423         for (final YType oneType : types) {
424             if (DataTypeHelper.isEmptyType(oneType.getDataType())) {
425                 return "";
426             }
427         }
428
429         /*
430          * So, not an empty. Guess its really a null value, so.
431          */
432         return null;
433     }
434
435     /**
436      * Merges the content of the source tree into the content of the target tree. This
437      * behaves in the same way as the NETCONF "merge" operation.
438      */
439     private static void mergeInDataTree(final FindingsManager findingsManager,
440             final AbstractStructureInstance targetParentStructure, final AbstractStructureInstance sourceParentStructure) {
441
442         /*
443          * Do the leafs and leaf-lists first.
444          */
445         for (final AbstractContentInstance sourceLeafOrLeafList : sourceParentStructure.getContentChildren()) {
446
447             if (sourceLeafOrLeafList instanceof LeafInstance) {
448                 /*
449                  * If the exact same leaf already exists, with the same value, then we are ok with that.
450                  */
451                 final LeafInstance leafInstanceInTarget = targetParentStructure.getLeafInstance(sourceLeafOrLeafList
452                         .getNamespace(), sourceLeafOrLeafList.getName());
453                 if (leafInstanceInTarget != null) {
454                     final Object sourceValue = ((LeafInstance) sourceLeafOrLeafList).getValue();
455                     final Object targetValue = leafInstanceInTarget.getValue();
456                     if (!Objects.equals(sourceValue, targetValue)) {
457                         findingsManager.addFinding(new Finding(sourceLeafOrLeafList.getDataDomNode(),
458                                 ParserFindingType.P073_LEAF_VALUE_ALREADY_SET.toString(),
459                                 "A different value for leaf '" + leafInstanceInTarget.getDataDomNode()
460                                         .getPath() + "' has already been set by input '" + leafInstanceInTarget
461                                                 .getDataDomNode().getYangData().getYangInput()
462                                                 .getName() + "' (" + sourceValue + " vs. " + targetValue + ")."));
463                         continue;
464                     }
465                 } else {                // leaf does not exist in target, then merge
466                     targetParentStructure.addContentChild(sourceLeafOrLeafList);
467                     sourceLeafOrLeafList.reparent(targetParentStructure);
468                 }
469             } else if (sourceLeafOrLeafList instanceof LeafListInstance) {
470                 /*
471                  * RFC states that a merge of these is such that existing instances are
472                  * ignored, i.e.only add instance if it does not exist yet.
473                  */
474                 final boolean leafListWithSameValueExistsInTarget = targetParentStructure.hasLeafListInstance(
475                         sourceLeafOrLeafList.getNamespace(), sourceLeafOrLeafList.getName(), sourceLeafOrLeafList
476                                 .getValue());
477                 if (!leafListWithSameValueExistsInTarget) {     // leaf-list with this value does not exist in target, then merge
478                     targetParentStructure.addContentChild(sourceLeafOrLeafList);
479                     sourceLeafOrLeafList.reparent(targetParentStructure);
480                 }
481             }
482
483             // TODO in the future: anydata and anyxml
484         }
485
486         /*
487          * Now do the containers and lists. This is a bit more complex - basically, where a
488          * container/list does not exist in the target, the whole tree is merged over. Otherwise
489          * recursion has to happen downwards.
490          */
491         for (final AbstractStructureInstance sourceContainerOrList : sourceParentStructure.getStructureChildren()) {
492
493             AbstractStructureInstance sameInstanceInTarget = null;
494
495             if (sourceContainerOrList instanceof ContainerInstance) {
496                 sameInstanceInTarget = targetParentStructure.getContainerInstance(sourceContainerOrList.getNamespace(),
497                         sourceContainerOrList.getName());
498             } else if (sourceContainerOrList instanceof ListInstance) {
499                 sameInstanceInTarget = targetParentStructure.getListInstance(sourceContainerOrList.getNamespace(),
500                         sourceContainerOrList.getName(), ((ListInstance) sourceContainerOrList).getKeyValues());
501             }
502
503             if (sameInstanceInTarget != null) {
504                 mergeInDataTree(findingsManager, sameInstanceInTarget, sourceContainerOrList);
505             } else {
506                 targetParentStructure.addStructureChild(sourceContainerOrList);
507                 sourceContainerOrList.reparent(targetParentStructure);
508             }
509         }
510     }
511 }