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