Bump Xtend to 2.35.0
[yangtools.git] / docs / src / main / asciidoc / developing.adoc
1 = Developer Guide
2 :rfc6020: https://tools.ietf.org/html/rfc6020
3 :RFC7950: https://tools.ietf.org/html/rfc7950
4 :lhotka-yang-json: https://tools.ietf.org/html/draft-lhotka-netmod-yang-json-01
5
6 == Overview
7 YANG Tools is set of libraries and tooling providing support for use {rfc6020}[YANG] and {RFC7950}[YANG](YANG 1.1) for Java (or other JVM-based language) projects and applications.
8
9 YANG Tools provides following features in OpenDaylight:
10
11 - parsing of YANG sources and
12 semantic inference of relationship across YANG models as defined in
13 {rfc6020}[RFC6020] and {RFC7950}[RFC7950]
14 - representation of YANG-modeled data in Java
15 ** *Normalized Node* representation - DOM-like tree model, which uses conceptual
16   meta-model more tailored to YANG and OpenDaylight use-cases than a standard XML
17   DOM model allows for.
18 - serialization / deserialization of YANG-modeled data driven by YANG
19 models
20 ** XML - as defined in {rfc6020}[RFC6020]
21 ** JSON - as defined in {rfc6020}[draft-lhotka-netmod-yang-json-01]
22 ** support for third-party generators processing YANG models.
23
24 === Architecture
25 YANG Tools project consists of following logical subsystems:
26
27 - *Commons* - Set of general purpose code, which is not specific to YANG, but
28   is also useful outside YANG Tools implementation.
29 - *YANG Model and Parser* - YANG semantic model and lexical and semantic parser
30   of YANG models, which creates in-memory cross-referenced representation of
31   YANG models, which is used by other components to determine their behaviour
32   based on the model.
33 - *YANG Data* - Definition of Normalized Node APIs and Data Tree APIs, reference
34   implementation of these APIs and implementation of XML and JSON codecs for
35   Normalized Nodes.
36 - *YANG Maven Plugin* - Maven plugin which integrates YANG parser into Maven
37   build lifecycle and provides code-generation framework for components, which
38   wants to generate code or other artefacts based on YANG model.
39
40 === Concepts
41 Project defines base concepts and helper classes which are project-agnostic and could be used outside of YANG Tools project scope.
42
43 === Components
44
45 - yang-common
46 - yang-data-api
47 - yang-data-codec-gson
48 - yang-data-codec-xml
49 - yang-data-impl
50 - yang-data-jaxen
51 - yang-data-transform
52 - yang-data-util
53 - yang-maven-plugin
54 - yang-maven-plugin-it
55 - yang-maven-plugin-spi
56 - yang-model-api
57 - yang-model-export
58 - yang-model-util
59 - yang-parser-api
60 - yang-parser-impl
61
62 ==== YANG Parser
63
64 Yang Statement Parser works on the idea of statement concepts as defined in RFC6020 and RFC7950, section 6.3. We come up here with basic ModelStatement and StatementDefinition, following RFC6020 idea of having sequence of statements, where
65 every statement contains keyword and zero or one argument. ModelStatement is extended by DeclaredStatement (as it comes from source, e.g. YANG source)
66 and EffectiveStatement, which contains other substatements and tends to represent result of semantic processing of other statements (uses, augment for YANG).
67 IdentifierNamespace represents common superclass for YANG model namespaces.
68
69 Input of the Yang Statement Parser is a collection of StatementStreamSource objects.
70 StatementStreamSource interface is used for inference of effective model
71 and is required to emit its statements using supplied StatementWriter.
72 Each source (e.g. YANG source) has to be processed in three steps
73 in order to emit different statements for each step.
74 This package provides support for various namespaces used across statement parser
75 in order to map relations during declaration phase process.
76
77 Currently, there are two implementations of StatementStreamSource in Yangtools:
78
79  - YangStatementStreamSource - intended for yang sources
80  - YinStatementStreamSource - intended for yin sources
81
82 ==== YANG Data API
83 Class diagram of yang data API
84
85 image:models/yang-data-api.png[]
86
87 ==== YANG Data Codecs
88 Codecs which enable serialization of NormalizedNodes into YANG-modeled data in XML or JSON format and deserialization of YANG-modeled data in XML or JSON format into NormalizedNodes.
89
90 ==== YANG Maven Plugin
91 Maven plugin which integrates YANG parser into Maven
92   build lifecycle and provides code-generation framework for components, which
93   wants to generate code or other artefacts based on YANG model.
94
95 == How to / Tutorials
96
97 === Working with YANG Model
98 First thing you need to do if you want to work with YANG models is to instantiate a SchemaContext object. This object type describes one or more parsed YANG modules.
99
100 In order to create it you need to utilize YANG statement parser which takes one or more StatementStreamSource objects as input and then produces the SchemaContext object.
101
102 StatementStreamSource object contains the source file information. It has two implementations, one for YANG sources - YangStatementStreamSource, and one for YIN sources - YinStatementStreamSource.
103
104 Here is an example of creating StatementStreamSource objects for YANG files, providing them to the YANG statement parser and building the SchemaContext:
105
106 [source,java]
107 ----
108 //
109 StatementStreamSource yangModuleSource = YangStatementStreamSource.create(YangTextSchemaSource.forResource("/example.yang"));
110 StatementStreamSource yangModuleSource2 = YangStatementStreamSource.create(YangTextSchemaSource.forResource("/example2.yang"));
111
112 CrossSourceStatementReactor.BuildAction reactor = RFC7950Reactors.defaultReactor().newBuild();
113 reactor.addSources(yangModuleSource, yangModuleSource2);
114 EffectiveSchemaContext schemaContext = reactor.buildEffective();
115 ----
116
117 First, StatementStreamSource objects should be instantiated: path to the yang source file (which is a regular String object).
118
119 Next comes the initiation of new yang parsing cycle - which is represented by CrossSourceStatementReactor.BuildAction object. You can get it by calling method newBuild() on CrossSourceStatementReactor object (RFC7950Reactors).
120
121 Then you should feed yang sources to it by calling method addSources() that takes one or more StatementStreamSource objects as arguments.
122
123 Finally you call the method buildEffective() on the reactor object which returns EffectiveSchemaContext (that is a concrete implementation of SchemaContext). Now you are ready to work with contents of the added yang sources.
124
125 Let us explain how to work with models contained in the newly created SchemaContext. If you want to get all the modules in the schemaContext, you have to call method getModules() which returns a Set of modules. If you want to get all the data definitions in schemaContext, you need to call method getDataDefinitions, etc.
126
127 [source, java]
128 Collection<? extends @NonNull Module> modules = schemaContext.getModules();
129 Collection<? extends @NonNull DataSchemaNode> dataDefinitions = schemaContext.getDataDefinitions();
130
131 Usually you want to access specific modules. Getting a concrete module from SchemaContext is a matter of calling one of these methods:
132
133 * findModule(Name),
134 * findModule(Namespace),
135 * findModule(Namespace, Revision).
136
137 In the first case, you need to provide module name as it is defined in the yang source file and module revision date if it specified in the yang source file (if it is not defined, you can just pass a null value). In order to provide the revision date in proper format, you can use a Revision.of.
138
139 [source, java]
140 Module exampleModule = schemaContext.findModule("example-module").get();
141 // or
142 Module exampleModule = schemaContext.findModule("example-module", Revision.of("2015-09-02")).get();
143
144 In the second case, you have to provide module namespace in form of an URI object.
145 [source, java]
146 Module exampleModule = schemaContext.findModule(XMLNamespace.of("opendaylight.org/example-module")).get();
147
148 In the third case, you provide both module namespace and revision date as arguments.
149
150 Once you have a Module object, you can access its contents as they are defined in YANG Model API.
151 One way to do this is to use method like getIdentities() or getRpcs() which will give you a Set of objects. Otherwise you can access a DataSchemaNode directly via the method getDataChildByName() which takes a QName object as its only argument. Here are a few examples.
152
153 [source, java]
154 ----
155 Collection<? extends @NonNull AugmentationSchemaNode> augmentations = exampleModule.getAugmentations();
156 Collection<? extends @NonNull ModuleImport> imports = exampleModule.getImports();
157
158 ChoiceSchemaNode choiceSchemaNode = (ChoiceSchemaNode) exampleModule.getDataChildByName(QName.create(exampleModule.getQNameModule(), "example-choice"));
159
160 ContainerSchemaNode containerSchemaNode = (ContainerSchemaNode) exampleModule.getDataChildByName(QName.create(exampleModule.getQNameModule(), "example-container"));
161 ----
162
163 The YANG statement parser can work in three modes:
164
165 * default mode
166 * mode with active resolution of if-feature statements
167 * mode with active semantic version processing
168
169 The default mode is active when you initialize the parsing cycle as usual by calling the method newBuild() without passing any arguments to it. The second and third mode can be activated by invoking the newBuild() with a special argument. You can either activate just one of them or both by passing proper arguments. Let us explain how these modes work.
170
171 Mode with active resolution of if-features makes yang statements containing an if-feature statement conditional based on the supported features. These features are provided in the form of a QName-based java.util.Set object. In the example below, only two features are supported: example-feature-1 and example-feature-2. The Set is passed to the method newBuild() and the mode is activated.
172
173 [source, java]
174 ----
175 Set<QName> supportedFeatures = ImmutableSet.of(
176     QName.create("example-namespace", "2016-08-31", "example-feature-1"),
177     QName.create("example-namespace", "2016-08-31", "example-feature-2"));
178
179 CrossSourceStatementReactor.BuildAction reactor = RFC7950Reactors.defaultReactor().newBuild().setSupportedFeatures(supportedFeatures);
180 ----
181
182 In case when no features should be supported, you should provide an empty Set<QName> object.
183
184 [source, java]
185 ----
186 Set<QName> supportedFeatures = ImmutableSet.of();
187
188 CrossSourceStatementReactor.BuildAction reactor = RFC7950Reactors.defaultReactor().newBuild().setSupportedFeatures(supportedFeatures);
189 ----
190
191 When this mode is not activated, all features in the processed YANG sources are supported.
192
193 Mode with active semantic version processing changes the way how YANG import statements work - each module import is processed based on the specified semantic version statement and the revision-date statement is ignored. In order to activate this mode, you have to provide StatementParserMode.SEMVER_MODE enum constant as argument to the method newBuild().
194
195 [source, java]
196 ----
197 CrossSourceStatementReactor.BuildAction reactor = RFC7950Reactors.defaultReactor().newBuild(StatementParserMode.SEMVER_MODE);
198 ----
199
200 Before you use a semantic version statement in a YANG module, you need to define an extension for it so that the YANG statement parser can recognize it.
201
202 [source, yang]
203 ----
204 module semantic-version {
205     namespace "urn:opendaylight:yang:extension:semantic-version";
206     prefix sv;
207     yang-version 1;
208
209     revision 2016-02-02 {
210         description "Initial version";
211     }
212     sv:semantic-version "0.0.1";
213
214     extension semantic-version {
215         argument "semantic-version" {
216             yin-element false;
217         }
218     }
219 }
220 ----
221
222 In the example above, you see a YANG module which defines semantic version as an extension. This extension can be imported to other modules in which we want to utilize the semantic versioning concept.
223
224 Below is a simple example of the semantic versioning usage. With semantic version processing mode being active, the foo module imports the bar module based on its semantic version. Notice how both modules import the module with the semantic-version extension.
225
226 [source, yang]
227 ----
228 module foo {
229     namespace foo;
230     prefix foo;
231     yang-version 1;
232
233     import semantic-version { prefix sv; revision-date 2016-02-02; sv:semantic-version "0.0.1"; }
234     import bar { prefix bar; sv:semantic-version "0.1.2";}
235
236     revision "2016-02-01" {
237         description "Initial version";
238     }
239     sv:semantic-version "0.1.1";
240
241     ...
242 }
243 ----
244
245 [source, yang]
246 ----
247 module bar {
248     namespace bar;
249     prefix bar;
250     yang-version 1;
251
252     import semantic-version { prefix sv; revision-date 2016-02-02; sv:semantic-version "0.0.1"; }
253
254     revision "2016-01-01" {
255         description "Initial version";
256     }
257     sv:semantic-version "0.1.2";
258
259     ...
260 }
261 ----
262
263 Every semantic version must have the following form: x.y.z. The x corresponds to a major version, the y corresponds to a minor version and the z corresponds to a patch version. If no semantic version is specified in a module or an import statement, then the default one is used - 0.0.0.
264
265 A major version number of 0 indicates that the model is still in development and is subject to change.
266
267 Following a release of major version 1, all modules will increment major version number when backwards incompatible changes to the model are made.
268
269 The minor version is changed when features are added to the model that do not impact current clients use of the model.
270
271 The patch version is incremented when non-feature changes (such as bugfixes or clarifications of human-readable descriptions that do not impact model functionality) are made that maintain backwards compatibility.
272
273 When importing a module with activated semantic version processing mode, only the module with the newest (highest) compatible semantic version is imported. Two semantic versions are compatible when all of the following conditions are met:
274
275 * the major version in the import statement and major version in the imported module are equal. For instance, 1.5.3 is compatible with 1.5.3, 1.5.4, 1.7.2, etc., but it is not compatible with 0.5.2 or 2.4.8, etc.
276
277 * the combination of minor version and patch version in the import statement is not higher than the one in the imported module. For instance, 1.5.2 is compatible with 1.5.2, 1.5.4, 1.6.8 etc. In fact, 1.5.2 is also compatible with versions like 1.5.1, 1.4.9 or 1.3.7 as they have equal major version. However, they will not be imported because their minor and patch version are lower (older).
278
279 If the import statement does not specify a semantic version, then the default one is chosen - 0.0.0. Thus, the module is imported only if it has a semantic version compatible with the default one, for example 0.0.0, 0.1.3, 0.3.5 and so on.
280
281 === Working with YANG Data
282 If you want to work with YANG Data you are going to need NormalizedNode objects that are specified in the YANG Data API. NormalizedNode is an interface at the top of the YANG Data hierarchy. It is extended through sub-interfaces which define the behaviour of specific NormalizedNode types like AnyXmlNode, ChoiceNode, LeafNode, ContainerNode, etc. Concrete implemenations of these interfaces are defined in yang-data-impl module. Once you have one or more NormalizedNode instances, you can perform CRUD operations on YANG data tree which is an in-memory database designed to store normalized nodes in a tree-like structure.
283
284 In some cases it is clear which NormalizedNode type belongs to which yang statement (e.g. AnyXmlNode, ChoiceNode, LeafNode). However, there are some normalized nodes which are named differently from their yang counterparts. They are listed below:
285
286 * LeafSetNode - leaf-list
287 * OrderedLeafSetNode - leaf-list that is ordered-by user
288 * LeafSetEntryNode - concrete entry in a leaf-list
289 * MapNode - keyed list
290 * OrderedMapNode - keyed list that is ordered-by user
291 * MapEntryNode - concrete entry in a keyed list
292 * UnkeyedListNode - unkeyed list
293 * UnkeyedListEntryNode - concrete entry in an unkeyed list
294
295 In order to create a concrete NormalizedNode object you can use the utility class Builders or ImmutableNodes. These classes can be found in yang-data-impl module and they provide methods for building each type of normalized node. Here is a simple example of building a normalized node:
296
297 [source, java]
298 ----
299 // example 1
300 ContainerNode containerNode = Builders.containerBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(QName.create(moduleQName, "example-container"))).build();
301
302 // example 2
303 ContainerNode containerNode2 = Builders.containerBuilder(containerSchemaNode).build();
304 ----
305 Both examples produce the same result. NodeIdentifier is one of the four types of YangInstanceIdentifier (these types are described in the javadoc of YangInstanceIdentifier). The purpose of YangInstanceIdentifier is to uniquely identify a particular node in the data tree. In the first example, you have to add NodeIdentifier before building the resulting node. In the second example it is also added using the provided ContainerSchemaNode object.
306
307 ImmutableNodes class offers similar builder methods and also adds an overloaded method called fromInstanceId() which allows you to create a NormalizedNode object based on YangInstanceIdentifier and SchemaContext. Below is an example which shows the use of this method.
308
309 [source, java]
310 ----
311 YangInstanceIdentifier.NodeIdentifier contId = new YangInstanceIdentifier.NodeIdentifier(QName.create(moduleQName, "example-container");
312
313 NormalizedNode contNode = ImmutableNodes.fromInstanceId(schemaContext, YangInstanceIdentifier.create(contId));
314 ----
315
316 Let us show a more complex example of creating a NormalizedNode. First, consider the following YANG module:
317
318 [source, yang]
319 ----
320 module example-module {
321     namespace "opendaylight.org/example-module";
322     prefix "example";
323
324     container parent-container {
325         container child-container {
326             list parent-ordered-list {
327                 ordered-by user;
328
329                 key "parent-key-leaf";
330
331                 leaf parent-key-leaf {
332                     type string;
333                 }
334
335                 leaf parent-ordinary-leaf {
336                     type string;
337                 }
338
339                 list child-ordered-list {
340                     ordered-by user;
341
342                     key "child-key-leaf";
343
344                     leaf child-key-leaf {
345                         type string;
346                     }
347
348                     leaf child-ordinary-leaf {
349                         type string;
350                     }
351                 }
352             }
353         }
354     }
355 }
356 ----
357
358 In the following example, two normalized nodes based on the module above are written to and read from the data tree.
359
360 [source, java]
361 ----
362         DataTree inMemoryDataTree = new InMemoryDataTreeFactory().create(
363                 DataTreeConfiguration.DEFAULT_OPERATIONAL, schemaContext);
364
365 // first data tree modification
366 MapEntryNode parentOrderedListEntryNode = Builders.mapEntryBuilder().withNodeIdentifier(
367 new YangInstanceIdentifier.NodeIdentifierWithPredicates(parentOrderedListQName, parentKeyLeafQName, "pkval1"))
368 .withChild(Builders.leafBuilder().withNodeIdentifier(
369 new YangInstanceIdentifier.NodeIdentifier(parentOrdinaryLeafQName)).withValue("plfval1").build()).build();
370
371 UserMapNode parentOrderedListNode = Builders.orderedMapBuilder().withNodeIdentifier(
372 new NodeIdentifier(parentOrderedListQName))
373 .withChild(parentOrderedListEntryNode).build();
374
375 ContainerNode parentContainerNode = Builders.containerBuilder().withNodeIdentifier(
376 new YangInstanceIdentifier.NodeIdentifier(parentContainerQName))
377 .withChild(Builders.containerBuilder().withNodeIdentifier(
378 new NodeIdentifier(childContainerQName)).withChild(parentOrderedListNode).build()).build();
379
380 YangInstanceIdentifier path1 = YangInstanceIdentifier.of(parentContainerQName);
381
382 DataTreeModification treeModification = inMemoryDataTree.takeSnapshot().newModification();
383 treeModification.write(path1, parentContainerNode);
384
385 // second data tree modification
386 MapEntryNode childOrderedListEntryNode = Builders.mapEntryBuilder().withNodeIdentifier(
387 new YangInstanceIdentifier.NodeIdentifierWithPredicates(
388 childOrderedListQName, childKeyLeafQName, "chkval1"))
389 .withChild(Builders.leafBuilder().withNodeIdentifier(
390 new YangInstanceIdentifier.NodeIdentifier(childOrdinaryLeafQName))
391 .withValue("chlfval1").build()).build();
392
393 UserMapNode childOrderedListNode = Builders.orderedMapBuilder().withNodeIdentifier(
394 new NodeIdentifier(childOrderedListQName))
395 .withChild(childOrderedListEntryNode).build();
396
397 ImmutableMap.Builder<QName, Object> builder = ImmutableMap.builder();
398 ImmutableMap<QName, Object> keys = builder.put(parentKeyLeafQName, "pkval1").build();
399
400 YangInstanceIdentifier path2 = YangInstanceIdentifier.of(parentContainerQName).node(childContainerQName)
401 .node(parentOrderedListQName).node(new NodeIdentifierWithPredicates(parentOrderedListQName, keys)).node(childOrderedListQName);
402
403 treeModification.write(path2, childOrderedListNode);
404 treeModification.ready();
405 inMemoryDataTree.validate(treeModification);
406 inMemoryDataTree.commit(inMemoryDataTree.prepare(treeModification));
407
408 DataTreeSnapshot snapshotAfterCommits = inMemoryDataTree.takeSnapshot();
409 Optional<NormalizedNode> readNode1 = snapshotAfterCommits.readNode(path1);
410 Optional<NormalizedNode> readNode2 = snapshotAfterCommits.readNode(path2);
411 ----
412 First comes the creation of in-memory data tree instance. The schema context (containing the model mentioned above) of this tree is set. After that, two normalized nodes are built. The first one consists of a parent container, a child container and a parent ordered list which contains a key leaf and an ordinary leaf. The second normalized node is a child ordered list that also contains a key leaf and an ordinary leaf.
413
414 In order to add a child node to a node, method withChild() is used. It takes a NormalizedNode as argument. When creating a list entry, YangInstanceIdentifier.NodeIdentifierWithPredicates should be used as its identifier. Its arguments are the QName of the list, QName of the list key and the value of the key. Method withValue() specifies a value for the ordinary leaf in the list.
415
416 Before writing a node to the data tree, a path (YangInstanceIdentifier) which determines its place in the data tree needs to be defined. The path of the first normalized node starts at the parent container. The path of the second normalized node points to the child ordered list contained in the parent ordered list entry specified by the key value "pkval1".
417
418 Write operation is performed with both normalized nodes mentioned earlier. It consist of several steps. The first step is to instantiate a DataTreeModification object based on a DataTreeSnapshot. DataTreeSnapshot gives you the current state of the data tree. Then comes the write operation which writes a normalized node at the provided path in the data tree. After doing both write operations, method ready() has to be called, marking the modification as ready for application to the data tree. No further operations within the modification are allowed. The modification is then validated - checked whether it can be applied to the data tree. Finally we commit it to the data tree.
419
420 Now you can access the written nodes. In order to do this, you have to create a new DataTreeSnapshot instance and call the method readNode() with path argument pointing to a particular node in the tree.
421
422 === Serialization / deserialization of YANG Data
423 If you want to deserialize YANG-modeled data which have the form of an XML document, you can use the XML parser found in the module yang-data-codec-xml. The parser walks through the XML document containing YANG-modeled data based on the provided SchemaContext and emits node events into a NormalizedNodeStreamWriter. The parser disallows multiple instances of the same element except for leaf-list and list entries. The parser also expects that the YANG-modeled data in the XML source are wrapped in a root element. Otherwise it will not work correctly.
424
425 Here is an example of using the XML parser.
426 [source, java]
427 ----
428 InputStream resourceAsStream = ExampleClass.class.getResourceAsStream("/example-module.yang");
429
430 XMLInputFactory factory = XMLInputFactory.newInstance();
431 XMLStreamReader reader = factory.createXMLStreamReader(resourceAsStream);
432
433 NormalizedNodeResult result = new NormalizedNodeResult();
434 NormalizedNodeStreamWriter streamWriter = ImmutableNormalizedNodeStreamWriter.from(result);
435
436 XmlParserStream xmlParser = XmlParserStream.create(streamWriter, schemaContext);
437 xmlParser.parse(reader);
438
439 NormalizedNode transformedInput = result.getResult();
440 ----
441 The XML parser utilizes the javax.xml.stream.XMLStreamReader for parsing an XML document. First, you should create an instance of this reader using XMLInputFactory and then load an XML document (in the form of InputStream object) into it.
442
443 In order to emit node events while parsing the data you need to instantiate a NormalizedNodeStreamWriter. This writer is actually an interface and therefore you need to use a concrete implementation of it. In this example it is the ImmutableNormalizedNodeStreamWriter, which constructs immutable instances of NormalizedNodes.
444
445 There are two ways how to create an instance of this writer using the static overloaded method from(). One version of this method takes a NormalizedNodeResult as argument. This object type is a result holder in which the resulting NormalizedNode will be stored. The other version takes a
446 NormalizedNodeContainerBuilder as argument. All created nodes will be written to this builder.
447
448 Next step is to create an instance of the XML parser. The parser itself is represented by a class named XmlParserStream. You can use one of two versions of the static overloaded method create() to construct this object. One version accepts a NormalizedNodeStreamWriter and a SchemaContext as arguments, the other version takes the same arguments plus a SchemaNode. Node events are emitted to the writer. The SchemaContext is used to check if the YANG data in the XML source comply with the provided YANG model(s). The last argument, a SchemaNode object, describes the node that is the parent of nodes defined in the XML data. If you do not provide this argument, the parser sets the SchemaContext as the parent node.
449
450 The parser is now ready to walk through the XML. Parsing is initiated by calling the method parse() on the XmlParserStream object with XMLStreamReader as its argument.
451
452 Finally you can access the result of parsing - a tree of NormalizedNodes containg the data as they are defined in the parsed XML document - by calling the method getResult() on the NormalizedNodeResult object.
453
454 === Introducing schema source repositories
455
456 === Writing YANG driven generators
457
458 === Introducing specific extension support for YANG parser
459
460 === Diagnostics