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.data.instance;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
28 import java.util.Objects;
29 import java.util.Optional;
30 import java.util.stream.Collectors;
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;
50 * Builds a type-safe Yang instance data tree from Yang data DOM trees.
52 * @author Mark Hollmann
54 public class InstanceDataTreeBuilder {
57 * Given a number of data DOM trees, merges these together and forms a (single) tree with type-safe
60 * This class requires the underlying Yang Model to be available, i.e. cannot be used with data only.
62 * If the input data was in JSON, module-name -> namespace resolution must have been performed on
65 public static RootInstance buildCombinedDataTree(final FindingsManager findingsManager, final List<YangData> yangDatas,
66 final ModuleRegistry moduleRegistry, final DataTreeBuilderPredicate topLevelInstancePredicate) {
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.
72 final List<RootInstance> rootInstances = new ArrayList<>();
74 for (final YangData yangData : yangDatas) {
76 if (yangData.getYangDataDomDocumentRoot() == null) {
80 if (containsYangLibraryInstanceOnly(yangData) && !yangLibraryModelPresent(moduleRegistry)) {
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.
87 final RootInstance rootInstance = new RootInstance();
89 * Schema root is handled slightly different from child-handling further down the tree...
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);
98 rootInstances.add(rootInstance);
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
107 final RootInstance result = new RootInstance();
108 for (final RootInstance rootInstance : rootInstances) {
109 mergeInDataTree(findingsManager, result, rootInstance);
115 public static List<AbstractStatement> getAllDataNodesAndChoiceAtTopLevel(final ModuleRegistry moduleRegistry) {
117 final List<AbstractStatement> result = new ArrayList<>();
119 for (final YangModel yangModelFile : moduleRegistry.getAllYangModels()) {
120 if (yangModelFile.getYangModelRoot().isModule()) {
121 result.addAll(getAllDataNodesAndChoiceUnderStatement(yangModelFile.getYangModelRoot().getModule()));
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());
134 * Returns whether this input only contains the YANG instance data.
136 private static boolean containsYangLibraryInstanceOnly(final YangData yangData) {
138 if (yangData.getYangDataDomDocumentRoot() == null) {
142 final List<YangDataDomNode> childrenUnderRoot = yangData.getYangDataDomDocumentRoot().getChildren();
143 if (childrenUnderRoot.size() != 1) {
147 final YangDataDomNode yangDataDomNode = childrenUnderRoot.get(0);
149 if (!IetfYangLibraryParser.IETF_YANG_LIBRARY_NAMESPACE.equals(yangDataDomNode.getNamespace())) {
153 return IetfYangLibraryParser.YANG_LIBRARY_MODULES_STATE.equals(yangDataDomNode
154 .getName()) || IetfYangLibraryParser.YANG_LIBRARY_YANG_LIBRARY.equals(yangDataDomNode.getName());
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.
161 private static boolean yangLibraryModelPresent(final ModuleRegistry moduleRegistry) {
163 final List<AbstractStatement> allDataNodesAtTopLevel = getAllDataNodesAndChoiceAtTopLevel(moduleRegistry);
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);
173 return YangLibContainer.isPresent();
176 private static void processDomNode(final FindingsManager findingsManager, final YangDataDomNode dataDomNode,
177 final AbstractStructureInstance parentInstance, final List<AbstractStatement> candidateDataNodes) {
179 final AbstractStatement matchingDataNode = Helper.findSchemaDataNode(candidateDataNodes, dataDomNode.getNamespace(),
180 dataDomNode.getName());
181 if (matchingDataNode == null) {
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.
187 final AbstractStatement childWithSameName = Helper.findSchemaDataNode(candidateDataNodes, dataDomNode
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."));
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() + "')."));
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);
221 private static void processContainer(final FindingsManager findingsManager, final YangDataDomNode dataDomNode,
222 final AbstractStructureInstance parentInstance, final AbstractStatement container) {
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."));
231 parentInstance.addStructureChild(containerInstance);
233 final List<AbstractStatement> allDataNodesUnderContainerStatement = getAllDataNodesAndChoiceUnderStatement(
235 for (final YangDataDomNode childDomNode : dataDomNode.getChildren()) {
236 processDomNode(findingsManager, childDomNode, containerInstance, allDataNodesUnderContainerStatement);
240 private static void processLeaf(final FindingsManager findingsManager, final YangDataDomNode domNode,
241 final AbstractStructureInstance parentInstance, final AbstractStatement leaf) {
243 Object leafValue = domNode.getValue();
244 if (leafValue == null && domNode.getSourceDataType() == SourceDataType.JSON) {
245 leafValue = adjustNullValueForEmpty(leaf);
247 if (leafValue == null) {
248 findingsManager.addFinding(new Finding(domNode, ParserFindingType.P080_NULL_VALUE.toString(), "Leaf '" + domNode
249 .getPath() + "' does not have a value."));
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."));
260 parentInstance.addContentChild(leafInstance);
263 private static void processAnydata(final FindingsManager findingsManager, final YangDataDomNode domNode,
264 final AbstractStructureInstance parentInstance, final AbstractStatement schemaLeaf) {
266 final String nodeValue = domNode.getReassembledChildren();
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."));
275 parentInstance.addContentChild(anyDataInstance);
278 private static void processAnyxml(final FindingsManager findingsManager, final YangDataDomNode domNode,
279 final AbstractStructureInstance parentInstance, final AbstractStatement schemaLeaf) {
281 final String nodeValue = domNode.getReassembledChildren();
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."));
290 parentInstance.addContentChild(anyXmlInstance);
293 private static void processLeafList(final FindingsManager findingsManager, final YangDataDomNode domNode,
294 final AbstractStructureInstance parentInstance, final AbstractStatement leafList) {
296 Object leafListValue = domNode.getValue();
297 if (leafListValue == null && domNode.getSourceDataType() == SourceDataType.JSON) {
298 leafListValue = adjustNullValueForEmpty(leafList);
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."));
306 final LeafListInstance leafListInstance = new LeafListInstance(leafList, domNode, parentInstance, leafListValue);
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.
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."));
321 parentInstance.addContentChild(leafListInstance);
324 private static void processList(final FindingsManager findingsManager, final YangDataDomNode domNode,
325 final AbstractStructureInstance parentInstance, final AbstractStatement list) {
327 * Create the list and check it doesn't exist yet. Then hook it up,
328 * and go recursively down the tree.
330 final ListInstance listInstance = createListInstance(findingsManager, parentInstance, domNode, list);
331 if (listInstance == null) {
333 * No need for extra finding, would have been issued when creating the list instance.
337 if (parentInstance.hasListInstance(listInstance.getNamespace(), listInstance.getName(), listInstance
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."));
344 parentInstance.addStructureChild(listInstance);
346 final List<AbstractStatement> allDataNodesUnderListStatement = getAllDataNodesAndChoiceUnderStatement(list);
347 for (final YangDataDomNode childDomNode : domNode.getChildren()) {
348 processDomNode(findingsManager, childDomNode, listInstance, allDataNodesUnderListStatement);
352 private static ListInstance createListInstance(final FindingsManager findingsManager,
353 final AbstractStructureInstance parentStructure, final YangDataDomNode dataDomNode,
354 final AbstractStatement list) {
357 * So it's a YANG list, get key(s), as these are important to identify the correct instance.
359 final YList yangList = (YList) list;
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<>();
366 for (final String keyName : keyNames) {
367 final String value = getValueOfKeyLeaf(dataDomNode, keyName);
370 * Note that RFC7950 states:
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.
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
379 keyValues.put(keyName, value);
382 return new ListInstance(list, dataDomNode, parentStructure, keyNames, keyValues);
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.
389 private static String getValueOfKeyLeaf(final YangDataDomNode dataDomNode, final String keyName) {
391 for (final YangDataDomNode child : dataDomNode.getChildren()) {
392 if (child.getName().equals(keyName)) {
393 return child.getStringValue();
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.
405 private static String adjustNullValueForEmpty(final AbstractStatement leafOrLeafList) {
407 final YType type = leafOrLeafList.getChild(CY.STMT_TYPE);
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).
417 * Handle union as well - although having an empty as part of a union does not make much sense?
419 final List<YType> types = DataTypeHelper.isUnionType(type.getDataType()) ?
421 Collections.singletonList(type);
423 for (final YType oneType : types) {
424 if (DataTypeHelper.isEmptyType(oneType.getDataType())) {
430 * So, not an empty. Guess its really a null value, so.
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.
439 private static void mergeInDataTree(final FindingsManager findingsManager,
440 final AbstractStructureInstance targetParentStructure, final AbstractStructureInstance sourceParentStructure) {
443 * Do the leafs and leaf-lists first.
445 for (final AbstractContentInstance sourceLeafOrLeafList : sourceParentStructure.getContentChildren()) {
447 if (sourceLeafOrLeafList instanceof LeafInstance) {
449 * If the exact same leaf already exists, with the same value, then we are ok with that.
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 + ")."));
465 } else { // leaf does not exist in target, then merge
466 targetParentStructure.addContentChild(sourceLeafOrLeafList);
467 sourceLeafOrLeafList.reparent(targetParentStructure);
469 } else if (sourceLeafOrLeafList instanceof LeafListInstance) {
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.
474 final boolean leafListWithSameValueExistsInTarget = targetParentStructure.hasLeafListInstance(
475 sourceLeafOrLeafList.getNamespace(), sourceLeafOrLeafList.getName(), sourceLeafOrLeafList
477 if (!leafListWithSameValueExistsInTarget) { // leaf-list with this value does not exist in target, then merge
478 targetParentStructure.addContentChild(sourceLeafOrLeafList);
479 sourceLeafOrLeafList.reparent(targetParentStructure);
483 // TODO in the future: anydata and anyxml
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.
491 for (final AbstractStructureInstance sourceContainerOrList : sourceParentStructure.getStructureChildren()) {
493 AbstractStructureInstance sameInstanceInTarget = null;
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());
503 if (sameInstanceInTarget != null) {
504 mergeInDataTree(findingsManager, sameInstanceInTarget, sourceContainerOrList);
506 targetParentStructure.addStructureChild(sourceContainerOrList);
507 sourceContainerOrList.reparent(targetParentStructure);