Bump upstreams
[netconf.git] / plugins / netconf-client-mdsal / src / main / java / org / opendaylight / netconf / client / mdsal / impl / SchemalessRpcStructureTransformer.java
1 /*
2  * Copyright (c) 2016 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.client.mdsal.impl;
9
10 import static org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_CONFIG_NODEID;
11 import static org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_DATA_NODEID;
12 import static org.opendaylight.netconf.common.mdsal.NormalizedDataUtil.appendListKeyNodes;
13 import static org.opendaylight.netconf.common.mdsal.NormalizedDataUtil.writeSchemalessFilter;
14
15 import java.util.List;
16 import java.util.Optional;
17 import javax.xml.transform.dom.DOMSource;
18 import org.opendaylight.netconf.api.DocumentedException;
19 import org.opendaylight.netconf.api.EffectiveOperation;
20 import org.opendaylight.netconf.api.NamespaceURN;
21 import org.opendaylight.netconf.api.xml.XmlElement;
22 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
23 import org.opendaylight.netconf.api.xml.XmlUtil;
24 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.get.input.Filter;
25 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
29 import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
30 import org.opendaylight.yangtools.yang.data.api.schema.DOMSourceAnyxmlNode;
31 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
32 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
33 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
34 import org.w3c.dom.Document;
35 import org.w3c.dom.Element;
36 import org.w3c.dom.Node;
37
38 /**
39  * Transforms rpc structures to anyxml and vice versa. Provides support for devices, which don't expose their schema.
40  */
41 class SchemalessRpcStructureTransformer implements RpcStructureTransformer {
42
43     /**
44      * Selects elements in anyxml data node, which matches path arguments QNames. Since class in not context aware,
45      * method searches for all elements as they are named in path.
46      * @param data data, must be of type {@link DOMSourceAnyxmlNode}
47      * @param path path to select
48      * @return selected data
49      */
50     @Override
51     public Optional<NormalizedNode> selectFromDataStructure(final DataContainerChild data,
52             final YangInstanceIdentifier path) {
53         if (!(data instanceof DOMSourceAnyxmlNode anyxml)) {
54             throw new IllegalArgumentException("Unexpected data " + data.prettyTree());
55         }
56
57         final var result = XmlUtil.newDocument();
58         final var dataElement = result.createElementNS(NamespaceURN.BASE, "data");
59         result.appendChild(dataElement);
60         for (var xmlElement : selectMatchingNodes(getSourceElement(anyxml.body()), path)) {
61             dataElement.appendChild(result.importNode(xmlElement.getDomElement(), true));
62         }
63         return Optional.of(Builders.anyXmlBuilder()
64             .withNodeIdentifier(NETCONF_DATA_NODEID)
65             .withValue(new DOMSource(result))
66             .build());
67     }
68
69     /**
70      * This class in not context aware. All elements are present in resulting structure, which are present in data path.
71      * @see RpcStructureTransformer#createEditConfigStructure(Optional, YangInstanceIdentifier, Optional)
72      * @param data data
73      * @param dataPath path, where data will be written
74      * @param operation operation
75      * @return config structure
76      */
77     @Override
78     public AnyxmlNode<DOMSource> createEditConfigStructure(final Optional<NormalizedNode> data,
79             final YangInstanceIdentifier dataPath, final Optional<EffectiveOperation> operation) {
80         final var dataValue = data.orElseThrow();
81         if (!(dataValue instanceof DOMSourceAnyxmlNode anxmlData)) {
82             throw new IllegalArgumentException("Unexpected data " + dataValue.prettyTree());
83         }
84
85         final var document = XmlUtil.newDocument();
86         final var dataNode = (Element) document.importNode(getSourceElement(anxmlData.body()), true);
87         checkDataValidForPath(dataPath, dataNode);
88
89         final var configElement = document.createElementNS(NamespaceURN.BASE, XmlNetconfConstants.CONFIG_KEY);
90         document.appendChild(configElement);
91
92         final Element parentXmlStructure;
93         if (dataPath.isEmpty()) {
94             parentXmlStructure = dataNode;
95             configElement.appendChild(parentXmlStructure);
96         } else {
97             final var pathArguments = dataPath.getPathArguments();
98             // last will be appended later
99             parentXmlStructure = instanceIdToXmlStructure(pathArguments.subList(0, pathArguments.size() - 1),
100                 configElement);
101         }
102         operation.ifPresent(modifyAction -> setOperationAttribute(modifyAction, document, dataNode));
103         //append data
104         parentXmlStructure.appendChild(document.importNode(dataNode, true));
105         return Builders.anyXmlBuilder()
106             .withNodeIdentifier(NETCONF_CONFIG_NODEID)
107             .withValue(new DOMSource(document.getDocumentElement()))
108             .build();
109     }
110
111     /**
112      * This class in not context aware. All elements are present in resulting structure, which are present in data path.
113      * @see RpcStructureTransformer#toFilterStructure(YangInstanceIdentifier)
114      * @param path path
115      * @return filter structure
116      */
117     @Override
118     public AnyxmlNode<?> toFilterStructure(final YangInstanceIdentifier path) {
119         final var document = XmlUtil.newDocument();
120         instanceIdToXmlStructure(path.getPathArguments(), prepareFilterElement(document));
121         return buildFilterXmlNode(document);
122     }
123
124     @Override
125     public AnyxmlNode<?> toFilterStructure(final List<FieldsFilter> fieldsFilters) {
126         final var document = XmlUtil.newDocument();
127         final var filterElement = prepareFilterElement(document);
128         for (var filter : fieldsFilters) {
129             writeSchemalessFilter(filter.path(), filter.fields(), filterElement);
130         }
131         return buildFilterXmlNode(document);
132     }
133
134     private static Element prepareFilterElement(final Document document) {
135         final var filter = document.createElementNS(NamespaceURN.BASE, "filter");
136         filter.setAttribute("type", "subtree");
137         document.appendChild(filter);
138         return filter;
139     }
140
141     private static AnyxmlNode<?> buildFilterXmlNode(final Document document) {
142         return Builders.anyXmlBuilder()
143             .withNodeIdentifier(new NodeIdentifier(Filter.QNAME))
144             .withValue(new DOMSource(document.getDocumentElement()))
145             .build();
146     }
147
148     private static void checkDataValidForPath(final YangInstanceIdentifier dataPath, final Element dataNode) {
149         //if datapath is empty, consider dataNode to be a root node
150         if (dataPath.isEmpty()) {
151             return;
152         }
153         final var dataElement = XmlElement.fromDomElement(dataNode);
154         final var  lastPathArgument = dataPath.getLastPathArgument();
155         final var nodeType = lastPathArgument.getNodeType();
156         if (!nodeType.getNamespace().toString().equals(dataNode.getNamespaceURI())
157                 || !nodeType.getLocalName().equals(dataElement.getName())) {
158             throw new IllegalStateException(
159                     String.format("Can't write data '%s' to path %s", dataNode.getTagName(), dataPath));
160         }
161         if (lastPathArgument instanceof NodeIdentifierWithPredicates) {
162             checkKeyValuesValidForPath(dataElement, lastPathArgument);
163         }
164     }
165
166     private static void checkKeyValuesValidForPath(final XmlElement dataElement, final PathArgument lastPathArgument) {
167         for (var entry : ((NodeIdentifierWithPredicates) lastPathArgument).entrySet()) {
168             final var qname = entry.getKey();
169             final var key = dataElement.getChildElementsWithinNamespace(qname.getLocalName(),
170                 qname.getNamespace().toString());
171             if (key.isEmpty()) {
172                 throw new IllegalStateException("No key present in xml");
173             }
174             if (key.size() > 1) {
175                 throw new IllegalStateException("Multiple values for same key present");
176             }
177             final String textContent;
178             try {
179                 textContent = key.get(0).getTextContent();
180             } catch (DocumentedException e) {
181                 throw new IllegalStateException("Key value not present in key element", e);
182             }
183             if (!entry.getValue().equals(textContent)) {
184                 throw new IllegalStateException("Key value in path not equal to key value in xml");
185             }
186         }
187     }
188
189     private static void setOperationAttribute(final EffectiveOperation operation, final Document document,
190             final Element dataNode) {
191         final var operationAttribute = document.createAttributeNS(NamespaceURN.BASE,
192             XmlNetconfConstants.OPERATION_ATTR_KEY);
193         operationAttribute.setTextContent(operation.xmlValue());
194         dataNode.setAttributeNode(operationAttribute);
195     }
196
197     private static Element instanceIdToXmlStructure(final List<PathArgument> pathArguments, final Element data) {
198         final var doc = data.getOwnerDocument();
199         var parent = data;
200         for (var pathArgument : pathArguments) {
201             final var nodeType = pathArgument.getNodeType();
202             final var element = doc.createElementNS(nodeType.getNamespace().toString(), nodeType.getLocalName());
203             parent.appendChild(element);
204             //if path argument is list id, add also keys to resulting xml
205             if (pathArgument instanceof NodeIdentifierWithPredicates nip) {
206                 appendListKeyNodes(element, nip);
207             }
208             parent = element;
209         }
210         return parent;
211     }
212
213     private static List<XmlElement> selectMatchingNodes(final Element domElement, final YangInstanceIdentifier path) {
214         var element = XmlElement.fromDomElement(domElement);
215         for (var pathArgument : path.getPathArguments()) {
216             var childElements = element.getChildElements(pathArgument.getNodeType().getLocalName());
217             if (childElements.size() == 1) {
218                 element = childElements.get(0);
219             } else {
220                 return childElements;
221             }
222         }
223         return List.of(element);
224     }
225
226     private static Element getSourceElement(final DOMSource source) {
227         final var node = source.getNode();
228         return switch (node.getNodeType()) {
229             case Node.DOCUMENT_NODE -> ((Document) node).getDocumentElement();
230             case Node.ELEMENT_NODE -> (Element) node;
231             default -> throw new IllegalStateException("DOMSource node must be document or element.");
232         };
233     }
234 }