66384fe93e4678f4b389bee6ed7b0bb0c040cd8b
[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.dom;
22
23 import java.io.BufferedReader;
24 import java.io.StringReader;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Map.Entry;
31 import java.util.Optional;
32 import java.util.Set;
33 import java.util.stream.Collectors;
34
35 import org.w3c.dom.Attr;
36 import org.w3c.dom.Element;
37 import org.w3c.dom.NamedNodeMap;
38 import org.w3c.dom.Node;
39 import org.w3c.dom.NodeList;
40
41 import org.oran.smo.yangtools.parser.ParserExecutionContext;
42 import org.oran.smo.yangtools.parser.PrefixResolver;
43 import org.oran.smo.yangtools.parser.data.YangData;
44 import org.oran.smo.yangtools.parser.data.dom.YangDataDomDocumentRoot.SourceDataType;
45 import org.oran.smo.yangtools.parser.data.parser.JsonParser.HasLineAndColumn;
46 import org.oran.smo.yangtools.parser.data.parser.JsonParser.JsonArray;
47 import org.oran.smo.yangtools.parser.data.parser.JsonParser.JsonObject;
48 import org.oran.smo.yangtools.parser.data.parser.JsonParser.JsonObjectMemberName;
49 import org.oran.smo.yangtools.parser.data.parser.JsonParser.JsonPrimitive;
50 import org.oran.smo.yangtools.parser.data.parser.JsonParser.JsonValue;
51 import org.oran.smo.yangtools.parser.findings.Finding;
52 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
53 import org.oran.smo.yangtools.parser.model.schema.ModuleAndNamespaceResolver;
54 import org.oran.smo.yangtools.parser.util.QNameHelper;
55
56 /**
57  * Represents a node in the data tree. The node can be structural (container,
58  * list) or content (leaf, leaf-list).
59  *
60  * @author Mark Hollmann
61  */
62 public class YangDataDomNode {
63
64     public final static String LINE_NUMBER_KEY_NAME = "lineNumber";
65     public final static String COLUMN_NUMBER_KEY_NAME = "colNumber";
66
67     protected final static String ROOT_SLASH = "/";
68
69     private final String name;
70     private String namespace;
71     private String moduleName;
72
73     private final Object value;
74
75     private final int lineNumber;
76     private final int columnNumber;
77
78     private final PrefixResolver prefixResolver;                // only applies to XML
79
80     private YangDataDomNode parentNode;
81     private final List<YangDataDomNode> children = new ArrayList<>();
82
83     private final YangDataDomDocumentRoot documentRoot;
84
85     private List<YangDataDomNodeAnnotationValue> annotations = null;
86
87     /**
88      * Findings made in respect of this piece of data, if any.
89      */
90     private Set<Finding> findings = null;
91
92     /**
93      * Special constructor just for the data DOM document root.
94      */
95     protected YangDataDomNode() {
96         this.name = ROOT_SLASH;
97         this.namespace = ROOT_SLASH;
98         this.moduleName = ROOT_SLASH;
99         this.value = null;
100         this.lineNumber = 0;
101         this.columnNumber = 0;
102         this.prefixResolver = new PrefixResolver();
103         this.documentRoot = (YangDataDomDocumentRoot) this;
104     }
105
106     /**
107      * Returns the name of the data node.
108      */
109     public String getName() {
110         return name;
111     }
112
113     /**
114      * Returns the namespace of the data node. May return null if data was JSON encoded and
115      * namespaces were not resolved yet.
116      */
117     public String getNamespace() {
118         return namespace;
119     }
120
121     /**
122      * Returns the module of the data node. May return null if data was XML encoded and
123      * module names were not resolved yet.
124      */
125     public String getModuleName() {
126         return moduleName;
127     }
128
129     /**
130      * Returns the source type of the data.
131      */
132     public SourceDataType getSourceDataType() {
133         return getDocumentRoot().getSourceDataType();
134     }
135
136     /**
137      * Returns the value of this data DOM node. The data type of the returned object
138      * depends on the input that was used to construct this object. If it was XML,
139      * then the data type will always be String. If the input was JSON, then the data
140      * type may be String, Double, or Boolean. May return null if explicitly set to
141      * NIL in XML input or null in JSON input.
142      */
143     public Object getValue() {
144         return value;
145     }
146
147     /**
148      * Returns a stringefied representation of the value. May return null. Note that
149      * Double objects that are integer will not be returned in integer format, but
150      * in double format.
151      */
152     public String getStringValue() {
153         return value == null ? null : value.toString();
154     }
155
156     public int getLineNumber() {
157         return lineNumber;
158     }
159
160     public int getColumnNumber() {
161         return columnNumber;
162     }
163
164     public YangDataDomNode getParentNode() {
165         return parentNode;
166     }
167
168     public List<YangDataDomNode> getChildren() {
169         return Collections.unmodifiableList(children);
170     }
171
172     public YangDataDomDocumentRoot getDocumentRoot() {
173         return documentRoot;
174     }
175
176     public YangData getYangData() {
177         return getDocumentRoot().getYangData();
178     }
179
180     /**
181      * For anydata and anyxml we need to re-construct the descendant tree as the client may wish to parse it further.
182      */
183     public String getReassembledChildren() {
184         // TODO when really needed.
185         return "";
186     }
187
188     /**
189      * Return all child DOM nodes with the specified name and namespace/module.
190      */
191     public List<YangDataDomNode> getChildren(final String soughtNamespace, final String soughtModuleName,
192             final String soughtName) {
193         return children.stream().filter(child -> {
194             if (!child.getName().equals(soughtName)) {
195                 return false;
196             }
197             if (child.getNamespace() != null && child.getNamespace().equals(soughtNamespace)) {
198                 return true;
199             }
200             if (child.getModuleName() != null && child.getModuleName().equals(soughtModuleName)) {
201                 return true;
202             }
203             return false;
204         }).collect(Collectors.toList());
205     }
206
207     /**
208      * Returns a single child DOM node with the specified name and namespace or module. Returns null if not found.
209      */
210     public YangDataDomNode getChild(final String soughtNamespace, final String soughtModuleName, final String soughtName) {
211
212         for (final YangDataDomNode child : children) {
213             if (child.getName().equals(soughtName)) {
214                 if (child.getNamespace() != null && child.getNamespace().equals(soughtNamespace)) {
215                     return child;
216                 }
217                 if (child.getModuleName() != null && child.getModuleName().equals(soughtModuleName)) {
218                     return child;
219                 }
220             }
221         }
222
223         return null;
224     }
225
226     public PrefixResolver getPrefixResolver() {
227         return prefixResolver;
228     }
229
230     public List<YangDataDomNodeAnnotationValue> getAnnotations() {
231         return annotations == null ?
232                 Collections.<YangDataDomNodeAnnotationValue> emptyList() :
233                 Collections.unmodifiableList(annotations);
234     }
235
236     /**
237      * Returns a human-readable string with the full path to the DOM node.
238      */
239     public String getPath() {
240
241         final List<YangDataDomNode> dataDomNodes = new ArrayList<>(10);
242         YangDataDomNode runDataDomNode = this;
243
244         while (!(runDataDomNode instanceof YangDataDomDocumentRoot)) {
245             dataDomNodes.add(0, runDataDomNode);
246             runDataDomNode = runDataDomNode.getParentNode();
247         }
248
249         final StringBuilder sb = new StringBuilder();
250         for (final YangDataDomNode domNode : dataDomNodes) {
251             sb.append('/').append(domNode.getName());
252         }
253
254         return sb.toString();
255     }
256
257     /**
258      * Depending on the source (JSON or XML) either module name or namespace will be missing after the
259      * initial construction of the tree. This here will fix up the missing bits of information.
260      */
261     public void resolveModuleOrNamespace(final ModuleAndNamespaceResolver resolver) {
262
263         if (moduleName == null && namespace != null) {
264             moduleName = resolver.getModuleForNamespace(namespace);
265         } else if (namespace == null && moduleName != null) {
266             namespace = resolver.getNamespaceForModule(moduleName);
267         }
268
269         if (annotations != null) {
270             annotations.forEach(anno -> anno.resolveModuleOrNamespace(resolver));
271         }
272
273         children.forEach(child -> child.resolveModuleOrNamespace(resolver));
274     }
275
276     public void addFinding(final Finding finding) {
277         if (findings == null) {
278             findings = new HashSet<>();
279         }
280         findings.add(finding);
281     }
282
283     /**
284      * Returns the findings for this data DOM node. Returns empty set if no findings found.
285      */
286     public Set<Finding> getFindings() {
287         return findings == null ? Collections.<Finding> emptySet() : findings;
288     }
289
290     @Override
291     public String toString() {
292         return value == null ? name : name + " " + value;
293     }
294
295     // ===================================== XML processing ==================================
296
297     /**
298      * Constructor for a data node instance encoded in XML.
299      */
300     public YangDataDomNode(final ParserExecutionContext context, final YangDataDomNode parentNode,
301             final Element xmlDomElement) {
302
303         parentNode.children.add(this);
304         this.parentNode = parentNode;
305
306         this.documentRoot = parentNode.getDocumentRoot();
307
308         this.lineNumber = xmlDomElement.getUserData(LINE_NUMBER_KEY_NAME) == null ?
309                 0 :
310                 ((Integer) xmlDomElement.getUserData(LINE_NUMBER_KEY_NAME)).intValue();
311         this.columnNumber = xmlDomElement.getUserData(COLUMN_NUMBER_KEY_NAME) == null ?
312                 0 :
313                 ((Integer) xmlDomElement.getUserData(COLUMN_NUMBER_KEY_NAME)).intValue();
314
315         final List<Attr> xmlAttributes = getAttributesfromXmlElement(xmlDomElement);
316
317         /*
318          * Namespace handling.
319          *
320          * Before doing anything else, we need to extract prefix mappings from the XML element. These are
321          * usually placed at the top of the document, but could be anywhere in the tree really.
322          *
323          * - If the element does not define any namespaces, then we use the prefix resolver of the parent
324          *   DOM node to save on memory.
325          * - If namespaces are defined, we create a new prefix resolver, clone the contents of the prefix
326          *   resolver of the parent, and overwrite with whatever is defined here.
327          *
328          * An example in XML is as follows:
329          *
330          * <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
331          *   <enable-nacm>true</enable-nacm>
332          *   ... stuff ...
333          * </nacm>
334          *
335          * The default namespace is always defined with xmlns="...namespace..."; a named namespace is
336          * always defined with xmlns:somename="...namespace...".
337          */
338         if (hasNamespaceMappings(xmlAttributes)) {
339             this.prefixResolver = parentNode.getPrefixResolver().clone();
340             populateXmlPrefixResolver(xmlAttributes, prefixResolver);
341         } else {
342             this.prefixResolver = parentNode.getPrefixResolver();
343         }
344
345         /*
346          * Annotation handling.
347          *
348          * RFC 7952 defines YANG annotations, which are encoded as XML attributes. Example:
349          *
350          * <foo
351          *       xmlns:elm="http://example.org/example-last-modified"
352          *       elm:last-modified="2015-09-16T10:27:35+02:00">
353          *   ...
354          * </foo>
355          *
356          * Above, the XML attribute "last-modified" denotes the value of the YANG annotation of the same name,
357          * that is defined in namespace "http://example.org/example-last-modified".
358          */
359         extractAnnotationsFromXmlAttributes(context, xmlAttributes, prefixResolver);
360
361         /*
362          * Extract the name and namespace of the data node. The name may or not be prefixed. Example:
363          *
364          * <foo>1234</foo>
365          * <bar:foo xmlns:bar="www.bar.com">1234</bar:foo>
366          *
367          * Note that a namespace does not have to be defined on the element itself - it could be defined
368          * further up the tree (and that's the reason why the prefix resolver is cloned if necessary).
369          */
370         this.name = QNameHelper.extractName(xmlDomElement.getTagName());
371
372         final String elemPrefix = QNameHelper.extractPrefix(xmlDomElement.getTagName());
373         this.namespace = prefixResolver.resolveNamespaceUri(elemPrefix);
374
375         if (namespace == null) {
376             context.addFinding(new Finding(this, ParserFindingType.P077_UNRESOLVABLE_PREFIX.toString(),
377                     "Prefix '" + elemPrefix + "' not resolvable to a namespace."));
378         }
379
380         /*
381          * Extract the value, if any, of the element. If it is a container / list, then it will not
382          * have a value. Example:
383          *
384          * <foo-container>
385          *   <bar-list>
386          *     <bar-name>name1</bar-name>
387          *     <bar-state>ENABLED</bar-state>
388          *   </bar-list>
389          *   <bar-list>
390          *     <bar-name>name1</bar-name>
391          *     <bar-state>ENABLED</bar-state>
392          *   </bar-list>
393          * </foo-container>
394          */
395         this.value = getValueOfXmlElement(xmlDomElement, prefixResolver);
396     }
397
398     public void processXmlChildElements(final ParserExecutionContext context, final Element xmlDomElement) {
399         /*
400          * Go through all XML child elements
401          */
402         final NodeList childXmlNodes = xmlDomElement.getChildNodes();
403         for (int i = 0; i < childXmlNodes.getLength(); ++i) {
404
405             final Node childXmlNode = childXmlNodes.item(i);
406
407             if (childXmlNode.getNodeType() == Node.ELEMENT_NODE) {
408                 final YangDataDomNode childYangDataDomNode = new YangDataDomNode(context, this, (Element) childXmlNode);
409                 childYangDataDomNode.processXmlChildElements(context, (Element) childXmlNode);
410             }
411         }
412     }
413
414     /**
415      * Extracts all XML Attributes from the XML element that define prefix-to-namespace mappings.
416      * Such XML attributes look as follows:
417      * <p>
418      * <supported-compression-types
419      * xmlns="urn:rdns:o-ran:oammodel:pm"
420      * xmlns:typese="urn:rdns:o-ran:oammodel:yang-types">
421      * </supported-compression-types>
422      */
423     private static boolean hasNamespaceMappings(final List<Attr> xmlAttributes) {
424         return xmlAttributes.stream().anyMatch(YangDataDomNode::attrDefinesPrefixMapping);
425     }
426
427     private static boolean attrDefinesPrefixMapping(final Attr attr) {
428         return attr.getName().equals("xmlns") || attr.getName().startsWith("xmlns:");
429     }
430
431     /**
432      * Given a prefix resolver, populates same with any namespace declarations
433      * found amongst the supplied list of XML attributes.
434      */
435     protected static void populateXmlPrefixResolver(final List<Attr> xmlAttributes, final PrefixResolver prefixResolver) {
436
437         for (final Attr attr : xmlAttributes) {
438             final String attrName = attr.getName();
439             final String attrValue = attr.getValue();
440
441             if (attrName.equals("xmlns")) {
442                 prefixResolver.setDefaultNamespaceUri(attrValue.intern());
443             } else if (attrName.startsWith("xmlns:")) {
444                 prefixResolver.addMapping(attrName.substring(6).intern(), attrValue.intern());
445             }
446         }
447     }
448
449     private void extractAnnotationsFromXmlAttributes(final ParserExecutionContext context, final List<Attr> xmlAttributes,
450             final PrefixResolver prefixResolver) {
451
452         if (xmlAttributes.isEmpty()) {
453             return;
454         }
455
456         /*
457          * We go over all XML attributes and whatever does not denote a namespace, or a nil value, we
458          * assume is an annotation.
459          */
460         for (final Attr attr : xmlAttributes) {
461
462             if (attrDefinesPrefixMapping(attr) || attrIsXsiNil(attr, prefixResolver)) {
463                 continue;
464             }
465
466             final String qName = attr.getName();
467
468             final String attrPrefix = QNameHelper.extractPrefix(qName);
469             final String attrNamespace = prefixResolver.resolveNamespaceUri(attrPrefix);
470             if (attrNamespace == null) {
471                 context.addFinding(new Finding(this, ParserFindingType.P077_UNRESOLVABLE_PREFIX.toString(),
472                         "Prefix '" + attrPrefix + "' not resolvable to a namespace."));
473                 continue;
474             }
475
476             if (this.annotations == null) {
477                 this.annotations = new ArrayList<>(2);
478             }
479
480             final String attrName = QNameHelper.extractName(qName);
481             this.annotations.add(new YangDataDomNodeAnnotationValue(attrNamespace, null, attrName, attr.getValue()));
482         }
483     }
484
485     /**
486      * Returns the value of the element. Note this may be null.
487      */
488     private static String getValueOfXmlElement(final Element xmlDomElement, final PrefixResolver prefixResolver) {
489
490         /*
491          * Null-value handling. An element can be explicitly expressed as being null by using xsi:isNull. Never
492          * seen in real life and not sure how it would make sense to have a null value in the data (just leave
493          * out the XML element...). Example:
494          *
495          * <foo
496          *       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
497          *       xsi:nil="true" />
498          *
499          * Note that according to w3 the XSI namespace must be explicitly declared.
500          */
501         if (elementIsNil(xmlDomElement, prefixResolver)) {
502             return null;
503         }
504
505         /*
506          * If the XML element has data (i.e. it is a leaf or leaf-list), then there should be a text node
507          * underneath (note: element.getNodeValue() is wrong to use).
508          */
509         final NodeList childXmlNodes = xmlDomElement.getChildNodes();
510         final StringBuilder value = new StringBuilder();
511
512         for (int i = 0; i < childXmlNodes.getLength(); ++i) {
513
514             final Node childXmlNode = childXmlNodes.item(i);
515
516             switch (childXmlNode.getNodeType()) {
517                 case Node.TEXT_NODE:
518                     /*
519                      * The text node will contain all of the control characters and the whitespaces; all of
520                      * which needs to be stripped out to arrive at something that makes sense (or nothing).
521                      */
522                     cleanXmlText(childXmlNode.getNodeValue(), value);
523                     break;
524
525                 case Node.CDATA_SECTION_NODE:
526
527                     value.append(childXmlNode.getNodeValue());
528                     break;
529
530                 case Node.ELEMENT_NODE:
531                     /*
532                      * An element node exists under this one here. That implies that this element here is a
533                      * container / list, so it cannot have a value. Would be incorrect XML.
534                      */
535                     return null;
536             }
537         }
538
539         return value.toString();
540     }
541
542     /**
543      * Returns whether xsi:nil is present in the list of XML attributes.
544      */
545     private static boolean elementIsNil(final Element xmlDomElement, final PrefixResolver prefixResolver) {
546         return getAttributesfromXmlElement(xmlDomElement).stream().anyMatch(attr -> attrIsXsiNilTrue(attr, prefixResolver));
547     }
548
549     /**
550      * Returns whether the XML attribute is XSI NIL.
551      */
552     private static boolean attrIsXsiNil(final Attr attr, final PrefixResolver prefixResolver) {
553
554         final String qName = attr.getName();
555
556         final String attrName = QNameHelper.extractName(qName);
557         final String attrPrefix = QNameHelper.extractPrefix(qName);
558         final String attrNamespace = prefixResolver.resolveNamespaceUri(attrPrefix);
559
560         return ("http://www.w3.org/2001/XMLSchema-instance".equals(attrNamespace) && "nil".equals(attrName));
561     }
562
563     /**
564      * Returns whether the XML attribute denotes XSI NIL with value true.
565      */
566     private static boolean attrIsXsiNilTrue(final Attr attr, final PrefixResolver prefixResolver) {
567         return "true".equalsIgnoreCase(attr.getValue()) && attrIsXsiNil(attr, prefixResolver);
568     }
569
570     protected static List<Attr> getAttributesfromXmlElement(final Element xmlDomElement) {
571
572         List<Attr> result = null;
573
574         final NamedNodeMap attributesNodeMap = xmlDomElement.getAttributes();
575         for (int i = 0; i < attributesNodeMap.getLength(); ++i) {
576             final Node item = attributesNodeMap.item(i);
577             if (item instanceof Attr) {
578                 if (result == null) {
579                     result = new ArrayList<>();
580                 }
581                 result.add((Attr) item);
582             }
583         }
584
585         return result != null ? result : Collections.<Attr> emptyList();
586     }
587
588     /**
589      * Strip out all control characters and leading and trailing whitespaces.
590      */
591     private static void cleanXmlText(final String inputString, final StringBuilder result) {
592
593         boolean containsNonWhitespaceChars = false;
594         boolean containsNewLine = false;
595
596         final char[] charArray = inputString.toCharArray();
597         for (int i = 0; i < charArray.length; ++i) {
598             final char c = charArray[i];
599             if (c == ' ' || c == '\t') {
600                 // all ok so far...
601             } else if (c == '\n') {
602                 containsNewLine = true;
603             } else {
604                 containsNonWhitespaceChars = true;
605             }
606
607             if (containsNewLine && containsNonWhitespaceChars) {
608                 break;
609             }
610         }
611
612         if (!containsNonWhitespaceChars) {
613             /*
614              * Contains only whitespace and/or new-line, all of which should be swallowed. We don't add
615              * anything to the result.
616              */
617             return;
618         }
619
620         /*
621          * Right...need to clean the string so. If there is no newline in it then we simply trim the string
622          * and we are done.
623          */
624         if (!containsNewLine) {
625             result.append(inputString.trim());
626             return;
627         }
628
629         /*
630          * Uhh...newlines in it...must strip these out, and trim every line individually...and then re-assemble.
631          */
632         final List<String> lines = new ArrayList<>();
633
634         try {
635             final BufferedReader br = new BufferedReader(new StringReader(inputString));
636             String str;
637             while ((str = br.readLine()) != null) {
638                 str = str.trim();
639                 if (!str.isEmpty()) {
640                     lines.add(str);
641                 }
642             }
643         } catch (Exception wontHappen) {
644         }
645
646         boolean first = true;
647         for (final String line : lines) {
648             if (!first) {
649                 result.append(' ');
650             }
651             result.append(line);
652             first = false;
653         }
654     }
655
656     // ====================================== JSON processing ===================================
657
658     /**
659      * Constructor for a data node instance encoded in JSON.
660      */
661     public YangDataDomNode(final ParserExecutionContext context, final YangDataDomNode parentNode, final String memberName,
662             final JsonValue jsonValue) {
663
664         parentNode.children.add(this);
665         this.parentNode = parentNode;
666
667         this.documentRoot = parentNode.getDocumentRoot();
668
669         this.lineNumber = jsonValue.line;
670         this.columnNumber = jsonValue.col;
671
672         this.name = extractName(memberName);
673         this.moduleName = extractModule(memberName, parentNode.getModuleName());
674         this.namespace = null;
675         this.value = jsonValue instanceof JsonPrimitive ? ((JsonPrimitive) jsonValue).getValue() : null;
676
677         this.prefixResolver = parentNode.getPrefixResolver();
678     }
679
680     /**
681      * Processing the child elements of the JSON object means that this instance here is either a container or a list.
682      */
683     protected void processJsonChildElements(final ParserExecutionContext context, final JsonObject jsonObject) {
684
685         final Map<JsonObjectMemberName, JsonValue> members = jsonObject.getValuesByMember();
686
687         /*
688          * Handle the annotations, if any, for this container or list.
689          */
690         final JsonObject annotationsForThis = getAnnotationJsonObject(context, jsonObject, "@");
691         extractAnnotationsFromJsonObject(context, annotationsForThis, this);
692
693         /*
694          * Handle any possible [null] element.
695          */
696         fixupEmptyHandling(jsonObject);
697
698         /*
699          * Process the data nodes
700          */
701         final Set<String> processedLeafAndLeafListMemberNames = new HashSet<>();
702
703         for (final Entry<JsonObjectMemberName, JsonValue> mapEntry : members.entrySet()) {
704
705             final String memberName = mapEntry.getKey().getMemberName();
706
707             /*
708              * Annotations will be handled separately as part of the data nodes, skip these here.
709              */
710             if (memberName.startsWith("@")) {
711                 continue;
712             }
713
714             /*
715              * What we do now depends on the type of value:
716              * - JsonObject = container
717              * - JsonScalar = leaf
718              * - JsonArray = list or leaf-list (need to peek at the array members)
719              */
720             final JsonValue value = mapEntry.getValue();
721
722             if (value instanceof JsonPrimitive) {                               // leaf
723                 /*
724                  * It is a leaf.
725                  */
726                 final YangDataDomNode leafDataDomNode = new YangDataDomNode(context, this, memberName,
727                         (JsonPrimitive) value);
728                 final JsonObject leafAnnotations = getAnnotationJsonObject(context, jsonObject, "@" + memberName);
729                 extractAnnotationsFromJsonObject(context, leafAnnotations, leafDataDomNode);
730
731                 processedLeafAndLeafListMemberNames.add(memberName);
732
733             } else if (value instanceof JsonObject) {           // container
734
735                 final YangDataDomNode containerDataDomNode = new YangDataDomNode(context, this, memberName,
736                         (JsonObject) value);
737                 containerDataDomNode.processJsonChildElements(context, (JsonObject) value);
738
739             } else {
740
741                 if (((JsonArray) value).getValues().isEmpty()) {
742
743                     // empty leaf list, nothing to do (should really not be in the JSON file)
744
745                 } else if (allMembersAreJsonPrimitives((JsonArray) value)) {            // leaf-list
746
747                     final List<JsonValue> values = ((JsonArray) value).getValues();
748                     final JsonArray leafListMemberAnnotations = getAnnotationJsonArray(context, jsonObject,
749                             "@" + memberName);
750                     final List<JsonValue> annotationValues = leafListMemberAnnotations.getValues();
751
752                     if (annotationValues.size() > values.size()) {
753                         issueFindingOnJsonElement(context, ParserFindingType.P069_UNEXPECTED_JSON_VALUE.toString(),
754                                 "The size of the JSON array for the annotations is bigger than the size of the JSON array used for the leaf-list values.",
755                                 leafListMemberAnnotations);
756                     }
757
758                     for (int i = 0; i < values.size(); ++i) {
759                         final YangDataDomNode leafListDataDomNode = new YangDataDomNode(context, this, memberName,
760                                 (JsonPrimitive) values.get(i));
761                         if (annotationValues.size() > i && annotationValues.get(i) != null) {
762                             extractAnnotationsFromJsonObject(context, (JsonObject) annotationValues.get(i),
763                                     leafListDataDomNode);
764                         }
765                     }
766
767                     processedLeafAndLeafListMemberNames.add(memberName);
768
769                 } else if (allMembersAreJsonObjects((JsonArray) value)) {               // list
770
771                     ((JsonArray) value).getValues().forEach(member -> {
772                         final YangDataDomNode childYangDataDomNode = new YangDataDomNode(context, this, memberName,
773                                 (JsonObject) member);
774                         childYangDataDomNode.processJsonChildElements(context, (JsonObject) member);
775                     });
776
777                 } else {                // wrong JSON
778
779                     issueFindingOnJsonElement(context, ParserFindingType.P070_WRONG_JSON_VALUE_TYPE.toString(),
780                             "The JSON array members must all either be objects or be primitives, but not a mixture of those.",
781                             value);
782                 }
783             }
784         }
785
786         /*
787          * We check for any orphaned annotations here.
788          */
789         for (final Entry<JsonObjectMemberName, JsonValue> mapEntry : members.entrySet()) {
790
791             final String memberName = mapEntry.getKey().getMemberName();
792
793             if (memberName.equals("@") || !memberName.startsWith("@")) {
794                 continue;
795             }
796
797             if (!processedLeafAndLeafListMemberNames.contains(memberName.substring(1))) {
798                 /*
799                  * We have an annotation with a member name for which we do not have a leaf or leaf-list.
800                  */
801                 issueFindingOnJsonElement(context, ParserFindingType.P069_UNEXPECTED_JSON_VALUE.toString(),
802                         "Annotation '" + memberName + "' cannot be matched up against a leaf or leaf-list with name '" + memberName
803                                 .substring(1) + "'.", mapEntry.getKey());
804             }
805         }
806     }
807
808     /**
809      * Replaces all occurrences of [null] (i.e. a JsonArray with a single primitive
810      * member being null) with null (i.e. a primitive value being null).
811      */
812     private static void fixupEmptyHandling(final JsonObject jsonObject) {
813
814         final Map<JsonObjectMemberName, JsonValue> members = jsonObject.getValuesByMember();
815         for (final JsonObjectMemberName key : new ArrayList<>(members.keySet())) {
816
817             final JsonValue memberValue = members.get(key);
818
819             if (denotesEmpty(memberValue)) {
820                 jsonObject.putMember(key, JsonPrimitive.valueOf(memberValue.line, memberValue.col, null));
821             } else if (memberValue instanceof JsonArray) {
822
823                 final JsonArray jsonArray = (JsonArray) memberValue;
824                 final List<JsonValue> arrayValues = jsonArray.getValues();
825
826                 for (int i = 0; i < arrayValues.size(); ++i) {
827                     final JsonValue arrayValue = arrayValues.get(i);
828                     if (denotesEmpty(arrayValue)) {
829                         jsonArray.setValue(i, JsonPrimitive.valueOf(arrayValue.line, arrayValue.col, null));
830                     }
831                 }
832             }
833         }
834     }
835
836     /**
837      * Returns whether the supplied JSON value is [null], denoting an empty value (special syntax for data type 'empty').
838      */
839     private static boolean denotesEmpty(final JsonValue jsonValue) {
840
841         if (jsonValue instanceof JsonArray) {
842             final JsonArray jsonArray = (JsonArray) jsonValue;
843             final List<JsonValue> values = jsonArray.getValues();
844
845             if (values.size() == 1) {
846                 final JsonValue firstArrayMember = values.get(0);
847                 if (firstArrayMember instanceof JsonPrimitive) {
848                     return ((JsonPrimitive) firstArrayMember).getValue() == null;
849                 }
850             }
851         }
852
853         return false;
854     }
855
856     /**
857      * Returns a JsonObject representing the annotations for the sought name.
858      */
859     private JsonObject getAnnotationJsonObject(final ParserExecutionContext context, final JsonObject parentJsonObject,
860             final String soughtName) {
861
862         final Optional<Entry<String, JsonValue>> anno = parentJsonObject.getValues().entrySet().stream().filter(
863                 entry -> entry.getKey().equals(soughtName)).findAny();
864
865         if (!anno.isPresent()) {
866             return new JsonObject();
867         }
868
869         /*
870          * Annotations must always be encoded as JSON objects.
871          */
872         final JsonValue annoObject = anno.get().getValue();
873         if (!(annoObject instanceof JsonObject)) {
874             issueFindingOnJsonElement(context, ParserFindingType.P070_WRONG_JSON_VALUE_TYPE.toString(),
875                     "Expected a JSON object to hold the annotations.", annoObject);
876             return new JsonObject();
877         }
878
879         return (JsonObject) annoObject;
880     }
881
882     /**
883      * Returns a JsonArray representing the annotations for the sought name.
884      */
885     private JsonArray getAnnotationJsonArray(final ParserExecutionContext context, final JsonObject parentJsonObject,
886             final String soughtName) {
887
888         final Optional<Entry<String, JsonValue>> anno = parentJsonObject.getValues().entrySet().stream().filter(
889                 entry -> entry.getKey().equals(soughtName)).findAny();
890
891         if (!anno.isPresent()) {
892             return new JsonArray();
893         }
894
895         final JsonValue annoObject = anno.get().getValue();
896         if (!(annoObject instanceof JsonArray)) {
897             issueFindingOnJsonElement(context, ParserFindingType.P070_WRONG_JSON_VALUE_TYPE.toString(),
898                     "Expected a JSON array to hold the annotations.", annoObject);
899             return new JsonArray();
900         }
901
902         /*
903          * All the members must be either a primitive null, or a JsonObject.
904          */
905         final JsonArray jsonArray = (JsonArray) annoObject;
906         final List<JsonValue> arrayValues = jsonArray.getValues();
907
908         for (int i = 0; i < arrayValues.size(); ++i) {
909
910             final JsonValue arrayMemberValue = arrayValues.get(i);
911
912             if (arrayMemberValue instanceof JsonPrimitive && ((JsonPrimitive) arrayMemberValue).equals(
913                     JsonPrimitive.NULL)) {
914                 // we generate an empty object to replace the null.
915                 jsonArray.setValue(i, new JsonObject());
916             } else if (arrayMemberValue instanceof JsonObject) {
917                 // ok, expected
918             } else {
919                 issueFindingOnJsonElement(context, ParserFindingType.P070_WRONG_JSON_VALUE_TYPE.toString(),
920                         "Expected a JSON object as array element to hold the annotations.", arrayMemberValue);
921                 return new JsonArray();
922             }
923         }
924
925         return jsonArray;
926     }
927
928     private void extractAnnotationsFromJsonObject(final ParserExecutionContext context,
929             final JsonObject jsonObjectWithAnnotations, final YangDataDomNode owningDomNode) {
930
931         if (jsonObjectWithAnnotations.getValues().isEmpty()) {
932             return;
933         }
934
935         owningDomNode.annotations = new ArrayList<>();
936
937         /*
938          * Clean up [null] handling, will be needed where the annotation does not have an argument.
939          */
940         fixupEmptyHandling(jsonObjectWithAnnotations);
941
942         /*
943          * Iterate over the members of the object - these are the annotations.
944          */
945         final Map<JsonObjectMemberName, JsonValue> annoMembers = jsonObjectWithAnnotations.getValuesByMember();
946         for (final Entry<JsonObjectMemberName, JsonValue> entry : annoMembers.entrySet()) {
947
948             final String moduleAndAnnoName = entry.getKey().getMemberName();
949
950             /*
951              * According to RFC, the annotation name MUST be prefixed with the module - section 5.2.1 in RFC 7952...
952              */
953             final String moduleName = extractModule(moduleAndAnnoName, null);
954             final String annoName = extractName(moduleAndAnnoName);
955
956             if (moduleName == null) {
957                 issueFindingOnJsonElement(context, ParserFindingType.P015_INVALID_SYNTAX_IN_DOCUMENT.toString(),
958                         "All members of the JSON object used for annotation values must be prefixed with the module name.",
959                         entry.getKey());
960                 continue;
961             }
962
963             owningDomNode.annotations.add(new YangDataDomNodeAnnotationValue(null, moduleName, annoName,
964                     ((JsonPrimitive) entry.getValue()).getValue()));
965         }
966     }
967
968     private static String extractName(final String memberName) {
969         return memberName.contains(":") ? memberName.split(":")[1] : memberName;
970     }
971
972     private static String extractModule(final String memberName, final String parentModuleName) {
973         return memberName.contains(":") ? memberName.split(":")[0] : parentModuleName;
974     }
975
976     private static boolean allMembersAreJsonPrimitives(final JsonArray array) {
977         final List<JsonValue> values = array.getValues();
978         for (final JsonValue value : values) {
979             if (!(value instanceof JsonPrimitive)) {
980                 return false;
981             }
982         }
983         return true;
984     }
985
986     private static boolean allMembersAreJsonObjects(final JsonArray array) {
987         final List<JsonValue> values = array.getValues();
988         for (final JsonValue value : values) {
989             if (!(value instanceof JsonObject)) {
990                 return false;
991             }
992         }
993         return true;
994     }
995
996     private void issueFindingOnJsonElement(final ParserExecutionContext context, final String findingType,
997             final String message, final HasLineAndColumn problematicJsonElement) {
998         context.addFinding(new Finding(getYangData(), findingType, message, problematicJsonElement.line,
999                 problematicJsonElement.col));
1000     }
1001 }