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