Eliminate use of SchemaPath in netconf-util
[netconf.git] / netconf / netconf-util / src / main / java / org / opendaylight / netconf / util / NetconfUtil.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.util;
9
10 import static com.google.common.base.Preconditions.checkState;
11
12 import java.io.IOException;
13 import java.net.URISyntaxException;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Map.Entry;
18 import java.util.stream.Collectors;
19 import javax.xml.stream.XMLOutputFactory;
20 import javax.xml.stream.XMLStreamException;
21 import javax.xml.stream.XMLStreamWriter;
22 import javax.xml.transform.dom.DOMResult;
23 import javax.xml.transform.dom.DOMSource;
24 import org.eclipse.jdt.annotation.NonNull;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.opendaylight.netconf.api.DocumentedException;
27 import org.opendaylight.netconf.api.xml.XmlElement;
28 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
29 import org.opendaylight.netconf.api.xml.XmlUtil;
30 import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadata;
31 import org.opendaylight.yangtools.rfc7952.data.api.StreamWriterMetadataExtension;
32 import org.opendaylight.yangtools.rfc7952.data.util.NormalizedMetadataWriter;
33 import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
34 import org.opendaylight.yangtools.rfc8528.data.util.EmptyMountPointContext;
35 import org.opendaylight.yangtools.yang.common.QName;
36 import org.opendaylight.yangtools.yang.common.QNameModule;
37 import org.opendaylight.yangtools.yang.common.Revision;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
41 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
42 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
43 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
44 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
45 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
46 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
47 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
48 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
49 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
50 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53 import org.w3c.dom.Document;
54 import org.w3c.dom.Element;
55 import org.xml.sax.SAXException;
56
57 public final class NetconfUtil {
58     /**
59      * Shim interface to handle differences around namespace handling between various XMLStreamWriter implementations.
60      * Specifically:
61      * <ul>
62      *   <li>OpenJDK DOM writer (com.sun.xml.internal.stream.writers.XMLDOMWriterImpl) throws
63      *       UnsupportedOperationException from its setNamespaceContext() method</li>
64      *   <li>Woodstox DOM writer (com.ctc.wstx.dom.WstxDOMWrappingWriter) works with namespace context, but treats
65      *       setPrefix() calls as hints -- which are not discoverable.</li>
66      * </ul>
67      *
68      * <p>
69      * Due to this we perform a quick test for behavior and decide the appropriate strategy.
70      */
71     @FunctionalInterface
72     private interface NamespaceSetter {
73         void initializeNamespace(XMLStreamWriter writer) throws XMLStreamException;
74
75         static NamespaceSetter forFactory(final XMLOutputFactory xmlFactory) {
76             final String netconfNamespace = NETCONF_QNAME.getNamespace().toString();
77             final AnyXmlNamespaceContext namespaceContext = new AnyXmlNamespaceContext(Map.of("op", netconfNamespace));
78
79             try {
80                 final XMLStreamWriter testWriter = xmlFactory.createXMLStreamWriter(new DOMResult(
81                     XmlUtil.newDocument()));
82                 testWriter.setNamespaceContext(namespaceContext);
83             } catch (final UnsupportedOperationException e) {
84                 // This happens with JDK's DOM writer, which we may be using
85                 LOG.debug("Unable to set namespace context, falling back to setPrefix()", e);
86                 return writer -> writer.setPrefix("op", netconfNamespace);
87             } catch (XMLStreamException e) {
88                 throw new ExceptionInInitializerError(e);
89             }
90
91             // Success, we can use setNamespaceContext()
92             return writer -> writer.setNamespaceContext(namespaceContext);
93         }
94     }
95
96     private static final Logger LOG = LoggerFactory.getLogger(NetconfUtil.class);
97
98     // FIXME: document what exactly this QName means, as it is not referring to a tangible node nor the ietf-module.
99     // FIXME: what is this contract saying?
100     //        - is it saying all data is going to be interpreted with this root?
101     //        - is this saying we are following a specific interface contract (i.e. do we have schema mounts?)
102     //        - is it also inferring some abilities w.r.t. RFC8342?
103     public static final QName NETCONF_QNAME = QName.create(QNameModule.create(SchemaContext.NAME.getNamespace(),
104         Revision.of("2011-06-01")), "netconf").intern();
105     // FIXME: is this the device-bound revision?
106     public static final QName NETCONF_DATA_QNAME = QName.create(NETCONF_QNAME, "data").intern();
107
108     public static final XMLOutputFactory XML_FACTORY;
109
110     static {
111         final XMLOutputFactory f = XMLOutputFactory.newFactory();
112         // FIXME: not repairing namespaces is probably common, this should be availabe as common XML constant.
113         f.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, false);
114         XML_FACTORY = f;
115     }
116
117     private static final NamespaceSetter XML_NAMESPACE_SETTER = NamespaceSetter.forFactory(XML_FACTORY);
118
119     private NetconfUtil() {
120         // No-op
121     }
122
123     public static Document checkIsMessageOk(final Document response) throws DocumentedException {
124         final XmlElement docElement = XmlElement.fromDomDocument(response);
125         // FIXME: we should throw DocumentedException here
126         checkState(XmlNetconfConstants.RPC_REPLY_KEY.equals(docElement.getName()));
127         final XmlElement element = docElement.getOnlyChildElement();
128         if (XmlNetconfConstants.OK.equals(element.getName())) {
129             return response;
130         }
131
132         LOG.warn("Can not load last configuration. Operation failed.");
133         // FIXME: we should be throwing a DocumentedException here
134         throw new IllegalStateException("Can not load last configuration. Operation failed: "
135                 + XmlUtil.toString(response));
136     }
137
138     /**
139      * Write {@code normalized} data into {@link DOMResult}.
140      *
141      * @param normalized data to be written
142      * @param result     DOM result holder
143      * @param context    mountpoint schema context
144      * @param path       optional schema node identifier of the parent node
145      * @throws IOException        when failed to write data into {@link NormalizedNodeStreamWriter}
146      * @throws XMLStreamException when failed to serialize data into XML document
147      */
148     public static void writeNormalizedNode(final NormalizedNode normalized, final DOMResult result,
149             final EffectiveModelContext context, final @Nullable Absolute path) throws IOException, XMLStreamException {
150         final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
151         try (var streamWriter = newWriter(xmlWriter, context, path);
152              var writer = NormalizedNodeWriter.forStreamWriter(streamWriter)) {
153             writer.write(normalized);
154             writer.flush();
155         } finally {
156             xmlWriter.close();
157         }
158     }
159
160     /**
161      * Write {@code normalized} data along with corresponding {@code metadata} into {@link DOMResult}.
162      *
163      * @param normalized data to be written
164      * @param metadata   metadata to be written
165      * @param result     DOM result holder
166      * @param context    mountpoint schema context
167      * @param path       optional schema node identifier of the parent node
168      * @throws IOException        when failed to write data into {@link NormalizedNodeStreamWriter}
169      * @throws XMLStreamException when failed to serialize data into XML document
170      */
171     public static void writeNormalizedNode(final NormalizedNode normalized, final @Nullable NormalizedMetadata metadata,
172             final DOMResult result, final EffectiveModelContext context, final @Nullable Absolute path)
173                 throws IOException, XMLStreamException {
174         if (metadata == null) {
175             writeNormalizedNode(normalized, result, context, path);
176             return;
177         }
178
179         final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
180         XML_NAMESPACE_SETTER.initializeNamespace(xmlWriter);
181         try (var streamWriter = newWriter(xmlWriter, context, path);
182              var writer = NormalizedMetadataWriter.forStreamWriter(streamWriter)) {
183             writer.write(normalized, metadata);
184             writer.flush();
185         } finally {
186             xmlWriter.close();
187         }
188     }
189
190     /**
191      * Write data specified by {@link YangInstanceIdentifier} into {@link DOMResult}.
192      *
193      * @param query      path to the root node
194      * @param result     DOM result holder
195      * @param context    mountpoint schema context
196      * @param path       optional schema node identifier of the parent node
197      * @throws IOException        when failed to write data into {@link NormalizedNodeStreamWriter}
198      * @throws XMLStreamException when failed to serialize data into XML document
199      */
200     public static void writeNormalizedNode(final YangInstanceIdentifier query, final DOMResult result,
201             final EffectiveModelContext context, final @Nullable Absolute path) throws IOException, XMLStreamException {
202         final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
203         XML_NAMESPACE_SETTER.initializeNamespace(xmlWriter);
204         try (var streamWriter = newWriter(xmlWriter, context, path);
205              var writer = new EmptyListXmlWriter(streamWriter, xmlWriter)) {
206             final Iterator<PathArgument> it = query.getPathArguments().iterator();
207             final PathArgument first = it.next();
208             StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType()).streamToWriter(writer, first, it);
209         } finally {
210             xmlWriter.close();
211         }
212     }
213
214     /**
215      * Write data specified by {@link YangInstanceIdentifier} along with corresponding {@code metadata}
216      * into {@link DOMResult}.
217      *
218      * @param query      path to the root node
219      * @param metadata   metadata to be written
220      * @param result     DOM result holder
221      * @param context    mountpoint schema context
222      * @param path       optional schema node identifier of the parent node
223      * @throws IOException        when failed to write data into {@link NormalizedNodeStreamWriter}
224      * @throws XMLStreamException when failed to serialize data into XML document
225      */
226     public static void writeNormalizedNode(final YangInstanceIdentifier query,
227             final @Nullable NormalizedMetadata metadata, final DOMResult result, final EffectiveModelContext context,
228             final @Nullable Absolute path) throws IOException, XMLStreamException {
229         if (metadata == null) {
230             writeNormalizedNode(query, result, context, path);
231             return;
232         }
233
234         final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
235         XML_NAMESPACE_SETTER.initializeNamespace(xmlWriter);
236         try (var streamWriter = newWriter(xmlWriter, context, path);
237              var writer = new EmptyListXmlMetadataWriter(streamWriter, xmlWriter,
238                  streamWriter.getExtensions().getInstance(StreamWriterMetadataExtension.class), metadata)) {
239             final Iterator<PathArgument> it = query.getPathArguments().iterator();
240             final PathArgument first = it.next();
241             StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType()).streamToWriter(writer, first, it);
242         } finally {
243             xmlWriter.close();
244         }
245     }
246
247     /**
248      * Writing subtree filter specified by {@link YangInstanceIdentifier} into {@link DOMResult}.
249      *
250      * @param query      path to the root node
251      * @param result     DOM result holder
252      * @param context    mountpoint schema context
253      * @param path       optional schema node identifier of the parent node
254      * @throws IOException        failed to write filter into {@link NormalizedNodeStreamWriter}
255      * @throws XMLStreamException failed to serialize filter into XML document
256      */
257     public static void writeFilter(final YangInstanceIdentifier query, final DOMResult result,
258             final EffectiveModelContext context, final @Nullable Absolute path) throws IOException, XMLStreamException {
259         if (query.isEmpty()) {
260             // No query at all
261             return;
262         }
263
264         final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
265         try (var streamWriter = newWriter(xmlWriter, context, path);
266              var writer = new EmptyListXmlWriter(streamWriter, xmlWriter)) {
267             final Iterator<PathArgument> it = query.getPathArguments().iterator();
268             final PathArgument first = it.next();
269             StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType()).streamToWriter(writer, first, it);
270         } finally {
271             xmlWriter.close();
272         }
273     }
274
275     /**
276      * Writing subtree filter specified by parent {@link YangInstanceIdentifier} and specific fields
277      * into {@link DOMResult}. Field paths are relative to parent query path.
278      *
279      * @param query      path to the root node
280      * @param result     DOM result holder
281      * @param context    mountpoint schema context
282      * @param path       optional schema node identifier of the parent node
283      * @param fields     list of specific fields for which the filter should be created
284      * @throws IOException        failed to write filter into {@link NormalizedNodeStreamWriter}
285      * @throws XMLStreamException failed to serialize filter into XML document
286      * @throws NullPointerException if any argument is null
287      */
288     public static void writeFilter(final YangInstanceIdentifier query, final DOMResult result,
289                                    final EffectiveModelContext context, final @Nullable Absolute path,
290                                    final List<YangInstanceIdentifier> fields) throws IOException, XMLStreamException {
291         if (query.isEmpty() || fields.isEmpty()) {
292             // No query at all
293             return;
294         }
295
296         final List<YangInstanceIdentifier> aggregatedFields = aggregateFields(fields);
297         final PathNode rootNode = constructPathArgumentTree(query, aggregatedFields);
298
299         final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
300         try {
301             try (var streamWriter = newWriter(xmlWriter, context, path);
302                  var writer = new EmptyListXmlWriter(streamWriter, xmlWriter)) {
303                 final PathArgument first = rootNode.element();
304                 StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType())
305                         .streamToWriter(writer, first, rootNode);
306             }
307         } finally {
308             xmlWriter.close();
309         }
310     }
311
312     /**
313      * Writing subtree filter specified by parent {@link YangInstanceIdentifier} and specific fields
314      * into {@link Element}. Field paths are relative to parent query path. Filter is created without following
315      * {@link EffectiveModelContext}.
316      *
317      * @param query         path to the root node
318      * @param fields        list of specific fields for which the filter should be created
319      * @param filterElement XML filter element to which the created filter will be written
320      */
321     public static void writeSchemalessFilter(final YangInstanceIdentifier query,
322                                              final List<YangInstanceIdentifier> fields, final Element filterElement) {
323         pathArgumentTreeToXmlStructure(constructPathArgumentTree(query, aggregateFields(fields)), filterElement);
324     }
325
326     private static void pathArgumentTreeToXmlStructure(final PathNode pathArgumentTree, final Element data) {
327         final PathArgument pathArg = pathArgumentTree.element();
328
329         final QName nodeType = pathArg.getNodeType();
330         final String elementNamespace = nodeType.getNamespace().toString();
331
332         if (data.getElementsByTagNameNS(elementNamespace, nodeType.getLocalName()).getLength() != 0) {
333             // element has already been written as list key
334             return;
335         }
336
337         final Element childElement = data.getOwnerDocument().createElementNS(elementNamespace, nodeType.getLocalName());
338         data.appendChild(childElement);
339         if (pathArg instanceof NodeIdentifierWithPredicates nip) {
340             appendListKeyNodes(childElement, nip);
341         }
342         for (final PathNode childrenNode : pathArgumentTree.children()) {
343             pathArgumentTreeToXmlStructure(childrenNode, childElement);
344         }
345     }
346
347     /**
348      * Appending list key elements to parent element.
349      *
350      * @param parentElement parent XML element to which children elements are appended
351      * @param listEntryId   list entry identifier
352      */
353     public static void appendListKeyNodes(final Element parentElement, final NodeIdentifierWithPredicates listEntryId) {
354         for (Entry<QName, Object> key : listEntryId.entrySet()) {
355             final Element keyElement = parentElement.getOwnerDocument().createElementNS(
356                     key.getKey().getNamespace().toString(), key.getKey().getLocalName());
357             keyElement.setTextContent(key.getValue().toString());
358             parentElement.appendChild(keyElement);
359         }
360     }
361
362     /**
363      * Aggregation of the fields paths based on parenthesis. Only parent/enclosing {@link YangInstanceIdentifier}
364      * are kept. For example, paths '/x/y/z', '/x/y', and '/x' are aggregated into single field path: '/x'
365      *
366      * @param fields paths of fields
367      * @return filtered {@link List} of paths
368      */
369     private static List<YangInstanceIdentifier> aggregateFields(final List<YangInstanceIdentifier> fields) {
370         return fields.stream()
371                 .filter(field -> fields.stream()
372                         .filter(fieldYiid -> !field.equals(fieldYiid))
373                         .noneMatch(fieldYiid -> fieldYiid.contains(field)))
374                 .collect(Collectors.toList());
375     }
376
377     /**
378      * Construct a tree based on the parent {@link YangInstanceIdentifier} and provided list of fields. The goal of this
379      * procedure is the elimination of the redundancy that is introduced by potentially overlapping parts of the fields
380      * paths.
381      *
382      * @param query  path to parent element
383      * @param fields subpaths relative to parent path that identify specific fields
384      * @return created {@link PathNode} structure
385      */
386     private static PathNode constructPathArgumentTree(final YangInstanceIdentifier query,
387             final List<YangInstanceIdentifier> fields) {
388         final Iterator<PathArgument> queryIterator = query.getPathArguments().iterator();
389         final PathNode rootTreeNode = new PathNode(queryIterator.next());
390
391         PathNode queryTreeNode = rootTreeNode;
392         while (queryIterator.hasNext()) {
393             queryTreeNode = queryTreeNode.ensureChild(queryIterator.next());
394         }
395
396         for (final YangInstanceIdentifier field : fields) {
397             PathNode actualFieldTreeNode = queryTreeNode;
398             for (final PathArgument fieldPathArg : field.getPathArguments()) {
399                 actualFieldTreeNode = actualFieldTreeNode.ensureChild(fieldPathArg);
400             }
401         }
402         return rootTreeNode;
403     }
404
405     public static NormalizedNodeResult transformDOMSourceToNormalizedNode(final MountPointContext mount,
406             final DOMSource value) throws XMLStreamException, URISyntaxException, IOException, SAXException {
407         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
408         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
409         try (XmlParserStream xmlParserStream = XmlParserStream.create(writer, new ProxyMountPointContext(mount))) {
410             xmlParserStream.traverse(value);
411         }
412         return resultHolder;
413     }
414
415     // FIXME: document this interface contract. Does it support RFC8528/RFC8542? How?
416     public static NormalizedNodeResult transformDOMSourceToNormalizedNode(final EffectiveModelContext schemaContext,
417             final DOMSource value) throws XMLStreamException, URISyntaxException, IOException, SAXException {
418         return transformDOMSourceToNormalizedNode(new EmptyMountPointContext(schemaContext), value);
419     }
420
421     // FIXME: this should not be needed once we have yangtools-10.0.1.
422     private static @NonNull NormalizedNodeStreamWriter newWriter(final XMLStreamWriter writer,
423             final EffectiveModelContext context, final @Nullable Absolute path) {
424         return path == null ? XMLStreamNormalizedNodeStreamWriter.create(writer, context)
425             : XMLStreamNormalizedNodeStreamWriter.create(writer, context, path);
426     }
427 }