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