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.yanglibrary;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.List;
29 import java.util.Map.Entry;
30 import java.util.Objects;
32 import java.util.stream.Collectors;
34 import org.oran.smo.yangtools.parser.ParserExecutionContext;
35 import org.oran.smo.yangtools.parser.data.YangData;
36 import org.oran.smo.yangtools.parser.data.dom.YangDataDomDocumentRoot;
37 import org.oran.smo.yangtools.parser.data.dom.YangDataDomDocumentRoot.SourceDataType;
38 import org.oran.smo.yangtools.parser.data.dom.YangDataDomNode;
39 import org.oran.smo.yangtools.parser.data.util.IdentityRefValue;
40 import org.oran.smo.yangtools.parser.findings.Finding;
41 import org.oran.smo.yangtools.parser.findings.FindingsManager;
42 import org.oran.smo.yangtools.parser.findings.ModifyableFindingSeverityCalculator;
43 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
44 import org.oran.smo.yangtools.parser.input.YangInputResolver;
45 import org.oran.smo.yangtools.parser.model.schema.ModuleAndNamespaceResolver;
46 import org.oran.smo.yangtools.parser.yanglibrary.Module.IetfYangLibraryConformanceType;
49 * Parser for Yang Library instance data. Can handle both the "yang-library" and "modules-state" containers.
51 * @author Mark Hollmann
53 public class IetfYangLibraryParser {
56 * The module name for the yang-library module as defined in RFC 8525.
58 public static final String IETF_YANG_LIBRARY_MODULE_NAME = "ietf-yang-library";
60 * The module namespace for the yang-library module as defined in RFC 8525.
62 public static final String IETF_YANG_LIBRARY_NAMESPACE = "urn:ietf:params:xml:ns:yang:ietf-yang-library";
64 public static final String YANG_LIBRARY_MODULES_STATE = "modules-state";
65 public static final String YANG_LIBRARY_YANG_LIBRARY = "yang-library";
67 private final List<YangLibraryPopulator> populators;
68 private final FindingsManager findingsManager;
69 private final ParserExecutionContext context;
71 private static final ModuleAndNamespaceResolver RESOLVER = new ModuleAndNamespaceResolver();
74 RESOLVER.recordModuleMapping(IETF_YANG_LIBRARY_MODULE_NAME, IETF_YANG_LIBRARY_NAMESPACE);
75 RESOLVER.recordNamespaceMapping(IETF_YANG_LIBRARY_NAMESPACE, IETF_YANG_LIBRARY_MODULE_NAME);
77 RESOLVER.recordModuleMapping(Datastore.IETF_DATASTORES_MODULE_NAME, Datastore.IETF_DATASTORES_NAMESPACE);
78 RESOLVER.recordNamespaceMapping(Datastore.IETF_DATASTORES_NAMESPACE, Datastore.IETF_DATASTORES_MODULE_NAME);
82 * Creates a Yang Library parser using the default populator for RFC8525.
84 public IetfYangLibraryParser() {
85 this(Collections.singletonList(new RFC8525Populator()));
89 * Creates a YL parser using the supplied populators. The client should make sure to supply a
90 * populator for extraction of core Yang Library data according to RFC8525.
92 public IetfYangLibraryParser(final List<YangLibraryPopulator> populators) {
93 this.populators = populators;
95 findingsManager = new FindingsManager(new ModifyableFindingSeverityCalculator());
96 context = new ParserExecutionContext(findingsManager);
98 context.setCheckModulesAgainstYangLibrary(false);
102 * Returns the FindingsManager used during parsing. Issues encountered during parsing will be
103 * captured as Findings.
105 public FindingsManager getFindingsManager() {
106 return findingsManager;
110 * Parses instance data into Yang Library instances. The supplied resolver must resolve to one or more
111 * data inputs (typically files containing XML or JSON data). All Yang Library instances encountered
112 * in the data will be extracted, not just the instance found at the top-level (ie., mount points are
115 * Yang library instance data using the (deprecated) "modules-state" will be converted to a corresponding
116 * representation of the "yang-library".
118 * Issues may be encountered during the parsing; after parsing, the findings manager should be inspected
121 public List<YangLibrary> parseIntoYangLibraries(final YangInputResolver instanceDataInputResolver) {
123 findingsManager.clear();
125 final List<YangData> yangDatas = instanceDataInputResolver.getResolvedYangInput().stream().map(YangData::new)
126 .collect(Collectors.toList());
129 * Parse in all of the data. The worst that can happen is that the XML or JSON is malformed, or that
130 * prefixes cannot be resolved.
132 for (final YangData yangData : yangDatas) {
133 yangData.parse(context);
136 if (!findingsManager.getAllFindings().isEmpty()) {
137 findingsManager.addFinding(new Finding(ParserFindingType.P000_UNSPECIFIED_ERROR,
138 "There were findings during parsing of the YANG instance data that may have resulted in the YANG Library data not being populated correctly."));
142 * We look at each input and try to find the relevant containers.
144 final List<YangLibrary> result = new ArrayList<>();
146 for (final YangData yangData : yangDatas) {
147 final YangDataDomDocumentRoot yangDataDomDocumentRoot = yangData.getYangDataDomDocumentRoot();
148 if (yangDataDomDocumentRoot != null) {
149 extractYangLibraryUnderDomNode(yangDataDomDocumentRoot, result);
157 * Works like {@link parseIntoYangLibraries}, but uses the supplied data DOm as input of Yang Library data, and will
158 * only extract the top-level Yang Library instance (if any).
160 public static YangLibrary getTopLevelYangLibrary(final YangDataDomDocumentRoot dataDomDocumentRoot) {
162 final IetfYangLibraryParser yangLibraryParser = new IetfYangLibraryParser();
163 final List<YangLibrary> extractedYangLibraries = new ArrayList<>();
164 yangLibraryParser.extractYangLibraryUnderDomNode(dataDomDocumentRoot, extractedYangLibraries);
166 return YangLibrary.getTopLevelSchema(extractedYangLibraries);
170 * Given a data DOM node (possibly being document root), tries to extract a YANG Library under the DOM node.
172 private void extractYangLibraryUnderDomNode(final YangDataDomNode domNode, List<YangLibrary> result) {
174 final ModulesState modulesStateInstance = createModulesStateBranch(domNode);
175 YangLibrary yangLibraryInstance = createYangLibraryBranch(domNode);
178 * The modules-state is deprecated, so we translate to yang-library if needed. This
179 * allows a client to always operate on the yang-library without having to worry
180 * about the deprecated modules-state branch.
182 * Note we will never translate back from yang-library to modules-state, as this
183 * would require us to figure out which is the correct set of modules (looking
184 * at datastore and schema elements), which can be ambiguous - plus it is
187 if (modulesStateInstance != null && yangLibraryInstance == null) {
188 yangLibraryInstance = translateModulesStateDataToYangLibraryData(modulesStateInstance);
191 if (yangLibraryInstance != null) {
192 result.add(yangLibraryInstance);
196 * Recursively work down the DOM tree - the YANG library can conceivably be under a mount point
197 * further down the tree.
199 domNode.getChildren().forEach(child -> extractYangLibraryUnderDomNode(child, result));
202 private static YangLibrary translateModulesStateDataToYangLibraryData(final ModulesState modulesState) {
204 final YangLibrary yangLibraryInstance = new YangLibrary(modulesState.getMountPoint());
205 yangLibraryInstance.setContentId(modulesState.getModuleSetId());
207 final ModuleSet defaultModuleSet = new ModuleSet();
208 defaultModuleSet.setName("module-set-auto-generated-by-yang-library-parser");
210 for (final Module module : modulesState.getModules()) {
211 if (module.getConformanceType() == IetfYangLibraryConformanceType.IMPLEMENT) {
212 defaultModuleSet.addImplementingModule(module);
213 } else if (module.getConformanceType() == IetfYangLibraryConformanceType.IMPORT) {
214 defaultModuleSet.addImportOnlyModule(module);
218 yangLibraryInstance.addDatastore(new Datastore(Datastore.RUNNING_DATASTORE_IDENTITY,
219 "schema-auto-generated-by-yang-library-parser", Collections.singletonList(defaultModuleSet)));
221 return yangLibraryInstance;
224 // ============================= Modules state branch ========================================
226 private ModulesState createModulesStateBranch(final YangDataDomNode parentDomNode) {
228 final YangDataDomNode modulesStateDomNode = getDomChild(parentDomNode, YANG_LIBRARY_MODULES_STATE);
230 if (modulesStateDomNode != null) {
231 final ModulesState modulesState = new ModulesState(parentDomNode);
232 populateModulesState(modulesState, modulesStateDomNode);
235 * The YANG model for RFC 7895 (modules-state) allows for semantic errors - for example, the
236 * same module, of different revisions, could be marked as IMPLEMENTING. This is likely to be
237 * causing problems for the client later on, so we do some validation here to try to catch
238 * these issues as early as possible.
240 validateModulesState(modulesState);
248 private void populateModulesState(final ModulesState modulesState, final YangDataDomNode modulesStateDomNode) {
250 populators.forEach(pop -> pop.populateModulesState(context, modulesState, modulesStateDomNode));
252 for (final YangDataDomNode moduleChildDomNode : getDomChildren(modulesStateDomNode, "module")) {
253 final Module module = new Module();
254 modulesState.addModule(module);
255 populateModuleInModulesState(module, moduleChildDomNode);
259 private void populateModuleInModulesState(final Module module, final YangDataDomNode moduleDomNode) {
261 populators.forEach(pop -> pop.populateModuleInModulesState(context, module, moduleDomNode));
263 for (final YangDataDomNode submoduleDomNode : getDomChildren(moduleDomNode, "submodule")) {
264 final Submodule submodule = new Submodule();
265 module.addSubmodule(submodule);
266 populators.forEach(pop -> pop.populateSubmoduleInModulesState(context, submodule, submoduleDomNode));
270 private void validateModulesState(final ModulesState modulesState) {
271 validateModulesStateForConformanceType(modulesState);
274 private void validateModulesStateForConformanceType(final ModulesState modulesState) {
276 * Couple basic rules:
278 * A correct enum value must be used for 'conformance-type'.
279 * The same module (by name) cannot have 'conformance-type implement'.
280 * There must be at least a single 'conformance-type implement'.
282 final Set<String> implementingModuleNames = new HashSet<>();
283 for (final Module module : modulesState.getModules()) {
284 if (module.getConformanceType() == IetfYangLibraryConformanceType.UNKNOWN) {
285 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
286 "Unknown conformance-type for module '" + module.getName() + "'."));
287 } else if (module.getConformanceType() == IetfYangLibraryConformanceType.IMPLEMENT) {
288 if (implementingModuleNames.contains(module.getName())) {
289 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
290 "Module '" + module.getName() + "' listed more than once with conformance-type 'implement'."));
292 implementingModuleNames.add(module.getName());
297 if (implementingModuleNames.isEmpty()) {
298 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
299 "YANG library does not have at least a single module entry with conformance-type 'implement'."));
303 // ============================ YANG Library branch ======================================
305 private YangLibrary createYangLibraryBranch(final YangDataDomNode parentDomNode) {
308 final YangDataDomNode yangLibraryDomNode = getDomChild(parentDomNode, YANG_LIBRARY_YANG_LIBRARY);
309 if (yangLibraryDomNode != null) {
310 final YangLibrary yangLibrary = new YangLibrary(parentDomNode);
311 populateYangLibrary(yangLibrary, yangLibraryDomNode);
314 } catch (final Exception ex) {
315 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
316 "Incorrect YANG library."));
322 private void populateYangLibrary(final YangLibrary yangLibrary, final YangDataDomNode yangLibraryDomNode) {
324 populators.forEach(pop -> pop.populateYangLibrary(context, yangLibrary, yangLibraryDomNode));
326 final Map<String, ModuleSet> extractedModuleSets = new HashMap<>();
328 for (final YangDataDomNode moduleSetDomNode : getDomChildren(yangLibraryDomNode, "module-set")) {
329 final ModuleSet moduleSet = new ModuleSet();
330 populateModuleSet(moduleSet, moduleSetDomNode);
331 extractedModuleSets.put(moduleSet.getName(), moduleSet);
334 final Map<String, List<ModuleSet>> schemaToModuleSetMappings = getSchemaToModuleSetMappings(context,
335 extractedModuleSets, yangLibraryDomNode);
337 handleDatastoreElements(yangLibrary, schemaToModuleSetMappings, yangLibraryDomNode);
340 private void populateModuleSet(final ModuleSet moduleSet, final YangDataDomNode moduleSetDomNode) {
342 populators.forEach(pop -> pop.populateModuleSet(context, moduleSet, moduleSetDomNode));
344 for (final YangDataDomNode moduleDomNode : getDomChildren(moduleSetDomNode, "module")) {
345 final Module module = new Module();
346 moduleSet.addImplementingModule(module);
347 populateModuleInYangLibrary(module, moduleDomNode, Module.MODULE_IMPLEMENT);
349 for (final YangDataDomNode moduleDomNode : getDomChildren(moduleSetDomNode, "import-only-module")) {
350 final Module module = new Module();
351 moduleSet.addImportOnlyModule(module);
352 populateModuleInYangLibrary(module, moduleDomNode, Module.MODULE_IMPORT);
356 private void populateModuleInYangLibrary(final Module module, final YangDataDomNode moduleDomNode,
357 final String conformanceType) {
359 populators.forEach(pop -> pop.populateModuleInYangLibrary(context, module, moduleDomNode, conformanceType));
361 for (final YangDataDomNode submoduleDomNode : getDomChildren(moduleDomNode, "submodule")) {
362 final Submodule submodule = new Submodule();
363 module.addSubmodule(submodule);
364 populators.forEach(pop -> pop.populateSubmoduleInYangLibrary(context, submodule, submoduleDomNode));
369 * Returns a mapping of schema name -to- list of module sets
371 private static Map<String, List<ModuleSet>> getSchemaToModuleSetMappings(final ParserExecutionContext context,
372 final Map<String, ModuleSet> extractedModuleSets, final YangDataDomNode yangLibraryDomNode) {
374 final Map<String, List<ModuleSet>> result = new HashMap<>();
376 for (final YangDataDomNode schemaDomNode : getDomChildren(yangLibraryDomNode, "schema")) {
378 final String schemaName = getValueOfChild(context, schemaDomNode, "name", "");
379 if (schemaName.isEmpty()) {
380 context.getFindingsManager().addFinding(new Finding(
381 ParserFindingType.P082_YANG_LIBRARY_MANDATORY_VALUE_MISSING, "Missing schema name."));
384 result.put(schemaName, new ArrayList<>());
386 final List<String> moduleSetNames = getValueOfChildren(context, schemaDomNode, "module-set");
388 for (final String moduleSetName : moduleSetNames) {
389 final ModuleSet moduleSet = extractedModuleSets.get(moduleSetName);
390 if (moduleSet != null) {
391 result.get(schemaName).add(moduleSet);
393 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
394 "schema '" + schemaName + "' refers to non-existing module-set '" + moduleSetName + "'."));
400 * Special handling - sometimes whoever creates the Yang Library data forgets to use the "schema"
401 * data node. We are nice here and simply auto-generate one, assigning all the module-sets found.
404 if (result.isEmpty()) {
405 result.put("schema-auto-generated-by-yang-library-parser", new ArrayList<>(extractedModuleSets.values()));
411 private void handleDatastoreElements(final YangLibrary yangLibrary,
412 final Map<String, List<ModuleSet>> schemaToModuleSetMappings, final YangDataDomNode yangLibraryDomNode) {
414 for (final YangDataDomNode datastoreDomNode : getDomChildren(yangLibraryDomNode, "datastore")) {
416 IdentityRefValue datastoreIdentityRef = null;
418 final YangDataDomNode datastoreNameDomNode = getDomChild(datastoreDomNode, "name");
419 if (datastoreNameDomNode != null) {
421 final String datastoreIdentityRefAsString = Objects.toString(datastoreNameDomNode.getValue());
424 * The value of this leaf is an identity ref, e.g.:
426 * <datastore xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores">
427 * <name>ds:running</name>
428 * <schema>schema1</schema>
433 * "ietf-yang-library:datastore" : [
435 * "name" : "ietf-datastores:running",
436 * "schema" : "schema1"
441 if (datastoreNameDomNode.getSourceDataType() == SourceDataType.XML) {
442 datastoreIdentityRef = new IdentityRefValue(datastoreIdentityRefAsString, datastoreNameDomNode
443 .getPrefixResolver(), datastoreNameDomNode.getNamespace());
445 datastoreIdentityRef = new IdentityRefValue(datastoreIdentityRefAsString, datastoreNameDomNode
450 * If the namespace is unknown (maybe the designer forgot to declare the namespace),
451 * we will assume the namespace of the "ietf-datastores" module.
453 if (datastoreIdentityRef.getIdentityNamespace() == null && datastoreIdentityRef
454 .getIdentityModuleName() == null) {
455 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
456 "Unresolvable/missing prefix/module for <datastore> name '" + datastoreIdentityRefAsString + "'."));
457 datastoreIdentityRef = new IdentityRefValue(Datastore.IETF_DATASTORES_NAMESPACE,
458 Datastore.IETF_DATASTORES_MODULE_NAME, datastoreIdentityRefAsString);
462 context.getFindingsManager().addFinding(new Finding(
463 ParserFindingType.P082_YANG_LIBRARY_MANDATORY_VALUE_MISSING, "Missing datastore name."));
466 final String schemaName = getValueOfChild(context, datastoreDomNode, "schema", "");
467 if (schemaName.isEmpty()) {
468 context.getFindingsManager().addFinding(new Finding(
469 ParserFindingType.P082_YANG_LIBRARY_MANDATORY_VALUE_MISSING,
470 "Missing value for leaf 'schema' for datastore."));
472 if (!schemaToModuleSetMappings.containsKey(schemaName)) {
473 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
474 "datastore refers to non-existing schema '" + schemaName + "'."));
476 yangLibrary.addDatastore(new Datastore(datastoreIdentityRef, schemaName, schemaToModuleSetMappings.get(
482 * Special handling: Designer did not specify a datastore in the YANG Library. If there is only a
483 * single schema we assume that this schema is the one to be used.
485 if (yangLibrary.getDatastores().isEmpty() && schemaToModuleSetMappings.size() == 1) {
486 final Entry<String, List<ModuleSet>> theOnlySchemaToModuleSetMappingEntry = schemaToModuleSetMappings.entrySet()
488 yangLibrary.addDatastore(new Datastore(Datastore.RUNNING_DATASTORE_IDENTITY,
489 theOnlySchemaToModuleSetMappingEntry.getKey(), theOnlySchemaToModuleSetMappingEntry.getValue()));
493 static YangDataDomNode getDomChild(final YangDataDomNode parentDomNode, final String soughtName) {
494 return parentDomNode.getChild(IETF_YANG_LIBRARY_NAMESPACE, IETF_YANG_LIBRARY_MODULE_NAME, soughtName);
497 static List<YangDataDomNode> getDomChildren(final YangDataDomNode parentDomNode, final String soughtName) {
498 return parentDomNode.getChildren(IETF_YANG_LIBRARY_NAMESPACE, IETF_YANG_LIBRARY_MODULE_NAME, soughtName);
501 static String getValueOfChild(final ParserExecutionContext context, final YangDataDomNode parentDomNode,
502 final String soughtName, final String defaultValue) {
504 final YangDataDomNode child = getDomChild(parentDomNode, soughtName);
505 final Object valueOfChild = child == null ? null : child.getValue();
507 if (valueOfChild != null && !(valueOfChild instanceof String)) {
508 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
509 "Expected a string value for '" + soughtName + "'."));
513 return valueOfChild == null ? defaultValue : ((String) valueOfChild).trim();
516 static List<String> getValueOfChildren(final ParserExecutionContext context, final YangDataDomNode parentDomNode,
517 final String soughtName) {
518 return getDomChildren(parentDomNode, soughtName).stream().map(YangDataDomNode::getStringValue).collect(Collectors