5f22e5c66331253fe13ad09f14ea32b0cf57b571
[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.util.ArrayList;
24 import java.util.Collection;
25 import java.util.List;
26
27 import org.w3c.dom.Attr;
28 import org.w3c.dom.Document;
29 import org.w3c.dom.Element;
30 import org.w3c.dom.Node;
31 import org.w3c.dom.NodeList;
32
33 import org.oran.smo.yangtools.parser.ParserExecutionContext;
34 import org.oran.smo.yangtools.parser.data.YangData;
35 import org.oran.smo.yangtools.parser.data.parser.JsonParser.JsonObject;
36 import org.oran.smo.yangtools.parser.findings.Finding;
37 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
38 import org.oran.smo.yangtools.parser.model.yangdom.YangDomDocumentRoot;
39 import org.oran.smo.yangtools.parser.util.QNameHelper;
40
41 /**
42  * Root element for a YANG data DOM. Contains a tree structure of data originating from XML of JSON. The direct
43  * children of this root object in effect represent the values or structure of top-level data nodes.
44  * <p/>
45  * Not to be confused with class {@link YangDomDocumentRoot}, which is the root of a *model* DOM.
46  * <p/>
47  * Will not perform a check against any YANG schema. Therefore, can also be used, if so desired, as a simple
48  * multi-purpose XML/JSON parser if so desired.
49  *
50  * @author Mark Hollmann
51  */
52 public class YangDataDomDocumentRoot extends YangDataDomNode {
53
54     /**
55      * Where the data originally came from. This can make a difference. In XML, the values are all
56      * represented as strings; in JSON, a primitive value can be a String, Double or Boolean. If conversion
57      * is required later on to a highly-typed Java object, a client must know where the data originally
58      * came from.
59      */
60     public enum SourceDataType {
61         XML,
62         JSON
63     }
64
65     private final YangData yangData;
66
67     private final SourceDataType sourceDataType;
68
69     public YangDataDomDocumentRoot(final YangData yangData, final SourceDataType sourceDataType) {
70         super();
71         this.yangData = yangData;
72         this.sourceDataType = sourceDataType;
73     }
74
75     @Override
76     public YangData getYangData() {
77         return yangData;
78     }
79
80     @Override
81     public SourceDataType getSourceDataType() {
82         return sourceDataType;
83     }
84
85     public void buildFromXmlDocument(final ParserExecutionContext context, final Document document) {
86
87         /*
88          * DOM Document root is a special case. It is not an element as such. It only
89          * has a single element as "child"; this single element is the actual root XML
90          * element.
91          */
92         final Element rootXmlElement = document.getDocumentElement();
93         final String rootXmlElementName = QNameHelper.extractName(rootXmlElement.getTagName());
94
95         /*
96          * It is perfectly valid and possible and realistic for the XML root element to declare
97          * namespaces, so we populate the prefix resolver of the root here.
98          */
99         final List<Attr> rootElementXmlAttributes = getAttributesfromXmlElement(rootXmlElement);
100         populateXmlPrefixResolver(rootElementXmlAttributes, getPrefixResolver());
101
102         /*
103          * There is always confusion about the root XML element - and it usually depends on the input.
104          * Sometimes, data is parsed that sits inside a file that a programmer has prepared; sometimes
105          * it could be a NETCONF reply, etc.
106          *
107          * We are trying to be as lenient as possible, and we support the following:
108          *
109          *
110          *
111          * 1.) As root element <config>, for example:
112          *
113          * <config>
114          *   <netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">
115          *     ...stuff ...
116          *
117          *
118          *
119          * 2.) RFC 9195 ("A File Format for YANG Instance Data")
120          *
121          * Lately, we are aligning with draft-ietf-netmod-yang-instance-file-format-10,
122          * which looks like this:
123          *
124          * <?xml version="1.0" encoding="UTF-8"?>
125          * <instance-data-set xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-instance-data">
126          *   <name>read-only-acm-rules</name>
127          *   <content-schema>
128          *     <module>ietf-netconf-acm@2018-02-14</module>
129          *   </content-schema>
130          *   <revision>
131          *     <date>1776-07-04</date>
132          *     <description>Initial version</description>
133          *   </revision>
134          *   <description>Access control rules for a read-only role.</description>
135          *   <content-data>
136          *     <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
137          *       ... stuff ...
138          *   </content-data>
139          * </instance-data-set>
140          *
141          *
142          *
143          * 3.) A NETCONF RPC reply:
144          *
145          * <rpc-reply message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
146          *   <data>
147          *     <netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">
148          *       ... stuff ...
149          *
150          *
151          *
152          * 4.) Just <data> as root:
153          *
154          * <data>
155          *   <netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">
156          *     ... stuff ...
157          *
158          *
159          *
160          * 5.) Data directly at the root:
161          *
162          * <netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">
163          *   ... stuff ...
164          */
165
166         final List<Element> xmlElementsToProcess = new ArrayList<>();
167
168         if (rootXmlElementName.equals("config")) {
169
170             xmlElementsToProcess.addAll(getChildElementNodes(rootXmlElement));
171
172         } else if (rootXmlElementName.equals("instance-data-set")) {
173             /*
174              * Need to find the <content-data> element underneath first. Whatever is
175              * underneath that, will be handled.
176              */
177             final Element contentDataElement = getNamedChildElement(rootXmlElement, "content-data");
178             if (contentDataElement != null) {
179                 /*
180                  * We must add handle whatever prefixes have been defined on the <content-data> element.
181                  * Never seen in the wild, but who knows....
182                  */
183                 final List<Attr> contentDataXmlAttributes = getAttributesfromXmlElement(contentDataElement);
184                 populateXmlPrefixResolver(contentDataXmlAttributes, getPrefixResolver());
185
186                 xmlElementsToProcess.addAll(getChildElementNodes(contentDataElement));
187             }
188         } else if (rootXmlElementName.equals("rpc-reply")) {
189             /*
190              * Need to find the <data> element underneath first. Whatever is
191              * underneath that, will be handled.
192              */
193             final Element dataElement = getNamedChildElement(rootXmlElement, "data");
194             if (dataElement != null) {
195                 /*
196                  * We must add handle whatever prefixes have been defined on the <data> element.
197                  */
198                 final List<Attr> dataXmlAttributes = getAttributesfromXmlElement(dataElement);
199                 populateXmlPrefixResolver(dataXmlAttributes, getPrefixResolver());
200
201                 xmlElementsToProcess.addAll(getChildElementNodes(dataElement));
202             }
203         } else if (rootXmlElementName.equals("data")) {
204
205             xmlElementsToProcess.addAll(getChildElementNodes(rootXmlElement));
206
207         } else {
208             /*
209              * Right... assume so that the root element is immediately data.
210              */
211             context.addFinding(new Finding(yangData, ParserFindingType.P071_INCORRECT_ROOT_ELEMENT_OF_DATA_FILE.toString(),
212                     "Expected <instance-data-set> (or <config> or <rpc-reply> or <data>) as first element in data file. Assume root element represents data. If this is not the case, this will likely lead to other findings."));
213
214             xmlElementsToProcess.add(rootXmlElement);
215         }
216
217         if (xmlElementsToProcess.isEmpty()) {
218             context.addFinding(new Finding(yangData, ParserFindingType.P079_EMPTY_DATA_FILE.toString(),
219                     "The instance data input seems to be empty."));
220         }
221
222         /*
223          * Now process them.
224          */
225         for (final Element childXmlNode : xmlElementsToProcess) {
226             final YangDataDomNode childYangDataDomNode = new YangDataDomNode(context, this, childXmlNode);
227             childYangDataDomNode.processXmlChildElements(context, childXmlNode);
228         }
229
230         /*
231          * It quite frequently (at least for hand-drafted XML files) happens that namespaces are not declared
232          * at the top of the document. We make sure that the children all have a valid namespace, otherwise
233          * there will be more processing errors later on, so highlight this here.
234          */
235         for (final YangDataDomNode child : getChildren()) {
236             if (child.getNamespace() == null) {
237                 context.addFinding(new Finding(this, ParserFindingType.P015_INVALID_SYNTAX_IN_DOCUMENT.toString(),
238                         "The top-level data nodes must have a namespace, but none is declared."));
239             }
240         }
241     }
242
243     /**
244      * Given the root XML element of a document, searches for a named child element, and return that child's children.
245      */
246     private Element getNamedChildElement(final Element element, final String soughtChildName) {
247
248         final Collection<? extends Element> childrenOfRoot = getChildElementNodes(element);
249         for (final Element child : childrenOfRoot) {
250             if (soughtChildName.equals(QNameHelper.extractName(child.getTagName()))) {
251                 return child;
252             }
253         }
254
255         return null;
256     }
257
258     private Collection<? extends Element> getChildElementNodes(final Element element) {
259
260         final List<Element> result = new ArrayList<>();
261
262         final NodeList childXmlNodes = element.getChildNodes();
263         for (int i = 0; i < childXmlNodes.getLength(); ++i) {
264             final Node childXmlNode = childXmlNodes.item(i);
265             if (childXmlNode.getNodeType() == Node.ELEMENT_NODE) {
266                 result.add((Element) childXmlNode);
267             }
268         }
269
270         return result;
271     }
272
273     // ============================ JSON handling ============================
274
275     public void buildFromJsonDocument(final ParserExecutionContext context, final JsonObject rootJsonObject) {
276
277         /*
278          * No fluffing around with namespaces here, of course.
279          */
280
281         processJsonChildElements(context, rootJsonObject);
282
283         /*
284          * We make sure that the children all have a valid module name, otherwise there will be
285          * more processing errors later on, so highlight this here.
286          */
287         for (final YangDataDomNode child : getChildren()) {
288             if (child.getModuleName().equals(ROOT_SLASH)) {
289                 context.addFinding(new Finding(this, ParserFindingType.P015_INVALID_SYNTAX_IN_DOCUMENT.toString(),
290                         "The name of all top-level data nodes name must be prefixed with the name of the module that owns the data node."));
291             }
292         }
293     }
294 }