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.dom;
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;
30 import java.util.Map.Entry;
31 import java.util.Optional;
33 import java.util.stream.Collectors;
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;
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;
57 * Represents a node in the data tree. The node can be structural (container,
58 * list) or content (leaf, leaf-list).
60 * @author Mark Hollmann
62 public class YangDataDomNode {
64 public final static String LINE_NUMBER_KEY_NAME = "lineNumber";
65 public final static String COLUMN_NUMBER_KEY_NAME = "colNumber";
67 protected final static String ROOT_SLASH = "/";
69 private final String name;
70 private String namespace;
71 private String moduleName;
73 private final Object value;
75 private final int lineNumber;
76 private final int columnNumber;
78 private final PrefixResolver prefixResolver; // only applies to XML
80 private YangDataDomNode parentNode;
81 private final List<YangDataDomNode> children = new ArrayList<>();
83 private final YangDataDomDocumentRoot documentRoot;
85 private List<YangDataDomNodeAnnotationValue> annotations = null;
88 * Findings made in respect of this piece of data, if any.
90 private Set<Finding> findings = null;
93 * Special constructor just for the data DOM document root.
95 protected YangDataDomNode() {
96 this.name = ROOT_SLASH;
97 this.namespace = ROOT_SLASH;
98 this.moduleName = ROOT_SLASH;
101 this.columnNumber = 0;
102 this.prefixResolver = new PrefixResolver();
103 this.documentRoot = (YangDataDomDocumentRoot) this;
107 * Returns the name of the data node.
109 public String getName() {
114 * Returns the namespace of the data node. May return null if data was JSON encoded and
115 * namespaces were not resolved yet.
117 public String getNamespace() {
122 * Returns the module of the data node. May return null if data was XML encoded and
123 * module names were not resolved yet.
125 public String getModuleName() {
130 * Returns the source type of the data.
132 public SourceDataType getSourceDataType() {
133 return getDocumentRoot().getSourceDataType();
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.
143 public Object getValue() {
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
152 public String getStringValue() {
153 return value == null ? null : value.toString();
156 public int getLineNumber() {
160 public int getColumnNumber() {
164 public YangDataDomNode getParentNode() {
168 public List<YangDataDomNode> getChildren() {
169 return Collections.unmodifiableList(children);
172 public YangDataDomDocumentRoot getDocumentRoot() {
176 public YangData getYangData() {
177 return getDocumentRoot().getYangData();
181 * For anydata and anyxml we need to re-construct the descendant tree as the client may wish to parse it further.
183 public String getReassembledChildren() {
184 // TODO when really needed.
189 * Return all child DOM nodes with the specified name and namespace/module.
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)) {
197 if (child.getNamespace() != null && child.getNamespace().equals(soughtNamespace)) {
200 if (child.getModuleName() != null && child.getModuleName().equals(soughtModuleName)) {
204 }).collect(Collectors.toList());
208 * Returns a single child DOM node with the specified name and namespace or module. Returns null if not found.
210 public YangDataDomNode getChild(final String soughtNamespace, final String soughtModuleName, final String soughtName) {
212 for (final YangDataDomNode child : children) {
213 if (child.getName().equals(soughtName)) {
214 if (child.getNamespace() != null && child.getNamespace().equals(soughtNamespace)) {
217 if (child.getModuleName() != null && child.getModuleName().equals(soughtModuleName)) {
226 public PrefixResolver getPrefixResolver() {
227 return prefixResolver;
230 public List<YangDataDomNodeAnnotationValue> getAnnotations() {
231 return annotations == null ?
232 Collections.<YangDataDomNodeAnnotationValue> emptyList() :
233 Collections.unmodifiableList(annotations);
237 * Returns a human-readable string with the full path to the DOM node.
239 public String getPath() {
241 final List<YangDataDomNode> dataDomNodes = new ArrayList<>(10);
242 YangDataDomNode runDataDomNode = this;
244 while (!(runDataDomNode instanceof YangDataDomDocumentRoot)) {
245 dataDomNodes.add(0, runDataDomNode);
246 runDataDomNode = runDataDomNode.getParentNode();
249 final StringBuilder sb = new StringBuilder();
250 for (final YangDataDomNode domNode : dataDomNodes) {
251 sb.append('/').append(domNode.getName());
254 return sb.toString();
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.
261 public void resolveModuleOrNamespace(final ModuleAndNamespaceResolver resolver) {
263 if (moduleName == null && namespace != null) {
264 moduleName = resolver.getModuleForNamespace(namespace);
265 } else if (namespace == null && moduleName != null) {
266 namespace = resolver.getNamespaceForModule(moduleName);
269 if (annotations != null) {
270 annotations.forEach(anno -> anno.resolveModuleOrNamespace(resolver));
273 children.forEach(child -> child.resolveModuleOrNamespace(resolver));
276 public void addFinding(final Finding finding) {
277 if (findings == null) {
278 findings = new HashSet<>();
280 findings.add(finding);
284 * Returns the findings for this data DOM node. Returns empty set if no findings found.
286 public Set<Finding> getFindings() {
287 return findings == null ? Collections.<Finding> emptySet() : findings;
291 public String toString() {
292 return value == null ? name : name + " " + value;
295 // ===================================== XML processing ==================================
298 * Constructor for a data node instance encoded in XML.
300 public YangDataDomNode(final ParserExecutionContext context, final YangDataDomNode parentNode,
301 final Element xmlDomElement) {
303 parentNode.children.add(this);
304 this.parentNode = parentNode;
306 this.documentRoot = parentNode.getDocumentRoot();
308 this.lineNumber = xmlDomElement.getUserData(LINE_NUMBER_KEY_NAME) == null ?
310 ((Integer) xmlDomElement.getUserData(LINE_NUMBER_KEY_NAME)).intValue();
311 this.columnNumber = xmlDomElement.getUserData(COLUMN_NUMBER_KEY_NAME) == null ?
313 ((Integer) xmlDomElement.getUserData(COLUMN_NUMBER_KEY_NAME)).intValue();
315 final List<Attr> xmlAttributes = getAttributesfromXmlElement(xmlDomElement);
318 * Namespace handling.
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.
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.
328 * An example in XML is as follows:
330 * <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
331 * <enable-nacm>true</enable-nacm>
335 * The default namespace is always defined with xmlns="...namespace..."; a named namespace is
336 * always defined with xmlns:somename="...namespace...".
338 if (hasNamespaceMappings(xmlAttributes)) {
339 this.prefixResolver = parentNode.getPrefixResolver().clone();
340 populateXmlPrefixResolver(xmlAttributes, prefixResolver);
342 this.prefixResolver = parentNode.getPrefixResolver();
346 * Annotation handling.
348 * RFC 7952 defines YANG annotations, which are encoded as XML attributes. Example:
351 * xmlns:elm="http://example.org/example-last-modified"
352 * elm:last-modified="2015-09-16T10:27:35+02:00">
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".
359 extractAnnotationsFromXmlAttributes(context, xmlAttributes, prefixResolver);
362 * Extract the name and namespace of the data node. The name may or not be prefixed. Example:
365 * <bar:foo xmlns:bar="www.bar.com">1234</bar:foo>
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).
370 this.name = QNameHelper.extractName(xmlDomElement.getTagName());
372 final String elemPrefix = QNameHelper.extractPrefix(xmlDomElement.getTagName());
373 this.namespace = prefixResolver.resolveNamespaceUri(elemPrefix);
375 if (namespace == null) {
376 context.addFinding(new Finding(this, ParserFindingType.P077_UNRESOLVABLE_PREFIX.toString(),
377 "Prefix '" + elemPrefix + "' not resolvable to a namespace."));
381 * Extract the value, if any, of the element. If it is a container / list, then it will not
382 * have a value. Example:
386 * <bar-name>name1</bar-name>
387 * <bar-state>ENABLED</bar-state>
390 * <bar-name>name1</bar-name>
391 * <bar-state>ENABLED</bar-state>
395 this.value = getValueOfXmlElement(xmlDomElement, prefixResolver);
398 public void processXmlChildElements(final ParserExecutionContext context, final Element xmlDomElement) {
400 * Go through all XML child elements
402 final NodeList childXmlNodes = xmlDomElement.getChildNodes();
403 for (int i = 0; i < childXmlNodes.getLength(); ++i) {
405 final Node childXmlNode = childXmlNodes.item(i);
407 if (childXmlNode.getNodeType() == Node.ELEMENT_NODE) {
408 final YangDataDomNode childYangDataDomNode = new YangDataDomNode(context, this, (Element) childXmlNode);
409 childYangDataDomNode.processXmlChildElements(context, (Element) childXmlNode);
415 * Extracts all XML Attributes from the XML element that define prefix-to-namespace mappings.
416 * Such XML attributes look as follows:
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>
423 private static boolean hasNamespaceMappings(final List<Attr> xmlAttributes) {
424 return xmlAttributes.stream().anyMatch(YangDataDomNode::attrDefinesPrefixMapping);
427 private static boolean attrDefinesPrefixMapping(final Attr attr) {
428 return attr.getName().equals("xmlns") || attr.getName().startsWith("xmlns:");
432 * Given a prefix resolver, populates same with any namespace declarations
433 * found amongst the supplied list of XML attributes.
435 protected static void populateXmlPrefixResolver(final List<Attr> xmlAttributes, final PrefixResolver prefixResolver) {
437 for (final Attr attr : xmlAttributes) {
438 final String attrName = attr.getName();
439 final String attrValue = attr.getValue();
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());
449 private void extractAnnotationsFromXmlAttributes(final ParserExecutionContext context, final List<Attr> xmlAttributes,
450 final PrefixResolver prefixResolver) {
452 if (xmlAttributes.isEmpty()) {
457 * We go over all XML attributes and whatever does not denote a namespace, or a nil value, we
458 * assume is an annotation.
460 for (final Attr attr : xmlAttributes) {
462 if (attrDefinesPrefixMapping(attr) || attrIsXsiNil(attr, prefixResolver)) {
466 final String qName = attr.getName();
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."));
476 if (this.annotations == null) {
477 this.annotations = new ArrayList<>(2);
480 final String attrName = QNameHelper.extractName(qName);
481 this.annotations.add(new YangDataDomNodeAnnotationValue(attrNamespace, null, attrName, attr.getValue()));
486 * Returns the value of the element. Note this may be null.
488 private static String getValueOfXmlElement(final Element xmlDomElement, final PrefixResolver prefixResolver) {
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:
496 * xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
499 * Note that according to w3 the XSI namespace must be explicitly declared.
501 if (elementIsNil(xmlDomElement, prefixResolver)) {
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).
509 final NodeList childXmlNodes = xmlDomElement.getChildNodes();
510 final StringBuilder value = new StringBuilder();
512 for (int i = 0; i < childXmlNodes.getLength(); ++i) {
514 final Node childXmlNode = childXmlNodes.item(i);
516 switch (childXmlNode.getNodeType()) {
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).
522 cleanXmlText(childXmlNode.getNodeValue(), value);
525 case Node.CDATA_SECTION_NODE:
527 value.append(childXmlNode.getNodeValue());
530 case Node.ELEMENT_NODE:
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.
539 return value.toString();
543 * Returns whether xsi:nil is present in the list of XML attributes.
545 private static boolean elementIsNil(final Element xmlDomElement, final PrefixResolver prefixResolver) {
546 return getAttributesfromXmlElement(xmlDomElement).stream().anyMatch(attr -> attrIsXsiNilTrue(attr, prefixResolver));
550 * Returns whether the XML attribute is XSI NIL.
552 private static boolean attrIsXsiNil(final Attr attr, final PrefixResolver prefixResolver) {
554 final String qName = attr.getName();
556 final String attrName = QNameHelper.extractName(qName);
557 final String attrPrefix = QNameHelper.extractPrefix(qName);
558 final String attrNamespace = prefixResolver.resolveNamespaceUri(attrPrefix);
560 return ("http://www.w3.org/2001/XMLSchema-instance".equals(attrNamespace) && "nil".equals(attrName));
564 * Returns whether the XML attribute denotes XSI NIL with value true.
566 private static boolean attrIsXsiNilTrue(final Attr attr, final PrefixResolver prefixResolver) {
567 return "true".equalsIgnoreCase(attr.getValue()) && attrIsXsiNil(attr, prefixResolver);
570 protected static List<Attr> getAttributesfromXmlElement(final Element xmlDomElement) {
572 List<Attr> result = null;
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<>();
581 result.add((Attr) item);
585 return result != null ? result : Collections.<Attr> emptyList();
589 * Strip out all control characters and leading and trailing whitespaces.
591 private static void cleanXmlText(final String inputString, final StringBuilder result) {
593 boolean containsNonWhitespaceChars = false;
594 boolean containsNewLine = false;
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') {
601 } else if (c == '\n') {
602 containsNewLine = true;
604 containsNonWhitespaceChars = true;
607 if (containsNewLine && containsNonWhitespaceChars) {
612 if (!containsNonWhitespaceChars) {
614 * Contains only whitespace and/or new-line, all of which should be swallowed. We don't add
615 * anything to the result.
621 * Right...need to clean the string so. If there is no newline in it then we simply trim the string
624 if (!containsNewLine) {
625 result.append(inputString.trim());
630 * Uhh...newlines in it...must strip these out, and trim every line individually...and then re-assemble.
632 final List<String> lines = new ArrayList<>();
635 final BufferedReader br = new BufferedReader(new StringReader(inputString));
637 while ((str = br.readLine()) != null) {
639 if (!str.isEmpty()) {
643 } catch (Exception wontHappen) {
646 boolean first = true;
647 for (final String line : lines) {
656 // ====================================== JSON processing ===================================
659 * Constructor for a data node instance encoded in JSON.
661 public YangDataDomNode(final ParserExecutionContext context, final YangDataDomNode parentNode, final String memberName,
662 final JsonValue jsonValue) {
664 parentNode.children.add(this);
665 this.parentNode = parentNode;
667 this.documentRoot = parentNode.getDocumentRoot();
669 this.lineNumber = jsonValue.line;
670 this.columnNumber = jsonValue.col;
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;
677 this.prefixResolver = parentNode.getPrefixResolver();
681 * Processing the child elements of the JSON object means that this instance here is either a container or a list.
683 protected void processJsonChildElements(final ParserExecutionContext context, final JsonObject jsonObject) {
685 final Map<JsonObjectMemberName, JsonValue> members = jsonObject.getValuesByMember();
688 * Handle the annotations, if any, for this container or list.
690 final JsonObject annotationsForThis = getAnnotationJsonObject(context, jsonObject, "@");
691 extractAnnotationsFromJsonObject(context, annotationsForThis, this);
694 * Handle any possible [null] element.
696 fixupEmptyHandling(jsonObject);
699 * Process the data nodes
701 final Set<String> processedLeafAndLeafListMemberNames = new HashSet<>();
703 for (final Entry<JsonObjectMemberName, JsonValue> mapEntry : members.entrySet()) {
705 final String memberName = mapEntry.getKey().getMemberName();
708 * Annotations will be handled separately as part of the data nodes, skip these here.
710 if (memberName.startsWith("@")) {
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)
720 final JsonValue value = mapEntry.getValue();
722 if (value instanceof JsonPrimitive) { // leaf
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);
731 processedLeafAndLeafListMemberNames.add(memberName);
733 } else if (value instanceof JsonObject) { // container
735 final YangDataDomNode containerDataDomNode = new YangDataDomNode(context, this, memberName,
737 containerDataDomNode.processJsonChildElements(context, (JsonObject) value);
741 if (((JsonArray) value).getValues().isEmpty()) {
743 // empty leaf list, nothing to do (should really not be in the JSON file)
745 } else if (allMembersAreJsonPrimitives((JsonArray) value)) { // leaf-list
747 final List<JsonValue> values = ((JsonArray) value).getValues();
748 final JsonArray leafListMemberAnnotations = getAnnotationJsonArray(context, jsonObject,
750 final List<JsonValue> annotationValues = leafListMemberAnnotations.getValues();
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);
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);
767 processedLeafAndLeafListMemberNames.add(memberName);
769 } else if (allMembersAreJsonObjects((JsonArray) value)) { // list
771 ((JsonArray) value).getValues().forEach(member -> {
772 final YangDataDomNode childYangDataDomNode = new YangDataDomNode(context, this, memberName,
773 (JsonObject) member);
774 childYangDataDomNode.processJsonChildElements(context, (JsonObject) member);
777 } else { // wrong JSON
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.",
787 * We check for any orphaned annotations here.
789 for (final Entry<JsonObjectMemberName, JsonValue> mapEntry : members.entrySet()) {
791 final String memberName = mapEntry.getKey().getMemberName();
793 if (memberName.equals("@") || !memberName.startsWith("@")) {
797 if (!processedLeafAndLeafListMemberNames.contains(memberName.substring(1))) {
799 * We have an annotation with a member name for which we do not have a leaf or leaf-list.
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());
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).
812 private static void fixupEmptyHandling(final JsonObject jsonObject) {
814 final Map<JsonObjectMemberName, JsonValue> members = jsonObject.getValuesByMember();
815 for (final JsonObjectMemberName key : new ArrayList<>(members.keySet())) {
817 final JsonValue memberValue = members.get(key);
819 if (denotesEmpty(memberValue)) {
820 jsonObject.putMember(key, JsonPrimitive.valueOf(memberValue.line, memberValue.col, null));
821 } else if (memberValue instanceof JsonArray) {
823 final JsonArray jsonArray = (JsonArray) memberValue;
824 final List<JsonValue> arrayValues = jsonArray.getValues();
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));
837 * Returns whether the supplied JSON value is [null], denoting an empty value (special syntax for data type 'empty').
839 private static boolean denotesEmpty(final JsonValue jsonValue) {
841 if (jsonValue instanceof JsonArray) {
842 final JsonArray jsonArray = (JsonArray) jsonValue;
843 final List<JsonValue> values = jsonArray.getValues();
845 if (values.size() == 1) {
846 final JsonValue firstArrayMember = values.get(0);
847 if (firstArrayMember instanceof JsonPrimitive) {
848 return ((JsonPrimitive) firstArrayMember).getValue() == null;
857 * Returns a JsonObject representing the annotations for the sought name.
859 private JsonObject getAnnotationJsonObject(final ParserExecutionContext context, final JsonObject parentJsonObject,
860 final String soughtName) {
862 final Optional<Entry<String, JsonValue>> anno = parentJsonObject.getValues().entrySet().stream().filter(
863 entry -> entry.getKey().equals(soughtName)).findAny();
865 if (!anno.isPresent()) {
866 return new JsonObject();
870 * Annotations must always be encoded as JSON objects.
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();
879 return (JsonObject) annoObject;
883 * Returns a JsonArray representing the annotations for the sought name.
885 private JsonArray getAnnotationJsonArray(final ParserExecutionContext context, final JsonObject parentJsonObject,
886 final String soughtName) {
888 final Optional<Entry<String, JsonValue>> anno = parentJsonObject.getValues().entrySet().stream().filter(
889 entry -> entry.getKey().equals(soughtName)).findAny();
891 if (!anno.isPresent()) {
892 return new JsonArray();
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();
903 * All the members must be either a primitive null, or a JsonObject.
905 final JsonArray jsonArray = (JsonArray) annoObject;
906 final List<JsonValue> arrayValues = jsonArray.getValues();
908 for (int i = 0; i < arrayValues.size(); ++i) {
910 final JsonValue arrayMemberValue = arrayValues.get(i);
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) {
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();
928 private void extractAnnotationsFromJsonObject(final ParserExecutionContext context,
929 final JsonObject jsonObjectWithAnnotations, final YangDataDomNode owningDomNode) {
931 if (jsonObjectWithAnnotations.getValues().isEmpty()) {
935 owningDomNode.annotations = new ArrayList<>();
938 * Clean up [null] handling, will be needed where the annotation does not have an argument.
940 fixupEmptyHandling(jsonObjectWithAnnotations);
943 * Iterate over the members of the object - these are the annotations.
945 final Map<JsonObjectMemberName, JsonValue> annoMembers = jsonObjectWithAnnotations.getValuesByMember();
946 for (final Entry<JsonObjectMemberName, JsonValue> entry : annoMembers.entrySet()) {
948 final String moduleAndAnnoName = entry.getKey().getMemberName();
951 * According to RFC, the annotation name MUST be prefixed with the module - section 5.2.1 in RFC 7952...
953 final String moduleName = extractModule(moduleAndAnnoName, null);
954 final String annoName = extractName(moduleAndAnnoName);
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.",
963 owningDomNode.annotations.add(new YangDataDomNodeAnnotationValue(null, moduleName, annoName,
964 ((JsonPrimitive) entry.getValue()).getValue()));
968 private static String extractName(final String memberName) {
969 return memberName.contains(":") ? memberName.split(":")[1] : memberName;
972 private static String extractModule(final String memberName, final String parentModuleName) {
973 return memberName.contains(":") ? memberName.split(":")[0] : parentModuleName;
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)) {
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)) {
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));